11 KiB
ТЗ #20 — Динамический проход Glow через открытые двери и ворота
- Issue: https://github.com/Matysh/houseplan-card/issues/20
- Приоритет: P2 по GitHub Project v2
- Статус ТЗ: готово к реализации
- Связано: #19 additive Glow, #36 room Glow override, #55 independent Glow overlay
Цель
Состояние привязанного к проёму контакта управляет только проходом Glow через этот проём: открытая дверь или ворота пропускают свет в соседнюю комнату, закрытая — не создаёт световой сектор. Окна по-прежнему не пропускают Glow; их световая модель остаётся частью солнечных лучей.
Актуализация 2026-08-11. Модель транспорта переписана (#71): отдельного
doorSector()больше нет. Свет — это полигон видимости из лампы, а проём — вырез в кладке (_lightBarriers). Поэтому реализация этого ТЗ сводится к одному: проём с нулевой открытой частью просто НЕ вырезается из кладки, то есть для света он такая же стена, как окно или наружная дверь. Всё, что ниже сказано про коэффициент открытия и его источник состояния, остаётся в силе; устарели только упоминания секторов.
Что происходит сейчас
_renderGlowLayer() в src/houseplan-card.ts считает passages как все проёмы,
кроме окон, и добавляет doorSector() для каждого подходящего проёма независимо
от _openingAmt(). Поэтому визуально закрытая по контакту дверь продолжает
пропускать Glow. Статические wall bodies уже имеют вырез проёма, а выход света
в соседнюю комнату создаёт именно дополнительный sector.
Следствие для реализации: не нужно на каждом HA tick перестраивать стеновые bodies или менять модель комнаты. Достаточно сделать состояние частью resolver проходов и генерировать sector только для ненулевой открытой части. Описанное в issue объединение interval понадобится лишь при будущем переходе к interval subtraction; в текущей SVG-модели отдельные clipPath children уже объединяются как union.
Канонический коэффициент открытия
Ввести чистый resolver openingLightAmount(opening, hass, bindingStatus) с
результатом 0..1. Он должен использовать тот же источник состояния, что и
визуал проёма, чтобы дверь не выглядела закрытой и одновременно не пропускала
полный свет.
Правила:
| Случай | Коэффициент |
|---|---|
| дверь/ворота без привязанного контакта | 1 — сохраняется нынешняя семантика постоянного проёма |
| известное закрытое/неактивное состояние | 0 |
| известное открытое/активное состояние | 1 |
cover с конечным current_position |
clamp(position / 100, 0, 1) |
тот же источник при invert: true |
1 - amount |
привязка missing/disabled либо unknown/unavailable |
семантика непривязанного проёма: 1 для двери/ворот |
| окно при любом состоянии | 0 для Glow |
Переходные opening/closing без числового прогресса не должны создавать
выдуманный процент: используется последний известный стабильный amount в
runtime-кеше, а если его нет — семантика непривязанного проёма. Runtime-кеш не
сохраняется в config и очищается при смене привязки/пространства.
Геометрия прохода
- Из
passagesисключаются окна и проёмы с amount<= 0. - Для
amount === 1используется текущийdoorSector()без изменения геометрии и отсечения стеновыми откосами. - Для
0 < amount < 1рабочая апертура сужается доrlen * amountвокруг центра проёма;doorSector()получает её новые концы и прежний tunnel depth. - Сектор по-прежнему создаётся только когда
hasRoomBehind()подтверждает соседнюю комнату. Наружная дверь не освещает фон/бумагу вне дома. - Физические стены, перегородки и колонны продолжают вычитаться существующим
floorMinusBodies(); новая логика не ослабляет их occlusion. - Несколько пересекающихся секторов объединяются SVG clip union. Порядок openings в config не должен менять результат.
Сужение вокруг центра — сознательная v1-аппроксимация: модель не знает тип механики створки (сдвижная, одно- или двустворчатая). Специфическое смещение апертуры от края возможно только после появления типа открывания в модели.
Кеширование и обновление
Статическая геометрия проёмов остаётся привязана к _cfgEpoch. Для Glow clip
добавляется openingStateSignature текущего пространства:
opening-id:round(amount,3) для дверей и ворот, отсортировано по id.
Signature включается в _glowClipCache key; hass revision, время и состояния
посторонних сущностей в key не входят. Изменение контакта должно дать новый clip
в ближайшем обычном render tick. Существующий LRU limit сохраняется; отдельная
полная очистка кеша на каждый HA update запрещена.
Совместимость и границы scope
- Config/model/migration не меняются.
- Непривязанные двери и ворота выглядят и освещают точно как до задачи.
- Состояние контакта влияет только на Glow и существующий визуал проёма; room fill, площадь, hover, tunnel fill и солнечные лучи не меняются.
- #55 обязан использовать этот же resolver после отделения Glow от fill mode.
- #19 смешивает уже рассчитанные pools и не меняет геометрию sectors.
- Недоступный или деактивированный HA contact не превращает архитектурный проём в стену и не блокирует сохранение/просмотр плана.
UX и диагностика
Новых настроек не добавляется. В существующей информации о проёме допустимо
показывать фактический процент только если HA действительно отдаёт
current_position; бинарный contact остаётся «открыто/закрыто». Ошибка чтения
контакта не должна создавать toast на каждый state tick.
Проверки
Unit
- amount для unbound/open/closed/inverted/unknown/unavailable/disabled;
cover.current_position: 0, 1, 50, 100 и значения вне диапазона;- переходные состояния с/без last stable value;
- narrowing endpoints и сохранение tunnel-depth clipping;
- signature детерминирована и не зависит от порядка openings.
Geometry/render
- лампа в A + закрытая дверь в B: Glow остаётся в A;
- та же дверь открыта: sector появляется в B за один tick;
- 50%: сектор уже полного и проходит между откосами толстой стены;
- ворота повторяют дверь; окно не пропускает Glow;
- наружная дверь не освещает фон;
- две соседние/перекрывающиеся двери не создают тёмный шов;
- перегородка или колонна за проёмом продолжает отсекать свет.
Regression/performance
Golden: closed/open/50% на толстой стене, light и dark HA themes. Large-house
fixture переключает 20 контактов; stateUpdate p95 остаётся внутри действующего
HP-PERF budget, cache bounded, geometry counters не растут от посторонних HA
updates.
Критерии приёмки
- Закрытый привязанный проём не создаёт Glow sector, открытый создаёт.
- Частичное открытие даёт пропорционально более узкий, корректно отсечённый стеновыми откосами сектор.
- Непривязанные проёмы и окна полностью сохраняют прежнее поведение.
- Изменение состояния не требует config save и отображается за один render tick.
- Model, room area, sun, opening tunnel fill и физическая occlusion не меняются.
- Unit, golden, geometry regression и performance gate зелёные.