Files
houseplan-card/docs/superpowers/specs/2026-08-05-free-wall-paths-design.md
T
Matysh d2bec266ed
Validate / hacs (push) Failing after 6s
Validate / hassfest (push) Failing after 6s
Validate / frontend (push) Successful in 3m19s
Validate / smoke (push) Failing after 1m12s
Validate / backend (push) Failing after 7m45s
v1.59.0-beta.10: unify device visuals and wall refinements
2026-08-05 23:38:01 +03:00

30 KiB
Raw Blame History

Свободные цепочки стен и разная толщина при рисовании — ТЗ

Статус: согласовано и отложено (2026-08-05).

Реализацию не начинать до отдельного прямого запроса владельца проекта. Этот документ фиксирует требования и принятые продуктовые решения для будущей реализации.

1. Цель

Добавить возможность:

  1. Рисовать одну цепочку стен, задавая отдельную толщину каждому следующему сегменту.
  2. Оставлять на плане незамкнутые стены, не принадлежащие комнате или контуру.
  3. Переключаться на другие инструменты без удаления уже нарисованной незамкнутой цепочки.
  4. После возвращения в инструмент или перезагрузки продолжать сохранённую цепочку с одного из её концов.
  5. Замыкать цепочку с выбором результата:
    • создать комнату;
    • оставить замкнутую цепочку самостоятельными стенами.

2. Текущее состояние и архитектурная причина изменения

Сейчас комната является единственным первичным источником геометрии стен:

  • контур хранится в rooms[].poly;
  • стены вычисляются из рёбер комнат;
  • walls[] хранит толщину рёбер, но не самостоятельную геометрию;
  • незаконченный _path является временным состоянием редактора;
  • смена инструмента очищает _path;
  • сервер удаляет старое legacy-поле segments;
  • статическая карточка, проёмы, привязки и расчёт видимой области получают геометрию стен преимущественно из комнат.

Поэтому функция должна вводить новую сохраняемую сущность. Простое сохранение текущего _path не решает рендер, восстановление после перезагрузки, посегментную толщину и интеграцию с остальными инструментами.

3. Термины

  • Свободная цепочка стен — упорядоченная открытая или замкнутая ломаная, которая не принадлежит комнате.
  • Сегмент — отрезок между двумя соседними вершинами; у замкнутой цепочки последний сегмент соединяет последнюю вершину с первой.
  • Активная цепочка — цепочка, которую пользователь сейчас продолжает.
  • Линейная стена — существующий вариант стены без объёмного тела.
  • Толстая стена — стена с положительной толщиной в сантиметрах.
  • Виртуальный участок — существующий open_span. Отсутствие толщины само по себе не превращает стену в виртуальную.

4. Модель данных

В конфигурацию пространства добавить необязательное поле wall_paths:

interface WallPathCfg {
  id: string;
  points: Array<[number, number]>;
  closed?: boolean;
  segments: Array<{
    cm?: number;
  }>;
}

Пример открытой цепочки:

{
  "id": "wp_01",
  "points": [[0.10, 0.20], [0.40, 0.20], [0.40, 0.55]],
  "segments": [{ "cm": 15 }, { "cm": 25 }]
}

Пример замкнутой самостоятельной цепочки:

{
  "id": "wp_02",
  "closed": true,
  "points": [[0.10, 0.20], [0.40, 0.20], [0.40, 0.55]],
  "segments": [{ "cm": 15 }, { "cm": 20 }, { "cm": 25 }]
}

Инварианты:

  • открытая цепочка: points.length >= 2 и segments.length === points.length - 1;
  • замкнутая цепочка: минимум три пригодные вершины и segments.length === points.length;
  • у замкнутой цепочки последний сегмент идёт от последней вершины к первой;
  • первая точка не дублируется в конце массива;
  • cm отсутствует для линейной стены без объёмного тела;
  • отсутствие cm не означает open_span;
  • соседние точки не совпадают;
  • id уникален в пределах пространства;
  • координаты нормализованы по тем же правилам, что и rooms[].poly;
  • старое корневое поле segments не возвращается и продолжает считаться legacy.

Толщина хранится внутри wall_paths, потому что существующий walls[] привязан к геометрии комнат и удаляет осиротевшие ключи при нормализации.

5. Рисование и посегментная толщина

5.1 Начало цепочки

  1. Пользователь выбирает инструмент «Стены».
  2. Первый клик создаёт временную начальную точку.
  3. Второй клик создаёт первый сегмент и постоянную запись в wall_paths.
  4. Одиночная точка без сегмента не сохраняется и исчезает при смене инструмента или перезагрузке.

5.2 Значение поля толщины

Поле толщины означает толщину следующего сегмента:

  • значение фиксируется при клике второй точки сегмента;
  • изменение поля не меняет уже созданные сегменты;
  • live-preview использует текущее значение;
  • пустое значение создаёт линейную стену;
  • конфиг всегда хранит сантиметры независимо от выбранной системы единиц;
  • допустимая толщина — 1–100 см;
  • нечисловое значение и значение вне диапазона показывают ошибку и не дают создать сегмент.

Пример: пользователь может последовательно создать сегменты 15 см, 25 см, линейный сегмент и снова 15 см в пределах одной цепочки.

6. Сохранение при смене инструмента

Как только создан первый сегмент:

  • цепочка становится частью конфигурации пространства;
  • каждый следующий законченный сегмент сразу добавляется в локальную конфигурацию;
  • запись на сервер может оставаться debounce-записью;
  • перед сменой этажа, режима, пространства или размонтированием карточки ожидающая запись принудительно завершается;
  • переключение на другой инструмент прекращает активное рисование, но не удаляет цепочку;
  • цепочка остаётся видимой во всех предусмотренных режимах.

Активное состояние редактора не требуется хранить на сервере.

7. Продолжение цепочки

Принятое поведение:

  • в той же сессии после возвращения в инструмент «Стены» автоматически продолжается последняя активная цепочка;
  • после перезагрузки пользователь выбирает конечную точку кликом;
  • конечные точки доступных цепочек подсвечиваются при наведении;
  • клик по последней точке продолжает цепочку в текущем направлении;
  • клик по первой точке разворачивает порядок точек и сегментных параметров, после чего продолжает цепочку с этого конца;
  • при продолжении поле толщины принимает значение крайнего сегмента выбранного конца, после чего пользователь может его изменить;
  • из середины сегмента и из внутренней вершины нельзя создавать ответвление.

7.1 Соединение двух цепочек

Разрешается соединение конец с концом:

  • при необходимости одна или обе цепочки разворачиваются;
  • точки и параметры сегментов объединяются без потери соответствия;
  • одна исходная запись сохраняет id, вторая удаляется;
  • нулевой соединительный сегмент не создаётся;
  • соединение через середину сегмента не поддерживается;
  • если соединение приводит к замыканию, открывается диалог завершения, описанный ниже.

8. Замыкание цепочки

При клике по начальной точке активной цепочки с минимум тремя пригодными вершинами:

  1. Проверяется замыкающий сегмент.
  2. Проверяются нулевая длина, самопересечения и недопустимые наложения.
  3. Открывается существующий диалог создания комнаты.
  4. Внизу диалога добавляется второе действие «Оставить замкнутыми стенами».

До выбора результата исходная открытая цепочка остаётся в конфигурации, а замыкающий сегмент хранится как ожидающее локальное изменение. Это исключает потерю стен при отмене диалога или ошибке сохранения.

8.1 Основное действие: создать комнату

При обычном сохранении комнаты одной транзакцией:

  • создаётся room.poly;
  • свободная цепочка удаляется из wall_paths;
  • толщина каждого сегмента переносится в существующую модель walls[];
  • замыкающий сегмент получает толщину, выбранную перед замыканием;
  • если новый контур разделяет физическую стену с существующей комнатой, побеждает толщина уже существующей стены.

Одна физическая стена не должна иметь две конкурирующие толщины.

8.2 Второе действие: оставить замкнутыми стенами

При выборе «Оставить замкнутыми стенами»:

  • комната не создаётся;
  • заливка, название и площадь не появляются;
  • замыкающий сегмент добавляется в segments;
  • цепочка сохраняется с closed: true;
  • активное рисование этой цепочки завершается.

Для продолжения замкнутой цепочки сначала потребуется удалить один из её сегментов. Перемещение вершин в первой версии не предусмотрено.

8.3 Отмена и ошибки

  • отмена диалога возвращает пользователя к открытой сохранённой цепочке;
  • ошибка записи не удаляет исходный wall_path;
  • удаление wall_path и добавление комнаты выполняются атомарно в одном изменении локальной конфигурации.

9. Рендер

Свободные стены отображаются:

  • в редакторе плана — всегда;
  • в остальных редакторах — по действующим правилам отображения границ;
  • в режиме просмотра — при включённом show_borders;
  • в основной и статической карточке.

9.1 Толстые стены

Для сегментов с толщиной необходимо:

  • строить тело относительно центральной линии;
  • объединять тела свободных и комнатных стен перед отрисовкой;
  • использовать существующие заливку и штриховку стен;
  • корректно формировать L- и T-образные стыки;
  • объединять сегменты разной толщины без щелей;
  • ограничивать miter на острых углах и переходить к bevel;
  • завершать действительно свободный конец ровным поперечным срезом;
  • прятать окончание линейного сегмента под телом примыкающей толстой стены.

9.2 Виртуальные участки

Действующее правило сохраняется:

  • в режиме просмотра виртуальный пунктир рисуется под телом стены;
  • в редакторах пунктир рисуется поверх тела полностью от центральной линии;
  • свободная линейная стена не является виртуальным участком.

9.3 Границы содержимого

wall_paths участвуют в расчёте видимой области с учётом половины толщины. План, состоящий только из свободных стен, должен корректно центрироваться и масштабироваться.

10. Влияние на комнаты, площадь и свет

До преобразования в комнату свободная цепочка:

  • не создаёт заливку пола;
  • не создаёт площадь и подпись м²;
  • не создаёт название комнаты;
  • не связывается с HA area;
  • не изменяет внутренний контур существующей комнаты;
  • не вычитает площадь из комнаты;
  • не разделяет комнату на световые зоны;
  • не участвует в распространении света между комнатами;
  • не создаёт солнечные лучи или дверной тоннель.

Свободная стена является отображаемой физической геометрией, но не топологической границей помещения.

Автоматическое распознавание комнат из произвольного графа пересекающихся стен не входит в задачу. Комната создаётся только явным замыканием цепочки и выбором основного действия в диалоге.

11. Работа инструментов

11.1 Толщина стены

Инструмент позволяет выбрать сегмент свободной цепочки и изменить только его толщину. Действие «Применить ко всей цепочке» можно добавить позднее как необязательное улучшение.

11.2 Удаление

  • единственный сегмент: удалить всю цепочку;
  • крайний сегмент: укоротить цепочку;
  • средний сегмент: разбить цепочку на две независимые цепочки;
  • одна часть сохраняет старый id, вторая получает новый;
  • часть без сегментов не сохраняется;
  • удаление сегмента замкнутой цепочки превращает её в открытую цепочку с closed: false.

11.3 Resize и перемещение вершин

Не входят в первую версию. Resize продолжает работать только с комнатами. Перемещение вершин свободных цепочек вынесено в следующий этап.

11.4 Merge и Split

Не считают свободные цепочки комнатами и не изменяют их автоматически.

11.5 Выравнивание

Глобальное выравнивание плана по сетке должно включать точки wall_paths, не нарушая соответствие сегментов и их толщин.

11.6 Привязка мебели

Свободные физические стены рекомендуется включить в привязку мебели, поскольку для пользователя они являются обычными стенами.

11.7 Двери и окна

Двери и окна устанавливаются только в стены комнат.

  • свободные открытые и замкнутые цепочки не являются целью инструмента проёмов;
  • при наведении на них инструмент не показывает доступную привязку;
  • проёмы становятся доступны только после преобразования цепочки в комнату.

12. Undo, Escape и Reset

Рекомендуемая безопасная модель:

  • Ctrl+Z или Escape удаляет последний сегмент, добавленный в текущей сессии редактирования;
  • при продолжении старой цепочки Undo не удаляет части, существовавшие до её выбора;
  • когда добавленных в сессии сегментов больше нет, следующее действие снимает активность с цепочки;
  • для новой цепочки отмена первого сегмента удаляет wall_path, оставляя временную стартовую точку;
  • следующая отмена удаляет стартовую точку.

Reset следует трактовать как «Отменить текущие изменения цепочки»:

  • новая цепочка возвращается к состоянию до текущего рисования;
  • у продолженной цепочки удаляются только добавленные в этой сессии сегменты;
  • ранее сохранённая часть не удаляется;
  • полное удаление выполняется инструментом удаления.

13. Сохранение и конкурентные изменения

  • каждый завершённый сегмент сразу попадает в локальную конфигурацию;
  • запись на сервер выполняется существующей сериализованной очередью;
  • ожидающий debounce принудительно завершается при уходе из контекста;
  • activeWallPathId является только состоянием редактора;
  • после перезагрузки продолжение начинается выбором конечной точки;
  • преобразование в комнату не должно сначала удалить цепочку и только потом пытаться сохранить комнату;
  • конфликты ревизий обрабатываются действующим механизмом конфигурации.

14. Серверная валидация и ограничения

Предлагаемые пределы:

  • не более 500 цепочек на пространство;
  • не более 500 точек в одной цепочке;
  • не более 2000 свободных сегментов суммарно;
  • только конечные числовые координаты в допустимых границах холста;
  • толщина 1–100 см;
  • уникальные идентификаторы;
  • отсутствие совпадающих соседних точек;
  • точное соответствие количества сегментов типу цепочки;
  • минимум три пригодные вершины у замкнутой цепочки;
  • лишние или повреждённые поля отклоняются либо нормализуются единообразно.

Существующие конфигурации миграции не требуют: wall_paths является необязательным полем.

15. Геометрические и UX edge cases

Обязательно учесть:

  • смену инструмента после единственной точки;
  • двойной клик в одной координате;
  • нулевой и очень короткий сегмент;
  • возврат в предыдущую точку;
  • попытку замкнуть цепочку с недостаточным количеством вершин;
  • самопересечение замыкающего или обычного сегмента;
  • прохождение свободной стены через существующую комнату;
  • полное или частичное совпадение с комнатной стеной;
  • полное или частичное совпадение двух свободных цепочек;
  • пересечение двух цепочек без общей вершины;
  • соединение двух цепочек с необходимостью развернуть обе;
  • попытку соединиться с серединой сегмента;
  • T-образный стык стен разной толщины;
  • переход толстая → линейная → толстая;
  • острый угол и ограничение miter;
  • удаление среднего сегмента открытой и замкнутой цепочки;
  • отмену диалога комнаты;
  • ошибку сервера при завершении комнаты;
  • изменение единиц измерения во время рисования;
  • координаты вне сетки;
  • достижение лимита точек или общего размера конфигурации;
  • план без комнат, содержащий только свободные стены;
  • скрытые границы в режиме просмотра;
  • конкурентное редактирование в другой вкладке.

16. Критерии готовности первой версии

  1. Незамкнутая цепочка с хотя бы одним сегментом не исчезает при смене инструмента.
  2. Она сохраняется после перезагрузки карточки и Home Assistant.
  3. В той же сессии рисование продолжается автоматически.
  4. После перезагрузки цепочку можно продолжить с любого конца.
  5. Две цепочки можно соединить конец с концом.
  6. Ответвление из середины сегмента невозможно.
  7. Толщина задаётся отдельно каждому следующему сегменту.
  8. Сегменты разной толщины образуют корректное тело без щелей.
  9. При замыкании доступны «Создать комнату» и «Оставить замкнутыми стенами».
  10. Создание комнаты переносит толщины и атомарно удаляет свободную цепочку.
  11. При общей стене сохраняется ранее существовавшая толщина.
  12. Отмена диалога не удаляет свободную цепочку.
  13. Замкнутая самостоятельная цепочка сохраняется без комнаты, заливки и площади.
  14. Свободные стены видны в основной и статической карточке.
  15. Они учитываются при масштабировании и центрировании.
  16. Удаление среднего сегмента корректно разделяет цепочку.
  17. Двери и окна нельзя устанавливать в свободные стены.
  18. Старые конфигурации продолжают работать без миграции.

17. Не входит в первую версию

  • перемещение вершин свободных стен;
  • Resize свободных цепочек;
  • ответвление из середины сегмента;
  • автоматическое распознавание комнат из сети стен;
  • установка дверей и окон в свободные стены;
  • участие свободных стен в комнатной топологии, площади и распространении света;
  • отдельные материалы, стили и свойства сторон свободной стены.

18. Предлагаемый порядок будущей реализации

  1. Схема wall_paths, серверная валидация и чистые операции над цепочками.
  2. Посегментная толщина в инструменте «Стены».
  3. Сохранение, автоматическое продолжение и выбор конца после перезагрузки.
  4. Соединение цепочек конец с концом.
  5. Диалог замыкания и два варианта завершения.
  6. Геометрия тел и стыков стен разной толщины.
  7. Основная и статическая карточки, расчёт видимой области.
  8. Инструменты толщины, удаления, привязки и выравнивания.
  9. Документация и тестовый чек-лист.
  10. Прогон тестов только перед пре-релизом согласно принятому процессу.

19. Зафиксированные ответы владельца

Вопрос Решение
Что делать при замыкании? Открывать диалог комнаты; внизу сразу добавить второе действие «Оставить замкнутыми стенами».
Как продолжать цепочку? В той же сессии продолжать автоматически; после перезагрузки выбирать конец кликом.
Как соединять цепочки? Соединять конец с концом; ответвление из середины сегмента не поддерживать.
Разрешать двери и окна? Нет, проёмы устанавливаются только в стены комнат.
Нужны ли перемещение вершин и Resize? Вынести в следующий этап; в первой версии оставить продолжение, изменение толщины и удаление сегментов.
Какая толщина побеждает у общей стены? Уже существующая физическая стена определяет толщину.
Сохранять ли одиночную точку? Нет, точка без сегмента остаётся временной.