feat: make Glow follow door opening state (#20)

Issue: #20
User-Visible: yes
This commit is contained in:
Matysh
2026-08-28 22:33:07 +03:00
parent 72e6939f67
commit f62926fbeb
41 changed files with 689 additions and 390 deletions
+11 -8
View File
@@ -1259,10 +1259,11 @@ its configured space.
`_lightBarriers` collects everything opaque: the wall bodies exactly as the
plan draws them (`wallBodiesGeometry`, real thickness, mitred junctions),
every independent body (partition, column, draft), and the bare outline of
any edge that carries no thickness. It cuts out the exceptions: doorways
and gates — cut through the masonry, so an opening is a real gap
between two jamb faces and a beam is narrowed by the returns of a thick wall
— plus dashed zero-thickness walls. Solid zero-thickness walls instead add
any edge that carries no thickness. It cuts out the exceptions: interior
doorways and gates according to their resolved live opening amount — a
closed bound opening keeps the masonry, while a positional cover cuts a
centre-aligned fraction between the jambs — and saved passages, which remain
fully open, plus dashed zero-thickness walls. Solid zero-thickness walls instead add
their exact axes as zero-area barriers. Windows stay
solid, so an indoor lamp never washes the street; the light's masonry is cut
by passages only and therefore differs on purpose from the drawn one. So does
@@ -1289,10 +1290,12 @@ its configured space.
open-zone graph and no shadow mask left in the light path.
Barriers are cached per space by a fingerprint of their complete geometry
(every body point, both wall endpoints and scale inputs), never by
`_cfgEpoch`: the epoch lags behind geometry edited in place, and a stale
barrier set is invisible — the plan simply keeps lighting through a wall that
now exists. The same fingerprint keys the per-source region cache. A cached per-
(every body point, both wall endpoints and scale inputs) plus a sorted
signature of bound interior door/gate opening amounts, never by `_cfgEpoch`:
the epoch lags behind geometry edited in place, and a stale barrier set is
invisible — the plan simply keeps lighting through a wall or closed door that
now exists. Unrelated HA updates retain the same signature and reuse the
barrier set. The combined fingerprint keys the per-source region cache. A cached per-
`Document` raster probe verifies actual SVG screen pixels rather than trusting
CSS syntax support; pending/unsupported/error/timeout states render with
deterministic normal blending and a successful probe requests one update.
+6
View File
@@ -8,6 +8,12 @@
first frame was also the last one
([#358](https://github.com/Matysh/houseplan-card/issues/358)).
- Glow now follows the live state of bound interior doors and gates: a closed
opening blocks light, an open one passes it, and a positional cover exposes
the matching fraction of the aperture. Passages remain open, windows and
exterior openings remain opaque, and unrelated Home Assistant updates reuse
the existing light geometry ([#20](https://github.com/Matysh/houseplan-card/issues/20)).
- Tapping a device on a freshly opened page works again without first
visiting any editor screen. A wall switch controlling virtual light sources
(and every other tap-to-toggle device) silently ignored the tap on a cold
+7
View File
@@ -14,6 +14,13 @@
загрузила, — первый кадр оказывался последним
([#358](https://github.com/Matysh/houseplan-card/issues/358)).
- Glow теперь учитывает текущее состояние привязанных внутренних дверей и
ворот: закрытый проём останавливает свет, открытый пропускает его, а cover с
позицией открывает соответствующую долю прохода. Открытые проходы остаются
прозрачными, окна и наружные проёмы — непрозрачными; посторонние обновления
Home Assistant переиспользуют готовую геометрию света
([#20](https://github.com/Matysh/houseplan-card/issues/20)).
- Нажатие на устройство на только что открытой странице снова работает без
предварительного захода в какой-либо редактор. Выключатель, управляющий
виртуальными источниками света (и любое другое устройство с тап-переключением),
+23 -9
View File
@@ -20,7 +20,8 @@ all. None of those states is expressible now.
## What stops light, and what does not
`_lightBarriers()` (`src/houseplan-card.ts`) builds the barrier set once per
plan geometry and shares it between every lamp in the space.
plan geometry and relevant opening-state signature, then shares it between
every lamp in the space.
**Opaque**
@@ -34,8 +35,11 @@ plan geometry and shares it between every lamp in the space.
**Transparent**
- doorways, gates and saved `passage` openings — cut through the masonry, so an opening is a real
gap between two jamb faces and a thick wall's returns narrow the beam;
- doorways and gates to floor on both sides, but only by their resolved opening
amount. An unbound opening is fully transparent, a bound closed one is opaque,
and a positional cover cuts a centre-aligned fraction of the full aperture;
- saved `passage` openings — always cut through the masonry, so an opening is a
real gap between two jamb faces and a thick wall's returns narrow the beam;
- every zero-thickness wall when its space uses the **Dashed** style.
The setting is intentionally one semantic switch, not just paint. Missing or
@@ -52,7 +56,12 @@ input is only a v8 read projection and migrates to ordinary `cm:0` atoms.
sides are floor; otherwise a front door glows halfway — up to the centreline,
where the room polygon ends — and the plan shows a lit doorway to nowhere.
The classifier is an explicit `door | gate | passage` allowlist. Any unknown
The classifier is an explicit `door | gate | passage` allowlist. Door and gate
state is resolved by the same `openingAmount()` used for their visible symbol:
binary contacts yield zero or full aperture, finite `current_position` yields
`clamp(position / 100)`, and `invert` mirrors a known amount. Missing,
disabled, `unknown` or `unavailable` contacts preserve the static-plan fallback
of a fully open door/gate rather than manufacturing a closure. Any unknown
future opening type remains opaque until its physical semantics are reviewed;
it never inherits transparency merely by not being a window.
@@ -129,11 +138,13 @@ pass over the layer costs a fifth of one blurred mask per source.
## Caching
Barriers are keyed by a fingerprint of their own geometry, never by
`_cfgEpoch`: the epoch lags behind geometry edited in place, and a stale barrier
set is invisible — the plan simply keeps lighting through a wall that now
exists. The same fingerprint, plus position and radius, keys the per-source
region cache (`_glowClipCache`).
Barriers are keyed by their geometry fingerprint plus a compact, sorted
signature of bound interior door/gate opening amounts, never by `_cfgEpoch`:
the epoch lags behind geometry edited in place, and a stale barrier set is
invisible — the plan simply keeps lighting through a wall or closed door that
now exists. Unrelated HA updates preserve the signature and hit the bounded
barrier cache. The combined fingerprint, plus source position and radius, keys
the per-source region cache (`_glowClipCache`).
The masonry boolean receives room walls after passage cuts plus the cached
joined independent body set. Its outer/hole rings are the authoritative
@@ -216,6 +227,9 @@ The complete UI and runtime truth table lives in
exterior opening masonry, wall bodies, partitions and columns are fail-dark,
while a real interior passage remains a valid source position; the rendered
Glow path is pinned to that shared guard.
- `test/logic.test.mjs` and `test/physical-geometry.test.mjs` — the shared
opening amount, stable state signature and centre-preserving partial cut for
contour and independent walls.
- `demo/smoke_glow.mjs` — pixels on a rendered plan: the aperture itself is lit,
the floor behind a door is lit, the visible beam is no wider than twice the
opening, there is no light behind a column while the floor beside it is lit,
Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 KiB

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 343 KiB

After

Width:  |  Height:  |  Size: 336 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 334 KiB

After

Width:  |  Height:  |  Size: 327 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 319 KiB

After

Width:  |  Height:  |  Size: 289 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 317 KiB

After

Width:  |  Height:  |  Size: 291 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 296 KiB

After

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

After

Width:  |  Height:  |  Size: 134 KiB

+21 -21
View File
@@ -3,7 +3,7 @@
"fixture": "synthetic-only",
"chromium": "151.0.7922.34",
"oxipng": null,
"sourceFingerprint": "2bb218477956888a84706e4a8a76248b823d70dc26fc80bcc5272c30e4556a38",
"sourceFingerprint": "0896c482848c70ed7071b4e2396294fd83072352403a859447b53fc3c10cc6ae",
"captureScriptSha256": "fe76c8e2e27501e9a377d7aa15dd6cba6a018d55c07deb602dbee097f0a0a5ba",
"command": "npm run build && node demo/docs/capture.mjs",
"scenarios": {
@@ -15,8 +15,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "2bb218477956888a84706e4a8a76248b823d70dc26fc80bcc5272c30e4556a38",
"imageSha256": "78d328498bd17a9ee1ec44b389d244b61fca9ac52c9aad6631aa85c53233fec0"
"sourceSha256": "0896c482848c70ed7071b4e2396294fd83072352403a859447b53fc3c10cc6ae",
"imageSha256": "36223106c073f07d8cc3ecf8eaab37192ebb2687daba65c5c21047d0b7890de0"
},
"view-touch": {
"file": "02-view-touch.png",
@@ -26,8 +26,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "2bb218477956888a84706e4a8a76248b823d70dc26fc80bcc5272c30e4556a38",
"imageSha256": "3be239a5a494f6776540a9c0049694ee6f9d2ba927c8bcb4b26ee5a69a228a7d"
"sourceSha256": "0896c482848c70ed7071b4e2396294fd83072352403a859447b53fc3c10cc6ae",
"imageSha256": "a4aa3af48578ee41594a9b1a6e98b4d39f3324a949a9fd9e63d5854576d5db85"
},
"space-create": {
"file": "03-space-create.png",
@@ -37,8 +37,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "2bb218477956888a84706e4a8a76248b823d70dc26fc80bcc5272c30e4556a38",
"imageSha256": "a55b463055a565892ca139014e314a8f63fb5301c87fff2e48d7e69210fe9b6c"
"sourceSha256": "0896c482848c70ed7071b4e2396294fd83072352403a859447b53fc3c10cc6ae",
"imageSha256": "0dc024726327f60f4a9ceaf3044381691f81f1090af81d1812e870f22d9343ba"
},
"room-contour-close": {
"file": "04-room-contour-close.png",
@@ -48,8 +48,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "2bb218477956888a84706e4a8a76248b823d70dc26fc80bcc5272c30e4556a38",
"imageSha256": "dfad9a27371cfa397a3fd8dce7005f42d24649743f5322105ee894067d3a9f16"
"sourceSha256": "0896c482848c70ed7071b4e2396294fd83072352403a859447b53fc3c10cc6ae",
"imageSha256": "44cfaa95bd51e2cd628400e28db0ad8b2f0cd904385845bd402494f3f5c0d93c"
},
"plan-context-tray": {
"file": "05-plan-context-tray.png",
@@ -59,8 +59,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "2bb218477956888a84706e4a8a76248b823d70dc26fc80bcc5272c30e4556a38",
"imageSha256": "b75f348c632296da5c2fc142d53df613f757e1516acac315be50f5b4557729cc"
"sourceSha256": "0896c482848c70ed7071b4e2396294fd83072352403a859447b53fc3c10cc6ae",
"imageSha256": "e0662429b423fb74151b583dcc2c8635b001b637d03bbf7a6b16aec46399c3f8"
},
"device-editor": {
"file": "06-device-editor.png",
@@ -70,8 +70,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "2bb218477956888a84706e4a8a76248b823d70dc26fc80bcc5272c30e4556a38",
"imageSha256": "6b9cd281450e7782cff292f11efc58d227bedd0df86afc3be987feb2edec9dc3"
"sourceSha256": "0896c482848c70ed7071b4e2396294fd83072352403a859447b53fc3c10cc6ae",
"imageSha256": "d0ffd31ce80bfde21ab75da356a5fc1af38246f2b301030880320620c228d89d"
},
"device-display-preview": {
"file": "06-device-display-preview.png",
@@ -81,8 +81,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "2bb218477956888a84706e4a8a76248b823d70dc26fc80bcc5272c30e4556a38",
"imageSha256": "6f628abe6b89d7c4b352e6d73ac0b126526f6fdba5927bb774c5b884e484ca19"
"sourceSha256": "0896c482848c70ed7071b4e2396294fd83072352403a859447b53fc3c10cc6ae",
"imageSha256": "2cdabae1f89c3286e4fac0ce30f757ee1690b707ab8a5488748b7cd420626160"
},
"background-editor": {
"file": "07-background-editor.png",
@@ -92,8 +92,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "2bb218477956888a84706e4a8a76248b823d70dc26fc80bcc5272c30e4556a38",
"imageSha256": "a1d1a905f1410f73231d2ea5d1dfbd8ae79c2fe8b0a79cff87ebb977ac7a3103"
"sourceSha256": "0896c482848c70ed7071b4e2396294fd83072352403a859447b53fc3c10cc6ae",
"imageSha256": "30147bb00a90eea7136b4cee30995f6e6a9217b5132f3e8d3ad7471413b1af8a"
},
"room-card": {
"file": "08-room-card.png",
@@ -103,8 +103,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "2bb218477956888a84706e4a8a76248b823d70dc26fc80bcc5272c30e4556a38",
"imageSha256": "48a5685039a5e0de2a28190d857b193c86e2e63606e3c7a8c40fc9d2b190da4e"
"sourceSha256": "0896c482848c70ed7071b4e2396294fd83072352403a859447b53fc3c10cc6ae",
"imageSha256": "029a3e69ec647a8a370d99e6bb7f9225833c526739076022f6b52ba54bff30ea"
},
"device-info": {
"file": "09-device-info.png",
@@ -114,8 +114,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "2bb218477956888a84706e4a8a76248b823d70dc26fc80bcc5272c30e4556a38",
"imageSha256": "29a342811aaedaebc54ba337792d60cb454a7cf4c3063b210f6c4bafe0a4d87b"
"sourceSha256": "0896c482848c70ed7071b4e2396294fd83072352403a859447b53fc3c10cc6ae",
"imageSha256": "dd492f53150b7149085daada5cce9eeae9bde9e7ea1d86679a54b3041f72f517"
}
}
}