Files
houseplan-card/docs/specs/020-glow-open-door-spill.md
T
2026-08-28 22:31:02 +03:00

224 lines
17 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.
# ТЗ #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 сценариев по файлам.