From 0033138ab2f97f97d89ecbc0df9d2e291e2b1efe Mon Sep 17 00:00:00 2001 From: Sergey Matyunin Date: Sat, 22 Aug 2026 20:08:48 +0300 Subject: [PATCH] docs: specify centered opening symbols Issue: #242 User-Visible: no --- docs/specs/242-opening-symbol-center.md | 298 ++++++++++++++++++++++++ docs/specs/README.md | 1 + 2 files changed, 299 insertions(+) create mode 100644 docs/specs/242-opening-symbol-center.md diff --git a/docs/specs/242-opening-symbol-center.md b/docs/specs/242-opening-symbol-center.md new file mode 100644 index 00000000..6170d2da --- /dev/null +++ b/docs/specs/242-opening-symbol-center.md @@ -0,0 +1,298 @@ +# Issue #242 — символ проёма по центру толщины стены + +- Дата: 2026-08-22 +- Тип: bug · приоритет P2 · ценность 6/10 · сложность/риск 5/10 и 6/10 +- Issue: [#242](https://github.com/Matysh/houseplan-card/issues/242) +- Ветка: `issue/242-opening-symbol-center` +- Статус ТЗ: на ревью + +Канонические документы: `docs/SCOPE.md`, `docs/ARCHITECTURE.md`, +`docs/WALL-THICKNESS.md`, `docs/TOUCH-SUPPORT.md`, +`docs/ISOMETRIC.md`, `docs/CONFIG-COMPATIBILITY.md`, +`docs/USER-GUIDE.ru.md`. + +## 1. Сценарий и персона + +Администратор дома размещает дверь, окно или ворота в толстой стене комнаты +либо независимой стене. Во View он ожидает, что символ обозначает один и тот же +физический проём независимо от порядка комнат в конфигурации и от направления, +в котором была нарисована стена. + +Сейчас видимая створка/дуга/стекло сдвигается к выбранной грани стены. Для +общей стены выбор зависит от первой комнаты в model order, для независимой — от +направления segment. Поэтому перестановка комнат или разворот endpoints меняет +вид готового плана без изменения его архитектуры. + +## 2. Что человек увидит до и после + +**До:** символ проёма находится у одной из граней толстой стены; выбранная грань +может измениться после перестановки комнат либо при эквивалентной записи стены с +обратным направлением. + +**После:** обычный символ проёма находится точно на центральной оси толщины +стены на всех render surfaces. Косяки по-прежнему показывают полную глубину +проёма. Сохранённый `flip_v: true` остаётся явным ручным исключением для двери +и окна, а у ворот меняет только направление лёгкого поворота створок. + +## 3. Подтверждённый диагноз + +Дефект подтверждён на актуальном `dev`: + +1. `openingInnerFaceOffsetFromIndex()` выбирает natural room side после + сортировки кандидатов по `order`, то есть зависит от порядка комнат. +2. `partitionOpeningFace()` выбирает normal side из направления endpoints + независимой стены. +3. `renderOpeningVisibleGeometry()` переводит всю видимую группу на `face.ox` + / `face.oy`; окно переносит вместе с ней стекло, дверь — створку и дугу, + ворота — обе створки. +4. `buildIsoOpeningBasis()` использует те же offsets для скрытого Iso. +5. Косяки вычисляются отдельно из полной `face.cm`, поэтому сам физический + tunnel уже имеет правильную глубину и не требует изменения. + +Тесты и golden сейчас закрепляют старое edge-aligned поведение, включая +room-order dependency; их необходимо заменить новым инвариантным контрактом. + +## 4. Зафиксированные продуктовые решения + +1. Если `flip_v` отсутствует либо равен `false`, дверь, окно и ворота + центрируются по толщине host wall. +2. Для двери и окна сохранённый `flip_v: true` остаётся ручным выравниванием к + грани на стороне «открывается в другую сторону». Сторона определяется + собственной канонической локальной осью проёма и не зависит от model order + комнат либо направления endpoints host wall. +3. Створки ворот остаются по центру при обоих значениях `flip_v`; флаг меняет + только знак существующего 10° поворота. +4. Один контракт действует в committed Flat/View, preview размещения, hosted + Static и скрытом Iso. +5. У окна центрируется вся видимая группа: стекло, створки и дуги. Косяки всех + типов сохраняют полную физическую глубину стены. + +## 5. Цели + +- убрать случайную зависимость положения символа от порядка комнат и + направления рисования стены; +- сделать default-положение физически честным и одинаковым на всех surfaces; +- сохранить совместимость сохранённых `flip_v` и направленность ворот; +- не менять cut, tunnel, hit geometry, свет, HA state и persisted schema. + +## 6. Границы задачи + +### Входит + +- разделение физической толщины/направления face и визуального translation; +- центрирование door/window/gate на room-wall и partition host; +- детерминированная локальная сторона для `flip_v: true` у door/window; +- preview, Flat/View, hosted Static и hidden Iso parity; +- обновление unit, browser smoke, golden и нормативной документации; +- оба changelog в user-visible implementation commit. + +### Не входит + +- изменение wall cut, jamb depth, opening length или безопасного остатка стены; +- новый persisted field, миграция, backend/schema/compatibility change; +- изменение passage: у него нет видимого символа; +- новая UI-настройка или переименование существующего `flip_v`; +- изменение lock badge, hitbox, info/action, contact/lock state; +- изменение Glow/spill, солнца, room fill, decor или vacuum; +- публикация скрытого Iso либо новый Iso UX. + +## 7. Геометрический контракт + +Resolver проёма обязан различать три независимые величины: + +- `cm` — полная физическая толщина host wall; +- `side` — детерминированное направление в локальной системе проёма; +- visual translation — сдвиг видимой группы относительно центральной оси. + +Для любого valid host и конечной положительной толщины: + +| Тип | `flip_v` | Translation видимой группы | Направление | +|---|---:|---|---| +| door | absent / `false` | `(0, 0)` | default local side | +| door | `true` | `normal * side * cm/2` | opposite local side | +| window | absent / `false` | `(0, 0)` | default local side | +| window | `true` | `normal * side * cm/2` | opposite local side | +| gate | absent / `false` | `(0, 0)` | знак текущего 10° поворота | +| gate | `true` | `(0, 0)` | противоположный знак 10° поворота | +| passage | любое | нет видимой группы | не применяется | + +`normal` вычисляется из канонической локальной оси opening, а не из первого +room candidate и не из сырого порядка endpoints host. Эквивалентная геометрия +с переставленными room objects или reversed partition endpoints должна давать +битово одинаковые center/translation и визуально одинаковое направление. + +Косяки всегда остаются на `±cm/2` от центральной оси и не входят в translated +группу. Window glass входит в ту же translated группу, что створки/дуги. +Gate leaves используют centered origin, а только их angle зависит от `side`. + +Invalid/non-finite host geometry сохраняет действующий fail-safe и не получает +новых попыток inference. Эта задача не меняет правила выбора host. + +## 8. Render parity + +Один resolved geometry contract используется: + +- в preview до клика и в записанном Flat/View; +- для legacy room-wall и explicit partition-hosted opening; +- в hosted Static card; +- в скрытом Iso через `buildIsoOpeningBasis()`. + +Переход между Plan/View, reload и HA state tick не должен менять center. Preview +и committed opening обязаны совпадать при той же геометрии и `flip_v`. +`hide_openings` продолжает скрывать только символ по существующему контракту. + +## 9. Данные, compatibility, i18n, a11y и touch + +**Данные и migration:** отсутствуют. `OpeningCfg.flip_v` сохраняет boolean +формат; read/write/export/import и backend schema не меняются. Обновление +`docs/CONFIG-COMPATIBILITY.md` не требуется. + +**i18n:** новых строк нет. Существующая подпись «Открывается в другую сторону» +остаётся применимой; подробная геометрическая семантика фиксируется в guide. + +**A11y:** DOM controls и accessible names не меняются. Исправление касается +только SVG-геометрии. + +**Touch editor: parity, no new interaction.** Preview и committed symbol на +touch используют тот же center contract; жесты, hit targets, pinch, pan и +`pointercancel` не меняются. + +## 10. Производительность и безопасность + +Изменение остаётся pure arithmetic на уже разрешённом opening/host. Нельзя +добавлять поиск комнат, обход всех стен или allocation cache на каждый render. +HA-only state tick не должен пересчитывать wall index чаще текущего поведения. + +Новых service calls, доверенных строк, HTML и persisted input нет. Existing +guards для orphan/invalid opening и actions остаются единственным authority. + +## 11. Acceptance criteria + +| AC | Требование | Доказательство | +|---|---|---| +| AC1 | Door/window/gate без `flip_v` находятся на центральной оси стен толщиной 1/15/100 cm для horizontal/vertical/diagonal room wall и partition | geometry unit + reviewed golden | +| AC2 | Перестановка room objects и reversed partition endpoints не меняют default-символ и направление gate; физически эквивалентные fixtures дают одинаковые metrics | unit permutation matrix + browser smoke | +| AC3 | `flip_v: true` у door/window детерминированно выравнивает видимую группу к противоположной локальной грани; window glass движется вместе со створками/дугами | unit + golden | +| AC4 | Gate при обоих значениях `flip_v` центрирован, но знак 10° поворота меняется; jambs остаются full-depth | unit + golden | +| AC5 | Preview, committed Flat/View, hosted Static и hidden Iso используют один center/flip contract | cross-render smoke + golden | +| AC6 | Passage, wall cut, tunnel, hitbox, lock badge, HA state/actions, Glow и sun остаются без изменений | targeted regression units/smokes | +| AC7 | Existing configs round-trip без migration; schema/backend diff отсутствует | diff review + existing config tests | +| AC8 | Typecheck, unit, build и три bundle-копии зелёные; оба changelog и normative docs обновлены | gates + diff review | + +## 12. План автотестов + +### 12.1. Unit + +- `openingInnerFaceOffsetFromIndex` либо новый resolver: empty/one/shared room, + room-order permutations, 1/15/100 cm и non-finite guards; +- partition resolver: original/reversed endpoints, horizontal/vertical/diagonal; +- `openingVisibleMetrics` и SVG geometry для door/window/gate, оба `flip_v`; +- window glass, leaves/arcs and gate angle; full-depth jamb endpoints; +- Iso basis center and flip direction; +- negative checks для passage, lock badge и hit metrics. + +### 12.2. Browser smoke + +- разместить door/window/gate в толстой room wall и independent partition; +- сравнить preview с committed View; +- переставить rooms в fixture и развернуть partition endpoints без видимого + изменения; +- переключить `flip_v`, reload и Plan/View; +- проверить hosted Static и hidden Iso semantic geometry; +- убедиться, что lock/action и HA state продолжают работать. + +### 12.3. Golden + +Обновить/добавить semantic scenes: + +- centered door/window/gate на толстой room wall, Light; +- те же типы на diagonal partition, Dark; +- door/window с `flip_v: true` и gate `false/true` рядом; +- hidden Iso parity для center и gate direction. + +Golden semantic guard обязан до PNG сравнения проверить wall centerline, +visible-group center, jamb depth, `flip_v` и gate angle. Baseline принимается +только из reviewed Linux release artifact перед бетой; локальный accept +запрещён. + +## 13. Mutation guards + +| id | Что ломает | Что обязано покраснеть | +|---|---|---| +| `opening-symbol-default-uses-room-face` | возвращает room-order-dependent translation по умолчанию | AC1/AC2 unit + smoke | +| `opening-symbol-partition-follows-endpoints` | разворот partition endpoints меняет symbol side | AC2 permutation matrix | +| `opening-gate-flip-translates-leaves` | `flip_v` снова переносит ворота к грани | AC4 unit + golden guard | + +Минимум основной мутант выполняется локально: clean test зелёный, возвращённый +дефект красный. Реестр mutation gate обновляется по действующему формату проекта. + +## 14. Гейты реализации + +Обязательные: + +```text +npm run typecheck +npm test +npm run build +сверка dist / integration frontend / demo bundle +node scripts/smoke-select.mjs --base origin/dev --head HEAD +все выбранные opening/static/iso browser smokes +node scripts/mutation-gate.mjs --check +основной мутант #242 +npm run golden:verify +node scripts/check-docs.mjs +``` + +Backend pytest не требуется, пока backend/schema не затронуты. Отдельный +performance benchmark не требуется; действующий geometry/performance smoke +прогоняется, если его выберет `smoke-select`. + +## 15. Release-артефакты + +User-visible implementation commit одновременно обновляет: + +- `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md`; +- `docs/USER-GUIDE.ru.md` — default center и сохранённая семантика `flip_v`; +- `docs/WALL-THICKNESS.md` — centerline, full-depth jamb и render parity; +- `docs/ARCHITECTURE.md` и `docs/ISOMETRIC.md` — разделённые physical direction + и visual translation; +- затронутые opening golden scenes/semantic guards. + +Пользовательский docs screenshot не нужен: инструкция текстовая, а визуальное +доказательство даёт reviewed golden. Если реализация всё же изменит canonical +изображение guide, оно обновляется только workflow `Docs screenshots` и +`npm run docs:accept -- --reviewed --from=`. + +## 16. Откат + +Одна code revision возвращает прежний face translation. Данные и migration для +отката не нужны: формат `flip_v` не меняется. Откат снова возвращает заявленный +визуальный дефект, но не повреждает сохранённые планы. + +## 17. Риски + +1. **Центрирование створки ломает jamb depth.** Митигация: separate metrics и + AC1/AC4 с толщинами 1/15/100 cm. +2. **`flip_v` теряет сохранённое значение.** Митигация: round-trip не меняется, + оба значения покрыты unit/golden. +3. **Gate становится визуально симметричным и теряет направление.** Митигация: + angle использует side отдельно от translation, AC4. +4. **Flat исправлен, preview/Static/Iso расходятся.** Митигация: shared resolver + и AC5. +5. **Новая канонизация меняет не только представление host.** Митигация: + permutation matrix и явные negative checks cut/hit/light. + +## 18. Принятые предположения (техническое, менять свободно) + +1. Предпочтительно расширить resolved face отдельным полем visual offset либо + передавать policy в общий renderer; точные имена типов и helper свободны. +2. Каноническая локальная ось может строиться из нормализованного opening angle + или эквивалентного stable tangent; важен инвариант, а не конкретная формула. +3. Lock badge остаётся на существующей позиции: задача исправляет только + видимый symbol opening и не меняет action affordance. +4. Existing golden можно заменить новой сценой, если прежняя была создана + только для доказательства ошибочного inner-face offset. + +**Не являются предположениями:** default center, ручное edge alignment только +для `flip_v: true` у door/window, centered gate с меняющимся 10° направлением, +full-depth jambs и parity всех четырёх render surfaces — решения владельца. diff --git a/docs/specs/README.md b/docs/specs/README.md index 170506e6..247c0b46 100644 --- a/docs/specs/README.md +++ b/docs/specs/README.md @@ -124,6 +124,7 @@ GitHub Issues и GitHub Projects (v2) остаются единственным | [#234](https://github.com/Matysh/houseplan-card/issues/234) Толщина отрезка цепочки не расходится между превью и записью | [234-chain-segment-thickness.md](234-chain-segment-thickness.md) | | [#233](https://github.com/Matysh/houseplan-card/issues/233) Ресайз показывает внутренние размеры, а не осевые | [233-resize-inner-dimensions.md](233-resize-inner-dimensions.md) | | [#238](https://github.com/Matysh/houseplan-card/issues/238) Размеры проёма до внутренних физических границ | [238-opening-inner-distances.md](238-opening-inner-distances.md) | +| [#242](https://github.com/Matysh/houseplan-card/issues/242) Символ проёма по центру толщины стены | [242-opening-symbol-center.md](242-opening-symbol-center.md) | ## P3