fix(led): preserve strip corners and feather the complete light field

Keep real end and corner emitters, robust decimal joins and wall-circle
sweep events. Render one positive-winding compound visibility clip so
Chromium cannot cancel or cut away overlapping light regions.

Add independent pixel oracles for glow falloff and wall-following tubes.
Replace the lossy fan-count limit with explicit cached-path bounds while
retaining the original timing and warm-cycle heap-growth limits.

Issue: #788
User-Visible: yes
This commit is contained in:
Matysh
2026-10-03 18:46:35 +03:00
parent ad10a5b099
commit c142e4f7b5
19 changed files with 881 additions and 121 deletions
+5
View File
@@ -2,6 +2,11 @@
## Unreleased
- Fixed remaining LED-strip rendering defects: clean joins on fractional and
slightly tilted contours, continuous light without cut-out wedges, and an
equally soft falloff at both ends and the outside of sharp turns
([#788](https://github.com/Matysh/houseplan-card/issues/788)).
## v1.79.0-beta.6 — 2026-10-03
- Fixed rectangular LED strips on wall faces: corners remain exact miters and
+5
View File
@@ -8,6 +8,11 @@
## Не выпущено
- Исправлены оставшиеся дефекты LED-лент: выступы на стыках с дробными
координатами и небольшим наклоном, вырезы внутри свечения и резкие края
вместо плавного затухания на концах и снаружи острых углов
([#788](https://github.com/Matysh/houseplan-card/issues/788)).
## v1.79.0-beta.6 — 2026-10-03
- Исправлены прямоугольные LED-ленты на гранях стен: углы теперь остаются
+39 -15
View File
@@ -420,18 +420,24 @@ only the geometry differs:
- **Radius.** 30 cm by default, independent of `settings.glow_radius_cm`; the
marker's personal `glow_radius_cm` wins.
- **Field.** A continuous band along every segment with round free ends: grey
luminance bands of the shared `GLOW_FALLOFF`, one piece per stretch, blended
with `lighten` inside one isolated group, so corners and the closing of a
loop neither seam nor double the brightness. Intensity and the 500 ms fade
- **Field.** One continuous stroked path with round free ends: 48 grey
luminance bands of the shared `GLOW_FALLOFF` in one mask, so corners and the
closing of a loop neither seam nor double the brightness. Intensity and the 500 ms fade
are the shared `glowAlpha` / `GLOW_FADE_MS`.
- **Visibility.** The strip is cut into consecutive pieces no longer than the
radius; each piece is clipped to the visibility fans of its own emitters
(the shared `visibilityPolygon` over the same barrier scene as pools; the
fans are separate paths of one clipPath, no boolean pass per piece), and the
whole field layer is clipped once to the floor. An unobstructed emitter uses
- **Visibility.** Classify and sample the full polyline at radius/4 or finer,
retaining every actual vertex and both ends; never thin a short final run
or an acute corner. The continuous field is clipped to the union of the
emitters' visibility fans (the shared `visibilityPolygon` over the same
barrier scene as pools), then once to the floor. Groups of five fans are
retained as compact positive-winding paths and rendered as one SVG clip
child with explicit `clip-rule=nonzero` (several compound clip children
caused Chromium raster holes despite correct geometric membership);
discs and blocked fans have the same winding, so overlap adds visibility
instead of cutting holes. An unobstructed emitter uses
an exact SVG disc; a blocked fan keeps hard obstacle edges and exact circular
arcs between them, so free ends cannot expose angular-sweep facets. Windows,
arcs between them. Before the sweep, barriers are clipped to the emitter's
radius: exact wall–circle intersections become angular events, not a chord
cutting away visible floor between two coarse rays. Windows,
columns,
thick walls and Solid zero walls block; doors/gates pass by their actual
opening; Dashed zero walls are transparent. Emitters on a thick face sit
@@ -442,9 +448,12 @@ only the geometry differs:
inherits their free-side normal. The visible stripe and its emitters
therefore stay on one straight line through the opening. At a genuine turn,
safely intersecting shifted sides use that bounded intersection as their
single miter; unsafe acute angles retain the short connector. A stored
four-corner loop consequently stays a four-corner rectangle without steps
at openings or diagonal corner inserts (#787).
single miter; a free side contributes its unshifted axis to the same join.
Numeric endpoint tails are not gaps; redundant collinear subdivisions are
removed only from the derived visible path before offsetting, never from
the saved points. Unsafe acute angles retain the short connector. A stored
four-corner loop stays four-cornered without steps or protruding hooks;
a genuinely tilted side stays tilted (#787, #788).
- **Core.** With effective Glow (space `glow_enabled` + room `glow`) the core
stays white and the colour is the field; without Glow the core takes the
source colour and there is no field. Off: white core, no field.
@@ -461,5 +470,20 @@ only the geometry differs:
- **Laziness.** The stripe/hit/2.5D code (`led-strip-runtime`) and the field
(`led-strip-field`) are separate lazy chunks; the initial graph holds only
the presence check and the loader (`led-strip-gate`). Caches are bounded per
space (50 shapes, 50 visibilities, 2500 retained fans) and released on space
change and on disconnect; a chunk that lands after disconnect applies nothing.
space (50 shapes and 50 visibility entries) and released on space change
and on disconnect; a chunk that lands after disconnect applies nothing.
The `led-strips-v1` maximum-load witness bounds the compact representation
to 2500 cached path batches and 4 Mi cached characters (at most 8 MiB UTF-16,
excluding the joined Lit/DOM clip string), with unchanged timing and 64 MiB
warm-cycle heap-growth budgets (not a total browser-memory bound).
The actual fan count remains a separate honest
diagnostic. The former 2500-fan acceptance bound depended on dropping
required vertices/endpoints and was incompatible with the 50×50-point
contract; compaction now reduces object/DOM overhead, not geometric detail.
`smoke_led_strip_field` compares rasterised production-field pixels with an
independent distance/falloff oracle across three radii, both path directions
and three raster densities: free ends, acute/reflected turns, decimal loops,
self-crossings and an opaque wall. Unit tests pin joins, circle events and
doorway emitter normals. The original household export is checked locally,
not stored as a public fixture.
+1
View File
@@ -29,6 +29,7 @@ Everything computable from the tree and git; regenerate, never edit by hand
|---|---|
| Current local cycle | **Beta v1.79.0-beta.6 candidate** — two integrated S8 issues since beta.5. LED-strip Glow has smooth free end caps, while rectangular strips keep clean corners and remain straight across doors, gates and passages (#786, #787). `main` remains on stable v1.78.0. |
| Branches | `main` carries stable releases only; pre-release tags point at `dev`. Work lands on `dev`, which is equal to or ahead of `main`, never behind. |
| LED regression acceptance | #788 covers mixed face/free and fractional-coordinate joins, winding-safe field unions, retained endpoints/acute vertices and circle–wall transition events. The actual owner export is exercised locally; public fixtures are synthetic. Pixel oracles, not just SVG counts, protect the visible falloff. |
| Warm remount | Delayed editor restoration preserves the original View camera, reserves pending chrome space, and yields synchronously to mode/space navigation (#762). Header/stage dimensions are published as one settled pair. Canonical: `docs/WARM-REMOUNT.md`. |
| 2.5D View | Public since #649: the installation-wide General settings switch `settings.volumetric_view` (Display). Flat stays the default and byte-for-byte unchanged; editors and `houseplan-space-card` stay Flat. Canonical: `docs/ISOMETRIC.md`. |
| Input support | Owner's rule since 2026-08-08: View and kiosk are fully supported and release-blocking on touch; the three editors are desktop-first, touch editing is best effort. Canonical: `docs/TOUCH-SUPPORT.md`. |
@@ -14,11 +14,11 @@ to prove that the Node witness actually kills it.
| --- | ---: | --- |
| Performance threshold | 4 | The witness measures real browser wall-time or frame work; a pure assertion cannot prove the budget. |
| Browser harness integrity | 4 | The mutation breaks page-error, round-trip or page-registration observation in the browser harness itself. |
| Paint, cascade and layer composition | 32 | The invariant depends on computed CSS, SVG paint, clipping, stacking or pixels produced by Chromium. |
| Paint, cascade and layer composition | 36 | The invariant depends on computed CSS, SVG paint, clipping, stacking or pixels produced by Chromium. |
| Pointer geometry and trusted interaction | 47 | The invariant depends on hit testing, pointer capture, touch/keyboard dispatch or live DOM geometry. |
| Responsive DOM layout | 38 | The invariant depends on measured element boxes, responsive breakpoints, native/HA dialog shells or focusable target size. |
| Custom-element and HA browser lifecycle | 101 | The invariant crosses Lit/custom-element lifecycle, browser storage/events, lazy loading or a complete HA-card state transition. |
| **Total** | **226 / 200** | Above the guideline `mutation-gate --check` warns rather than fails (#699); each guard above it is held by its own reason in this inventory and its `because`. |
| **Total** | **230 / 200** | Above the guideline `mutation-gate --check` warns rather than fails (#699); each guard above it is held by its own reason in this inventory and its `because`. |
## Measured effect
@@ -93,6 +93,10 @@ The invariant depends on computed CSS, SVG paint, clipping, stacking or pixels p
- `iso-theme-dark-wall-rule-returns`
- `led-badge-dropped`
- `led-core-coloured-under-glow`
- `led-field-endpoint-dropped`
- `led-field-disc-cancels-fan`
- `led-field-compound-clip-children`
- `led-field-circle-events-missing`
- `led-icon-not-suppressed`
- `led-source-stays-round-at-anchor`
- `led-static-live-ignored`