mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-01 04:09:17 +00:00
434 lines
29 KiB
Markdown
434 lines
29 KiB
Markdown
# Issue #150 — Точная геометрия коллинеарного перепада толщины стен
|
||
|
||
- **Issue:** https://github.com/Matysh/houseplan-card/issues/150
|
||
- **Редакция:** r2 после `SPEC-REVIEW-150-r1`; статус задачи определяется только
|
||
метками 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 см` и
|
||
нажимает «Применить ко всем стенам комнаты».
|
||
|
||
**До → после, без терминов реализации:** сейчас после Split толщина выбранной
|
||
комнаты сохраняется правильно, но у точки разделителя не заканчивается и
|
||
визуально продолжается по внешним стенам соседней комнаты, для которых задано
|
||
`0`. После исправления выбранные участки имеют полные 10 см до точки
|
||
разделителя, соседняя комната остаётся без толщины, а граница видна как точная
|
||
ступень.
|
||
|
||
Первоначальное описание «примерно 5 вместо 10» уточнено после ревью. На
|
||
скриншоте внутренняя половина внешней стены действительно занимает 5 см, потому
|
||
что действующий контракт стены — `centreline ±½`; внешняя половина лежит на
|
||
белом поле и читается по штриховке. Полная глубина выбранного участка равна
|
||
10 см. Реальный дефект скриншота — отсутствие границы `10 → 0`: штриховка
|
||
10-сантиметрового body продолжается снаружи вдоль нулевого фасада соседа.
|
||
|
||
Задача поддерживает J4 и J6 из `docs/SCOPE.md`: штатное редактирование должно
|
||
создавать измеримо правдивую и стабильно читаемую архитектуру.
|
||
|
||
## 2. Красное воспроизведение на `dev` и подтверждённая причина
|
||
|
||
На конфигурационном уровне команда уже работает правильно:
|
||
|
||
- все допустимые atomic intervals выбранной комнаты получают `10 см`;
|
||
- общий разделитель получает `10 см` как одна физическая стена;
|
||
- внешние стены соседней комнаты остаются `0`.
|
||
|
||
Повторная проверка после `SPEC-REVIEW-150-r1` выполнена на текущем `dev`
|
||
`a05aa5d` двумя путями: через фактические обработчики Plan Editor
|
||
`_splitClick()` → `_wallThickClick()` → `_wallThickApply(true)` и через
|
||
исполняемые `setWallThicknessForRoom()` → `wallIntervals()` →
|
||
`wallBodiesGeometry()` с production-параметром `coordScale = 1000`.
|
||
|
||
Красный fixture: прямоугольник `[100,100]–[900,900]`, вертикальный Split в
|
||
`x = 500`, слева `10 см`, справа `0`, разделитель `10 см`. Данные дают:
|
||
|
||
- левый верхний interval: `cm = 10`;
|
||
- правый верхний interval: `cm = 0`;
|
||
- divider: `cm = 10`;
|
||
- ожидаемая полная глубина: `8.333333` render units.
|
||
|
||
Но текущий masonry path содержит верхнюю грань
|
||
`M 95.833… 95.833… L 900 95.833…`: глубина 10 см протянута через весь правый
|
||
нулевой interval до `x = 900`. Поэтому probe на правом участке, например
|
||
`[700, 96]`, ошибочно находится внутри masonry, хотя обязан быть вне его.
|
||
Сечение выбранного левого участка при этом полное — `8.333333` units; тест,
|
||
проверяющий только толстую сторону, дефект не обнаруживает.
|
||
|
||
Подтверждённая причина находится в `exteriorBoundaryProfile()`. Коэффициент
|
||
`t` точки разбиения нормирован на диапазон `0…1`, но сравнивается с `eps` в
|
||
render units. При обычных `pitch = 1/240` и `coordScale = 1000` используемый
|
||
`eps` равен примерно `0.6667`; проверки `t > eps && t < 1 - eps` становятся
|
||
невыполнимыми, а дедупликация `t` тем же допуском также размерностно неверна.
|
||
Breakpoint `x = 500` после boolean union не материализуется, и весь
|
||
коллинеарный edge получает depth того interval, на который попал его midpoint.
|
||
Воспроизведение обязано использовать production scale; проверка с
|
||
`coordScale = 1` не ловит ошибку.
|
||
|
||
Это не дефект сохранения команды «Применить ко всем», не изменение семантики
|
||
`centreline ±½` и не исправляется дублированием 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 / intentionally
|
||
degraded.** Safety floor обязателен: тот же сохранённый config на touch не может
|
||
получить другую физическую геометрию из-за pointer type. View и kiosk fully
|
||
supported и обязаны показывать тот же полный stepped body.
|
||
|
||
`prefers-reduced-motion` не затрагивается. Исправление geometry не добавляет
|
||
animation и не использует цвет как единственное доказательство толщины.
|
||
|
||
## 12. Acceptance criteria
|
||
|
||
1. **[unit]** Production-scale fixture из §2 сохраняет data profile `10 см` на
|
||
внешних стенах выбранной комнаты и shared divider, `0` на внешних стенах
|
||
соседа; тест явно передаёт `coordScale = 1000`.
|
||
2. **[unit]** Нормальное сечение каждого 10-сантиметрового участка до endpoint
|
||
имеет полные 10 см: 5 см наружу и 5 см внутрь, в численном пересчёте render
|
||
units.
|
||
3. **[unit + golden]** На endpoint разделителя фасад меняется `10 → 0` точной
|
||
поперечной ступенью; probes на нулевой стороне находятся вне masonry, нет
|
||
taper, переноса 10 см на соседа, зуба или щели.
|
||
4. **[unit]** Матрица `0 ↔ 10`, `10 ↔ 20`, `1 ↔ 100` и равных толщин корректна
|
||
в обоих направлениях, при обратном winding и перестановке/переименовании
|
||
комнат.
|
||
5. **[unit]** Shared divider сохраняет полную толщину, соединяется внутри и не
|
||
расширяет фасад наружу.
|
||
6. **[unit]** Paper, clean floor и displayed area следуют локальным depths и
|
||
точной ступени без клина/утечки пола.
|
||
7. **[smoke + golden]** Plan, View/kiosk, static и hidden Iso имеют одинаковый
|
||
wall/floor outline; Glow и солнце блокируются ровно нарисованным body.
|
||
8. **[unit + smoke]** Openings около/через transition используют локальные
|
||
depths и не создают ложных cuts или двойной fill.
|
||
9. **[unit + smoke]** Регрессии #123 (corner Split facade) и #141 (independent
|
||
junctions) остаются закрытыми; nested/courtyard/open-span fixtures не
|
||
меняются ошибочно.
|
||
10. **[unit + ревью кода]** Старый config исправляется вычисляемо без
|
||
save/migration и остаётся совместимым с rollback.
|
||
11. **[unit + ревью кода]** Structural geometry кешируется; ordinary HA tick не
|
||
повторяет topology.
|
||
12. **[unit + ревью кода]** Boolean failure остаётся fail-closed и не
|
||
воскрешает raw per-room rings.
|
||
|
||
## 13. Проверки и доказательства
|
||
|
||
### Unit — данные и точная геометрия
|
||
|
||
- красный production-scale fixture из §2: Split rectangle, all walls `0`,
|
||
all-room `10` для одной child room; assert effective intervals, shared wall
|
||
ownership, полную толстую сторону и отсутствие masonry probes на нулевой
|
||
стороне;
|
||
- отдельная нормализованная вариация с `coordScale = 1` не заменяет fixture с
|
||
`coordScale = 1000`;
|
||
- 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.
|