Files
houseplan-card/docs/specs/250-opening-centerline.md
T
2026-08-23 01:46:22 +03:00

22 KiB
Raw Blame History

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 это закреплено тремя уровнями:

  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

Для всех комбинаций:

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. Риски

  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 — решения владельца.