# ТЗ #20 — Glow проходит через дверь по её фактическому состоянию - Issue: https://github.com/Matysh/houseplan-card/issues/20 - Каноническая модель света: [`docs/LIGHT.md`](../LIGHT.md) - Связано: #19, #55, #71, #92, #306 ## Сценарий Домочадец или гость смотрит House Plan в обычном View либо на киоск-панели в момент, когда в одной комнате горит свет, а дверь с привязанным контактным датчиком между комнатами открывается или закрывается. ## Что человек увидит до и после Сейчас свет проходит через такую дверь независимо от её состояния; после изменения закрытая дверь остановит Glow, открытая пропустит его, а частично открытые ворота или cover оставят проход соответствующей ширины. ## Проблема Каноническая модель `docs/LIGHT.md` строит полигон видимости по реальной кладке: дверь, ворота или сохранённый passage вырезаются из стены и поэтому всегда прозрачны для Glow. `_lightBarriers()` классифицирует тип и наличие пола по обе стороны, но не учитывает состояние привязанного `opening.contact`. В результате визуальный символ может показывать закрытую дверь, а свет продолжает проходить через полный проём. Старое описание задачи упоминало `doorSector()` и отдельные световые секторы. Такой модели после #71 нет: задача меняет только ширину выреза в той же кладке, которая уже является источником барьеров и fail-dark проверки источника. ## Скоуп - двери и ворота, привязанные к доступному `binary_sensor` или door-like `cover`; - полный, нулевой и частичный коэффициент открытия; - проёмы в контурных стенах и в независимых перегородках; - единый коэффициент для символа проёма и апертуры Glow; - инвалидация barrier/region cache только при изменении релевантного состояния; - View и kiosk; та же геометрия участвует в существующих golden/smoke сценах. ## Не-скоуп - новая настройка, новый элемент интерфейса или сохранение runtime-состояния; - изменение конфигурационной схемы и миграция; - солнечные лучи, room fill, площадь, бумага, tunnel fill и геометрия символа кроме уже поддерживаемого коэффициента открытия; - попытка угадать механику створки: частичная апертура всегда сужается симметрично относительно центра; - свет за наружной дверью или воротами; - изменение fallback переходных состояний cover без числовой позиции; - автоматическое управление дверью или связанной сущностью. ## Контракт поведения ### 1. Один коэффициент открытия Канонический `openingAmount()` остаётся единственным источником числа `0..1`, которое используют и видимый символ, и Glow. Resolver получает тип проёма, состояние, `invert` и при наличии — `attributes.current_position` связанного cover. | Проём / источник | Коэффициент `amount` | |---|---:| | `passage` | всегда `1` | | дверь/ворота без contact | `1` — действующая семантика постоянного проёма | | доступный бинарный contact, закрыто/неактивно | `0` | | доступный бинарный contact, открыто/активно | `1` | | доступный cover с конечным `current_position` | `clamp(position / 100, 0, 1)` | | известное значение при `invert: true` | `1 - amount` | | contact missing/disabled либо `unknown`/`unavailable` | fallback непривязанной двери/ворот: `1`; `invert` его не меняет | | окно | его символ использует resolver как сейчас, но апертура Glow всегда `0` | Для cover без конечного `current_position` сохраняется действующая интерпретация его строкового state. Задача не вводит last-known кеш и не меняет существующее поведение `opening`/`closing` без позиции. Значения `NaN`, бесконечность и нечисловые строки считаются отсутствующей позицией. ### 2. Реальная апертура в кладке 1. Сначала действует существующая allowlist `door | gate | passage` и проверка пола по обе стороны. Окно, неизвестный будущий тип и наружный проём остаются непрозрачными при любом состоянии. 2. Для прошедшего классификацию проёма вычисляется `amount`. 3. При `amount <= 0` вырез не создаётся: исходная кладка остаётся целой и останавливает свет. 4. При `amount >= 1` используется существующий полный вырез без изменения. 5. При `0 < amount < 1` длина выреза равна `renderedLength * amount`, а его центр, ось и глубина стены не меняются. 6. Правило одинаково для контурной стены и независимой перегородки. Для перегородки масштабируется именно `PartitionOpeningCut`, а не сохранённый `OpeningCfg` и не видимая геометрия стены. 7. Несколько проёмов обрабатываются действующей boolean-геометрией. Порядок записей в конфиге не меняет результат и не создаёт швов между пересекающимися вырезами. Источник, поставленный внутрь закрывшегося проёма, попадает в непрозрачную кладку и полностью подавляется существующим fail-dark guard — так же, как источник внутри стены. Отдельного исключения для такого источника нет. ### 3. Обновление и кеши Статическая геометрия и HA-состояние разделяются: - `geometryFingerprint` по-прежнему считается только из пространства, `cell_cm` и grid pitch и используется для повторного recut общей стеновой геометрии; - `openingStateSignature` содержит отсортированные по id записи только интерьерных дверей/ворот с привязкой: `id:amount`, где amount округлён до `0.001`; - `barrierFingerprint` включает `geometryFingerprint` и `openingStateSignature`; он ключует `_lightBarrierPool` и возвращается в region cache; - кеш физических тел независимых стен учитывает фактические частичные cuts, а не только множество id прозрачных проёмов; - посторонний HA tick с той же signature обязан попадать в кеш; глобальная очистка на каждый update запрещена. Изменение contact должно попасть в обычный render tick без сохранения config. Пулы остаются ограниченными действующими LRU-лимитами. ## UX Новых контролов и сообщений нет. Пользователь продолжает привязывать contact в существующем диалоге проёма. Результат виден непосредственно на плане: - закрытие убирает Glow за дверью; - открытие возвращает его; - позиционный cover меняет ширину светового прохода вместе с отображаемой степенью открытия. Недоступность или деактивация contact не превращает архитектурный проём в стену и не порождает повторяющиеся toast/ошибки. ## Модель данных и миграция Модель и сохранённый JSON не меняются. Используются существующие поля `opening.contact` и `opening.invert` и live-state Home Assistant. Старые планы не мигрируются и при отсутствии доступной привязки отображаются как раньше. ## i18n Новых строк нет. Существующие названия состояния проёма и contact не меняются. ## Критерии приёмки и доказательства - **AC1.** Лампа в комнате A и закрытая привязанная дверь в B: B не получает Glow. Доказательство: unit геометрии + `demo/smoke_glow.mjs`. - **AC2.** После перехода того же contact в открытое состояние Glow появляется в B не позднее следующего render tick, без config save. Доказательство: browser smoke с переключением fake `hass`. - **AC3.** `cover.current_position = 50` создаёт центрированный вырез ровно в половину полной длины, а символ получает тот же `amount`. Доказательство: unit единого resolver и geometry input + smoke-пиксели по обе стороны jamb. - **AC4.** `invert` меняет известное бинарное/позиционное значение, но не меняет fallback missing/disabled/unknown/unavailable. Доказательство: unit-матрица. - **AC5.** Ворота повторяют дверь; passage всегда прозрачен; окно, неизвестный тип и наружная дверь/ворота всегда непрозрачны для Glow. Доказательство: unit классификатора + расширенный glow smoke. - **AC6.** Те же full/zero/partial правила работают для проёма с `host.kind = partition`; закрытая перегородка и её jamb остаются барьерами. Доказательство: geometry unit + smoke fixture с независимой перегородкой. - **AC7.** Закрытие проёма, внутри которого стоит источник, полностью убирает его Glow. Доказательство: source-guard unit/smoke. - **AC8.** Одинаковая signature переиспользует barrier cache при постороннем HA update; изменение amount создаёт новый barrier/region key; LRU остаётся bounded. Доказательство: cache contract unit. - **AC9.** На large-house fixture переключение 20 door contacts не нарушает действующий `stateUpdate`/Glow performance budget. Доказательство: exact-SHA CI performance smoke перед бетой. - **AC10.** Golden-сцена показывает closed/open/50% на толстой стене без тёмных швов и без света снаружи. Доказательство: reviewed Linux golden arteфакт. ## План автотестов 1. Расширить `test/logic.test.mjs`: binary, cover position `0/1/50/100`, clamp, invalid value, invert, outage fallback, passage/window. 2. Вынести чистую подготовку light apertures/signature либо покрыть существующий чистый geometry boundary так, чтобы unit проверял длину, центрирование, порядок и partition cut без Lit/DOM. 3. Расширить `demo/smoke_glow.mjs` динамической сменой contact и измерением пикселей в A, в апертуре, в B, за jamb и снаружи. Тот же smoke проверяет контурную стену и перегородку. 4. Добавить/обновить одну детерминированную golden-сцену closed/open/50%; baseline принимается только из полного Linux CI arteфакта по процессу. 5. Перед `S7-code-review`: `npm run typecheck`, `npm test`, `npm run build`, `npm run bundle:sync`, `npm run bundle:budget`, `node demo/smoke_glow.mjs`, `node demo/docs/capture.mjs`, затем проверка синхронности bundle tree. 6. Перед бетой: exact-SHA Validate с golden, полным browser matrix и performance smoke. ## Риски и меры | Риск | Мера | |---|---| | stale Glow после изменения contact | state signature входит и в barriers, и в region fingerprint | | частичный проём режется в контуре, но не в перегородке | отдельный AC6 и общий чистый aperture input | | HA update пересчитывает дорогую boolean-геометрию без нужды | signature содержит только релевантные id/amount, LRU остаётся bounded | | источник внутри закрытой двери протекает на одну сторону | AC7 фиксирует общий fail-dark body | | визуал и свет используют разные проценты | один `openingAmount()` и unit single-source contract | | partial cut создаёт микрошов или вылезает за jamb | clamp, центрирование, существующая boolean-геометрия и golden | ## Откат Изменение не пишет данные. Безопасный откат — вернуть полную апертуру для всех интерьерных door/gate/passage и удалить state signature из barrier cache. Планы, привязки и сохранённые trails при этом не меняются. ## Release-артефакты - записи RU/EN changelog со ссылкой на #20; - актуализация `docs/LIGHT.md` и при необходимости архитектурного описания кеша; - синхронные production/demo bundle trees; - reviewed golden baseline, если пиксели закономерно изменились; - в release body — отдельный пункт только если владелец сочтёт эффект значимым, иначе общий `Small fixes and improvements`. ## Принято предположительно, поменять свободно на ревью - форма чистого aperture helper и место его файла; - точная сериализация signature при сохранении семантики и округления `0.001`; - способ построения частичного `PartitionOpeningCut` без мутации config; - разбиение smoke/golden сценариев по файлам.