Files
houseplan-card/docs/superpowers/specs/2026-08-08-unified-boundary-tool-design.md
T

58 KiB
Raw Blame History

Единый инструмент «Граница» — техническое задание

Статус: реализовано и проверено для v1.60.2-beta.3 Дата: 2026-08-08 Область: только UX и клиентская логика редактора плана

1. Зафиксированное решение

В редакторе плана две отдельные кнопки:

  • «Виртуальная стена»;
  • «Физическая стена»

заменяются одной кнопкой «Граница».

Единый инструмент сам определяет действие по объекту под курсором:

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

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

2. Причина изменения

Текущие названия создают ложное впечатление, что три инструмента работают с одинаковым типом объектов:

Текущая кнопка Фактическое действие
«Перегородка» Создаёт новый самостоятельный физический объект стены
«Виртуальная стена» Убирает тело стены с участка общей границы двух комнат
«Физическая стена» Восстанавливает тело стены на уже открытом участке границы

«Виртуальная стена» и «Физическая стена» не создают самостоятельные стены. Они меняют состояние существующей комнатной границы. Две длинные кнопки также занимают непропорционально много места на панели редактора.

3. Цели

  1. Убрать из панели две взаимообратные команды и заменить их одним понятным инструментом.
  2. Исключить терминологический конфликт с «Перегородкой».
  3. Сократить ширину панели без переноса функции в скрытое меню.
  4. Сделать действие очевидным по наведению, курсору, предварительному просмотру и текстовой подсказке.
  5. Не допустить случайного открытия или восстановления стены в неоднозначной точке.
  6. Полностью сохранить существующий формат конфигурации и результаты геометрических операций.
  7. Сохранить работоспособность мыши, пера и сенсорного ввода.

4. Не входит в задачу

  • изменение схемы space.open_spans;
  • изменение связей rooms[].open_to;
  • миграция конфигурации;
  • изменение правил распространения Glow;
  • изменение расчёта площади;
  • изменение визуала открытых границ в режиме просмотра;
  • разрешение открытых участков на внешних стенах;
  • частичное восстановление одного сохранённого открытого участка;
  • установка дверей или окон на открытых участках;
  • изменение правил толщины при открытии или восстановлении стены;
  • превращение перегородок, колонн или незавершённых контуров в границы комнат;
  • редактирование границ в режиме «Выбрать»;
  • объединение комнат или удаление комнат;
  • изменение backend API или backend-валидации.

5. Термины

  • Граница комнаты — ребро контура room.poly.
  • Общая граница — совпадающий участок контуров двух комнат.
  • Физический участок — участок общей границы, на котором существует тело стены положительной толщины.
  • Открытый участок — участок общей границы из space.open_spans, визуально показанный пунктиром и связывающий комнаты через open_to.
  • Открыть границу — существующая операция создания открытого участка.
  • Восстановить стену — существующая операция удаления открытого участка и восстановления физической стены по действующим правилам толщины.
  • Перегородка — самостоятельная запись space.partitions; она не является границей комнаты и новым инструментом не редактируется.

В пользовательском интерфейсе не используются названия «виртуальная стена» и «физическая стена» как названия инструментов. В поясняющем тексте допустимы слова «открытый участок» и «восстановить стену».

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

6. Состав панели редактора

На месте двух старых кнопок отображается одна кнопка:

Свойство Значение
Русская подпись Граница
Английская подпись Boundary
Рекомендуемая иконка mdi:border-style
Положение Между «Проём» и «Толщина», на месте старых кнопок
Тип Обычная кнопка инструмента, не выпадающее меню
Активное состояние То же оформление, что у остальных активных инструментов

mdi:border-style является рекомендуемой, но не окончательно утверждённой иконкой. Перед завершением реализации её нужно проверить на живой панели рядом с «Проёмом» и «Толщиной» в RU/EN и на узкой ширине. Если иконка визуально сливается с соседними инструментами, выбрать другой доступный MDI-символ границы без возврата к двум отдельным кнопкам и без использования общей иконки mdi:wall, уже занятой физическими стенами.

Постоянная подсказка кнопки:

  • RU: «Открыть участок общей границы или восстановить на нём стену»;
  • EN: «Open a shared boundary stretch or restore its wall».

Кнопка должна иметь локализованные title, aria-label и корректное aria-pressed. Подпись остаётся видимой: делать кнопку только иконкой не нужно.

Старые кнопки и их пользовательские подписи на панели больше не отображаются.

7. Общая модель взаимодействия

Инструмент работает как автоматический диспетчер двух существующих операций. Пока первая точка нового открытого участка не установлена, намерение определяется по цели под курсором:

  1. самостоятельный физический объект → не редактировать скрытую под ним комнатную границу;
  2. открытый пунктирный участок → восстановить стену;
  3. физический участок общей границы → начать открытие участка;
  4. внешняя стена → отказ: открывать можно только общую границу комнат;
  5. любой другой объект или пустое место → отказ без изменения данных.

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

8. Машина состояний

Состояние Условие Визуальная реакция Клик / tap
idle Инструмент включён, подходящей цели нет Базовая подсказка Сообщение о допустимой цели, данных не меняет
hover-solid Под курсором физическая общая граница Локальная метка будущей первой точки, crosshair Устанавливает первую точку
hover-open Под курсором пунктирный открытый участок Сплошное preview-тело восстанавливаемой стены предсказанной толщины, pointer Восстанавливает весь выбранный открытый участок
hover-outer Под курсором внешняя комнатная стена Недоступное состояние, not-allowed Toast о том, что нужна общая граница
anchored Установлена первая точка открытия Пунктирный preview от первой точки до курсора Валидная вторая точка открывает участок
anchored-invalid Вторая точка не может завершить участок Preview недоступного состояния Ничего не сохраняет; первая точка остаётся активной
anchored-too-short Вторая точка находится на выбранной грани, но диапазон короче допустимого минимума Короткий недоступный preview Ничего не сохраняет; показывает toast и сбрасывает якорь как завершённую неудачную попытку

После успешного открытия или восстановления инструмент остаётся активным, как и инструменты «Перегородка», «Колонна» и «Проём». Пользователь может выполнить следующую операцию без повторного выбора кнопки.

9. Определение цели и приоритеты hit-test

9.1 До установки первой точки

Приоритет целей строгий:

  1. самостоятельный физический объект (partition, wall_column или room_draft), если курсор находится в его собственной hit-зоне;
  2. открытый участок, если точка действительно находится над его продольным диапазоном;
  3. физическая общая граница;
  4. внешняя граница комнаты;
  5. пустое место.

Один и тот же resolver должен использоваться и для hover, и для клика. Нельзя допускать ситуацию, когда preview обещает восстановление стены, а клик устанавливает начало нового открытого участка.

9.2 Конец открытого участка

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

  • поперечное расстояние до линии — с допуском 12 CSS px для fine pointer и 22 CSS px для coarse pointer;
  • проекцию точки вдоль линии — она должна попадать внутрь участка либо в допуск 6/10 CSS px у торца соответственно.

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

На самой общей точке стыка приоритет имеет открытый участок. Чтобы начать новый участок рядом с ним, пользователь смещает курсор на физическую часть. Preview и курсор должны однозначно показывать выбранное намерение.

9.3 Неоднозначные стыки

В точке T-, X-образного или сложного стыка действие разрешается только тогда, когда hit-test однозначно определил один коллинеарный участок.

Если два разных кандидата имеют практически одинаковое расстояние и нельзя надёжно определить намерение:

  • данные не меняются;
  • первая точка не устанавливается;
  • показывается сообщение «Выберите участок чуть дальше от стыка»;
  • курсор показывает недоступное состояние.

Нельзя молча выбирать участок только по порядку комнат в массиве или порядку SVG-элементов.

9.4 Самостоятельный объект поверх комнатной границы

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

  • инструмент «Граница» не открывает и не восстанавливает участок сквозь тело самостоятельного объекта;
  • данные самостоятельного объекта не меняются;
  • показывается подсказка «Границу перекрывает самостоятельный объект — переместите или удалите его в режиме “Выбрать”»;
  • чтобы отредактировать комнатную границу, пользователь сначала перемещает или удаляет перекрывающий объект либо выбирает незакрытую им часть границы.

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

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

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

Визуальное тело толстой стены является удобной кликабельной зоной, но итоговая геометрия по-прежнему привязывается к осевой линии комнатной границы. Клик по любому месту поперёк тела одной и той же стены должен давать одинаковую точку на её оси.

9.6 Экранные допуски

Все интерактивные допуски задаются в CSS-пикселях, а не в единицах плана и не в физических пикселях экрана. При каждом hit-test они пересчитываются в render units через фактическую экранную матрицу SVG/текущий zoom.

Допуск Fine pointer (мышь) Coarse pointer (touch/перо)
Половина минимальной поперечной hit-зоны линии 12 CSS px 22 CSS px
Продольный допуск за торцом открытого участка 6 CSS px 10 CSS px
Порог неоднозначности двух кандидатов 6 CSS px 6 CSS px

Для толстой стены поперечная hit-зона равна максимуму из половины видимой толщины её тела и указанного минимального допуска. Поэтому вся видимая стена кликабельна даже тогда, когда она толще 24/44 CSS px.

Кандидат считается неоднозначным, если два разных допустимых участка попали в hit-зону и разница их экранных расстояний до указателя меньше 6 CSS px. Значение не умножается на devicePixelRatio. При изменении zoom экранный размер допусков остаётся постоянным.

10. Открытие участка общей границы

10.1 Первый клик

Первый клик разрешён только на физическом участке общей границы двух комнат. Он:

  1. выбирает ту же общую грань, которую выбирал старый инструмент «Виртуальная стена»;
  2. проецирует и привязывает точку по существующим правилам;
  3. сохраняет runtime-якорь;
  4. не меняет конфигурацию;
  5. не создаёт команду Undo;
  6. не вызывает сохранение.

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

10.2 Вторая точка

После первого клика:

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

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

Если вторая точка слишком близка к первой:

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

Если второй клик заметно ушёл с зафиксированной грани или попал на другое ребро:

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

Состояние anchored-invalid относится именно к промаху мимо выбранной грани и сохраняет якорь. Слишком короткий диапазон является отдельным состоянием anchored-too-short и, как указано выше, сбрасывает якорь.

10.3 Пересечение с уже открытым участком

Если новый диапазон касается или частично перекрывает существующий открытый диапазон, применяются текущие правила нормализации open_spans. Дублирующие записи не создаются. Результат остаётся каноническим и синхронизируется с open_to текущим кодом.

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

10.4 Двери, окна и ворота

Если выбранный участок содержит дверные или оконные проёмы, сохраняется текущее поведение:

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

11. Восстановление стены

Клик по открытому пунктирному участку:

  1. выбирает один существующий канонический открытый участок;
  2. немедленно вызывает существующую операцию закрытия этого участка;
  3. удаляет соответствующий диапазон из open_spans;
  4. синхронизирует open_to;
  5. восстанавливает толщину по действующим правилам;
  6. создаёт одну именованную команду Undo;
  7. сохраняет конфигурацию существующим способом.

Восстанавливается весь участок, который сейчас выбирала отдельная кнопка «Физическая стена». Частичное восстановление двумя точками в эту задачу не входит.

Подтверждение не требуется. Операция обратима через Undo.

Правило толщины не меняется:

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

12. Preview, курсоры и подсказки

12.1 Визуальные состояния

Preview восстановления показывает не условную осевую линию, а полупрозрачное тело стены той толщины, которая будет применена после клика. Толщина вычисляется тем же чистым правилом, что использует commit: наследование от коллинеарного физического остатка, иначе действующее значение по умолчанию. Preview ничего не записывает в walls и не создаёт snapshot.

Намерение Preview Курсор
Можно начать открытие Небольшая метка точки на оси стены crosshair
Выбирается диапазон открытия Толстый пунктир от якоря до текущей второй точки + метки концов crosshair
Будет восстановлена стена Полупрозрачное сплошное тело предсказанной толщины поверх выбранного пунктира pointer
Внешняя или неоднозначная граница Недоступная локальная метка, без ложного диапазона not-allowed
Нет цели Без preview default

Открытие и восстановление различаются не только цветом: обязательны пунктирная и сплошная формы. Это сохраняет понятность при нарушении цветового восприятия и в высококонтрастных темах.

Красный цвет для нормального восстановления стены не используется: операция не является удалением. Основной акцентный цвет можно сохранить, различая намерения формой линии.

12.2 Динамическая подсказка панели

Состояние RU EN
Нет цели «Выберите общую границу комнат или пунктирный участок» “Select a shared room boundary or a dashed stretch”
Физическая граница «Кликните начало открытого участка» “Click the start of the open stretch”
Якорь установлен «Укажите конец на выбранной границе · Esc — отмена» “Choose the end on the selected boundary · Esc to cancel”
Открытый участок «Кликните, чтобы восстановить стену» “Click to restore the wall”
Внешняя стена «Открыть можно только общую границу двух комнат» “Only a boundary shared by two rooms can be opened”
Неоднозначный стык «Выберите участок чуть дальше от стыка» “Choose a stretch farther from the junction”

Подсказка должна находиться в существующей области hint панели, не менять высоту панели при переключении состояний и по возможности иметь aria-live="polite".

12.3 Toast после операции

Рекомендуемый пользовательский текст:

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

В тексте больше не должно быть требования выбрать другой инструмент. Сообщения вида «Для этого участка выберите “Физическая стена”» удаляются.

13. Отмена, переключение инструментов и навигация

13.1 Escape

  • Если установлен якорь, первый Esc удаляет только якорь и preview, оставляя «Границу» активной.
  • Если якоря нет, следующий Esc возвращает редактор к его базовому инструменту «Контур комнаты», как это происходит сейчас для старых инструментов.

13.2 Смена инструмента

Переход к «Выбрать», «Контур комнаты», «Перегородка», «Колонна», «Проём», «Толщина» или любому другому инструменту:

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

13.3 Смена пространства или редактора

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

Открытые участки, уже сохранённые до навигации, остаются без изменений.

13.4 Внешнее обновление конфигурации

Любая внешняя ревизия конфигурации, полученная после установки первой точки и до commit второй точки, сначала отменяет якорь и preview. Это относится к:

  • websocket echo;
  • изменению из другой вкладки или другого клиента;
  • resync после конфликта ревизии;
  • замене _serverCfg при reconnect.

Отмена не сохраняется, не добавляется в command stack и не пытается переносить якорь на новую геометрию. После применения новой конфигурации пользователь при необходимости начинает выбор заново.

13.5 Undo / Redo

  • Якорь и hover не являются командами и не попадают в command stack.
  • Если якорь активен, первое нажатие Undo (Ctrl+Z / Cmd+Z либо кнопка) только отменяет якорь и preview, не изменяя command stack. Следующее нажатие выполняет обычную команду Undo.
  • Redo (Ctrl+Y, Cmd+Shift+Z либо кнопка) при активном якоре также сначала отменяет якорь без выполнения команды. Это не позволяет истории заменить геометрию под живой ссылкой на грань.
  • Успешное открытие — одна существующая команда «Открытие границы».
  • Успешное восстановление — одна существующая команда «Закрытие границы» или её актуализованное пользовательское название.
  • Ctrl+Z / Cmd+Z и кнопка Undo возвращают конфигурацию целиком, включая удалённые при открытии проёмы и толщины.
  • Redo повторяет операцию без необходимости повторно выбирать инструмент.

14. Мышь, перо, touch и повторные клики

14.1 Touch

На устройствах без hover:

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

Визуальная метка выбранного якоря обязательна, поскольку курсор на touch отсутствует.

14.2 Двойной клик и двойной tap

Единый инструмент не должен выполнять взаимообратные действия из двух событий одного двойного клика.

  • Двойной клик по открытому участку восстанавливает стену не более одного раза; второе событие не должно сразу поставить якорь на ставшей физической стене.
  • Для этого после восстановления используется короткий guard по указателю, координате и времени либо эквивалентная проверка event.detail.
  • Двойной клик в одной точке физической стены не создаёт участок нулевой длины: срабатывает существующая проверка минимальной длины, затем якорь очищается.
  • Guard не должен блокировать обычную следующую операцию в другой точке.

14.3 Несколько указателей

Если во время установленного якоря появляется второй touch/pointer:

  • геометрия не фиксируется по случайному второму указателю;
  • жест панорамирования или масштабирования имеет приоритет;
  • после multi-touch якорь безопасно отменяется;
  • конфигурация не меняется.

15. Матрица граничных случаев

Случай Ожидаемое поведение
Клик по общей физической стене Ставит первую точку
Второй клик на той же грани Открывает валидный диапазон
Второй клик слишком близко Не открывает, toast, сбрасывает якорь
Второй клик вне выбранной грани Не открывает, сохраняет якорь для повтора
Второй клик на соседней грани через угол Не переключает грань и не создаёт диагональ
Клик по пунктиру без якоря Восстанавливает весь выбранный участок
Клик по пунктиру с активным якорем Пытается завершить начатое открытие, не восстанавливает стену
Клик сразу за торцом пунктира Выбирает физическое продолжение, если курсор уже вне продольного допуска пунктира
Клик точно в торец пунктира Выбирает существующий открытый участок
Два соседних открытых участка Выбирается один канонический hit; дубликаты не создаются
Перекрывающий новый открытый диапазон Нормализуется текущими helper-функциями
Клик по внешней стене Отказ с понятным toast, без якоря
Клик по перегородке Не редактирует её; подсказка «Инструмент работает с границами комнат»
Перегородка совпадает с общей границей Самостоятельный объект имеет приоритет; лежащая под ним граница не редактируется до перемещения/удаления перегородки
Клик по колонне Не редактирует её и ничего не создаёт
Клик по незавершённому контуру Не редактирует его и ничего не создаёт
Клик по комнате вдали от границы Не меняет комнату
Клик по пустому месту Не меняет данные
Клик по толстой стене у внутренней/внешней кромки Проецируется на одну и ту же ось границы
Клик в сложный стык с несколькими равными кандидатами Отказ с просьбой сместиться от стыка
Открытие участка с дверью/окном Проём удаляется по текущему правилу и возвращается через Undo
Восстановление полностью открытой стены Получает действующую толщину по умолчанию
Восстановление частично открытой стены Наследует толщину физического коллинеарного остатка
show_borders: false В редакторе цели остаются доступными; физика и данные не меняются
Масштабирование плана Hit и preview соответствуют визуальной границе; допуски остаются 12/22, 6/10 и 6 CSS px при любом разрешённом zoom
Смена инструмента после первой точки Отменяет якорь без сохранения
Выход из редактора после первой точки Отменяет якорь без сохранения
Undo или Redo при активном якоре Первое действие только отменяет якорь; история и конфигурация не меняются
Внешняя ревизия конфига при активном якоре Якорь и preview отменяются до применения новой конфигурации; сохранения и history нет
Потеря save / конфликт ревизии Сохраняется действующая для геометрии политика ошибок; двойной commit не возникает
Быстрый двойной клик по пунктиру Только одно восстановление, без немедленного повторного открытия

16. Данные и обратная совместимость

16.1 Запрет на изменение модели

Не добавляются, не удаляются и не переименовываются поля конфигурации. В частности, без изменений остаются:

  • space.open_spans;
  • rooms[].open_to;
  • space.walls;
  • space.openings;
  • геометрия rooms;
  • partitions, wall_columns, room_drafts;
  • формат snapshot общего command stack.

Backend-схема и websocket-контракты не меняются. Миграция версии данных не нужна.

16.2 Эквивалентность операций

Для одинаковой исходной конфигурации и одинаковых геометрических точек:

  • новый сценарий открытия обязан давать тот же результат, что старый openwall;
  • новый сценарий восстановления обязан давать тот же результат, что старый closewall;
  • порядок нормализации толщин, удаления проёмов, синхронизации open_to, записи history и сохранения не меняется.

До второго клика открытия конфигурация должна быть строго неизменной.

16.3 Runtime-состояние

Допускается заменить два внутренних значения инструмента одним runtime-режимом boundary. Это состояние не сериализуется и не является изменением модели данных.

Значение _tool не записывается в конфигурацию, hash/deeplink или LS_NAV: персистентная навигация хранит только пространство и режим редактора. Оно может временно находиться лишь в module-level warm-remount memo внутри уже загруженной страницы. При восстановлении такого memo старые openwall и closewall должны нормализоваться в boundary, а неизвестное значение — в базовый draw. После полной перезагрузки страницы legacy-состояния инструмента не существует.

Рекомендуется иметь только один активный runtime-режим, а не скрывать старую кнопку при сохранении двух независимых машин состояний. Старые функции открытия и восстановления могут переиспользоваться как операции commit, но выбор операции должен выполняться единым resolver инструмента.

17. Требования к реализации

  1. В MarkupTool остаётся один пользовательский режим границы.
  2. В _renderMarkupBar() отображается одна кнопка.
  3. Один pure/helper resolver определяет open | solid-shared | outer | ambiguous | unsupported | none.
  4. Resolver используется и hover-preview, и обработчиком клика.
  5. При активном якоре resolver целей не меняет тип операции: обрабатывается только завершение открытия.
  6. Операция открытия переиспользует текущие helpers snap, clamp, нормализации, purge openings и _persistOpenCuts().
  7. Операция восстановления переиспользует _closeOpenSpan() и текущую логику толщин.
  8. На одну пользовательскую операцию приходится ровно один geometry snapshot, одна запись command stack и один запуск сохранения.
  9. Hover, установка и отмена якоря не вызывают _saveConfig().
  10. Старые взаимоисключающие подсказки и ветка «выберите другой инструмент» удаляются.
  11. Визуал реальных открытых границ в View не меняется.
  12. Документированные data-hp, data-id, data-kind и CSS hooks являются публичным styling-контрактом и сохраняются. Если переименование всё же неизбежно, оно считается breaking change: одновременно обновляются docs/STYLING-HOOKS.md, оба changelog и регрессионные проверки.
  13. Недокументированные test hooks можно заменить стабильными эквивалентами с одновременной актуализацией smoke-тестов.
  14. Толщина preview восстановления вычисляется тем же pure helper, что и толщина реального commit, чтобы preview и результат не расходились.

18. Локализация и тексты

Нужны новые ключи RU/EN для:

  • названия кнопки;
  • постоянного tooltip;
  • базовой динамической подсказки;
  • первой точки;
  • второй точки;
  • восстановления;
  • внешней стены;
  • неоднозначного стыка;
  • неподдерживаемого объекта;
  • успешного восстановления без термина «виртуальный отрезок».

Старые ключи можно оставить временно только если они используются для совместимости тестов или истории. Они не должны отображаться пользователю. Наборы ключей ru.json и en.json должны оставаться симметричными.

Именованные шаги истории рекомендуется показывать как:

  • RU: «Открытие границы» / «Восстановление стены»;
  • EN: “Open boundary” / “Restore wall”.

Изменение пользовательского названия шага не меняет формат snapshot.

19. Доступность

  1. Кнопка доступна с клавиатуры и имеет видимую подпись.
  2. Активность передаётся не только цветом, но и aria-pressed.
  3. Действия открытия и восстановления отличаются формой preview — пунктиром и сплошной линией.
  4. Динамическая подсказка доступна screen reader через ненавязчивый status/live region без многократного озвучивания каждого движения мыши.
  5. Toast не является единственным признаком активного якоря: на плане остаётся его визуальная метка.
  6. prefers-reduced-motion соблюдается; новая анимация для preview не нужна.
  7. Touch target не становится меньше текущего допуска hit-test.

Полное клавиатурное рисование геометрии в эту задачу не входит.

20. План тестирования реализации

По принятому процессу тесты запускаются при подготовке пре-релиза; само создание этого ТЗ тестового прогона не требует.

20.1 Unit

  • resolver отдаёт открытый участок при клике внутри его продольного диапазона;
  • resolver не растягивает hit открытого участка далеко за его торец;
  • физическое продолжение после торца определяется как solid-shared;
  • внешний участок определяется как outer;
  • неоднозначный стык не выбирается по порядку массива;
  • все hit-допуски сохраняют заданный размер в CSS px при разных zoom;
  • внутренняя и внешняя кромки одного толстого тела проецируются на одну ось;
  • самостоятельная перегородка поверх общей границы блокирует выбор лежащей под ней границы;
  • активный якорь блокирует переключение на close;
  • Undo и Redo с активным якорем отменяют только якорь, не двигая stack;
  • внешняя ревизия конфига отменяет якорь до замены геометрии;
  • отмена якоря не меняет geometry snapshot;
  • одна операция даёт одну запись Undo;
  • результаты open/close совпадают с текущими эталонными fixtures;
  • двойной клик после close не создаёт новый якорь;
  • RU/EN содержат одинаковый набор новых ключей.

20.2 Browser smoke

  1. На панели видна одна кнопка «Граница»; старых двух кнопок нет.
  2. Два клика по общей физической стене создают пунктирный участок.
  3. Не меняя инструмент, один клик по этому пунктиру восстанавливает стену.
  4. Hover по физической и открытой целям показывает разные preview и курсоры.
  5. Внешняя стена не открывается.
  6. Клик по перегородке и колонне их не меняет.
  7. Esc, смена инструмента, пространства и редактора отменяют якорь.
  8. Undo/Redo работают для обеих операций и сначала отменяют активный якорь.
  9. Дверь или окно на открываемом участке удаляется и возвращается Undo.
  10. Восстановление сохраняет текущие правила толщины.
  11. При show_borders: false инструмент остаётся работоспособным в редакторе.
  12. Быстрый двойной клик по пунктиру не закрывает и тут же повторно не открывает стену.
  13. Touch-сценарий проходит без зависимости от hover.
  14. Внешний config echo между первым и вторым кликом отменяет якорь без мутации новой геометрии.
  15. Перегородка поверх общей границы блокирует редактирование скрытой границы и показывает понятную подсказку.

20.3 Регрессия

Актуализировать существующие проверки smoke_openwall, smoke_openwall_hover, smoke_hide_layers, smoke_wall_thickness и тесты command stack. Геометрические fixtures и ожидаемая модель после операций не должны измениться, кроме названий пользовательского runtime-инструмента и текстов UI.

21. Документация при реализации

При фактической реализации обновить:

  • docs/ARCHITECTURE.md — один runtime-инструмент вместо двух;
  • docs/UX-MODES.md — жесты и динамические состояния «Границы»;
  • docs/TESTING.md — ручные и автоматизированные сценарии;
  • docs/CHANGELOG.md и docs/CHANGELOG.ru.md — пользовательское изменение;
  • docs/STATUS.md — только при попадании задачи в сборку/релиз;
  • docs/STYLING-HOOKS.md — обязательно, если меняется хотя бы один документированный hook или селектор.

Backend-документация и описание схемы хранения не меняются.

22. Критерии приёмки

Задача считается выполненной, если одновременно выполнены все условия:

  1. В редакторе плана отображается ровно одна кнопка «Граница» / Boundary.
  2. Подписи «Виртуальная стена» и «Физическая стена» отсутствуют на панели.
  3. Одним активным инструментом можно открыть физический участок и восстановить пунктирный.
  4. Открытие по-прежнему требует двух точек; восстановление — одного клика.
  5. Hover/preview однозначно показывает предстоящее действие.
  6. Физическое продолжение у торца пунктира остаётся доступным для открытия.
  7. Неоднозначный стык не приводит к случайной мутации.
  8. Перегородки, колонны, drafts и внешние стены инструментом не изменяются.
  9. Незавершённый жест безопасно отменяется во всех сценариях навигации.
  10. Двойной клик не выполняет последовательность close → open.
  11. Undo/Redo содержат по одному именованному шагу на операцию.
  12. Результаты open_spans, open_to, walls и openings совпадают с результатами прежних операций.
  13. Не добавлены поля конфигурации, миграции, backend-команды или новая версия модели.
  14. RU/EN локализация полна и симметрична.
  15. Целевые unit/smoke и прежние регрессионные проверки проходят перед пре-релизом.
  16. Undo/Redo и внешнее обновление конфигурации не могут оставить якорь, ссылающийся на устаревшую грань.
  17. Все hit-допуски выражены в CSS px и визуально постоянны при zoom.
  18. Preview восстановления совпадает с итоговым телом стены по толщине.
  19. Перегородка или колонна поверх границы блокирует скрытое редактирование, а не создаёт визуально неразличимую открытую границу под собой.
  20. Документированные styling hooks сохранены либо проведено явно задокументированное breaking-изменение.

23. Итоговая UX-схема

Инструмент «Граница»
        │
        ├─ курсор над пунктиром ── клик ── восстановить стену
        │
        ├─ курсор над общей физической границей
        │       └─ клик 1 ── якорь ── клик 2 ── открыть участок
        │
        ├─ самостоятельный объект поверх границы ── заблокировать скрытое действие
        │
        ├─ курсор над внешней стеной ── отказ: нужна общая граница
        │
        └─ перегородка / колонна / draft / пустое место ── без изменений

Таким образом, для пользователя существует один объект действия — граница комнат, а конкретная команда выводится из её текущего состояния. Хранимая модель и вся физика плана остаются прежними.