Files
houseplan-card/docs/specs/374-space-card-light-pools.md
T
2026-08-29 14:08:15 +03:00

435 lines
31 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# #374 — Полный Glow в `houseplan-space-card` по явному включению
Issue: [#374](https://github.com/Matysh/houseplan-card/issues/374)
## Сценарий
Персона **домочадец** или **гость/киоск** смотрит на отдельную
`custom:houseplan-space-card`, размещённую как основной визуальный элемент
Home Assistant dashboard. Администратор дома заранее включил для этого
экземпляра полный Glow.
Когда лампа включается, человек должен сразу увидеть не только жёлтый маркер или
ровную заливку комнаты, но и пространственный свет: радиальный pool начинается
в реальном положении источника, проходит через открытые внутренние проёмы и
оставляет тени за стенами, перегородками и колоннами.
## Что человек увидит до и после
Сейчас отдельная карточка показывает живые маркеры и ровную Glow-подложку
комнат, но никогда не рисует радиальный свет и тени. После изменения новый
переключатель **Light pools and wall shadows / Световые пулы и тени от стен**
позволит получить в ней тот же полноценный Glow, что на полном плане; без
включения переключателя карточка останется визуально и вычислительно прежней.
## Проблема
Закрытый документационный issue
[#370](https://github.com/Matysh/houseplan-card/issues/370) подтвердил, что
отсутствие теней в static card не было поломкой: `renderSpaceStatic()` намеренно
не выполняет самый тяжёлый этап светового рендера. Но для dashboard, где
отдельная карточка является основным представлением дома, это удаляет одну из
ключевых пространственных возможностей House Plan.
Канонический путь полной карточки сейчас делает гораздо больше, чем рисует
градиент:
1. строит реальные тела стен, перегородок и колонн;
2. вырезает только проходимую часть открытых **внутренних** дверей/ворот и
учитывает сохранённые passages и zero-thickness walls;
3. блокирует свет окнами, наружными проёмами и телом, внутрь которого ошибочно
попал источник;
4. вычисляет visibility polygon и пересекает его с полом всех комнат;
5. применяет один радиальный field на источник, additive/screen composition,
экранное feathering и bounded fade переходы;
6. кеширует тяжёлую геометрию по конфигурации и квантованным состояниям проёмов.
`houseplan-space-card` уже имеет тот же House Plan snapshot, HA states,
виртуальные источники, позиции маркеров, room Glow/base fills и каноническую
геометрию стен. Отсутствует именно общая световая сцена. Копирование приватной
`_renderGlowLayer()` в static renderer недопустимо: две поверхности начнут
по-разному трактовать проёмы, толщины и последующие исправления света.
## Скоуп
- новый boolean `light_pools` только у `custom:houseplan-space-card`;
- настройка в YAML и visual editor, default `false`;
- при `true` — визуальная и семантическая parity с Glow полной
`custom:houseplan-card` для выбранного пространства;
- общий framework-light расчёт transport/clip/source appearance и общий SVG
contract для обеих поверхностей вместо новой копии алгоритма;
- реальные и manual/virtual источники, marker-owned radius/color/brightness;
- стены, независимые перегородки, колонны, room drafts, zero-thickness walls,
внутренние двери/ворота/passages и наружные проёмы по канону `docs/LIGHT.md`;
- текущие room-level/global Glow gates, data fills и Glow base/tunnels;
- включение/выключение источника, смена цвета/яркости, перемещение маркера,
изменение конфигурации и состояния проёма без перезагрузки карточки;
- те же bounded fade, reduced-motion и source-failure semantics;
- bounded caches и отдельный performance-профиль opt-in static card;
- regression-тесты default-off, parity, окклюзии, lifecycle и inert surface;
- UI/i18n, RU/EN guide, `docs/LIGHT.md`, testing contract и оба changelog.
## Не-скоуп
- включение `light_pools` по умолчанию или миграция существующих Lovelace cards;
- изменение визуального результата Glow полной карточки;
- новая упрощённая/low-quality модель света, лимит количества источников,
адаптивное отключение теней или скрытая деградация на слабых устройствах;
- sunlight/window wedges: это независимая система `docs/SUN.md`;
- hover/click/more-info/toggle на самой схеме static card;
- изменение footer, deep link, title/crop, room/device presentation или
`show_button`;
- новый глобальный House Plan setting, per-space setting или сохранение флага в
backend config: флаг принадлежит конкретному Lovelace card instance;
- изменение редакторов геометрии и скрытой изометрии;
- изменение алгоритмов стен, проёмов, clean floor или Optimize Plans;
- повышение существующих performance/bundle budgets без отдельного решения по
измеренному Linux artifact.
## Контракт поведения
### 1. Конфигурация
```yaml
type: custom:houseplan-space-card
space: ground_floor
light_pools: true
```
1. Поле отсутствует или равно `false`: static card не строит barrier/visibility
scene, не создаёт SVG pool layer и сохраняет текущий результат.
2. Поле равно `true`: карточка строит полноценный Glow выбранного пространства.
3. Visual editor показывает один boolean с локализованным названием. Изменение
применяется стандартным Lovelace config update без backend write.
4. Неизвестные значения не получают новой truthy-семантики: включением является
только нормализованное boolean `true`, как у остальных boolean полей editor.
### 2. Источники и видимость
1. Источники разрешаются через тот же `resolvedLightSources()` и
`selectSpatialGlowSource()`, что в полной карточке. Один marker владеет не
более чем одним spatial pool даже при смене resolved HA entity.
2. Реальный, manual virtual и связанный virtual light дают тот же pool при тех
же state, marker settings и позиции. Hidden/removed/non-spatial devices не
начинают светить из-за новой поверхности.
3. `resolveGlowAppearance()` и `glowAlpha()` остаются единственным контрактом
цвета, яркости и центральной alpha. Per-marker `glow_color` и
`glow_radius_cm` имеют тот же приоритет над palette/global radius.
4. Off/unknown/unavailable источник pool не рисует; on источник рисует. Переход
on↔off и смена resolved source сохраняют marker-stable identity и тот же
bounded fade, что полный план.
5. `live_states:false` продолжает подавлять обычное state/activity оформление
маркеров, но не выключает Glow. Это существующая независимость Glow от
device display полной карточки.
### 3. Геометрия и тени
1. Для одинакового snapshot, HA states, пространства, позиции и радиуса обе
карточки получают одинаковые barrier fingerprint, floor и lit clip paths с
точностью существующего geometry tolerance.
2. Стена блокирует свет своим реальным телом и толщиной, а не центральной осью.
Перегородка, колонна, draft и solid zero wall непрозрачны; dashed zero wall
прозрачен.
3. Внутренняя door/gate пропускает только квантованную открытую часть; passage
прозрачен полностью. Window и opening без пола с обеих сторон остаются
непрозрачными.
4. Источник внутри opaque plan body fail-dark: ни половинчатого pool, ни
засвеченного tunnel не появляется.
5. Visibility region пересекается со всем floor пространства до visual clip по
Glow-enabled rooms. Поэтому свет может пройти через отключённую для покраски
комнату, но пиксели pool/base появляются только в комнатах, где effective
room Glow включён.
6. Вырожденная или повреждённая геометрия сохраняет существующий bounded
fail-dark/fallback и один диагностический warning на revision/room, а не
падает и не освещает сквозь препятствие.
### 4. Слои и визуальная parity
1. Порядок static scene становится: paper/backdrop → data room/tunnel fills →
Glow base/tunnels → radial pools → стены/openings → labels/devices.
Pool не перекрывает стену, маркер или подпись.
2. Один источник создаёт один painted radial field и один clip; второго spill,
sector, tunnel-light или shadow-mask слоя нет.
3. `GLOW_FALLOFF`, gradient units, blend mode, isolation, edge feather и
fallback composition совпадают с полной карточкой и читаются из общего
контракта, а не из двух наборов констант.
4. Data fill с валидным цветом/alpha остаётся видимым по действующим правилам;
Glow base добавляется только там, где его добавляет полный renderer.
5. Light/dark theme не меняет физическую область света; цвет и alpha приходят
из существующей palette/marker state. Day-cycle background продолжает жить
под тем же pool.
### 5. Lifecycle и кеши
1. Каждый `houseplan-space-card` владеет своим bounded Glow runtime: transition
state, timers, last appearance и per-source clips не протекают между card
instances и освобождаются при disconnect.
2. Barrier cache зависит от полного geometry fingerprint и отсортированной
сигнатуры квантованных interior opening amounts. Обычный HA tick не
перестраивает geometry; смена состояния проёма перестраивает ровно нужную
сцену.
3. Clip cache дополнительно зависит от space, barrier fingerprint, source
position и radius. Перемещение marker/config revision не показывает stale
shadow.
4. Continuity snapshot/recovery не очищает уже видимый корректный кадр раньше
действующего контракта. После нового authoritative snapshot pool и marker
соответствуют одной device/config revision.
5. При `light_pools:false` runtime не ставит transition timers и не выполняет
visibility/boolean work. Выключение флага очищает оставшиеся timers/caches и
удаляет layer на следующем render.
## UX и доступность
- Visual editor: boolean рядом с live-state visual settings; название явно
предупреждает, что это одновременно pools и wall shadows.
- Помогающий текст в guide сообщает, что режим тяжелее default static path;
отдельный warning dialog/toast не добавляется.
- `.hp-static-stage` остаётся `pointer-events:none`; Glow layer имеет
`aria-hidden="true"` и не участвует в tab order/accessible name.
- Footer остаётся единственной интерактивной частью card.
- На desktop, wall tablet и phone визуальная модель одинакова; touch не включает
новое действие. `prefers-reduced-motion` использует существующий reduced
transition contract без потери финального светового состояния.
## Модель данных, миграция и совместимость
Публичный Lovelace config расширяется одним полем:
```ts
interface SpaceCardConfig {
light_pools?: boolean;
}
```
- default в `setConfig()` и stub config — `false`/отсутствие;
- schema House Plan, backend store, model version и config migration не
меняются;
- существующие YAML и GUI cards без поля визуально и по runtime cost не
меняются;
- visual editor round-trip сохраняет явное значение стандартным механизмом HA;
- откат безопасен простым удалением поля или `light_pools: false`;
- deprecated alias `show_light_shadows` не принимается и не записывается: у
функции один канонический ключ.
## i18n
Добавить один editor key во все поставляемые словари
`src/i18n/{en,ru,de,fr}.json`:
- EN: `Light pools and wall shadows`;
- RU: `Световые пулы и тени от стен`;
- DE/FR — эквивалентный перевод без английского fallback.
Новых runtime errors, toast, dialogs или ARIA-фраз нет.
## Архитектурный контракт
1. Канонические transport и pool primitives выносятся из приватного монолита в
framework-light shared module(s). Допустимое разбиение:
- pure barrier/clip scene builder с явными geometry inputs;
- shared source-to-spot resolver;
- shared SVG pool template/константы;
- caller-owned bounded runtime/cache adapter.
2. `houseplan-card` и `space-render` могут по-разному собирать входы, но не
содержат две реализации aperture classification, visibility intersection,
source guard, falloff или SVG field.
3. Shared module не читает DOM, глобальный `hass`, приватные поля карточки и не
владеет неограниченным module-level cache. Side effects — только переданные
callbacks диагностики/transition scheduling.
4. Существующая full-card output должна быть regression-equivalent. Extraction
не является поводом менять её geometry, identifiers, blend/fade или budgets.
5. Static renderer получает marker positions в plan coordinates до процентного
преобразования devlayer и использует тот же frame/viewBox, что его SVG.
Точные имена shared files/functions — техническая деталь реализации, но
ревьюер должен видеть один алгоритмический источник истины по пяти пунктам выше.
## Производительность и bundle
- Default-off static card не вызывает `visibilityPolygon`, boolean
intersection и barrier build. Измеренный cold/warm default сценарий не должен
регрессировать сверх действующего шума/бюджета.
- Opt-in использует fingerprinted barrier cache один раз на geometry/opening
revision и bounded per-source clip cache; обычные state ticks переиспользуют
геометрию.
- Добавить кандидатный профиль `large-space-card-glow-overlay-v1` либо
эквивалентное расширение канонического Glow profile: cold first render, warm
HA tick, opening-state invalidation и teardown для одной large static card.
- Порог утверждается только из полного Linux performance artifact по
`demo/performance/README.md`; локальное число не становится budget.
- Скрытый source cap, sampling и повышение существующего
`large-house-glow-overlay-v1` запрещены.
- Shared extraction не добавляет runtime dependency. Bundle budget может
измениться только в пределах действующего gate; повышение требует отдельного
решения и не входит в #374.
## Touch, темы и размеры
- static stage остаётся inert на touch и mouse;
- pool использует plan coordinates и сохраняет геометрию при любом CSS width,
DPR и `title: ""` compact frame;
- screen-space edge feather пересчитывается из фактической ширины stage, чтобы
не становиться толще/тоньше на phone и desktop;
- light/dark, reduced motion и continuity overlay входят в blocking browser
matrix;
- никакого horizontal overflow, нового scroll/gesture listener или захвата
pointer нет.
## Затронутые файлы и модули
Ожидаемый минимум (точное разбиение может уточнить реализация):
- `src/houseplan-card.ts` — перейти на shared Glow scene/render contract без
изменения полного плана;
- новый `src/light-scene.ts` и/или `src/glow-render.ts` — общий transport,
spots, SVG и bounded runtime primitives;
- `src/space-render.ts` — opt-in layer и передача static geometry/positions;
- `src/space-card.ts` — `light_pools`, per-instance runtime/lifecycle и stage
measurement;
- `src/space-editor.ts` — visual boolean;
- `src/types.ts` при необходимости общего config/runtime type;
- `src/i18n/{en,ru,de,fr}.json`;
- unit-тесты light scene/cache/source parity;
- `demo/smoke_space_card.mjs` и/или отдельный targeted static Glow smoke;
- `demo/performance/**` — opt-in static profile;
- `docs/LIGHT.md`, `docs/ARCHITECTURE.md`, `docs/TESTING.md`;
- `docs/USER-GUIDE.md`, `docs/USER-GUIDE.ru.md`;
- `docs/CHANGELOG.md`, `docs/CHANGELOG.ru.md`;
- canonical Docs screenshots manifest/artifact по правилу репозитория.
## Критерии приёмки
- **AC1 — opt-in и обратная совместимость:** omitted/`false` не создаёт pool
layer и не выполняет тяжёлую light geometry; `true` создаёт layer, visual
editor и YAML дают один результат, неизвестный `space` сохраняет текущую
error card. **Доказательство:** unit config contract + browser smoke с тремя
static cards (`omitted`, `false`, `true`).
- **AC2 — визуальная parity источника:** одинаковая full/static fixture с одним
real light и одним manual/linked virtual light даёт одинаковые center,
radius, color, alpha, falloff stops, clip geometry и один painted field на
marker. **Доказательство:** shared-module unit + browser DOM/SVG assertions +
semantic pixel witness light/dark.
- **AC3 — окклюзия parity:** pool проходит через открытые interior door/gate,
passage и dashed zero wall; закрывается закрытым проёмом, window, exterior
door, solid zero wall, partition и column; source inside masonry fail-dark.
Full и static расходятся не более существующего raster tolerance.
**Доказательство:** shared geometry unit + targeted browser smoke на
канонической lighting fixture.
- **AC4 — rooms/fills/layering:** global/per-room Glow gate, data fill,
Glow base и opening tunnels совпадают с full card; transport проходит через
disabled room, но там не красит; стены, symbols, labels и devices остаются
поверх pool. **Доказательство:** browser smoke + reviewed light/dark visual
artifact.
- **AC5 — live lifecycle:** on/off, brightness/RGB, resolved source swap,
marker move, config revision и door amount обновляют ожидаемую часть сцены;
fade bounded, reduced motion корректен, stale clip не остаётся и timers
исчезают после disconnect/flag-off. **Доказательство:** fake-timer/cache unit
+ browser smoke.
- **AC6 — static surface остаётся static:** при включённом pool hover/click/tap
по pool, стене, комнате и marker не вызывают action/more-info; footer deep
link работает; phone/desktop и `title: ""` не меняют plan-coordinate
геометрию. **Доказательство:** browser pointer/touch smoke на двух widths.
- **AC7 — производительность и bounded memory:** default-off не регрессирует;
opt-in cold/warm/opening invalidation соответствует утверждённому Linux
профилю; barrier не перестраивается на обычном HA tick, caches bounded и
teardown очищает instance state. **Доказательство:** instrumentation unit +
полный performance artifact без повышения действующих budgets.
- **AC8 — full card без регрессии:** существующие light visibility, opening,
zero-wall, Glow smoke/golden и `large-house-glow-overlay-v1` проходят после
shared extraction без принятых визуальных дельт полной карточки.
**Доказательство:** unit + полный Linux smoke/golden/performance artifact.
- **AC9 — документация, i18n и релиз:** visual editor локализован во всех
поставляемых языках; RU/EN guide описывает default, YAML и performance cost;
`docs/LIGHT.md` больше не утверждает, что static card всегда без pools; оба
changelog содержат #374. **Доказательство:** i18n/config tests,
`check-docs`, docs screenshot artifact и ревью кода.
- **AC10 — сборка и budgets:** typecheck, unit, build, bundle sync/budget,
process/docs gates и Validate на точном SHA зелёные; новых зависимостей и
повышения budget нет. **Доказательство:** CI checks ветки.
## План автотестов
1. Pure unit для shared barrier builder: wall body, interior open/closed
aperture, passage, window/exterior opening, partition, column, dashed/solid
zero wall, malformed fallback и deterministic fingerprint.
2. Pure unit source-to-spot: real/manual/linked virtual, hidden, off,
unavailable, RGB/brightness, marker radius/color, fail-dark и stable key при
source swap.
3. Instrumented cache/runtime unit: geometry build count на first render,
ordinary HA tick, door quantization change, marker move, config revision,
`light_pools:false` и disconnect; fake timers не остаются.
4. Расширить static browser harness двумя одинаковыми экземплярами пространства:
default-off control и opt-in. Проверить отсутствие/наличие layer и inert DOM.
5. На той же canonical lighting fixture рядом снять full card и static card;
сравнить source centers/radii/gradient stops/clip topology и pixels в точках
«у источника», «через проём», «за колонной», «за окном/наружной дверью».
6. Повторить для light/dark, 390/900 px, DPR 1/2, `title` omitted/empty и
reduced motion; CSS scaling не меняет plan-coordinate clip.
7. Mutation witnesses: вернуть unconditional no-pool static path; пропустить
wall occluder; разрешить exterior door; не очистить runtime при flag-off —
соответствующие targeted checks должны падать.
8. В цикле реализации: `npm run typecheck`, `npm test`, `npm run build`; перед
S7 — targeted browser smoke. Полные golden/smoke/performance и принятие Docs
screenshots выполняются только по каноническому Linux artifact процесса.
## Риски
- **Расхождение двух моделей света.** Снижается shared pure scene/render
contract и cross-surface AC2/AC3; копия `_renderGlowLayer()` запрещена.
- **Регрессия полной карточки при extraction.** Снижается output-equivalence,
полными существующими light goldens/smokes и запретом попутной смены
констант.
- **Default static card становится тяжёлой.** Снижается точным boolean gate до
barrier/visibility work, instrumentation и default-off performance case.
- **Неправильный слой закрывает стены/маркеры.** Снижается явным render order и
semantic pixel witness.
- **Stale тень после проёма/позиции.** Снижается полным fingerprint/cache key и
lifecycle test.
- **Несколько static cards делят timers/caches.** Снижается per-instance owner,
bounded LRU и teardown test.
- **Feather зависит от CSS size.** Снижается stage measurement и DPR/width
matrix.
- **Docs screenshots fingerprint устаревает после `src/**`.** Снижается только
полным canonical workflow и запретом частичного принятия artifact.
## Откат
Пользовательский откат — удалить `light_pools` или установить `false`; данные и
House Plan config не меняются. Кодовый откат удаляет static adapter/editor key,
но сохраняет shared extraction, если она regression-equivalent и продолжает
обслуживать full card. Если shared extraction сама является причиной регрессии,
она откатывается целиком вместе с static layer. Обратной миграции нет.
## Release-артефакты
- `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md`: opt-in полный Glow static card со
ссылкой на #374;
- `docs/USER-GUIDE.md` и `.ru.md`: YAML, default `false`, visual setting,
`live_states` independence и performance warning;
- `docs/LIGHT.md`/`ARCHITECTURE.md`: две поверхности одного канонического
transport/runtime contract;
- `docs/TESTING.md`: static-card Glow parity matrix;
- reviewed browser artifact: full/static рядом, light/dark и narrow/wide с
открытым проёмом и тенью за колонной;
- полный Linux golden/smoke artifact; визуальная дельта full card не принимается;
- полный Linux performance artifact с default-off и opt-in static profiles,
без повышения существующих budgets;
- canonical **Docs screenshots** workflow после `src/**`; принять только полный
reviewed artifact через `npm run docs:accept -- --reviewed --from=<artifact>`
и закоммитить актуальный `docs/images/screenshots.json`;
- backend, migration и security artifacts не требуются; bundle/process gates
обязательны.
## Принято предположительно, поменять свободно
- точные имена и границы shared `light-scene`/`glow-render` модулей;
- форма caller-owned runtime API и конкретные размеры LRU, если они не меньше
действующих возможностей и остаются bounded;
- способ передать static wall-union/opening inputs без повторного boolean pass;
- имя targeted smoke и конкретные synthetic fixtures;
- точные raster tolerances берутся из действующих Glow tests, а не вводятся
шире ради прохождения новой проверки;
- настройка стоит рядом с `live_states` в visual editor; отдельная секция формы
не создаётся;
- documentation warning остаётся текстовым: dialog/toast при включении не нужен.