Files
houseplan-card/docs/specs/123-corner-split-wall.md
T
2026-08-13 18:55:49 +00:00

423 lines
30 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Issue #123 — Split из вершины не меняет наружную геометрию стен
- **Issue:** https://github.com/Matysh/houseplan-card/issues/123
- **Редакция:** первая редакция для независимого ревью; статус задачи определяется
только метками issue
- **Тип / приоритет:** bug / P2
- **Оценка:** пользовательская ценность 7/10; ценность для разработки 8/10;
сложность и риск 7/10
- **Область:** Split в «Редакторе плана», наружные и общие толстые стены,
полный и статический рендеры, скрытая изометрия, чистая площадь, Glow и солнце
- **Модель данных:** без изменений и миграции
- **Связано:** `docs/WALL-THICKNESS.md`, `docs/CANVAS.md`,
`docs/UX-MODES.md`, `docs/TOUCH-SUPPORT.md`, `docs/ARCHITECTURE.md`
## 1. Сценарий и продуктовый контекст
**Персона:** администратор дома, который поддерживает архитектуру плана в
desktop-браузере.
**Поверхность и момент:** в «Редакторе плана» пользователь выбирает Split,
указывает комнату и начинает разрез точно из существующего угла комнаты. После
создания второй комнаты он при необходимости задаёт новой общей стене толщину.
**До → после, без терминов реализации:** сейчас разделение комнаты из угла
деформирует наружную стену и может вытянуть из фасада большой зуб; после
исправления фасад выглядит ровно как до разделения, а новая внутренняя стена
аккуратно примыкает к нему изнутри.
Задача поддерживает:
- **J4:** встроенный редактор должен позволять построить правдивый план без
внешних инструментов;
- **J6:** последующее изменение комнат не должно искажать уже созданную
архитектуру;
- **J1/J2/J3:** View, состояния и действия остаются на той же пространственной
модели, а не на отдельной исправленной только для редактора картинке.
## 2. Проблема и подтверждённая причина
`splitRoomPath()` правильно делит исходный полигон: площади двух частей дают
площадь исходной комнаты. Сохранение исходных интервалов толщины вокруг новых
дочерних рёбер также уже покрыто тестами.
Ошибка появляется позже, при построении физического тела стены.
`wallBodiesGeometry()` создаёт для каждой комнаты отдельное кольцо
`outset(room) − inset(room)`, затем объединяет кольца. Split из вершины заменяет
один исходный угол двумя углами дочерних комнат. Общая диагональная стена входит
в оба новых контура, поэтому их митры ошибочно становятся частью наружного
силуэта дома:
- даже при нулевой толщине разделителя bbox кладки отличается от исходного;
- при ненулевой толщине острый митр вытягивается наружу заметным зубом;
- нарисованная кладка одновременно используется как препятствие, поэтому это
не только косметический дефект редактора.
Диагностика на `dev` SHA `948f284` для прямоугольной комнаты с наружными
стенами 15 см:
| Состояние | bbox кладки |
|---|---|
| До Split | `[93.75, 93.75, 906.25, 706.25]` |
| Split из вершины, разделитель 0 см | `[92.9167, 93.75, 906.25, 706.25]` |
| Тот же разделитель 15 см | `[83.4702, 86.1502, 906.25, 706.25]` |
CSS-скругление обычного room border не исправляет физическое тело стены,
чистую площадь или световые препятствия. Превращение Split в независимую
`Перегородку` тоже неверно: Split обязан сохранить две комнаты, две возможные
HA-зоны и настоящую общую границу.
## 3. Решения владельца
Владелец принял defaults Q1–Q3 и приоритет P2 13.08.2026. Каноническая запись:
https://github.com/Matysh/houseplan-card/issues/123#issuecomment-5283843252
1. Наружный силуэт и наружная грань остаются такими же, как до Split, при любой
допустимой толщине внутренней стены. Общая стена заканчивается у внутренней
грани наружной кладки и не выступает за фасад.
2. Исправленная геометрия едина для Plan, View/киоска,
`houseplan-space-card`, скрытой изометрии, чистой площади, Glow и солнца.
3. Существующие планы исправляются вычисляемо сразу после обновления, без
миграции и перезаписи конфигурации.
## 4. Скоуп
В задачу входят:
1. любой валидный Split, у которого хотя бы один endpoint после действующего
wall-snap совпадает с вершиной исходной комнаты;
2. случаи, где endpoint-вершина выпуклая или вогнутая и где вершинами являются
один либо оба конца разреза;
3. нулевая и любая допустимая толщина новой общей стены, включая толщину больше
толщины примыкающей наружной стены;
4. нулевая, одинаковая и различная толщина двух наружных рёбер у вершины;
5. сохранение исходного внешнего силуэта, наружной грани и настоящего внешнего
угла;
6. чистое внутреннее примыкание без щели пола, наружного зуба или лишнего
митра дочерней комнаты;
7. единая физическая геометрия полного и статического рендеров, скрытой
изометрии, чистого пола и препятствий Glow/солнца;
8. автоматическое исправление уже сохранённых планов на чтении/рендере;
9. unit, browser smoke, visual golden, документация и RU/EN changelog.
## 5. Не входит в задачу
- запрет или предупреждение для Split из угла;
- изменение выбора комнаты, snapping, маршрута кликов, диалога новой комнаты,
правил имени/HA-зоны или выбора большей части;
- изменение модели `rooms`, `walls`, `open_spans`, `partitions` или
`wall_columns`;
- превращение общей стены Split в независимую `Перегородку`;
- изменение поведения инструмента `Перегородка`;
- новый тип стыка, пользовательская настройка cap/join или новые i18n-тексты;
- общая переработка всех пересечений стен, которые не воспроизводят дефект
endpoint-вершины;
- изменение дверей, окон, ворот, виртуальных границ или их конфигурации;
- миграция, schema version, backend и import/export;
- свободная 3D-геометрия или отдельная модель для изометрии.
## 6. Контракт поведения
### 6.1. Split остаётся Split
После подтверждения диалога:
- создаются две комнаты по действующим правилам `splitRoomPath()`;
- их площади по центровым контурам дают площадь исходной комнаты в пределах
действующего epsilon;
- большая часть сохраняет id, имя, HA-зону и устройства исходной комнаты;
- меньшая часть получает новый id и данные из диалога;
- линия разреза является общей производной границей комнат, а не записью в
`partitions[]`;
- Undo/Redo и сохранение работают как сейчас.
### 6.2. Наружная кладка
Пусть `before` — физический внешний контур комнаты непосредственно перед
Split, а `after` — внешний контур объединения получившихся комнат при тех же
наружных интервалах толщины.
Для endpoint в вершине:
1. `after` не содержит кладки снаружи `before`;
2. существующая наружная кладка не исчезает и не получает щель;
3. настоящий внешний угол и его bevel/mitre остаются такими же, как до Split;
4. искусственный угол дочерней комнаты между внешним ребром и линией Split не
участвует в формировании фасада;
5. правило действует и при нулевой наружной толщине: внутренняя стена
обрезается по границе пола и не выступает наружу;
6. разная толщина двух наружных плеч сохраняется без усреднения или
выравнивания.
Численное сравнение использует единый геометрический epsilon; визуально
различимый зуб, щель или ступень не может быть оправдан погрешностью.
### 6.3. Внутренняя общая стена
- При `cm > 0` тело общей стены заканчивается у внутренней грани наружной
кладки. Внутри пола оно сохраняет полную заданную толщину.
- При `cm = 0` Split остаётся общей осевой границей, но не меняет тело
примыкающих наружных стен.
- При толщине разделителя больше наружной лишняя ширина остаётся внутри дома;
она не расширяет фасад.
- Примыкание не оставляет между стенами участок чистого пола и не создаёт
двойную непрозрачность/штриховку.
- Обычный endpoint в середине стены и существующие L/T/virtual junctions не
меняют нынешний контракт.
### 6.4. Все поверхности видят одну геометрию
Исправленный результат обязан быть общим для:
- Plan editor;
- View и киоска;
- `houseplan-space-card`;
- скрытого изометрического Labs-режима;
- paper/room fill и чистой площади;
- физических препятствий Glow и солнечных лучей.
Запрещено исправить только SVG полного card renderer отдельной маской: нарисованная
и физическая кладка снова разойдутся.
### 6.5. Существующие планы
План с уже сохранёнными дочерними полигонами и интервалами толщины:
- отображается правильно после обновления без открытия редактора;
- не получает новый config key;
- не вызывает скрытое сохранение или оптимизацию;
- при простом открытии/рендере сохраняет конфигурацию побайтно;
- остаётся совместимым с предыдущей версией при откате.
## 7. Архитектурный контракт реализации
Конкретные helper names и разбиение файлов являются техническим выбором автора,
но должны соблюдаться следующие границы:
1. Фасадная кладка определяется exterior envelope объединения комнат и
`outer` atomic intervals, а не острыми углами каждого дочернего room ring.
2. `shared` interval Split строится как внутренняя физическая стена и
ограничивается внутренней стороной exterior envelope до объединения тел.
3. Настоящие внешние углы продолжают использовать действующий
mitre/bevel-контракт и `MITRE_LIMIT`; искусственный child corner на endpoint
общей стены не считается внешним углом.
4. Один канонический результат wall-body geometry потребляют drawing,
clean-floor projection и light/sun occlusion. Отдельные исправленные копии
геометрии по render surface запрещены.
5. Расчёт детерминирован относительно порядка комнат, их id и winding.
6. Исправление не мутирует `rooms`, `walls`, `open_spans` и не материализует
config при чтении.
7. Boolean/fallback path также соблюдает exterior invariant; при ошибке
операции нельзя молча вернуться к известной геометрии с наружным зубом.
8. Новые вычисления входят в существующий geometry fingerprint/cache и не
выполняются заново на каждом HA state tick.
Предполагаемые файлы реализации:
- `src/wall-thickness.ts`;
- при необходимости `src/iso-walls.ts`, только для потребления общей
исправленной геометрии без второй модели;
- `test/wall-thickness.test.mjs` и при необходимости `test/logic.test.mjs`;
- новый узкий `demo/smoke_split_corner_wall.mjs` либо эквивалент;
- golden scenario/baseline по правилам review;
- документы из раздела 13.
## 8. Модель данных, compatibility и миграция
Форматы не меняются:
```ts
interface RoomCfg {
poly?: number[][];
}
interface WallEntry {
key: string;
cm: number;
a?: number[];
b?: number[];
}
```
- новых полей и compatibility aliases нет;
- legacy midpoint-only wall keys остаются читаемыми;
- materialisation/normalisation при явном редактировании сохраняет текущий
контракт;
- schema version и backend validation не меняются;
- прямой и обратной миграции нет.
## 9. UX, i18n, accessibility и touch
Новых controls, диалогов, текстов, фокуса или keyboard semantics нет. Поэтому
новые i18n-ключи не требуются.
Plan editor остаётся desktop-first. Touch editor — **best effort**, но safety
floor обязателен: Split на touch не может сохранить другую геометрию из-за
pointer cancellation или второго касания.
View и киоск полностью поддерживаются: исправленный фасад, room fills и
световые препятствия должны совпадать с desktop. `prefers-reduced-motion` не
затрагивается.
## 10. Критерии приёмки
- **AC1 (`unit`):** Split из вершины по-прежнему создаёт точное разбиение:
площади частей суммируются в исходную, большая часть сохраняет identity,
линия разреза не появляется в `partitions[]`.
- **AC2 (`unit`):** для прямоугольника из воспроизведения внешний wall-body
после Split при разделителе 0 см геометрически совпадает с исходным фасадом;
текущая bbox-регрессия отсутствует.
- **AC3 (`unit`):** тот же инвариант выполняется при толщине разделителя 1, 15
и 100 см и при наружной толщине 0, 15 и 100 см; никакая точка кладки не
выступает за допустимый исходный exterior envelope.
- **AC4 (`unit`):** матрица включает острый и тупой угол разреза, один и два
endpoint-угла, выпуклую и вогнутую вершину, а также разные толщины двух
наружных плеч. Результат не зависит от room order, id и winding.
- **AC5 (`unit` + `golden`):** внутренний разделитель примыкает к внутренней
грани наружной стены без щели, зуба, ступени и двойной штриховки; настоящая
форма exterior corner до/после визуально идентична.
- **AC6 (`unit`):** clean-floor geometry двух комнат учитывает внутреннюю стену
только внутри дома; суммарная потеря пола соответствует внутреннему телу
разделителя и не включает наружный spike.
- **AC7 (`unit` + `smoke`):** Glow и солнце используют то же исправленное
препятствие: свет не проходит через примыкание, но и не блокируется
несуществующей кладкой за фасадом.
- **AC8 (`smoke` + `golden`):** Plan, View/киоск и `houseplan-space-card`
показывают один фасад для fixture #123; скрытая изометрия не возвращает зуб и
не вводит вторую геометрию.
- **AC9 (`unit` + `smoke`):** сохранённые ранее room polygons и wall entries
исправляются без записи, миграции или изменения сериализованного config.
- **AC10 (`unit`):** обычный Split от середины стены, wall materialisation,
partial shared intervals, virtual-T mitre, openings и independent partitions
сохраняют действующее поведение.
- **AC11 (ревью кода):** новый exterior/shared расчёт использует существующее
geometry caching; HA state tick не пересчитывает topology. Существующие
`performance_smoke` и large-house benchmark остаются общим предрелизным
подтверждением, а не отдельным типом доказательства AC.
- **AC12 (`typecheck` + `unit` + `build`):** быстрые гейты зелёные; три bundle
snapshot побайтно совпадают.
- **AC13 (ревью документации):** RU/EN changelog и пользовательские документы
описывают исправление как сохранение фасада при Split, не как изменение
инструмента `Перегородка` или новый 3D-контракт.
## 11. План автотестов
### 11.1. Unit
1. Зафиксировать исходную комнату и её exterior wall-body geometry.
2. Выполнить `splitRoomPath()` из точной вершины к середине другого ребра,
materialise/normalise текущие wall intervals и применить толщину общей
стене.
3. Сравнивать не только bbox, а boolean difference exterior geometry до/после:
лишняя и потерянная фасадная площадь должны быть меньше epsilon.
4. Отдельно проверить полное покрытие внутреннего примыкания и отсутствие тела
разделителя снаружи исходного exterior envelope.
5. Повторить матрицу AC3/AC4, включая reverse winding и перестановку rooms.
6. Проверить clean-floor area и барьеры `wallBodiesGeometry()`.
7. Regression suite: partial shared wall, virtual T, nested room, opening cut,
materialisation после Split и independent body union.
8. Проверить отсутствие мутации входных rooms/walls и сериализованного fixture.
Тест из пункта 3 обязан краснеть на `948f284`, а не только подтверждать новое
вспомогательное вычисление.
### 11.2. Browser smoke
Один узкий сценарий на production bundle:
1. создать прямоугольную комнату с наружными стенами 15 см;
2. сохранить внешний SVG bbox/path signature;
3. выполнить пользовательский путь Split из угла и подтвердить новую комнату;
4. проверить 0 см, затем 15 см и 100 см общей стены;
5. переключить Plan → View, киоск, static card и Labs iso;
6. доказать одинаковый exterior bbox, отсутствие наружного зуба и наличие
внутренней стены;
7. проверить Undo/Redo и отсутствие скрытой config write;
8. поставить источник света/солнце у примыкания и подтвердить общий occluder.
По текущему процессу smoke добавляется при реализации, но запускается перед
бетой; в цикле реализации выполняются только typecheck, unit и build.
### 11.3. Golden
Добавить deterministic scenario `split-corner-wall` либо эквивалент с тремя
кадрами:
1. исходный внешний угол;
2. Split из угла с тонкой общей границей;
3. тот же Split с толстой общей стеной.
Кадр должен включать Plan и View либо паритетные full/static поверхности.
Baseline принимается только через `npm run golden:accept -- --reviewed` по
полному Linux CI artifact с обязательными trailers `Release:` и
`Baseline-Reviewed:`. Принятие ради зелёного CI запрещено.
### 11.4. Performance и backend
Backend не меняется. Отдельный backend gate не нужен.
Перед бетой выполняются общий `performance_smoke` и целевой large-house render
benchmark, если реализация меняет асимптотику wall topology. Отдельного нового
численного бюджета нет: действующие бюджеты и exact-SHA CI остаются
release-blocking.
## 12. Производительность и безопасность
**Производительность:** wall topology — cached structural input. Исправление не
должно переносить boolean union/difference в HA state hot path, создавать
отдельный расчёт для каждой render surface или обходить geometry fingerprint.
**Безопасность:** HA service calls, locks, permissions и destructive actions не
затрагиваются. Главный safety-риск здесь — расхождение нарисованного пола и
физического препятствия; единая canonical geometry обязательна.
## 13. Документация и release-артефакты
В том же user-visible implementation commit обновить:
- `docs/CHANGELOG.md`;
- `docs/CHANGELOG.ru.md`;
- `docs/USER-GUIDE.ru.md` — раздел Split/толстые стены: Split из вершины не
меняет фасад, внутренняя общая стена примыкает изнутри;
- `docs/WALL-THICKNESS.md` — exterior/shared junction invariant;
- `docs/ARCHITECTURE.md` — каноническое разделение exterior shell и shared
divider body без второй модели;
- `docs/STATUS.md` — текущая релизная линия после фактической реализации.
Visual change требует targeted golden из §11.3 и review полного Linux artifact.
Отдельного security artifact нет. Performance подтверждается §11.4. Issue
должна пройти опубликованную beta до stable release.
## 14. Риски и снижение
| Риск | Вероятность / ущерб | Снижение |
|---|---|---|
| Новый exterior shell сотрёт shared/nested wall | средняя / высокий | строить outer/shared отдельно; nested и partial regression unit |
| Разная толщина наружных плеч усреднится | средняя / средний | atomic interval matrix и точное сравнение фасада |
| Появится щель между divider и наружной стеной | средняя / высокий | coverage/difference unit плюс golden крупного угла |
| Drawing исправится, Glow/солнце останутся старыми | средняя / высокий | один canonical body и occlusion smoke |
| Room order/winding изменит boolean result | средняя / высокий | permutation/reverse-winding unit |
| Existing config перепишется при чтении | низкая / высокий | immutable fixture и browser no-write assertion |
| Geometry hot path станет дороже | средняя / средний | существующий fingerprint/cache и pre-beta performance |
| Исправление сломает обычные T/virtual/opening joins | средняя / высокий | полный целевой regression unit set |
## 15. Откат
Откат — revert implementation commit. Данные и schema не мигрируются, поэтому
планы остаются читаемыми. После отката вернётся прежний визуальный дефект, но
никакого восстановления конфигурации не потребуется. Feature flag и обратная
миграция не нужны.
## 16. Принятые технические предположения — можно менять без пересмотра продукта
1. Endpoint считается вершиной по уже существующему wall-snap/geometry epsilon;
отдельный пользовательский tolerance не вводится.
2. Предпочтительная реализация разделяет exterior shell и shared wall bodies,
но конкретная boolean decomposition может быть другой, если AC доказываются.
3. `wallBodiesGeometry()` остаётся canonical entry point; имя и внутренние
helpers можно менять.
4. Имя smoke/golden scenario не является частью продукта.
5. Новая настройка cap/join не нужна: поведение однозначно следует решениям
владельца.
6. Решения Q1–Q3 из раздела 3 не относятся к изменяемым предположениям.