# 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 не относятся к изменяемым предположениям.