mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
docs: specify resilient glow floor geometry
Issue: #218 User-Visible: no
This commit is contained in:
@@ -0,0 +1,337 @@
|
||||
# Issue #218 — floating-point шум одной комнаты не гасит Glow всего пространства
|
||||
|
||||
- **Issue:** https://github.com/Matysh/houseplan-card/issues/218
|
||||
- **Связанные контракты:** #19, #55, #61, #65–#67, #197
|
||||
- **Тип:** bug, обычный полный трек
|
||||
- **Приоритет:** P1
|
||||
- **Пользовательское изменение:** да
|
||||
|
||||
## 1. Сценарий и персона
|
||||
|
||||
**Персона:** домочадец или гость, который смотрит состояние освещения в View или
|
||||
kiosk, и администратор, проверяющий тот же план в редакторе устройств.
|
||||
|
||||
**Сценарий:** в пространстве есть несколько комнат и включённый источник света.
|
||||
После обычных операций редактора две координаты общей границы могут отличаться
|
||||
на один ULP (`5.55e-17` или `1.11e-16`). На таком плане polyclip не завершает
|
||||
объединение полов. Сейчас одна комната с этим численным шумом делает пустым
|
||||
clipPath каждого источника пространства, поэтому Glow не виден ни при каких
|
||||
настройках, хотя источник включён и visibility polygon построен.
|
||||
|
||||
## 2. Что человек увидит до и после
|
||||
|
||||
До исправления одна численно нестабильная комната может молча погасить все
|
||||
световые пятна пространства. После исправления обычный floating-point шум не
|
||||
влияет на свет, а действительно некорректная комната в худшем случае исключает
|
||||
только свою часть пола: остальные комнаты продолжают освещаться.
|
||||
|
||||
## 3. Подтверждённая причина
|
||||
|
||||
Текущая цепочка:
|
||||
|
||||
```text
|
||||
_renderGlowLayer
|
||||
→ intersectionPaths([visibilityPolygon], floor всех комнат)
|
||||
→ unionBodies(floor)
|
||||
→ polyclip union с исходными double-координатами
|
||||
→ exception → null → lit=[] → пустой clipPath
|
||||
```
|
||||
|
||||
`src/physical-geometry.ts` передаёт координаты в polyclip без численной
|
||||
стабилизации. `unionBodies()` проглатывает исключение и возвращает `null`, а
|
||||
`intersectionPaths()` не отличает сбой одной комнаты от отсутствующего пола.
|
||||
В результате fail-dark, нужный против утечки света за пределы дома, применяется
|
||||
к целому пространству вместо минимального проблемного фрагмента.
|
||||
|
||||
Реальный экспорт владельца локализовал две пары координат с расхождением в
|
||||
последнем бите. Экспериментальное квантование входа union до `1e-6` единицы
|
||||
плана восстановило непустой clipPath. Экспериментальный патч в код не вошёл.
|
||||
|
||||
## 4. Нормативные источники и приоритет
|
||||
|
||||
При расхождении применяются:
|
||||
|
||||
1. решения владельца и приёмка в #218;
|
||||
2. это ТЗ после зелёного SPEC-REVIEW;
|
||||
3. действующий fail-dark контракт Glow и smoke `smoke_glow_fail_dark`;
|
||||
4. контракт #197: локальный сбой булевой операции не удаляет валидную геометрию
|
||||
всего пространства;
|
||||
5. текущая реализация как compatibility baseline для явно не изменяемого
|
||||
поведения.
|
||||
|
||||
## 5. Цели
|
||||
|
||||
1. Сделать булевы операции пола устойчивыми к невидимому floating-point шуму.
|
||||
2. Ограничить отказ одной комнаты этой комнатой, не всем пространством.
|
||||
3. Сохранить fail-dark там, где свет действительно не должен рисоваться.
|
||||
4. Сделать остаточную деградацию диагностируемой и не заспамить консоль.
|
||||
5. Не менять сохранённые координаты и вид валидных планов.
|
||||
|
||||
## 6. Scope
|
||||
|
||||
В задачу входят:
|
||||
|
||||
- численная стабилизация входных колец общего boolean-слоя
|
||||
`src/physical-geometry.ts` непосредственно перед polyclip;
|
||||
- устойчивое пересечение visibility fan с набором комнат;
|
||||
- покомнатная деградация при исключении после стабилизации;
|
||||
- диагностический callback из pure geometry и дедуплицированный `console.warn`
|
||||
на уровне карточки с id пространства и id проблемной комнаты;
|
||||
- минимизированная regression fixture, сохраняющая проблемные координаты из
|
||||
реального экспорта без пользовательских имён, entity ids и остального конфига;
|
||||
- проверка full View, kiosk/тот же renderer и Glow-base;
|
||||
- unit, targeted smoke, mutation gate, golden verification и документация.
|
||||
|
||||
## 7. Не входит в задачу
|
||||
|
||||
- изменение радиуса, цвета, opacity, falloff, blend или анимации Glow;
|
||||
- изменение visibility algorithm, occluders, проёмов или кладки;
|
||||
- ослабление защиты источника внутри стены и луча вне пола;
|
||||
- исправление произвольной самопересекающейся комнаты или автоматический repair;
|
||||
- изменение/сохранение/миграция координат пользователя;
|
||||
- предупреждение в UI, repair issue или новая настройка точности;
|
||||
- логирование названий комнат, координат, entity ids или полного конфига;
|
||||
- изменение статических room fills, если наблюдение Glow-base не связано с той
|
||||
же численной причиной;
|
||||
- принятие golden baseline на Windows или выпуск без команды владельца.
|
||||
|
||||
## 8. Численный контракт
|
||||
|
||||
### 8.1. Стабилизация
|
||||
|
||||
- каждое конечное `x/y`, передаваемое из `unionBodies()` в polyclip,
|
||||
нормализуется с quantum `1e-6` единицы плана;
|
||||
- квантование применяется только к вычисляемой копии; входные массивы не
|
||||
мутируются;
|
||||
- `-0` канонизируется в `0`;
|
||||
- соседние дубликаты, возникшие после квантования, удаляются, замыкающая точка
|
||||
добавляется ровно один раз;
|
||||
- кольцо, у которого после нормализации меньше трёх различных точек или нулевая
|
||||
площадь в пределах quantum, считается непригодным и не передаётся polyclip;
|
||||
- результат детерминирован, не зависит от порядка тел и повторного вызова;
|
||||
- смещение любой точки не превышает половины quantum и многократно меньше
|
||||
физической/экранной точности плана.
|
||||
|
||||
Quantum является техническим контрактом этой задачи. Его изменение после
|
||||
ревью требует доказать те же ULP-fixtures, raster parity и отсутствие слипания
|
||||
реально различных геометрических деталей.
|
||||
|
||||
### 8.2. Пересечение с полом
|
||||
|
||||
Нормальный путь сохраняет объединение валидных комнат до intersection, чтобы
|
||||
перекрытия legacy-планов не превращались в evenodd-дыры. Если объединение или
|
||||
пересечение всего набора всё же падает, применяется покомнатный путь:
|
||||
|
||||
1. visibility fan остаётся ограничивающей геометрией и никогда не возвращается
|
||||
без floor clipping;
|
||||
2. комнаты обрабатываются независимо;
|
||||
3. успешные результаты объединяются в итоговый набор path fragments;
|
||||
4. комната, для которой boolean operation упала, пропускается и сообщает свой
|
||||
индекс/id через diagnostic callback;
|
||||
5. отсутствие всех валидных комнат возвращает `[]`.
|
||||
|
||||
Fallback не может вернуть raw visibility fan или осветить область вне пола.
|
||||
|
||||
## 9. Диагностика
|
||||
|
||||
Pure-функции геометрии не вызывают `console` напрямую. Они предоставляют
|
||||
структурированный сигнал о покомнатной ошибке. `_renderGlowLayer()` связывает
|
||||
индекс пола с `room.id` и пишет предупреждение формата, эквивалентного:
|
||||
|
||||
```text
|
||||
HOUSEPLAN GLOW GEOMETRY FALLBACK: #218, space <space-id>, room <room-id>
|
||||
```
|
||||
|
||||
Требования:
|
||||
|
||||
- нет room name, entity id, координат и текста polyclip exception;
|
||||
- одинаковая ошибка логируется не чаще одного раза на сочетание
|
||||
`space id + geometry fingerprint + room id`;
|
||||
- новый fingerprint может дать новое предупреждение, чтобы ремонт геометрии и
|
||||
повторный дефект были различимы;
|
||||
- валидный ULP-шум, исправленный квантованием без fallback, не создаёт warning;
|
||||
- warning не влияет на render result и не требует i18n.
|
||||
|
||||
## 10. Glow-base
|
||||
|
||||
Наблюдение `0 из 6` проверяется на той же минимизированной fixture при условиях,
|
||||
когда текущий контракт действительно требует тёмную базу: Glow включён, а
|
||||
effective room fill отсутствует. Для каждой пригодной комнаты должен
|
||||
существовать `.glow-base`.
|
||||
|
||||
Если тест уже зелёный, код Glow-base не меняется и результат фиксируется в
|
||||
implementation evidence. Если тест красный из-за того же raw-coordinate
|
||||
boolean path, исправление выполняется общей стабилизацией без отдельного
|
||||
renderer. Иная причина является отдельной задачей и не расширяет #218.
|
||||
|
||||
## 11. UX, accessibility и touch
|
||||
|
||||
Новых элементов UI и взаимодействий нет. View/kiosk, mouse, touch и pen получают
|
||||
один и тот же исправленный render. Pointer targets, focus, tooltip, reduced
|
||||
motion и editor gestures не меняются. Ошибка не показывается домочадцу: для
|
||||
администратора остаётся redacted console warning только при реальной деградации.
|
||||
|
||||
## 12. Данные, migration, privacy и i18n
|
||||
|
||||
Config, layout, backend schema, localStorage и сериализация не меняются.
|
||||
Миграция не нужна. Исходные координаты остаются байт-в-байт прежними после
|
||||
Open → Save без пользовательского изменения. Regression fixture хранит только
|
||||
минимальную анонимизированную геометрию. Новых строк и i18n-ключей нет.
|
||||
|
||||
## 13. Архитектура и зоны изменений
|
||||
|
||||
Ожидаемая цепочка:
|
||||
|
||||
```text
|
||||
saved room coordinates (immutable)
|
||||
→ normalized boolean input copy
|
||||
→ normal union + intersection
|
||||
→ per-room fail-dark fallback only on exception
|
||||
→ path fragments + structured diagnostics
|
||||
→ existing Glow clipPath
|
||||
```
|
||||
|
||||
Ожидаемые зоны:
|
||||
|
||||
- `src/physical-geometry.ts` — normalization, robust union/intersection API;
|
||||
- `src/houseplan-card.ts` — room ids, deduplicated redacted warning и cache key;
|
||||
- `test/physical-geometry.test.mjs` — ULP, malformed-room, immutability,
|
||||
permutation и fail-dark unit tests;
|
||||
- targeted browser smoke/fixture для реального шестикомнатного случая и
|
||||
Glow-base assertion;
|
||||
- `scripts/mutation-gate.mjs` и его registry test;
|
||||
- `docs/TESTING.md`, `docs/CHANGELOG.md`, `docs/CHANGELOG.ru.md`;
|
||||
- generated bundles после build.
|
||||
|
||||
Нельзя создавать второй Glow renderer или применять квантование при записи
|
||||
данных.
|
||||
|
||||
## 14. Performance и security
|
||||
|
||||
- нормальный render path выполняет не больше одного дополнительного линейного
|
||||
прохода по вершинам перед уже существующей boolean operation;
|
||||
- покомнатная обработка запускается только после исключения/невалидного общего
|
||||
результата и не является постоянным per-frame N×boolean path;
|
||||
- существующий LRU clip cache сохраняется; diagnostic dedupe ограничен текущими
|
||||
space/fingerprint keys и очищается/ограничивается вместе с жизненным циклом
|
||||
карточки;
|
||||
- нельзя добавлять таймеры, observers, network calls или persisted telemetry;
|
||||
- warning redacted согласно §9;
|
||||
- fail-dark security boundary не ослабляется.
|
||||
|
||||
## 15. Acceptance criteria
|
||||
|
||||
1. **AC1 — ULP union.** Два прямоугольника с общей гранью, отличающейся на один
|
||||
ULP, после `unionBodies()` дают валидную объединённую геометрию.
|
||||
**Доказательство:** unit, красный при mutant без normalization.
|
||||
2. **AC2 — реальная regression fixture.** Анонимизированная шестикомнатная
|
||||
fixture с точными проблемными парами координат даёт непустой `lit` и
|
||||
непустой DOM clipPath включённого источника. **Доказательство:** unit +
|
||||
targeted browser smoke; тест красный на коде до #218.
|
||||
3. **AC3 — локальная деградация.** Самопересекающаяся/намеренно непригодная
|
||||
комната не удаляет пересечение с валидными комнатами. Проблемная комната не
|
||||
получает свет. **Доказательство:** unit с injected/deterministic failure.
|
||||
4. **AC4 — диагностический след.** Fallback сообщает space/room id один раз на
|
||||
fingerprint и не раскрывает names, entities, coordinates или exception.
|
||||
**Доказательство:** unit/browser console capture и повторный render.
|
||||
5. **AC5 — fail-dark.** Источник в кладке, visibility fan мимо пола, пустой floor
|
||||
и полный отказ всех комнат остаются тёмными. **Доказательство:** существующий
|
||||
`smoke_glow_fail_dark` без ослабления плюс unit.
|
||||
6. **AC6 — неизменность валидных планов.** Обычные Glow-сцены дают ту же площадь
|
||||
с допуском boolean quantum и те же pixels в reviewed golden; входные arrays
|
||||
и persisted config не мутируются. **Доказательство:** unit, golden verify и
|
||||
config round-trip/source review.
|
||||
7. **AC7 — Glow-base.** На regression fixture каждая eligible-комната имеет
|
||||
текущую Glow-base; если observation не воспроизводится, implementation
|
||||
evidence прямо это фиксирует без лишнего продуктового изменения.
|
||||
**Доказательство:** targeted browser smoke.
|
||||
8. **AC8 — deterministic geometry.** Повторный вызов и перестановка room bodies
|
||||
дают эквивалентную площадь/path coverage; `-0`, adjacent duplicates и
|
||||
quantum-degenerate rings обрабатываются явно. **Доказательство:** unit.
|
||||
9. **AC9 — performance/privacy.** Fast path остаётся O(vertices + текущие
|
||||
boolean operations), fallback не работает постоянно, warning redacted и
|
||||
deduplicated. **Доказательство:** source review и targeted performance smoke.
|
||||
10. **AC10 — release artifacts.** Оба changelog, testing docs, mutation registry,
|
||||
generated bundles и screenshot/golden fingerprints актуальны.
|
||||
**Доказательство:** docs check, build parity и review diff.
|
||||
|
||||
## 16. План автотестов
|
||||
|
||||
### 16.1. Цикл реализации
|
||||
|
||||
```bash
|
||||
npm run typecheck
|
||||
npm test
|
||||
npm run build
|
||||
```
|
||||
|
||||
Unit matrix:
|
||||
|
||||
- 1 ULP и `±1e-12/±1e-9` на общей вертикальной/горизонтальной границе;
|
||||
- exact six-room minimized fixture;
|
||||
- invalid room среди двух валидных;
|
||||
- empty/missed floor и source-outside-floor;
|
||||
- input immutability, repeatability, reversed/permuted bodies;
|
||||
- structured callback и diagnostic dedupe.
|
||||
|
||||
### 16.2. Targeted smoke перед S7
|
||||
|
||||
```bash
|
||||
node demo/smoke_glow.mjs
|
||||
node demo/smoke_glow_blending.mjs
|
||||
node demo/smoke_glow_fail_dark.mjs
|
||||
```
|
||||
|
||||
Добавляется отдельный узкий smoke либо детерминированное расширение
|
||||
`smoke_glow` для ULP-fixture, DOM clipPath, Glow-base и console warning.
|
||||
|
||||
### 16.3. Mutation gate
|
||||
|
||||
| id | Мутация | Обязанный guard |
|
||||
|---|---|---|
|
||||
| `union-quantization-removed` | убрать normalization перед polyclip | AC1/AC2 unit |
|
||||
| `union-failure-kills-space` | вернуть общий отказ всего floor | AC3 unit/smoke |
|
||||
| `union-failure-silent` | убрать structured diagnostic/warning | AC4 console test |
|
||||
| `glow-fail-dark-weakened` | вернуть raw fan или пропустить source guard | существующий fail-dark smoke |
|
||||
|
||||
Перед S7 выполняются `node scripts/mutation-gate.mjs --check` и целевые мутанты,
|
||||
если их runtime укладывается в локальный review gate.
|
||||
|
||||
### 16.4. Golden и предрелиз
|
||||
|
||||
Golden verify выполняется на существующих Glow-сценах: валидные планы не должны
|
||||
получить видимый raster diff. Если для #218 добавляется новый visual scenario,
|
||||
его baseline принимается только через `npm run golden:accept -- --reviewed` по
|
||||
полному Linux CI artifact перед бетой. Полный smoke/golden/performance —
|
||||
предрелизный гейт по PROCESS, не цикл реализации.
|
||||
|
||||
## 17. Release-артефакты
|
||||
|
||||
В user-visible implementation commit обязательны:
|
||||
|
||||
- `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md` — одна комната с численным шумом
|
||||
больше не гасит Glow пространства;
|
||||
- `docs/TESTING.md` — ULP-fixture, покомнатный fail-dark fallback и диагностика;
|
||||
- targeted smoke/mutation registry;
|
||||
- generated `dist`, demo и integration bundles;
|
||||
- `docs/images/screenshots.json`, только если build/visual harness этого требует.
|
||||
|
||||
User Guide не меняется: настройки и пользовательский сценарий остаются прежними.
|
||||
|
||||
## 18. Откат
|
||||
|
||||
Откат выполняется одним user-visible implementation commit вместе с tests,
|
||||
bundles и changelog. Persisted rollback/migration не нужен. При откате вернётся
|
||||
старый полный fail-dark, но сохранённые планы останутся совместимыми.
|
||||
|
||||
## 19. Принятые предположения
|
||||
|
||||
Можно свободно изменить на ревью ТЗ без вопроса владельцу:
|
||||
|
||||
1. quantum `1e-6` применяется ко всем входам `unionBodies()`, а не только Glow,
|
||||
потому что это общий boolean boundary и эксперимент #218 подтвердил его;
|
||||
2. pure geometry сообщает индексы, а room/space ids добавляет renderer;
|
||||
3. warning дедуплицируется по space + fingerprint + room;
|
||||
4. приватный реальный экспорт не коммитится — только минимальная геометрия с
|
||||
сохранёнными проблемными double-значениями;
|
||||
5. наблюдение Glow-base считается частью #218 только при той же корневой причине.
|
||||
@@ -57,6 +57,7 @@ GitHub Issues и GitHub Projects (v2) остаются единственным
|
||||
| [#210](https://github.com/Matysh/houseplan-card/issues/210) Фиксированное пространство экземпляра карточки | [210-fixed-floor-card.md](210-fixed-floor-card.md) |
|
||||
| [#211](https://github.com/Matysh/houseplan-card/issues/211) Визуальное соответствие маркеров дизайн-пакету #179 | [211-device-icons-visual-parity.md](211-device-icons-visual-parity.md) |
|
||||
| [#217](https://github.com/Matysh/houseplan-card/issues/217) Внешняя рамка Text-маркера должна быть капсулой | [217-text-shell-outline.md](217-text-shell-outline.md) |
|
||||
| [#218](https://github.com/Matysh/houseplan-card/issues/218) Floating-point шум комнаты не гасит Glow пространства | [218-glow-floor-geometry.md](218-glow-floor-geometry.md) |
|
||||
| [#205](https://github.com/Matysh/houseplan-card/issues/205) Продолжение следа после короткой остановки пылесоса | [205-vacuum-trail-resume-grace.md](205-vacuum-trail-resume-grace.md) |
|
||||
|
||||
## P2
|
||||
|
||||
Reference in New Issue
Block a user