feat: LED strips — editor notes, docs, demo, smokes, golden scenes, profile (#780)

Stage 5 of #780.

- Import summary: «Strips left unbound after import: {n}» from the backend
  `unbound_led_strips` count; «Optimize plans» reports strips passing
  through walls per space and edits none (AC16).
- Linear field for long strips (ТЗ §13.2): pieces of at most the radius
  along the polyline, emitters thinned to r/4, each piece clipped to the
  visibility fans of its own emitters as separate clipPath children (no
  boolean pass per piece), one floor clip for the whole layer, no fan at
  all where nothing blocks within the radius; a grid index of body faces
  and boxed inside tests; unchanged fields skip re-diffing. 50×50 on the
  large house: first stable frame ~1.4 s, warm space ~1.1 s locally.
- led-strips-v1 profile: demo/benchmark_led_strips.mjs with the derived
  large-house fixture (10×5, 50×50, none), absolute limits of the ТЗ table
  in demo/performance/budgets-led-strips.json, exact counters (zero
  recomputes on HA ticks/camera/colour, ≤50 cache entries, no growth over
  20 cycles); added to the full performance workflow.
- Bundle: LAZY_LED_GZIP_CEILING 10 KiB, LAZY_LED_EDITOR_GZIP_CEILING 11 KiB
  (measured + 10 %, rounded up); overlaps with the initial and editor
  graphs refused; the lazy editor graph stays inside its ceiling.
- Smokes smoke_led_strip_draw/bind/glow, linked in smoke-links; 13 mutants
  in the registry (7 browser guards in the inventory); config field registry
  entry `spaces[].led_strips`.
- Golden: five new scenes on the `golden-led` space of the visual fixture
  (`ledStrips` option, the designer's four strips on #868D94), matrix v71.
- Docs: LIGHT, DEVICE-PRESENTATION, USER-GUIDE (en/ru), UX-MODES,
  ARCHITECTURE, ISOMETRIC, CONFIG-COMPATIBILITY, TOUCH-SUPPORT, demo/stand
  README, performance README; docs/design/led-strips with the unchanged
  designer archive, two paired frames and ACCEPTANCE.md; both changelogs.

Issue: #780
User-Visible: yes
This commit is contained in:
Codex
2026-10-02 13:35:11 +03:00
parent 1511996c5e
commit 9825819cd4
64 changed files with 2075 additions and 134 deletions
+72
View File
@@ -0,0 +1,72 @@
# #780 · LED strips — acceptance against the designer frames
AC8/AC18 of [#780](https://github.com/Matysh/houseplan-card/issues/780). The
product reproduces the designer's four strips — a free straight strip (blue),
a strip on the inner face of the bottom wall (violet), a polyline along the
wall faces around a corner (yellow) and a closed rectangle (green) — on a
synthetic plan with the reference floor `#868D94`. Scene colours
`#80D5FF`, `#E680FF`, `#FFEA80`, `#58FF58` are values of the four sources, not
a product palette.
## How the frames were made
- **Designer** — `source/previews/Led-On.png`, `source/previews/Led-Off.png`
(2543 × 1572, unchanged).
- **Product** — golden scenes `led-strip-design-reference-on-light` and
`led-strip-design-reference-off-light` (`demo/golden/matrix.mjs`, fixture
`makeVisualMatrixFixture({ ledStrips: true })` in
`demo/fixtures/visual-matrix.mjs`: viewport 1000 × 760, DPR 1, English, light
theme, `icon_size` 3.4, 20 cm walls, cell 5 cm). Captured with
`node demo/golden/run.mjs --mode=capture --scenario=<id>` on the built bundle;
the reviewed baselines are accepted from the Linux CI artifact of the task
(label `ci:golden`).
The plans differ (the designer frame is a fragment of a real floor with
furniture and icons), so the comparison is of the visual language and the four
behaviours, not of positions.
## On
![Designer Led On (left) and product (right)](pairs/pair-on.png)
## Off
![Designer Led Off (left) and product (right)](pairs/pair-off.png)
## ТЗ §3 — visual contract
| Requirement | Product | Evidence |
|---|---|---|
| `D = icon_size/100 × iconUnit(space)`, not `marker.size`; 2.5D uses the shared scale | `ledFrame` takes `iconPct` from the full card's or the space card's own `icon_size`; `ISO_ICON_SCALE` in 2.5D | golden `iso-led-strip-dark`, reference pair |
| Thickness 0.08 D off / 0.12 D on; outline `#383838` t, core t/2, round joins and caps | `renderLedStripes` | `led-strip-off-light`, reference pair |
| Off: white core, no field, both themes | stripe state `off` | smoke `smoke_led_strip_glow.mjs`, `led-strip-off-light` |
| On with Glow: white core + coloured field; without Glow: core in the source colour, no field | `ledStripView` + `resolveGlowAppearance` | smoke `smoke_led_strip_glow.mjs` |
| Glow is the space/room switch, independent of `fill_mode` | `glowFor(room)` | `lighting-led-strip-glow-dark` uses `fill_mode: none` |
| Per-piece offset: t/2 on a thick face into free floor, 0 on free floor and zero walls; continuous transition | `visibleStripPath` | unit `test/led-strip-geometry.test.mjs` (AC8) |
| Field 50 cm by default, own `glow_radius_cm` wins; round free ends; no seams or doubled brightness at corners/closure | `ledFrame`, `led-strip-field` (`lighten` in one group) | unit `test/led-strip-runtime.test.mjs`; reference pair |
| Shared `glowAlpha` / `GLOW_FALLOFF` / `GLOW_FADE_MS` | field bands from `falloffAt` | unit `test/led-strip-runtime.test.mjs` |
| Field under icons, badges and labels; icons not tinted | glow layer below the device layer | reference pair (designer tinting deliberately not reproduced) |
## Designer §11 — ten visual criteria
| # | Criterion | Result |
|---|---|---|
| 1 | Same geometry on/off; only colour/thickness/field change | Stored points never change; only `t` and the derived offset follow the state — pairs on/off |
| 2 | White core and dark outline keep contrast on grey floor, near hatched walls, over the field | Visible in both pairs; `#383838` outline, opaque core |
| 3 | Straight parts are not a chain of circles | One stroked path per strip; the field is bands of stroked paths, not discs |
| 4 | No hard rectangular cut of light at the ends | Round caps of the field bands (blue strip in the pair) |
| 5 | No dark gaps or bright spots at corners | Round joins; `lighten` blend of pieces (yellow corner, green loop) |
| 6 | A wall strip lights into the room, not behind the wall | Visibility clip from emitters `epsilonGeom` outward (violet, yellow) |
| 7 | A closed rectangle gives a continuous field along the perimeter | Green loop in the On pair |
| 8 | Colour changes with the entity without changing shape/radius/edges | Colour from `resolveGlowAppearance`; the stored points and the radius do not depend on state (four colours of one fixture in the On pair) |
| 9 | When off the field fades smoothly and the thin stripe stays | Shared 500 ms fade; Off pair keeps the stripe |
| 10 | Zoom keeps the proportions of thickness, outline, hit area and 2.5D lift | All sizes in plan units of D; hit radius `max(22 px, t/2)` in screen px (unit AC12) |
## Accepted differences
- Pixel sizes and the mockup blur (22.2–30 px) are not product filters: the
product uses 0.08/0.12 D and the shared falloff, so its stripe is thinner at
the default `icon_size` and the band edge is the shared Glow edge.
- Icons and labels are not tinted by the field.
- The mockup's "half the shared radius" and "always coloured core" are replaced
by 50 cm and the white core under Glow (owner's decision in #780).
+22
View File
@@ -0,0 +1,22 @@
# LED strips — designer materials (#780)
The designer's archive for the LED strips, kept **unchanged** under
[`source/`](source/): `Issue-662-LED-light-spec-2026-10-01.zip` attached to
[#662 (comment)](https://github.com/Matysh/houseplan-card/issues/662#issuecomment-5935038397)
(archive SHA-256 of every member in [`source/MANIFEST-SHA256.txt`](source/MANIFEST-SHA256.txt);
all six checksums verified when the files were copied). The Figma section is
[Led Light, 626:34](https://www.figma.com/design/cpGN8MhJGydwVOUzZv8dia/House-plan?node-id=626-34)
with the frames Led On (626:35) and Led Off (626:53).
**The contract is the ТЗ in the body of
[#780](https://github.com/Matysh/houseplan-card/issues/780)**, not the archived
`source/TZ-issue-662-LED-strips.md`. Where they differ the issue wins — notably
the 50 cm default radius, the white core under Glow, editing in the Devices
editor and the product thicknesses 0.08/0.12 D instead of the mockup's pixel
sizes and blur. These files are documentation only; nothing here reaches the
runtime bundle.
- [`ACCEPTANCE.md`](ACCEPTANCE.md) — the two paired frames and the acceptance
table against §3 of the ТЗ and the ten visual criteria of the designer's §11.
- [`pairs/`](pairs/) — designer frame (left) next to the product scene (right),
256-colour diagnostic copies.
Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

@@ -0,0 +1,3 @@
[InternetShortcut]
URL=https://www.figma.com/design/cpGN8MhJGydwVOUzZv8dia/House-plan?node-id=626-34
@@ -0,0 +1,3 @@
[InternetShortcut]
URL=https://github.com/Matysh/houseplan-card/issues/662
@@ -0,0 +1,6 @@
A0E61CCF1416CE8F92332CD6B855C8131628E32C27ADDD77EDBEE3A6A2B3F37A README.md
1A98198905F147E3D1B8720F45481B6D5295FBDB94F4AD407F92FAC044B7A1DA TZ-issue-662-LED-strips.md
7AD21EE0042384443B2E41AE337ED31416127E183A72765EBB493AD808FCB651 FIGMA-LINK.url
16317687BA78439DA6052B384825E5FEF480A71EB3F46FCDDC4B180EDD09760C ISSUE-LINK.url
EBAC64901AAA055DD74A2D04E2EE18093AA59FE65E152CB699BECD8C5C9A82FB previews/Led-On.png
8DCDD5F2FE784EE9C9DAB98D092A89457CA171554481586927E23E0DF7FFC18D previews/Led-Off.png
+22
View File
@@ -0,0 +1,22 @@
# Issue #662 — LED-ленты
Архив содержит техническое задание и визуальные референсы для задачи [Matysh/houseplan-card#662](https://github.com/Matysh/houseplan-card/issues/662).
## Состав
- [`TZ-issue-662-LED-strips.md`](./TZ-issue-662-LED-strips.md) — сводное ТЗ: поведение из issue и визуальные требования из Figma.
- [`previews/Led-Off.png`](./previews/Led-Off.png) — четыре примера выключенных LED-лент.
- [`previews/Led-On.png`](./previews/Led-On.png) — четыре примера включённых LED-лент и рассеивания света.
- [`FIGMA-LINK.url`](./FIGMA-LINK.url) — ссылка на исходный макет.
- [`ISSUE-LINK.url`](./ISSUE-LINK.url) — ссылка на исходную задачу.
- [`MANIFEST-SHA256.txt`](./MANIFEST-SHA256.txt) — контрольные суммы файлов архива.
## Основные ссылки
- Issue: <https://github.com/Matysh/houseplan-card/issues/662>
- Figma, секция `Led Light`: <https://www.figma.com/design/cpGN8MhJGydwVOUzZv8dia/House-plan?node-id=626-34>
- Фрейм `Led On`: <https://www.figma.com/design/cpGN8MhJGydwVOUzZv8dia/House-plan?node-id=626-35>
- Фрейм `Led Off`: <https://www.figma.com/design/cpGN8MhJGydwVOUzZv8dia/House-plan?node-id=626-53>
Дата подготовки: 1 октября 2026 года.
@@ -0,0 +1,210 @@
# Техническое задание: LED-ленты на плане
Связанная задача: [Matysh/houseplan-card#662](https://github.com/Matysh/houseplan-card/issues/662)
Визуальный макет: [Figma — `Led Light`](https://www.figma.com/design/cpGN8MhJGydwVOUzZv8dia/House-plan?node-id=626-34)
Фреймы: [`Led On`](https://www.figma.com/design/cpGN8MhJGydwVOUzZv8dia/House-plan?node-id=626-35) и [`Led Off`](https://www.figma.com/design/cpGN8MhJGydwVOUzZv8dia/House-plan?node-id=626-53)
Дата фиксации: 01.10.2026
## 1. Назначение
Добавить в House Plan протяжённое представление устройств `light.*`: LED-лента отображается не точечным значком, а ломаной линией в реальном месте установки. Пользователь должен сразу понимать положение, форму, состояние и цвет ленты, а также включать и выключать её нажатием по любой части линии.
Поведенческий контракт определяется issue #662. Figma определяет визуальное направление, форму выключенной полосы, характер свечения, работу света у стен, в углах и на свободном участке пола. При возможном расхождении логика и размеры в единицах плана берутся из issue, внешний вид — из Figma.
## 2. Визуальные референсы
### 2.1. Выключенное состояние
![Led Off — четыре выключенные ленты](./previews/Led-Off.png)
Во фрейме `Led Off` показаны четыре варианта геометрии и размещения:
1. прямая лента на свободном участке плана;
2. прямая лента вплотную к стене;
3. Г-образная лента с поворотом в углу;
4. замкнутая прямоугольная лента по периметру зоны.
Требования к выключенной ленте:
- световое пятно отсутствует;
- ядро белое `#FFFFFF`;
- внешняя обводка тёмная `#383838`;
- концы и стыки визуально скруглены;
- линия остаётся различимой на светлом и тёмном фоне плана;
- в исходном Figma-фрейме номинальная высота белого ядра — 6 px, внешняя обводка — 3 px, радиус скругления — 5 px;
- в продукте итоговая толщина масштабируется с планом и подчиняется C6 issue: `0,08 D` для выключенного состояния, где `D` — диаметр значка маркера.
### 2.2. Включённое состояние
![Led On — четыре включённые ленты](./previews/Led-On.png)
Во фрейме `Led On` показаны четыре сценария:
1. синяя прямая лента на расстоянии от стены — свет расходится по обе стороны;
2. фиолетовая лента вдоль нижней стены — свет направлен в комнату;
3. жёлтая Г-образная лента вдоль верхней и правой стен — свет корректно продолжается через угол и ограничивается стенами;
4. зелёная лента по периметру помещения — свет соединяется на углах без разрывов и не выходит за стены.
Требования к включённой ленте:
- сама полоса остаётся читаемой поверх свечения;
- цвет свечения поступает от устройства: live RGB/цветовая температура либо `glow_color`;
- показанные в макете синий, фиолетовый, жёлтый и зелёный цвета являются примерами, а не фиксированной палитрой;
- свет распространяется непрерывно вдоль каждого сегмента;
- на свободных концах должен быть мягкий округлый спад без прямоугольного обрыва;
- в точках поворота световые поля сегментов соединяются без тёмного шва и без заметного удвоения яркости;
- свет, направленный к толстой стене, обрезается стеной; лента на грани стены освещает только сторону комнаты;
- в продукте толщина включённой линии — `0,12 D`; свечение не входит в эту толщину;
- появление и исчезновение поля света происходит через существующий `GLOW_FADE_MS`.
## 3. Измеренные параметры макета Figma
Параметры ниже документируют исходник 2543×1572 px и нужны для визуального сравнения. Они не заменяют масштабируемые величины `D` из issue.
| Элемент | Параметр в Figma |
|---|---|
| Ядро выключенной полосы | `#FFFFFF`, высота 6 px |
| Обводка | `#383838`, 3 px, Outside |
| Скругление | 5 px |
| Синий пример | `#80D5FF` → прозрачный |
| Фиолетовый пример | `#E680FF` → прозрачный |
| Жёлтый пример | `#FFEA80` → прозрачный |
| Зелёный пример | `#58FF58`, мягкие внутренние и внешние тени |
| Линейное поле | ориентировочная глубина 86–102 px в исходном фрейме |
| Радиальные окончания | диаметр 188–206 px |
| Blur радиальных окончаний | 22,2 px |
| Blur протяжённого поля | до 30 px в примерах |
В рабочем рендерере мягкость должна строиться градиентами по `GLOW_FALLOFF`; CSS/SVG `filter` не является обязательной частью реализации и не должен ухудшать производительность.
## 4. Модель данных
В каждом пространстве допускается необязательный массив:
```ts
space.led_strips: {
id: string;
points: number[][];
marker: string | null;
}[];
```
- `points` содержит от 2 до 50 точек в канонических координатах пространства;
- не более 50 лент в одном пространстве;
- `id` уникален в пространстве;
- один маркер может быть привязан не более чем к одной ленте во всех пространствах;
- версия модели не меняется, миграция не требуется;
- полный и per-space экспорт/импорт сохраняют геометрию; отсутствующий при импорте маркер превращает ленту в непривязанную.
Якорь ленты — точка на половине суммарной длины ломаной. Он используется для определения комнаты, подписи, бейджа, подсказки и агрегатов.
## 5. Рисование и редактирование
- В редакторе плана после инструмента «Перегородка» появляется кнопка `LED-лента` с иконкой `mdi:led-strip-variant`.
- Лента рисуется цепочкой точек по контракту инструмента стен.
- Клик или тап добавляет вершину; Shift ограничивает направление шагом 45°; Ctrl+Z удаляет последнюю добавленную точку.
- Завершение: Esc, двойной клик по последней точке, смена инструмента или выход из редактора.
- Цепочка короче двух точек отбрасывается.
- Панорамирование, pinch, второй палец и `pointercancel` не добавляют точки.
- Точки прилипают к сетке и к физическим граням стен с порогом магнита мебели.
- В v1 можно перетаскивать вершины и удалить ленту целиком. Вставка и удаление отдельной вершины не входят в задачу.
## 6. Стены и проёмы
- Лента не может пересекать тело стены с толщиной.
- При попытке пересечения новый сегмент останавливается на ближайшей грани тела стены.
- Касание стены и движение точно вдоль её грани допустимы.
- Через `door`, `gate` и `passage` лента проходит по геометрическому проёму независимо от текущего состояния двери.
- Окно остаётся частью тела стены и блокирует ленту.
- Стены нулевой толщины, Solid и Dashed, не являются телом и не блокируют рисование.
- Перетаскивание вершины проверяет оба соседних сегмента по тем же правилам.
- Если стена была утолщена или добавлена позже, геометрия ленты автоматически не исправляется; Optimize сообщает число проблемных лент.
## 7. Привязка устройства
- После завершения ломаной открывается существующий диалог добавления устройства; сущности `light.*` показываются первыми.
- Сохранение создаёт или переиспользует live-маркер и связывает его с лентой атомарно.
- Закрытие или «Позже» сохраняет непривязанную ленту.
- Непривязанная лента отображается серым пунктиром только в редакторах и не видна во View, киоске и space-card.
- После привязки обычный значок маркера исчезает, но его `layout` сохраняется.
- В лотке выбранной ленты доступны: привязать, сменить устройство, отвязать и удалить ленту.
- После отвязки или удаления маркер снова отображается значком на сохранённой позиции либо в авторасстановке.
## 8. Представление и состояния
| Состояние | Представление |
|---|---|
| On | линия `0,12 D`, полный цвет `resolveGlowAppearance`, поле света при активной заливке «Свечение источников света» |
| Off | линия `0,08 D`, белое/приглушённое ядро с тёмной обводкой по визуальному референсу, без поля света |
| Unavailable | серый пунктир, без поля света |
| Hidden | не отображается |
| Unbound | серый пунктир только в редакторах |
- Линия строится SVG `path` со скруглёнными концами и соединениями.
- Подпись и `value_badge` располагаются относительно якоря.
- Hover, long press, контекстное меню и клавиатурный фокус работают как у обычного маркера.
- Enter и Space запускают тот же `_clickDevice()`.
- У ленты нет пульсации и тревожного красного состояния.
## 9. Свечение
- Световой источник линейный: по сегментам строятся капсулы радиуса `glow_radius_cm`, а при отсутствии персонального значения — половины общего радиуса.
- Точки выборки: все вершины плюс промежуточные точки с шагом около 100 см, не более восьми точек на ленту.
- Капсулы одной ленты смешиваются через `lighten` внутри изолированной группы; с остальными пятнами группа смешивается через `screen`.
- Поле пересекается с полом и с объединением полигонов видимости; за стеной свет отсутствует.
- Для ленты на грани толстой стены точки выборки сдвигаются на эпсилон в сторону комнаты.
- Лента на стене нулевой толщины освещает обе стороны.
- Точка выборки внутри тела стены не создаёт веер; если внутри все точки, светового кандидата нет.
- В остальных режимах заливки световое поле не отображается.
## 10. Взаимодействие и 2.5D
- Активная зона проходит по всей ломаной: расстояние до неё не более `max(22 px экрана, половина толщины)`.
- Точка в 20 px от ленты должна попадать в неё, в 30 px — нет.
- При наложении с окрашенной капсулой значка приоритет у значка; между лентами выбирается ближайшая, затем стабильный порядок по `id`.
- В 2.5D лента поднимается на `0,075 D`, получает торец `0,1 D` и тень на полу; само световое поле остаётся на полу.
- `houseplan-space-card` отображает ленту и свет, но остаётся read-only.
## 11. Критерии визуальной приёмки по Figma
1. Во включённом и выключенном состоянии геометрия совпадает: изменяются цвет/толщина и наличие свечения, а не траектория.
2. Белое ядро и тёмная обводка сохраняют контраст на сером полу, возле штрихованных стен и поверх светового поля.
3. Прямые участки не выглядят как цепочка отдельных кругов.
4. На концах нет резкого прямоугольного обрыва света.
5. На углах отсутствуют тёмные разрывы и чрезмерно яркие круглые пятна.
6. Свечение ленты у стены направлено в помещение и не окрашивает пол за стеной.
7. Замкнутая прямоугольная лента даёт непрерывное поле по периметру.
8. Цвет меняется от сущности без изменения формы, радиуса и поведения границ.
9. При выключении поле плавно исчезает, а тонкая полоса остаётся видимой.
10. Масштабирование плана сохраняет пропорции толщины, обводки, активной зоны и 2.5D-подъёма.
## 12. Проверки
Обязательные группы приёмки из issue #662:
- backend: лимиты, уникальность, конечность координат, ссылки на маркеры, экспорт/импорт;
- unit: якорь, рендер состояний, hit-test, линейные кандидаты света, стены, auto-grid, space-card;
- smoke: рисование, привязка/отвязка, свечение и clip-path, 2.5D, упор в стену;
- golden: `lighting-led-strip-glow-dark`, `led-strip-off-light`, `iso-led-strip-dark`;
- i18n: паритет ru/en/de/fr;
- performance: benchmark десяти лент по пять точек.
## 13. Вне скоупа
- эффекты и адресные сегменты WLED;
- вставка или удаление отдельных вершин;
- отдельная высота крепления и дополнительная 3D-модель света от стены;
- кривые Безье и произвольные гирлянды;
- интерактивность в `houseplan-space-card`;
- автоматическая починка лент, оказавшихся внутри стены.
## 14. Комплект поставки
- реализация модели, редактора, представления, света, 2.5D, импорта/экспорта и валидации;
- сущность `light.demo_led_strip` в demo/golden/smoke fixtures;
- четыре словаря i18n;
- unit, backend, smoke, golden и mutant-проверки по issue;
- документация и changelog RU/EN;
- замер производительности в handoff;
- визуальное сравнение с двумя PNG из этого архива.
Binary file not shown.

After

Width:  |  Height:  |  Size: 466 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 920 KiB