Files
houseplan-card/docs/specs/020-glow-open-door-spill.md
T

11 KiB
Raw Blame History

ТЗ #20 — Динамический проход Glow через открытые двери и ворота

Цель

Состояние привязанного к проёму контакта управляет только проходом 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 и очищается при смене привязки/пространства.

Геометрия прохода

  1. Из passages исключаются окна и проёмы с amount <= 0.
  2. Для amount === 1 используется текущий doorSector() без изменения геометрии и отсечения стеновыми откосами.
  3. Для 0 < amount < 1 рабочая апертура сужается до rlen * amount вокруг центра проёма; doorSector() получает её новые концы и прежний tunnel depth.
  4. Сектор по-прежнему создаётся только когда hasRoomBehind() подтверждает соседнюю комнату. Наружная дверь не освещает фон/бумагу вне дома.
  5. Физические стены, перегородки и колонны продолжают вычитаться существующим floorMinusBodies(); новая логика не ослабляет их occlusion.
  6. Несколько пересекающихся секторов объединяются 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 зелёные.