Specify corner split wall geometry

Issue: #123
User-Visible: no
This commit is contained in:
Sergey Matyunin
2026-08-13 18:55:49 +00:00
committed by claude[bot]
parent 516257e322
commit a43602934c
2 changed files with 422 additions and 0 deletions
+421
View File
@@ -0,0 +1,421 @@
# 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 (`performance` + ревью кода):** новый exterior/shared расчёт использует
существующее geometry caching; HA state tick не пересчитывает topology, а
общий pre-beta performance gate остаётся зелёным.
- **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 не относятся к изменяемым предположениям.
+1
View File
@@ -77,6 +77,7 @@ GitHub Issues и GitHub Projects (v2) остаются единственным
| [#90](https://github.com/Matysh/houseplan-card/issues/90) Управляемый бейдж со значением | [090-device-value-badge.md](090-device-value-badge.md) |
| [#94](https://github.com/Matysh/houseplan-card/issues/94) Универсальное действие «Переключить состояние» | [094-universal-state-toggle.md](094-universal-state-toggle.md) |
| [#101](https://github.com/Matysh/houseplan-card/issues/101) Плавный переход View ↔ редакторы | [101-view-editor-transition.md](101-view-editor-transition.md) |
| [#123](https://github.com/Matysh/houseplan-card/issues/123) Split из вершины не меняет наружную геометрию стен | [123-corner-split-wall.md](123-corner-split-wall.md) |
## Правило актуализации