From 2c776b0c6d00b1276c04d0b51c696f405beddcbb Mon Sep 17 00:00:00 2001 From: Sergey Matyunin Date: Sun, 23 Aug 2026 01:46:22 +0300 Subject: [PATCH] docs: specify opening centerline invariant Issue: #250 User-Visible: no --- docs/specs/250-opening-centerline.md | 348 +++++++++++++++++++++++++++ 1 file changed, 348 insertions(+) create mode 100644 docs/specs/250-opening-centerline.md diff --git a/docs/specs/250-opening-centerline.md b/docs/specs/250-opening-centerline.md new file mode 100644 index 00000000..d05ce1a3 --- /dev/null +++ b/docs/specs/250-opening-centerline.md @@ -0,0 +1,348 @@ +# Issue #250 — символы всех проёмов всегда на осевой стены + +- Дата: 2026-08-23 +- Тип: bug + polish · приоритет P2 +- Оценка: пользовательская ценность 6/10 · сложность 3/10 · риск 4/10 +- Issue: [#250](https://github.com/Matysh/houseplan-card/issues/250) +- Предшественник: [#242](https://github.com/Matysh/houseplan-card/issues/242) +- Ветка: `issue/250-opening-centerline` +- Статус ТЗ: на ревью + +Канонические документы: `docs/SCOPE.md`, `docs/ARCHITECTURE.md`, +`docs/WALL-THICKNESS.md`, `docs/ISOMETRIC.md`, +`docs/CONFIG-COMPATIBILITY.md`, `docs/TOUCH-SUPPORT.md`, +`docs/USER-GUIDE.ru.md`. + +## 1. Сценарий и персона + +Администратор дома размещает дверь или окно в толстой стене и выбирает +«Открывается в другую сторону». Затем он и остальные жильцы видят проём во +View, на статической карточке либо в скрытом изометрическом режиме. + +Человек ожидает, что настройка меняет сторону открывания створки, но не +переносит весь архитектурный символ к одной из граней стены. Одна и та же дверь +должна оставаться на осевой линии стены при любом направлении створки. + +## 2. Что человек увидит до и после + +**До:** дверь или окно с включённым «Открывается в другую сторону» сдвинуто на +половину толщины стены и выглядит стоящим у её грани; такое же смещение видно в +скрытой изометрии. + +**После:** дверь, окно, ворота и открытый проём всегда привязаны к осевой линии +стены. Переключение направления только зеркалит створку или меняет направление +её поворота; положение проёма не меняется. + +## 3. Проблема и подтверждённый диагноз + +#242 ввела общий pure helper `openingSymbolOffset()` и центрировала default +состояние, но намеренно сохранила compatibility-исключение: door/window с +`flip_v: true` получают translation на одну локальную полутолщину стены. + +На текущем `dev` это закреплено тремя уровнями: + +1. `src/opening-symbol-placement.ts` возвращает ненулевой offset для + door/window при `flip_v: true`; +2. `src/render/opening-symbol.ts` переводит внутреннюю группу створок, дуг и + оконного стекла на этот offset; +3. `src/iso-openings.ts` прибавляет offset к structural origin каждой створки. + +При этом направление уже представлено отдельно. Flat renderer зеркалит +door/window через `sy`; Iso применяет эквивалентное преобразование basis. У +ворот знак 10° поворота приходит через `face.side`. Полная глубина косяков +берётся из `face.cm` и не зависит от visual translation. + +Следовательно, позиционное значение `flip_v` можно удалить без изменения +persisted данных, wall cut, tunnel либо выбора стороны створки. + +## 4. Зафиксированное решение владельца + +1. Видимый архитектурный символ любого проёма располагается на осевой линии + host wall при обоих значениях `flip_v`. +2. `flip_v` остаётся только командой направления: + - у door/window зеркалит створки, дуги и оконную группу относительно оси; + - у gate меняет знак существующего 10° поворота; + - у passage неприменим, потому что видимого символа нет. +3. Настройка направления не меняет saved `x/y`, host, wall cut, tunnel, + jamb depth, hitbox или отдельный lock badge. +4. Контракт одинаков для preview, committed Flat/View, hosted Static и + скрытого Iso. + +Продуктовых вопросов не осталось. + +## 5. Цели + +- убрать последнее позиционное исключение из контракта #242; +- сделать centerline безусловным инвариантом любого opening symbol; +- сохранить направление створок и ворот при `flip_v`; +- сохранить единую геометрию на всех render surfaces; +- не переписывать и не мигрировать существующие конфиги. + +## 6. Скоуп + +### Входит + +- безусловный нулевой visual offset для door/window/gate/passage; +- сохранение зеркала door/window и смены знака поворота gate; +- Flat/View, placement preview, hosted Static и hidden Iso parity; +- room-wall и explicit partition-hosted openings; +- горизонтальные, вертикальные и диагональные стены любой валидной толщины; +- unit, mutation guard, targeted browser smoke и semantic golden; +- нормативные документы, пользовательская инструкция и changelog RU/EN. + +### Не входит + +- изменение `flip_h`, петель либо длины проёма; +- изменение wall association, `compareOpeningSides`, `face.side` или + `partitionOpeningFace`; +- изменение cut, room-coloured tunnel, jambs, wall fill, Glow или солнца; +- изменение lock badge, contact/lock state, actions и interaction hitbox; +- изменение формы дуг, створок, ворот или окна помимо удаления translation; +- persisted migration, backend/schema либо новый compatibility field; +- новый UI-control или переименование существующей настройки; +- публикация скрытого Iso. + +## 7. Контракт поведения и геометрии + +### 7.1. Безусловный centerline + +Для всех комбинаций: + +```text +type ∈ {door, window, gate, passage} +flip_v ∈ {false, true} +angle ∈ любое конечное направление, включая диагональ +face ∈ valid room-wall или partition face любой положительной толщины +``` + +visual translation относительно saved opening origin равен точно +`{ ox: 0, oy: 0 }`. Это точный инвариант, а не значение в пределах pixel +tolerance. Порядок комнат, направление endpoints и знак resolved face его не +меняют. + +Для malformed/non-finite angle или face helper также fail-safe возвращает +точный ноль: вычислять позицию по повреждённой геометрии не требуется. + +### 7.2. Направление остаётся отдельным + +Нулевой translation не отменяет transform направления: + +| Тип | `flip_v: false` | `flip_v: true` | Origin | +|---|---|---|---| +| door | текущая сторона дуги/полотна | зеркальная сторона | saved `x/y` | +| window | текущая сторона створок/дуг | зеркальная сторона | saved `x/y` | +| gate | текущий знак поворота 10° | противоположный знак 10° | saved `x/y` | +| passage | символ отсутствует | неприменимо | saved `x/y` cut | + +Для door/window точка крепления створки остаётся на том же centerline origin, +а открытая геометрия уходит на противоположную сторону. Для window стекло, +дуги и створки используют один origin и не расходятся друг с другом. + +### 7.3. Толстая стена + +На стене 70 см: + +- translation всей movable visible group от оси равен 0 см / 0 render units; +- jamb endpoints остаются на `±35 см` от оси по нормали стены; +- wall cut и coloured tunnel продолжаются через полные 70 см; +- переключение `flip_v` не меняет ни одно из этих трёх чисел. + +То же правило действует для толщин 1, 15 и 100 см и для `cell_cm` 1/2.54/5/30: +физическая толщина меняет jamb depth, но никогда symbol origin. + +## 8. UX и render parity + +- Preview до клика и committed opening после Save имеют одинаковый origin. +- Переключение Plan/View, reload и HA state tick не меняют origin. +- Static использует тот же shared visible renderer, а Iso — тот же pure offset + contract при построении structural basis. +- Открывание/закрывание меняет только live amount, не structural center. +- `hide_openings` и passage сохраняют существующее поведение. +- Lock badge остаётся отдельным status/action affordance и не считается частью + архитектурной группы, центрируемой этой задачей. + +## 9. Модель данных, migration и compatibility + +Модель данных не меняется. `OpeningCfg.flip_v` остаётся boolean, сохраняется и +round-trip проходит как прежде. Существующие записи не переписываются: их новое +отображение получается на render-time. + +Это намеренное изменение видимой семантики уже сохранённого `flip_v`, одобренное +владельцем, а не migration. Старый reader продолжит edge-align, новый reader +всегда центрирует; отдельный model-version bump не требуется, потому что формат +и валидность данных не изменились. + +`docs/CONFIG-COMPATIBILITY.md` проверяется, но содержательно не меняется, если +реализация не затронет формат. + +## 10. i18n, accessibility и touch + +**i18n:** новых ключей нет. Текущие `opening.flip_v` — «Открывается в другую +сторону» / “Opens to the other side” — после исправления точнее соответствуют +реальному поведению. + +**Accessibility:** controls, accessible names и DOM-порядок не меняются. + +**Touch:** новых жестов нет. Preview и committed symbol на touch получают тот +же centerline contract; pan, pinch, pointercancel и hit targets не меняются. +View/kiosk parity блокирующая, потому что View является основной поверхностью. + +## 11. Производительность и безопасность + +Helper становится константным O(1) без тригонометрии. Нельзя добавлять обход +комнат/стен, новый cache или state-dependent geometry. Structural Iso cache +по-прежнему может учитывать flips, потому что они меняют basis direction, хотя +больше не меняют origin. + +Новых service calls, HTML, доверенных строк или persisted input нет. Lock и +opening action guards остаются без изменений. + +## 12. Критерии приёмки + +| AC | Требование | Доказательство | +|---|---|---| +| AC1 | `openingSymbolOffset()` возвращает точный `{ox:0, oy:0}` для матрицы 4 типов × 2 flip × horizontal/vertical/diagonal angles × positive/negative/zero/malformed face | unit matrix + основной mutant | +| AC2 | Flat door/window при `flip_v: false/true` имеют один centerline origin; flip зеркалит дугу/створку, а window glass остаётся в общей группе | geometry/SVG unit + semantic golden | +| AC3 | Hidden Iso door/window при обоих flip имеют тот же origin, но противоположную направленную basis; Flat и Iso согласованы | Iso unit + cross-render smoke + golden | +| AC4 | Gate остаётся центрированным, `flip_v` по-прежнему меняет знак 10° поворота; passage не создаёт visible symbol | unit + существующий gate mutant + smoke | +| AC5 | На room wall и reversed partition host preview, committed View и Static совпадают; room order/endpoints не влияют на origin | browser smoke + golden semantic guard | +| AC6 | Для стены 70 см offset равен 0, jamb depth равен 70 см; cut, tunnel, hitbox, lock badge, bindings/actions, Glow и sun не меняются | focused units/smokes + code review | +| AC7 | Existing `flip_v` configs не переписываются, round-trip/export/import прежние; backend/schema/model_version diff отсутствует | existing config tests + diff review | +| AC8 | Нормативные документы и guide больше не обещают edge alignment; changelog RU/EN описывает видимое изменение | check-docs + code review | +| AC9 | Typecheck, unit и build зелёные; три поставляемые bundle-копии побайтово одинаковы | локальный implementation gate | + +## 13. План автотестов + +### 13.1. Unit + +- заменить edge-aligned ожидания в + `test/opening-symbol-placement.test.mjs` полной exact-zero матрицей; +- в `test/opening-symbol.test.mjs` доказать общий origin, противоположное + направление door/window и неизменный full-depth jamb; +- в `test/iso-openings.test.mjs` отделить same-origin от mirrored basis; +- сохранить тест противоположного gate turn при `flip_v`; +- проверить passage и malformed inputs; +- проверить, что `openingVisibleMetrics()` и hit geometry не зависят от + удалённого translation. + +### 13.2. Browser smoke + +Расширить существующие `demo/smoke_wall_thickness.mjs`, +`demo/smoke_opening_preview.mjs` и `demo/smoke_isometric_contract.mjs` либо +добавить один узкий smoke, если существующие не дают наблюдать origin: + +- 70-см room wall и diagonal partition; +- door/window с `flip_v=false/true` рядом; +- preview → Save → View → reload; +- hosted Static и hidden Iso; +- gate turn sign, jamb depth, opening cut/tunnel и lock action regression. + +### 13.3. Golden + +Переиспользовать четыре semantic scenes #242: + +- `opening-symbol-room-wall-light`; +- `opening-symbol-diagonal-partition-dark`; +- `opening-symbol-flip-pairs-light`; +- `isometric-opening-symbol-parity-dark`. + +Их semantic contract меняется только для flipped door/window offset: ожидается +точный ноль вместо полутолщины. До PNG guard проверяет centerline, jamb depth, +зеркальное направление door/window и противоположный gate turn. + +Полный `golden:verify` и принятие baseline выполняются перед бетой из reviewed +Linux CI artifact; локальный accept запрещён. Реализация обязана перечислить +весь фактический golden impact, а не только четыре целевые сцены. + +## 14. Mutation guards + +Старые мутанты #242, завязанные на удаляемую ветку +`opening-symbol-default-uses-room-face`, +`opening-symbol-partition-follows-endpoints` и +`opening-gate-flip-translates-leaves`, выводятся из реестра либо заменяются: +после безусловного нуля их anchors больше не описывают production contract. + +Добавляется основной мутант: + +| id | Что ломает | Что обязано покраснеть | +|---|---|---| +| `opening-symbol-flip-restores-edge-offset` | возвращает для door/window с `flip_v:true` старый сдвиг на `cm/2` | AC1 placement unit, AC2 Flat unit, AC3 Iso unit | + +Существующий `opening-gate-flip-cancels-turn` сохраняется и обязан продолжать +падать, если новая правка случайно отменит направление ворот. + +## 15. Гейты реализации + +В implementation loop: + +```text +npm run typecheck +npm test +npm run build +сверка dist / integration frontend / demo bundle +``` + +Перед передачей в code review дополнительно выполняются выбранные +`smoke-select` opening/Static/Iso smokes, clean mutation gate, основной mutant, +`check-docs` и `process-gate` в объёме задачи. + +Полный golden, полный smoke suite и performance — предбетовые гейты по +PROCESS.md. Backend pytest не требуется, пока backend/schema не затронуты. + +## 16. Release-артефакты + +User-visible implementation commit одновременно обновляет: + +- `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md` со ссылкой на #250; +- `docs/USER-GUIDE.ru.md` — `flip_v` меняет только направление; +- `docs/WALL-THICKNESS.md` — безусловный centerline contract; +- `docs/ARCHITECTURE.md` и `docs/ISOMETRIC.md` — removal compatibility offset; +- `docs/TESTING.md` и semantic golden guards. + +Любое изменение `src/**` делает screenshot provenance предметом проверки. +Канонический workflow `Docs screenshots` запускается после implementation +commit; результат принимается только через +`npm run docs:accept -- --reviewed --from=`. Если пиксели не меняются, +обновляется только проверяемый provenance; если целевая сцена содержит flipped +opening, принимается и ожидаемая визуальная дельта. + +Golden baselines принимаются только в предрелизном цикле из reviewed Linux +artifact. + +## 17. Откат + +Одна code revision возвращает прежнюю compatibility-ветку translation. Данные +и migration для отката не нужны; сохранённый `flip_v` не меняется. Откат +возвращает видимый дефект, но не повреждает план. + +## 18. Риски + +1. **Вместе со сдвигом исчезает зеркало door/window.** Митигация: AC2/AC3 + сравнивают направление при одном origin. +2. **Gate теряет противоположный 10° turn.** Митигация: AC4 и существующий + mutation guard. +3. **Косяки схлопываются к оси.** Митигация: AC6 фиксирует 70-см depth отдельно + от нулевого symbol offset. +4. **Flat исправлен, Iso остаётся у грани.** Митигация: общий helper, AC3/AC5 и + hidden-Iso golden. +5. **Golden impact недооценён.** Митигация: полный pre-beta verify, exact impact + list и запрет локального accept. +6. **Старые мутанты становятся зелёными без предмета.** Митигация: явная замена + трёх obsolete anchors в §14. + +## 19. Принятые предположения — технические, менять свободно + +1. Предпочтительно сохранить `openingSymbolOffset()` как единственную pure + точку безусловного centerline-контракта и вернуть из неё константный ноль. + Удаление helper допустимо, если Flat и Iso продолжают использовать один + доказуемый contract без дублирования. +2. Текущую сигнатуру с `type/flipV/angle/face` можно временно оставить ради + ясного вызова и mutation test; unused parameters допустимо переименовать. +3. Existing four golden scenes #242 достаточны и не требуют пятой сцены, если + semantic guard действительно различает старый edge offset и новый zero. +4. Отдельный lock badge остаётся на текущей стороне стены: он сообщает статус + замка и не является частью архитектурной opening group. + +**Не являются предположениями:** нулевой offset при любом `flip_v`, сохранение +направления створок/ворот, full-depth jambs, отсутствие migration и parity +Flat/preview/Static/Iso — решения владельца.