mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-06 14:39:22 +00:00
File diff suppressed because one or more lines are too long
@@ -0,0 +1,105 @@
|
||||
/** Issue #187: Glow source fallback uses light-policy bodies when boolean masonry fails. */
|
||||
import { launch, checkAll, finish } from './serve.mjs';
|
||||
|
||||
const { page, browser } = await launch({ width: 900, height: 760 }, 1);
|
||||
|
||||
const out = await page.evaluate(async () => {
|
||||
const result = {};
|
||||
const card = window.__card;
|
||||
const root = () => card.shadowRoot || card.renderRoot;
|
||||
const update = async () => {
|
||||
card.requestUpdate();
|
||||
await card.updateComplete;
|
||||
await new Promise((resolve) => setTimeout(resolve, 60));
|
||||
await card.updateComplete;
|
||||
};
|
||||
const pointIn = (point, body) => {
|
||||
let inside = false;
|
||||
for (let i = 0, j = body.length - 1; i < body.length; j = i++) {
|
||||
const a = body[i], b = body[j];
|
||||
if (((a[1] > point[1]) !== (b[1] > point[1]))
|
||||
&& point[0] < ((b[0] - a[0]) * (point[1] - a[1]))
|
||||
/ ((b[1] - a[1]) || 1e-12) + a[0]) inside = !inside;
|
||||
}
|
||||
return inside;
|
||||
};
|
||||
|
||||
const spaceId = card._space;
|
||||
const lightDevice = card._devices.find((device) => device.space === spaceId
|
||||
&& device.entities.some((entityId) => entityId.startsWith('light.')));
|
||||
const sourceEid = lightDevice?.entities.find((entityId) => entityId.startsWith('light.'));
|
||||
if (!lightDevice || !sourceEid) return {
|
||||
fixtureHasLightSource: false,
|
||||
windowFallbackBodyIsOpaque: false,
|
||||
windowSourceSuppressedOnBooleanFailure: false,
|
||||
interiorPassageRemainsTransparent: false,
|
||||
};
|
||||
result.fixtureHasLightSource = true;
|
||||
|
||||
const centre = [500, 500];
|
||||
const opening = (type) => ({
|
||||
id: 'hosted-opening', type, x: 0.5, y: 0.5, angle: 0, length: 0.12,
|
||||
host: { kind: 'partition', id: 'host', t: 0.5 },
|
||||
});
|
||||
const currentState = card.hass.states[sourceEid];
|
||||
card.hass = {
|
||||
...card.hass,
|
||||
states: {
|
||||
...card.hass.states,
|
||||
[sourceEid]: { ...currentState, state: 'on' },
|
||||
},
|
||||
};
|
||||
card._layout = {
|
||||
...card._layout,
|
||||
[lightDevice.id]: { s: spaceId, x: 0.5, y: 0.5 },
|
||||
};
|
||||
const replaceSpace = (type) => {
|
||||
card._serverCfg = {
|
||||
...card._serverCfg,
|
||||
spaces: card._serverCfg.spaces.map((space) => space.id !== spaceId ? space : ({
|
||||
...space,
|
||||
rooms: [{
|
||||
id: 'glow-room', name: 'Glow room', area: null,
|
||||
poly: [[0.1, 0.1], [0.9, 0.1], [0.9, 0.9], [0.1, 0.9]],
|
||||
}],
|
||||
settings: { ...(space.settings || {}), fill_mode: 'glow' },
|
||||
partitions: [{ id: 'host', a: [0.25, 0.5], b: [0.75, 0.5], cm: 15 }],
|
||||
openings: [opening(type)],
|
||||
room_drafts: [], wall_columns: [],
|
||||
})),
|
||||
};
|
||||
card._cfgEpoch++;
|
||||
card._modelCache = null;
|
||||
card._physicalBodiesCache = null;
|
||||
card._lightPhysicalBodiesCache = null;
|
||||
card._lightBarrierCache = null;
|
||||
card._renderDeviceSnapshot = null;
|
||||
card._glowClipCache.clear();
|
||||
};
|
||||
|
||||
// Simulate wallBodiesGeometry() returning null after a clipping exception.
|
||||
// The remainder of the real _lightBarriers path still computes the exact
|
||||
// light-policy body set, then only its boolean geometry is made unavailable.
|
||||
const realLightBarriers = card._lightBarriers.bind(card);
|
||||
card._lightBarriers = (...args) => {
|
||||
const barriers = realLightBarriers(...args);
|
||||
if (result.windowFallbackBodyIsOpaque === undefined) {
|
||||
result.windowFallbackBodyIsOpaque = barriers.opaqueBodies
|
||||
.some((body) => pointIn(centre, body));
|
||||
}
|
||||
return { ...barriers, masonryGeometry: [] };
|
||||
};
|
||||
|
||||
replaceSpace('window');
|
||||
await update();
|
||||
const selector = `[data-glow-source="${CSS.escape(sourceEid)}"] .glow-pool`;
|
||||
result.windowSourceSuppressedOnBooleanFailure = !root().querySelector(selector);
|
||||
|
||||
replaceSpace('passage');
|
||||
await update();
|
||||
result.interiorPassageRemainsTransparent = !!root().querySelector(selector);
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
await finish(browser, checkAll(out));
|
||||
File diff suppressed because one or more lines are too long
Vendored
+2
-2
File diff suppressed because one or more lines are too long
@@ -38,6 +38,10 @@
|
||||
editor's visual snap guide keeps its physical gap across the opening
|
||||
([#132](https://github.com/Matysh/houseplan-card/issues/132),
|
||||
[#185](https://github.com/Matysh/houseplan-card/issues/185)).
|
||||
- Glow now remains fail-dark if wall boolean geometry cannot be built: a light
|
||||
source inside a partition-hosted window or exterior opening stays suppressed,
|
||||
while valid interior passages remain transparent
|
||||
([#187](https://github.com/Matysh/houseplan-card/issues/187)).
|
||||
- Collinear exterior walls now change thickness exactly at their saved
|
||||
breakpoint. After splitting a room, a 10 cm wall therefore keeps its full
|
||||
depth up to the divider without leaking onto an adjacent zero-thickness
|
||||
|
||||
@@ -47,6 +47,10 @@
|
||||
направляющая привязки по-прежнему показывает физический разрыв в месте проёма
|
||||
([#132](https://github.com/Matysh/houseplan-card/issues/132),
|
||||
[#185](https://github.com/Matysh/houseplan-card/issues/185)).
|
||||
- Glow теперь остаётся непрозрачным при сбое построения объединённой геометрии
|
||||
стен: источник света внутри окна или внешнего проёма в независимой стене
|
||||
подавляется, а корректный внутренний проход по-прежнему пропускает свет
|
||||
([#187](https://github.com/Matysh/houseplan-card/issues/187)).
|
||||
- Коллинеарные внешние стены теперь меняют толщину точно в сохранённой точке
|
||||
перехода. После разделения комнаты стена 10 см сохраняет полную толщину до
|
||||
перегородки и не продолжается по соседнему фасаду с нулевой толщиной; План,
|
||||
|
||||
+11
-7
@@ -1272,7 +1272,7 @@ class HouseplanCard extends LitElement {
|
||||
key: string;
|
||||
value: {
|
||||
occluders: LightSegment[]; floor: number[][][]; fingerprint: string;
|
||||
masonryGeometry: any;
|
||||
masonryGeometry: any; opaqueBodies: number[][][];
|
||||
};
|
||||
} | null = null;
|
||||
/** Freeze the SVG blur while a pinch/pan emits animation frames, then adopt
|
||||
@@ -14628,10 +14628,10 @@ class HouseplanCard extends LitElement {
|
||||
* The result depends only on the plan, so it is shared by every lamp.
|
||||
*/
|
||||
private _lightBarriers(
|
||||
space: SpaceModel, polys: { r: RoomCfg; poly: number[][] }[], physical: number[][][],
|
||||
space: SpaceModel, polys: { r: RoomCfg; poly: number[][] }[],
|
||||
): {
|
||||
occluders: LightSegment[]; floor: number[][][]; fingerprint: string;
|
||||
masonryGeometry: any;
|
||||
masonryGeometry: any; opaqueBodies: number[][][];
|
||||
} {
|
||||
// Gates are door-like: their different symbol must not change how light
|
||||
// crosses the clear opening.
|
||||
@@ -14737,6 +14737,11 @@ class HouseplanCard extends LitElement {
|
||||
floor: polys.map((x) => x.poly),
|
||||
fingerprint,
|
||||
masonryGeometry: masonry?.geom || [],
|
||||
// The source guard must fail dark against the same type/floor-filtered
|
||||
// bodies used to build the light masonry. The ordinary render bodies are
|
||||
// cut by every hosted opening, including opaque windows and exterior
|
||||
// doors, and therefore cannot be used when the boolean geometry is empty.
|
||||
opaqueBodies: lightPhysical,
|
||||
};
|
||||
this._lightBarrierCache = { key: cacheKey, value };
|
||||
return value;
|
||||
@@ -14754,10 +14759,9 @@ class HouseplanCard extends LitElement {
|
||||
this._forgetGlowSpace(space.id);
|
||||
return svg`` as unknown as TemplateResult;
|
||||
}
|
||||
const physical = this._physicalBodiesR(space);
|
||||
const {
|
||||
occluders, floor, fingerprint, masonryGeometry,
|
||||
} = this._lightBarriers(space, polys, physical);
|
||||
occluders, floor, fingerprint, masonryGeometry, opaqueBodies,
|
||||
} = this._lightBarriers(space, polys);
|
||||
// Resolve against the whole plan: a controller and its passive lamp may
|
||||
// legitimately live in different spaces. Ownership is filtered afterwards.
|
||||
const resolvedSources = resolvedLightSources(
|
||||
@@ -14812,7 +14816,7 @@ class HouseplanCard extends LitElement {
|
||||
// Exterior opening tunnels deliberately remain in `masonryGeometry`,
|
||||
// so placing the source there suppresses the entire pool rather than
|
||||
// lighting only the indoor half of the tunnel.
|
||||
if (pointInOpaquePlanBody(sourcePoint, masonryGeometry, physical)) {
|
||||
if (pointInOpaquePlanBody(sourcePoint, masonryGeometry, opaqueBodies)) {
|
||||
// This placement cannot produce a valid previous-frame fade: the old
|
||||
// clip belongs to a different position. Remove its transition state as
|
||||
// well, rather than leaving a timer for a DOM node no longer rendered.
|
||||
|
||||
@@ -177,6 +177,24 @@ test('the source guard combines wall masonry with partitions and columns', () =>
|
||||
assert.equal(pointInOpaquePlanBody([20, 5], masonry, [partition]), false, 'clear floor');
|
||||
});
|
||||
|
||||
test('empty boolean masonry falls back to light-policy partition bodies', () => {
|
||||
const space = {
|
||||
room_drafts: [], wall_columns: [],
|
||||
partitions: [{ id: 'host', a: [0, 0], b: [10, 0], cm: 10 }],
|
||||
};
|
||||
const hostedSlot = { hostId: 'host', a: [4, 0], b: [6, 0], depth: 2 };
|
||||
const drawnBodies = physicalBodyParts(space, 5, 1, 1e-6, [hostedSlot]).all;
|
||||
const opaqueWindowBodies = physicalBodyParts(space, 5, 1, 1e-6, []).all;
|
||||
const source = [5, 0];
|
||||
|
||||
assert.equal(pointInOpaquePlanBody(source, [], drawnBodies), false,
|
||||
'the presentation body has a slot for every hosted opening');
|
||||
assert.equal(pointInOpaquePlanBody(source, [], opaqueWindowBodies), true,
|
||||
'a window/exterior opening stays opaque when boolean masonry is unavailable');
|
||||
assert.equal(pointInOpaquePlanBody(source, [], drawnBodies), false,
|
||||
'an interior passage remains a valid transparent source position');
|
||||
});
|
||||
|
||||
test('exact column overlays are rejected but rotated square bodies remain distinct', () => {
|
||||
const square = { id: 'a', shape: 'square', center: [1, 1], cm: 30, angle: 0 };
|
||||
assert.equal(sameColumnPlacement(square, { ...square, id: 'b', angle: 90 }, 1e-9), true);
|
||||
|
||||
Reference in New Issue
Block a user