22 KiB
Issue #250 — символы всех проёмов всегда на осевой стены
- Дата: 2026-08-23
- Тип: bug + polish · приоритет P2
- Оценка: пользовательская ценность 6/10 · сложность 3/10 · риск 4/10
- Issue: #250
- Предшественник: #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 это закреплено тремя уровнями:
src/opening-symbol-placement.tsвозвращает ненулевой offset для door/window приflip_v: true;src/render/opening-symbol.tsпереводит внутреннюю группу створок, дуг и оконного стекла на этот offset;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. Зафиксированное решение владельца
- Видимый архитектурный символ любого проёма располагается на осевой линии
host wall при обоих значениях
flip_v. flip_vостаётся только командой направления:- у door/window зеркалит створки, дуги и оконную группу относительно оси;
- у gate меняет знак существующего 10° поворота;
- у passage неприменим, потому что видимого символа нет.
- Настройка направления не меняет saved
x/y, host, wall cut, tunnel, jamb depth, hitbox или отдельный lock badge. - Контракт одинаков для 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
Для всех комбинаций:
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:
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=<run-id>. Если пиксели не меняются,
обновляется только проверяемый provenance; если целевая сцена содержит flipped
opening, принимается и ожидаемая визуальная дельта.
Golden baselines принимаются только в предрелизном цикле из reviewed Linux artifact.
17. Откат
Одна code revision возвращает прежнюю compatibility-ветку translation. Данные
и migration для отката не нужны; сохранённый flip_v не меняется. Откат
возвращает видимый дефект, но не повреждает план.
18. Риски
- Вместе со сдвигом исчезает зеркало door/window. Митигация: AC2/AC3 сравнивают направление при одном origin.
- Gate теряет противоположный 10° turn. Митигация: AC4 и существующий mutation guard.
- Косяки схлопываются к оси. Митигация: AC6 фиксирует 70-см depth отдельно от нулевого symbol offset.
- Flat исправлен, Iso остаётся у грани. Митигация: общий helper, AC3/AC5 и hidden-Iso golden.
- Golden impact недооценён. Митигация: полный pre-beta verify, exact impact list и запрет локального accept.
- Старые мутанты становятся зелёными без предмета. Митигация: явная замена трёх obsolete anchors в §14.
19. Принятые предположения — технические, менять свободно
- Предпочтительно сохранить
openingSymbolOffset()как единственную pure точку безусловного centerline-контракта и вернуть из неё константный ноль. Удаление helper допустимо, если Flat и Iso продолжают использовать один доказуемый contract без дублирования. - Текущую сигнатуру с
type/flipV/angle/faceможно временно оставить ради ясного вызова и mutation test; unused parameters допустимо переименовать. - Existing four golden scenes #242 достаточны и не требуют пятой сцены, если semantic guard действительно различает старый edge offset и новый zero.
- Отдельный lock badge остаётся на текущей стороне стены: он сообщает статус замка и не является частью архитектурной opening group.
Не являются предположениями: нулевой offset при любом flip_v, сохранение
направления створок/ворот, full-depth jambs, отсутствие migration и parity
Flat/preview/Static/Iso — решения владельца.