diff --git a/docs/specs/123-corner-split-wall.md b/docs/specs/123-corner-split-wall.md new file mode 100644 index 00000000..77e01ea8 --- /dev/null +++ b/docs/specs/123-corner-split-wall.md @@ -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 не относятся к изменяемым предположениям. diff --git a/docs/specs/README.md b/docs/specs/README.md index 2cf54183..29533251 100644 --- a/docs/specs/README.md +++ b/docs/specs/README.md @@ -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) | ## Правило актуализации