mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-04 05:41:34 +00:00
224 lines
17 KiB
Markdown
224 lines
17 KiB
Markdown
# ТЗ #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 сценариев по файлам.
|