29 KiB
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
- Наружный силуэт и наружная грань остаются такими же, как до Split, при любой допустимой толщине внутренней стены. Общая стена заканчивается у внутренней грани наружной кладки и не выступает за фасад.
- Исправленная геометрия едина для Plan, View/киоска,
houseplan-space-card, скрытой изометрии, чистой площади, Glow и солнца. - Существующие планы исправляются вычисляемо сразу после обновления, без миграции и перезаписи конфигурации.
4. Скоуп
В задачу входят:
- любой валидный Split, у которого хотя бы один endpoint после действующего wall-snap совпадает с вершиной исходной комнаты;
- случаи, где endpoint-вершина выпуклая или вогнутая и где вершинами являются один либо оба конца разреза;
- нулевая и любая допустимая толщина новой общей стены, включая толщину больше толщины примыкающей наружной стены;
- нулевая, одинаковая и различная толщина двух наружных рёбер у вершины;
- сохранение исходного внешнего силуэта, наружной грани и настоящего внешнего угла;
- чистое внутреннее примыкание без щели пола, наружного зуба или лишнего митра дочерней комнаты;
- единая физическая геометрия полного и статического рендеров, скрытой изометрии, чистого пола и препятствий Glow/солнца;
- автоматическое исправление уже сохранённых планов на чтении/рендере;
- 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 в вершине:
afterне содержит кладки снаружиbefore;- существующая наружная кладка не исчезает и не получает щель;
- настоящий внешний угол и его bevel/mitre остаются такими же, как до Split;
- искусственный угол дочерней комнаты между внешним ребром и линией Split не участвует в формировании фасада;
- правило действует и при нулевой наружной толщине: внутренняя стена обрезается по границе пола и не выступает наружу;
- разная толщина двух наружных плеч сохраняется без усреднения или выравнивания.
Численное сравнение использует единый геометрический epsilon; визуально различимый зуб, щель или ступень не может быть оправдан погрешностью.
6.3. Внутренняя общая стена
- При
cm > 0тело общей стены заканчивается у внутренней грани наружной кладки. Внутри пола оно сохраняет полную заданную толщину. - При
cm = 0Split остаётся общей осевой границей, но не меняет тело примыкающих наружных стен. - При толщине разделителя больше наружной лишняя ширина остаётся внутри дома; она не расширяет фасад.
- Примыкание не оставляет между стенами участок чистого пола и не создаёт двойную непрозрачность/штриховку.
- Обычный 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 и разбиение файлов являются техническим выбором автора, но должны соблюдаться следующие границы:
- Фасадная кладка определяется exterior envelope объединения комнат и
outeratomic intervals, а не острыми углами каждого дочернего room ring. sharedinterval Split строится как внутренняя физическая стена и ограничивается внутренней стороной exterior envelope до объединения тел.- Настоящие внешние углы продолжают использовать действующий
mitre/bevel-контракт и
MITRE_LIMIT; искусственный child corner на endpoint общей стены не считается внешним углом. - Один канонический результат wall-body geometry потребляют drawing, clean-floor projection и light/sun occlusion. Отдельные исправленные копии геометрии по render surface запрещены.
- Расчёт детерминирован относительно порядка комнат, их id и winding.
- Исправление не мутирует
rooms,walls,open_spansи не материализует config при чтении. - Boolean/fallback path также соблюдает exterior invariant; при ошибке операции нельзя молча вернуться к известной геометрии с наружным зубом.
- Новые вычисления входят в существующий 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 и миграция
Форматы не меняются:
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
- Зафиксировать исходную комнату и её exterior wall-body geometry.
- Выполнить
splitRoomPath()из точной вершины к середине другого ребра, materialise/normalise текущие wall intervals и применить толщину общей стене. - Сравнивать не только bbox, а boolean difference exterior geometry до/после: лишняя и потерянная фасадная площадь должны быть меньше epsilon.
- Отдельно проверить полное покрытие внутреннего примыкания и отсутствие тела разделителя снаружи исходного exterior envelope.
- Повторить матрицу AC3/AC4, включая reverse winding и перестановку rooms.
- Проверить clean-floor area и барьеры
wallBodiesGeometry(). - Regression suite: partial shared wall, virtual T, nested room, opening cut, materialisation после Split и independent body union.
- Проверить отсутствие мутации входных rooms/walls и сериализованного fixture.
Тест из пункта 3 обязан краснеть на 948f284, а не только подтверждать новое
вспомогательное вычисление.
11.2. Browser smoke
Один узкий сценарий на production bundle:
- создать прямоугольную комнату с наружными стенами 15 см;
- сохранить внешний SVG bbox/path signature;
- выполнить пользовательский путь Split из угла и подтвердить новую комнату;
- проверить 0 см, затем 15 см и 100 см общей стены;
- переключить Plan → View, киоск, static card и Labs iso;
- доказать одинаковый exterior bbox, отсутствие наружного зуба и наличие внутренней стены;
- проверить Undo/Redo и отсутствие скрытой config write;
- поставить источник света/солнце у примыкания и подтвердить общий occluder.
По текущему процессу smoke добавляется при реализации, но запускается перед бетой; в цикле реализации выполняются только typecheck, unit и build.
11.3. Golden
Добавить deterministic scenario split-corner-wall либо эквивалент с тремя
кадрами:
- исходный внешний угол;
- Split из угла с тонкой общей границей;
- тот же 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. Принятые технические предположения — можно менять без пересмотра продукта
- Endpoint считается вершиной по уже существующему wall-snap/geometry epsilon; отдельный пользовательский tolerance не вводится.
- Предпочтительная реализация разделяет exterior shell и shared wall bodies, но конкретная boolean decomposition может быть другой, если AC доказываются.
wallBodiesGeometry()остаётся canonical entry point; имя и внутренние helpers можно менять.- Имя smoke/golden scenario не является частью продукта.
- Новая настройка cap/join не нужна: поведение однозначно следует решениям владельца.
- Решения Q1–Q3 из раздела 3 не относятся к изменяемым предположениям.