docs(spec): define wall thickness transitions

Issue: #150
User-Visible: no
This commit is contained in:
Sergey Matyunin
2026-08-18 17:36:54 +03:00
parent c27185cfa4
commit 2667f71bd6
2 changed files with 390 additions and 0 deletions
+389
View File
@@ -0,0 +1,389 @@
# Issue #150 — Точная геометрия коллинеарного перепада толщины стен
- **Issue:** https://github.com/Matysh/houseplan-card/issues/150
- **Редакция:** первая редакция для независимого ревью; статус задачи определяется
только метками issue
- **Тип / приоритет:** bug / P2
- **Оценка:** пользовательская ценность 8/10; ценность для разработки 8/10;
сложность 7/10, риск 8/10
- **Область:** atomic wall intervals, внешний фасад, wall body/paper/clean floor,
Plan, View/киоск, static renderer, hidden Iso, Glow и солнце
- **Модель данных:** без изменений и миграции
- **Связано:** `docs/WALL-THICKNESS.md`, `docs/ARCHITECTURE.md`,
`docs/CANVAS.md`, `docs/ISOMETRIC.md`; #123 и #141
## 1. Сценарий и продуктовый контекст
**Персона:** администратор дома, исправляющий архитектуру плана в desktop Plan
Editor.
**Поверхность и момент:** после Split исходной комнаты с нулевыми стенами
пользователь выбирает внешнюю стену одной дочерней комнаты, задаёт `10 см` и
нажимает «Применить ко всем стенам комнаты».
**До → после, без терминов реализации:** сейчас стены выбранной комнаты рядом
с разделителем выглядят примерно вдвое тоньше указанного значения; после
исправления они имеют полные 10 см до самой точки разделителя, соседняя комната
остаётся без толщины, а переход между участками виден как точная ступень.
Задача поддерживает J4 и J6 из `docs/SCOPE.md`: штатное редактирование должно
создавать измеримо правдивую и стабильно читаемую архитектуру.
## 2. Воспроизведение и подтверждённая причина
На конфигурационном уровне команда уже работает правильно:
- все допустимые atomic intervals выбранной комнаты получают `10 см`;
- общий разделитель получает `10 см` как одна физическая стена;
- внешние стены соседней комнаты остаются `0`.
Ошибка появляется при построении канонической геометрии. После Split общий
внешний фасад является одной collinear boundary, но на ней встречаются соседние
atomic intervals `10 → 0`. Boolean union комнат схлопывает child-room vertex, а
последующая variable-offset exterior envelope не сохраняет локальный перепад
как жёсткую поперечную ступень. В результате wall shell у endpoint разделителя
сужается/интерполируется и выглядит как половина заданной толщины.
Это не дефект сохранения команды «Применить ко всем» и не исправляется
дублированием wall key, CSS stroke или визуальной накладкой только в Plan.
## 3. Решения владельца
Владелец 15.08.2026 принял defaults Q1–Q4. Каноническая запись:
https://github.com/Matysh/houseplan-card/issues/150#issuecomment-5301107134
1. Выбранная комната получает полные 10 см на внешних участках вплоть до
разделителя; общий разделитель — 10 см; остальные внешние стены соседней
комнаты — 0.
2. На общем внешнем фасаде переход точный и ступенчатый на endpoint
разделителя, без сужения, растягивания на соседа или taper.
3. Инвариант действует для любых соседних коллинеарных интервалов разной
толщины (`10 → 0`, `10 → 20` и т. п.).
4. Исправляется общая каноническая геометрия Plan, View/киоска, static, hidden
Iso/floor footprint и препятствий Glow/солнца; существующие конфиги
исправляются без миграции.
## 4. Скоуп
В задачу входят:
1. все пары соседних collinear exterior atomic intervals с неравной допустимой
толщиной, включая один нулевой;
2. точная граница перепада в сохранённом atomic endpoint;
3. сценарий endpoint разделителя после Split и аналогичная топология,
независимо от направления, winding, room id и порядка комнат;
4. полная толщина каждого участка, корректные inward/outward faces и clean
floor по обе стороны перехода;
5. корректное соединение с общей стеной/разделителем, заканчивающейся в той же
точке;
6. единая masonry/paper geometry для всех render/light consumers;
7. уже сохранённые планы без config rewrite;
8. unit, browser smoke, visual golden, документация и RU/EN changelog.
## 5. Не входит в задачу
- изменение UX или семантики «Применить ко всем стенам комнаты»;
- изменение выбора atomic interval, диапазона толщины или wall key format;
- автоматическое выравнивание толщины соседних комнат;
- плавный bevel/taper между коллинеарными участками;
- новая пользовательская настройка вида перехода;
- изменение Split, Merge, Resize или Boundary как редакторских операций;
- исправление произвольных near-miss, X-crossing или независимых junctions,
которые относятся к #141;
- изменение фасадного инварианта corner Split из #123;
- миграция, schema version, backend, import/export или materialisation на read;
- отдельная wall model для Iso или света;
- изменение opening semantics, кроме обязательного отсутствия регрессии на
opening, расположенном рядом с перепадом.
## 6. Каноническая математическая семантика
### 6.1. Atomic intervals
Максимальная collinear boundary разбивается во всех структурных точках:
- endpoints исходных room edges и дочерних edges после Split;
- endpoints точных `WallEntry.a/b` и совместимых legacy breaks;
- начало/конец `open_spans` и physical opening coverage, когда они создают
действующую атомарную границу;
- junctions и точки изменения effective thickness.
Каждый получившийся solid interval имеет постоянную effective thickness `cm_i`
и half-depth
```text
h_i = wallCmToUnits(cm_i, cell_cm, grid_pitch) / 2
```
Zero interval (`cm_i = 0`) остаётся границей пола, но не создаёт masonry body.
Ни порядок комнат, ни midpoint compacted key не могут удалить точный breakpoint.
### 6.2. Полная толщина участка
Для любой точки внутри solid interval, расположенной дальше geometry epsilon
от его endpoints и openings, сечение wall body по нормали к centreline обязано
иметь:
- полную глубину `2 × h_i`, соответствующую `cm_i`;
- внутреннюю грань на `h_i` от centreline;
- внешнюю грань на `h_i` от centreline;
- отсутствие удвоения или потери alpha/hatch из-за перекрывающихся room rings.
Допуск используется только для численной устойчивости и не может объяснять
видимое сужение 10 см до 5 см.
### 6.3. Жёсткая ступень `h1 → h2`
Пусть два соседних collinear intervals встречаются в точке `P`, имеют
half-depth `h1` и `h2`, `h1 != h2`.
1. До `P` обе faces находятся ровно на `±h1`; после `P` — на `±h2`.
2. Разность глубин закрывается в `P` сегментами, перпендикулярными centreline.
3. На ненулевой длине вокруг `P` нет линейной интерполяции, mitre вдоль прямой,
taper, усреднения, overshoot или распространения большего `h` на соседа.
4. Breakpoint в результирующей geometry совпадает с `P` в пределах общего
geometry epsilon.
5. Правило симметрично для `0 → h`, `h → 0`, `h1 → h2`, обратного направления
boundary и любого winding.
6. Boolean cleanup может объединить совпадающие рёбра, но не имеет права
удалить поперечные faces ступени или сдвинуть их от `P`.
### 6.4. Endpoint разделителя
Если в `P` к фасаду примыкает shared wall после Split:
- shared wall сохраняет полную назначенную толщину и одну physical identity;
- её тело заканчивается/соединяется с внутренней частью фасада без щели;
- она не выступает наружу и не изменяет exterior face фасада, как требует
контракт #123;
- внешний interval выбранной комнаты сохраняет `h_selected` до `P`;
- соседний внешний interval начинает собственный `h_neighbor` непосредственно
после `P`;
- соединение не оставляет triangular notch, half-depth strip, двойной hatch или
ложный passage для света.
Для сценария issue это означает `10 см exterior → P → 0 см exterior`, при этом
shared divider в `P` имеет `10 см` и остаётся внутри facade envelope.
### 6.5. Clean floor и paper
- paper footprint растёт наружу по локальному `h_i` и повторяет ту же ступень;
- clean floor каждой комнаты отступает внутрь по её effective local half-depth;
- выбранная комната не получает клин пола между 10-сантиметровой стеной и
разделителем;
- соседняя комната с `0` сохраняет floor до centreline своего внешнего участка;
- displayed area вычисляется из исправленного clean floor без изменения
centreline room polygon или stored area data;
- courtyard/nested geometry сохраняет действующий evenodd/topology contract.
## 7. Архитектурный контракт реализации
Конкретный helper и boolean primitive выбирает автор, но обязательны границы:
1. `wallIntervals()`/эквивалент остаётся каноническим источником interval
endpoints и effective `cm`.
2. Exterior profile обязан материализовать каждый thickness breakpoint до
offset/union и сохранить две независимые face depths в этой точке. Обычный
polygon vertex с одним offset, не способный выразить discontinuity, не
является достаточным представлением.
3. Допустимые решения — interval slabs/quads с точными butt faces, профиль с
duplicated transition vertices/explicit step faces или эквивалентная
детерминированная конструкция. Рисование corrected CSS stroke поверх
ошибочного body запрещено.
4. Настоящие non-collinear фасадные углы продолжают использовать действующий
bounded mitre/bevel (`MITRE_LIMIT`); collinear thickness step не является
углом для mitre.
5. Shared/internal bodies сначала ограничиваются interior side exterior
envelope, затем объединяются с точным shell — инвариант #123 не ослабляется.
6. Один результат `wallBodiesGeometry()`/эквивалента порождает masonry path,
paper path, clean-floor relation и light/sun barriers. Static и full card
не имеют отдельных корректирующих веток.
7. Iso потребляет тот же canonical masonry/floor footprint и экструдирует уже
правильный stepped outline; самостоятельное сглаживание ступени запрещено.
8. Порядок комнат, stable id rename, winding и направление интервала не меняют
геометрическое множество результата.
9. Новая топология входит в существующий structural fingerprint/cache. HA
state tick, hover и activity effects не запускают boolean rebuild.
10. При boolean failure действует существующий fail-closed contract: нельзя
воскресить raw per-room rings, известные half-depth/facade дефектом.
## 8. Openings, виртуальные границы и junction regression contract
Задача не меняет типы проёмов, но исправленная geometry обязана сохранять:
- opening association с фактическим atomic interval;
- full-depth cut и jamb по локальной толщине каждой покрытой части;
- при crossing thickness transition — точный stepped tunnel без cut соседнего
interval и без neutral-paper leak;
- window/door/gate light semantics из `docs/WALL-THICKNESS.md`;
- virtual `open_span` как реальный break, а не мост для переноса большей
толщины;
- примыкания независимых partitions/drafts/columns по контракту #141;
- corner Split exterior envelope по контракту #123.
Если opening не может быть однозначно связан после исправления, действует
существующий fail-dark/fail-closed путь; новая nearest-wall эвристика не
вводится.
## 9. Поверхности-потребители
Одинаковое геометрическое множество требуется для:
1. Plan Editor, включая hover/selection Thickness поверх фактического body;
2. View и kiosk full renderer;
3. `houseplan-space-card`/static renderer;
4. hidden Iso wall volume, top face и floor slab footprint;
5. room fill и clean-floor clipping;
6. displayed clean area;
7. Glow/spill occluders и placement checks;
8. sun barriers/wedges;
9. paper/content frame, когда внешняя половина стены расширяет план.
Разница только из-за presentation policy (например, hatch suppression при
малой экранной толщине) допустима; physical outline и проходы должны совпадать.
## 10. Модель данных, compatibility и миграция
Формат остаётся прежним:
```ts
interface WallEntry {
key: string;
cm: number;
a?: number[];
b?: number[];
}
```
- новые поля, aliases и schema version не добавляются;
- точные `a/b` и legacy midpoint-only keys читаются по действующему contract;
- normalisation/materialisation выполняются только при явном редактировании,
как сейчас;
- открытие/рендер старого плана не вызывает save и не переписывает config;
- backend validation, import/export и wire payload не меняются;
- rollback возвращает прежний renderer без потери данных, хотя визуальный
дефект снова проявится.
## 11. UX, i18n, accessibility и touch
Controls, dialogs, tool sequence, labels и focus не меняются. Новые locale keys
не требуются.
Plan Editor остаётся desktop-first. Touch editor — best effort, но safety floor
обязателен: тот же сохранённый config на touch не может получить другую
физическую геометрию из-за pointer type. View и kiosk fully supported и обязаны
показывать тот же полный stepped body.
`prefers-reduced-motion` не затрагивается. Исправление geometry не добавляет
animation и не использует цвет как единственное доказательство толщины.
## 12. Acceptance criteria
1. Точный сценарий issue сохраняет data profile `10 см` на всех внешних стенах
выбранной комнаты и shared divider, `0` на остальных внешних стенах соседа.
2. Нормальное сечение каждого 10-сантиметрового участка до endpoint имеет
полные 10 см: 5 см наружу и 5 см внутрь, в численном пересчёте render units.
3. На endpoint разделителя фасад меняется `10 → 0` точной поперечной ступенью;
нет taper, half-depth strip, переноса 10 см на соседа, зуба или щели.
4. Матрица `0 ↔ 10`, `10 ↔ 20`, `1 ↔ 100` и равных толщин корректна в обоих
направлениях, при обратном winding и перестановке/переименовании комнат.
5. Shared divider сохраняет свою полную толщину, соединяется внутри и не
расширяет фасад наружу.
6. Paper, clean floor и displayed area следуют локальным depths и точной
ступени без клина/утечки пола.
7. Plan, View/kiosk, static и hidden Iso имеют одинаковый wall/floor outline;
Glow и солнце блокируются ровно нарисованным body.
8. Openings около/через transition используют локальные depths и не создают
ложных cuts или двойной fill.
9. Регрессии #123 (corner Split facade) и #141 (independent junctions) остаются
закрытыми; nested/courtyard/open-span fixtures не меняются ошибочно.
10. Старый config исправляется вычисляемо без save/migration и остаётся
совместимым с rollback.
11. Structural geometry кешируется; ordinary HA tick не повторяет topology.
12. Boolean failure остаётся fail-closed, не показывает известную ошибочную
half-depth геометрию.
## 13. Проверки и доказательства
### Unit — данные и точная геометрия
- fixture из issue: Split rectangle, all walls `0`, all-room `10` для одной
child room; assert effective intervals и shared wall ownership;
- cross-section/point containment на interior участках доказывает exact
`±h_i`, а не только сравнивает bbox/скриншот;
- transition faces совпадают с divider endpoint и перпендикулярны centreline;
- parameter matrix `0/1/10/20/100`, оба направления, horizontal/vertical/45°;
- room order/id/winding invariance через symmetric-difference area;
- equal neighbouring depths не создают лишнюю seam/step;
- shared divider: no exterior difference сверх локального exterior profile;
- paper geometry и clean-floor areas по обе стороны;
- opening до, после и пересекающий transition;
- #123 corner Split, #141 virtual/independent junction, open spans,
nested/courtyard и boolean failure regressions;
- full/static/Iso consumers получают один fingerprinted canonical result.
### Browser smoke
1. В Plan повторить пять шагов issue и визуально/инструментом проверить каждую
стену выбранной и соседней комнаты.
2. Переключить Plan → View → kiosk/static fixture → hidden Iso без изменения
silhouette/step.
3. Проверить Undo/Redo all-room thickness: одна команда возвращает прежний
профиль, повтор применяет исправленную geometry.
4. Добавить opening рядом с transition и проверить cut/fill/Glow.
5. Изменить HA light state: topology fingerprint и masonry path не меняются.
### Golden и performance
- golden исходного issue крупным планом в Plan и View;
- static и hidden Iso того же fixture;
- `10 → 20`, 45° transition, opening near transition, light/sun state;
- diff review проверяет именно ступень и полную толщину, а не массовое
обновление anti-aliasing;
- performance evidence подтверждает отсутствие geometry rebuild на HA ticks и
отсутствие существенной регрессии canonical pass на representative plan.
Golden, smoke и performance запускаются в release gate перед бетой; цикл
реализации — `typecheck`, `unit`, `build` по процессу. Полный HA harness
каноничен в Linux CI.
## 14. Release-артефакты
В том же user-visible коммите реализации обязательны:
- `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md`;
- `docs/WALL-THICKNESS.md` — явный контракт collinear discontinuity;
- `docs/ARCHITECTURE.md` — canonical interval/step representation и consumers;
- при необходимости `docs/ISOMETRIC.md` и `docs/CANVAS.md`, если комментарии о
footprint требуют уточнения;
- `docs/USER-GUIDE.ru.md` — только если там описан отличавшийся результат
all-room thickness; новых controls нет;
- unit fixture, smoke scenario и принятые golden/baselines;
- `docs/TESTING.md`, если меняется или добавляется release scenario.
Новых i18n, backend/security artifacts не требуется. Если реализация потребует
формат данных или отдельную пользовательскую настройку, задача возвращается
владельцу до кода.
## 15. Риски и rollback
Основные риски: новый внешний зуб вместо half-depth, потеря floor hole,
расхождение full/static/Iso, ошибочный opening cut и рост boolean cost. Они
закрываются symmetric-difference, cross-section, consumer parity и performance
проверками раздела 13.
Rollback — возврат geometry-кода и документации без миграции. Сохранённые
`walls` не изменяются и остаются читаемыми обеими версиями.
## 16. Принятые технические предположения
1. Breakpoint уже доступен из точных atomic intervals; исправлению не нужен
новый persisted vertex или WallEntry field.
2. Ступень существует на обеих faces wall body. При `h2 = 0` обе faces второго
участка сходятся на centreline ровно в `P`, а не заранее.
3. Поперечная face в transition является частью physical boundary, но не новым
самостоятельным wall object и не target инструмента Thickness.
4. Opening, пересекающий transition, остаётся допустимым и режет union реальных
локальных depths; запрет такого opening не является исправлением.
5. Existing structural cache key на `_cfgEpoch`/complete geometry расширяется
только если текущий fingerprint не содержит всех endpoints/thicknesses.
+1
View File
@@ -91,6 +91,7 @@ GitHub Issues и GitHub Projects (v2) остаются единственным
| [#137](https://github.com/Matysh/houseplan-card/issues/137) Узлы и линии привязки в редакторе Плана | [137-plan-snap-overlay.md](137-plan-snap-overlay.md) |
| [#141](https://github.com/Matysh/houseplan-card/issues/141) Бесшовные стыки перегородок и открытых контуров | [141-wall-junctions.md](141-wall-junctions.md) |
| [#157](https://github.com/Matysh/houseplan-card/issues/157) Тип проёма «Открытый проём» | [157-open-passage.md](157-open-passage.md) |
| [#150](https://github.com/Matysh/houseplan-card/issues/150) Точная геометрия коллинеарного перепада толщины | [150-wall-thickness-transition.md](150-wall-thickness-transition.md) |
## Правило актуализации