mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
423 lines
30 KiB
Markdown
423 lines
30 KiB
Markdown
# 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 не относятся к изменяемым предположениям.
|