mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-01 04:09:17 +00:00
510 lines
35 KiB
Markdown
510 lines
35 KiB
Markdown
# Единый поток размещения проёмов: выбор типа и архитектурный preview
|
||
|
||
Связанные задачи: [#75](https://github.com/Matysh/houseplan-card/issues/75),
|
||
[#76](https://github.com/Matysh/houseplan-card/issues/76).
|
||
|
||
Статус ТЗ: **реализовано и локально отревьюено; ожидает prerelease/CI gate**. ТЗ прошло ревью против текущих placement,
|
||
wall-thickness, opening renderer, command stack и общей editor sub-panel. Открытых
|
||
продуктовых решений перед реализацией нет.
|
||
|
||
## 1. Контекст и проблема
|
||
|
||
Сейчас кнопка **«Проём»** сразу включает единый инструмент размещения, а тип объекта
|
||
выбирается только после клика по стене. Hover-preview при этом представляет собой
|
||
условную пунктирную линию фиксированной длины и центральную точку. Такой поток не даёт
|
||
до клика понять, будет создано окно, дверь или ворота, и не показывает реальную
|
||
архитектурную геометрию объекта.
|
||
|
||
Отдельный воспроизводимый дефект: на стенах с толщиной текущий preview может полностью
|
||
или частично скрываться телом стены либо не появляться при наведении на видимую часть
|
||
стены, удалённую от её оси. Пользователь кликает практически вслепую.
|
||
|
||
Задачи #75 и #76 являются одним пользовательским сценарием и выпускаются вместе:
|
||
|
||
1. пользователь открывает меню **«Проём»**;
|
||
2. выбирает окно, дверь или ворота;
|
||
3. видит на стене полный полупрозрачный символ выбранного типа;
|
||
4. кликает по валидной позиции;
|
||
5. при необходимости уточняет свойства в существующем диалоге;
|
||
6. сохраняет объект и может продолжить серийное размещение.
|
||
|
||
## 2. Цели
|
||
|
||
1. Сделать тип будущего проёма явным до начала размещения.
|
||
2. Показывать до клика форму, ширину, ориентацию и сторону открывания будущего объекта.
|
||
3. Полностью устранить невидимость preview на толстых физических стенах.
|
||
4. Гарантировать единый детерминированный resolver для hover, click и начального
|
||
состояния диалога без зависимости click от обязательного предварительного hover.
|
||
5. Переиспользовать существующую editor sub-panel и геометрию сохранённых openings.
|
||
6. Не менять модель данных, backend API, migrations и семантику сохранённых проёмов.
|
||
|
||
## 3. Не входит в задачу
|
||
|
||
- новые типы и разновидности окон, дверей или ворот;
|
||
- размещение проёмов в перегородках и колоннах;
|
||
- изменение существующих правил fit, overlap и минимальной допустимой длины: эта задача
|
||
не вводит новый запрет на выступающий за границы сегмента проём и новую проверку
|
||
пересечения проёмов;
|
||
- live-preview изменений за открытым модальным диалогом;
|
||
- preview выреза кладки, заливки тоннеля, Glow, солнечных лучей или площади до Save;
|
||
- сохранение последнего типа/размера в config, layout, backend или localStorage;
|
||
- полноценный touch-first hover UX;
|
||
- изменение рисунка уже сохранённых openings;
|
||
- новый command stack: история появляется только после успешного Save по текущим правилам.
|
||
|
||
## 4. Нормативный пользовательский поток
|
||
|
||
### 4.1. До выбора типа
|
||
|
||
- Основная кнопка **«Проём»** является launcher существующей общей суб-панели.
|
||
- Нажатие launcher только открывает или закрывает меню и само по себе не меняет
|
||
`_tool`, не вызывает `_cancelPath()`, не создаёт preview и не пишет данные.
|
||
- Пока пользователь в текущем входе в Plan editor явно не выбрал тип, новый opening
|
||
placement не активен.
|
||
- Надпись основной кнопки всегда остаётся **«Проём»**.
|
||
|
||
### 4.2. Выбор типа
|
||
|
||
В суб-панели находятся ровно три действия:
|
||
|
||
| Кнопка | `OpeningCfg.type` | Начальная ширина | Базовая геометрия |
|
||
|---|---|---:|---|
|
||
| Окно | `window` | 120 см | существующий символ окна |
|
||
| Дверь | `door` | 90 см | одна створка и существующая дуга |
|
||
| Ворота | `gate` | 300 см | две створки, открытые **наружу** на 10° |
|
||
|
||
После выбора:
|
||
|
||
1. применяется существующий контракт переключения инструмента: незавершённая операция
|
||
предыдущего инструмента очищается/сохраняется как draft ровно так же, как при выборе
|
||
любого другого инструмента; новая логика не делает скрытый auto-save;
|
||
2. создаётся единый session-only preset проёма;
|
||
3. активируется `_tool = 'opening'`;
|
||
4. суб-панель закрывается;
|
||
5. placement ждёт первый новый pointer move и затем показывает preview.
|
||
|
||
Начальные `flip_h` и `flip_v` равны `false`. Выбор типа не создаёт config mutation,
|
||
history command или save request.
|
||
|
||
### 4.3. Hover и клик
|
||
|
||
- Валидный hover показывает полный символ выбранного типа и вспомогательные измерения.
|
||
- Hover и click вызывают один pure resolver с одинаковыми geometry inputs, preset и
|
||
tie-break правилами.
|
||
- Click всегда является авторитетным: он разрешает candidate в координатах самого click.
|
||
Если кэшированный hover-candidate имеет те же pointer coordinates (в принятом epsilon),
|
||
geometry/config revision и preset revision, разрешено повторно использовать именно его;
|
||
иначе candidate пересчитывается. Наличие предварительного hover не требуется.
|
||
- Полученный click-candidate открывает существующий диалог создания.
|
||
- После клика preview и pointer hints исчезают; за диалогом не остаётся ghost.
|
||
- Если между последним pointer move и click геометрия/config revision изменилась,
|
||
candidate пересчитывается. Невалидный результат не открывает диалог и использует
|
||
существующее локализованное сообщение об ошибке. Старый preview не считается
|
||
разрешением на создание.
|
||
|
||
### 4.4. Диалог, Save и Cancel
|
||
|
||
- Диалог нового объекта открывается с типом, шириной, позицией, углом и flip-флагами
|
||
показанного preview.
|
||
- В диалоге по-прежнему можно изменить тип, ширину и flip-настройки. Live-preview за
|
||
диалогом не появляется.
|
||
- Изменения в диалоге относятся только к текущему объекту. Toolbar preset остаётся тем,
|
||
который пользователь явно выбрал в суб-панели.
|
||
- Save, кнопка **«Отмена»**, крестик диалога и Escape закрывают диалог по существующему
|
||
контракту; только Save записывает объект/history command.
|
||
- После Save или любого отменяющего закрытия placement остаётся активным с исходным toolbar preset, позволяя
|
||
последовательно размещать объекты одного типа.
|
||
- После закрытия диалога новый preview появляется только после следующего pointer move;
|
||
устаревший candidate не восстанавливается автоматически.
|
||
- Чтобы изменить тип серии, пользователь повторно открывает **«Проём»** и выбирает
|
||
другой пункт.
|
||
|
||
### 4.5. Существующие openings
|
||
|
||
- Наведение на существующий opening не показывает второй preview.
|
||
- Клик по существующему opening имеет приоритет и сразу открывает его редактирование.
|
||
- Редактирование существующего opening не открывает суб-панель и не меняет toolbar preset.
|
||
- Существующие type, размеры, flip, contact и lock не мигрируют и не нормализуются.
|
||
|
||
## 5. Состояния интерфейса
|
||
|
||
| Состояние | Launcher | Суб-панель | Preview | Клик по валидной стене |
|
||
|---|---|---|---|---|
|
||
| Другой инструмент | неактивен | закрыта | нет | работает текущий инструмент |
|
||
| Меню открыто, тип не выбран | открыт | три типа | нет | outside-click только закрывает меню |
|
||
| Placement активен | активен | закрыта | по валидному hover | открывает диалог создания |
|
||
| Меню поверх placement | активен | открыта | текущий preset сохранён | outside-dismiss не создаёт opening |
|
||
| Диалог создания открыт | активен | blocked | нет | canvas недоступен |
|
||
| Редактирование existing | без изменения | blocked | нет | canvas недоступен |
|
||
| Смена редактора/пространства | сброшен | закрыта | нет | placement прекращён |
|
||
|
||
Открытие меню поверх другого инструмента не отменяет его draft. Отмена происходит только
|
||
после явного выбора одного из типов проёма.
|
||
|
||
## 6. UI-only preset и candidate
|
||
|
||
### 6.1. Preset
|
||
|
||
Рекомендуемый минимальный тип:
|
||
|
||
```ts
|
||
type OpeningPlacementPreset = {
|
||
type: 'window' | 'door' | 'gate';
|
||
lengthCm: number;
|
||
flipH: boolean;
|
||
flipV: boolean;
|
||
revision: number;
|
||
};
|
||
```
|
||
|
||
- Preset существует только в памяти экземпляра карточки.
|
||
- `revision` меняется при выборе типа и не допускает использования старого candidate.
|
||
- Дефолты определяет один resolver/таблица. Нельзя независимо держать `door/90`,
|
||
`window/120`, `gate/300` в toolbar, preview и диалоге.
|
||
- Выход из Plan editor сбрасывает placement. Возвращение не включает opening tool.
|
||
|
||
### 6.2. Candidate
|
||
|
||
Hover и click используют одну структуру, например:
|
||
|
||
```ts
|
||
type OpeningPlacementCandidate = {
|
||
presetRevision: number;
|
||
geometryRevision: number;
|
||
pointer: [number, number];
|
||
type: 'window' | 'door' | 'gate';
|
||
lengthCm: number;
|
||
flipH: boolean;
|
||
flipV: boolean;
|
||
x: number;
|
||
y: number;
|
||
angle: number;
|
||
renderedLength: number;
|
||
target: {
|
||
segmentKey: string;
|
||
axisA: [number, number];
|
||
axisB: [number, number];
|
||
physicalHalfWidth: number;
|
||
sourceOrder: number;
|
||
};
|
||
renderSpec: OpeningVisibleGeometrySpec;
|
||
measure: OpMeasure | null;
|
||
};
|
||
```
|
||
|
||
Имена полей не нормативны. `target` — временная identity выбранного производного
|
||
сегмента только для текущего geometry revision. Она не сохраняется в `OpeningCfg`: у
|
||
текущей модели проём остаётся привязан абсолютными `x/y/angle`, а постоянного wall id или
|
||
edge index в данных нет. `physicalHalfWidth` берётся из локально разрешённого wall profile,
|
||
а не из одной «толщины стены»: на одной оси могут встречаться разные интервалы толщины.
|
||
|
||
Нормативно наличие одного resolved result, из которого строятся preview и
|
||
`_openingDialog`. Resolver чист относительно config/layout: не пишет данные, не запускает
|
||
save, не создаёт command и не заполняет committed opening/tunnel caches. `renderSpec`
|
||
содержит уже разрешённую видимую геометрию; preview не должен независимо повторять выбор
|
||
грани стены и получить другую сторону на общем сегменте.
|
||
|
||
## 7. Валидность и hit-testing
|
||
|
||
Preview существует только там, где тот же click разрешает создать opening.
|
||
|
||
### 7.1. Допустимые поверхности
|
||
|
||
- физические стены контуров комнат;
|
||
- внешние и общие физические стены;
|
||
- стены нулевой и ненулевой толщины, включая максимальную допустимую настройками;
|
||
- горизонтальные, вертикальные, 45° и произвольные диагональные сегменты.
|
||
|
||
### 7.2. Недопустимые поверхности
|
||
|
||
- виртуальная граница/open span;
|
||
- перегородка;
|
||
- квадратная или круглая колонна;
|
||
- hit-area существующего opening: он имеет приоритет и открывает редактирование;
|
||
- область вдали от подходящей стены.
|
||
|
||
Текущая реализация не запрещает частичный выход широкого проёма за концы выбранного
|
||
сегмента и не выполняет общую overlap-проверку. Resolver этой задачи обязан сохранить
|
||
это поведение: отсутствие shoulder measurements не делает candidate невалидным, ширина
|
||
не уменьшается и тип не подменяется. Новые fit/overlap-правила требуют отдельного ТЗ.
|
||
|
||
### 7.3. Толстые стены
|
||
|
||
Видимое тело толстой стены является частью hit-area. Для каждого производного
|
||
физического segment hit-envelope равен максимуму из существующего placement tolerance и
|
||
`local physical half-width + pointer tolerance`. Расстояние от pointer до оси не должно
|
||
отбрасывать candidate, пока pointer находится в этом envelope. Envelope не расширяется
|
||
на соседние виртуальные интервалы, перегородки или колонны.
|
||
|
||
Resolver проецирует pointer на каноническую ось физической стены, затем применяет
|
||
существующие along-wall grid snap, centre magnet и endpoint-clamp rules. Это не расширяет набор допустимых объектов:
|
||
перегородки и колонны не становятся стенами комнат.
|
||
|
||
На углах, общих стенах и пересечении hit-envelopes выбор не зависит от SVG paint order.
|
||
Проекция должна попадать в ограниченный segment с существующим endpoint tolerance.
|
||
Среди допустимых целей сначала сравнивается дистанция до ограниченного segment, затем
|
||
перпендикулярная дистанция до его оси, затем канонический segment key и стабильный
|
||
`sourceOrder` производных room edges. Точно совпадающие общие границы дедуплицируются по
|
||
каноническому segment key до выбора candidate.
|
||
|
||
Правило закрывает оба источника исходного дефекта:
|
||
|
||
1. preview не вычислялся над внешней/внутренней частью широкого wall body;
|
||
2. вычисленный preview рисовался под кладкой и визуально исчезал.
|
||
|
||
### 7.4. Детерминированность
|
||
|
||
- В углу/стыке выбирается один и тот же segment для hover и click.
|
||
- Над existing opening hit существующего объекта имеет приоритет.
|
||
- При одинаковых pointer/preset/geometry inputs hover и click возвращают эквивалентный
|
||
resolved candidate; click без hover работает самостоятельно.
|
||
- Ворота 300 см не подменяются дверью и не уменьшаются, в том числе на коротком сегменте.
|
||
|
||
## 8. Архитектурный preview
|
||
|
||
### 8.1. Общий renderer
|
||
|
||
Нельзя создавать отдельную упрощённую дверь рядом с committed renderer. Видимая
|
||
геометрия собирается общим pure helper из подготовленного render spec:
|
||
|
||
- `committed` — обычные visible paths плюс отдельные interaction/live wrappers;
|
||
- `preview` — те же jambs, leaf/leafs, arc, orientation и face offsets без interaction.
|
||
|
||
Preview использует состояние только что созданного opening без привязанной сущности:
|
||
дверь и ворота показываются открытыми, окно — закрытым, как сразу после Save. Он не
|
||
читает HA state и не создаёт фиктивные contact/lock bindings.
|
||
|
||
Committed-only `.op-hit`, contact/lock badges, live state, data attributes и обработчики
|
||
остаются снаружи общего visible-geometry helper.
|
||
|
||
### 8.2. Внешний вид
|
||
|
||
- Preview-группа имеет класс `.opening-preview`.
|
||
- Итоговая opacity всей архитектурной группы — `0.5`.
|
||
- Цвет — editor accent/существующий `--hp-open`, читаемый в обеих темах.
|
||
- Opacity задаётся группе, а не каждому path отдельно.
|
||
- Группа рисуется **поверх тела физической стены**, включая штриховку, но ниже
|
||
measurement badges и editor hints.
|
||
- Нет blur, filter, mix-blend-mode и догоняющей анимации.
|
||
- Preview немедленно следует за pointer и масштабируется вместе с архитектурой.
|
||
- `pointer-events: none`, `aria-hidden="true"`, без tabindex, `.op-hit`, `data-hp` и
|
||
`data-id` сохранённого объекта.
|
||
|
||
### 8.3. Толстая стена
|
||
|
||
Preview показывает те же каноническую ось, грань, face offset, глубину jambs, створки и
|
||
дуги, что committed symbol после Save. Тело стены не перекрывает ни базовую линию, ни
|
||
створку, ни jambs preview.
|
||
|
||
Для общей стены сохраняется существующее детерминированное правило выбора стороны из
|
||
opening wall association. Задача не переопределяет понятие «наружу» для общей стены:
|
||
важно, чтобы preview и committed renderer выбрали одну сторону на одном geometry revision.
|
||
|
||
Preview не вырезает кладку и не создаёт tunnel fill. Он осознанно лежит поверх ещё
|
||
цельного тела стены до Save.
|
||
|
||
### 8.4. Вспомогательные элементы
|
||
|
||
Без понижения контраста сохраняются:
|
||
|
||
- центральная snap-точка;
|
||
- две линейки/shoulder badges до краёв выбранной room edge;
|
||
- perpendicular magnet tick;
|
||
- текущие formatter метрических и imperial единиц.
|
||
|
||
Они используют тот же candidate, но находятся вне группы opacity `0.5`.
|
||
|
||
## 9. Суб-панель «Проём»
|
||
|
||
- Использовать `EditorToolbarGroup` и существующий `EditorSecondaryController`.
|
||
- В DOM остаётся один `.editor-secondary-host`.
|
||
- Не создавать новый popover, dialog или дополнительную строку toolbar.
|
||
- Открытие/закрытие не меняет высоту editor chrome и stage.
|
||
- Launcher использует `aria-expanded`/`aria-controls` общего компонента.
|
||
- Активный preset отражается через `aria-pressed` и общий active style.
|
||
- `ArrowDown` на launcher открывает меню; внутри группы работают `ArrowLeft`/
|
||
`ArrowRight`, Home/End и нативные Enter/Space. Escape обрабатывается общим
|
||
верхнеуровневым keyboard-контрактом и закрывает только меню.
|
||
- Escape восстанавливает фокус на launcher. Outside-dismiss закрывает меню, не крадёт
|
||
фокус у целевого элемента и не пропускает тот же click на canvas. Оба действия не
|
||
меняют preset и не создают opening.
|
||
- Выбор пункта закрывает группу до вызова action. После keyboard-выбора focus не должен
|
||
оставаться на удалённом DOM-узле: он возвращается на launcher; pointer-выбор не
|
||
перехватывает последующее наведение на stage.
|
||
- Смена/закрытие редактора имеет приоритет над dismiss меню.
|
||
- Все пункты имеют полноценные touch targets; touch-редакторы остаются best effort.
|
||
|
||
## 10. Lifecycle и очистка
|
||
|
||
Candidate, preview и hints полностью очищаются при:
|
||
|
||
- click и открытии диалога;
|
||
- Save/Cancel/закрытии диалога;
|
||
- уходе pointer от валидной стены;
|
||
- наведении на existing opening;
|
||
- выборе другого типа до пересчёта candidate;
|
||
- выборе другого инструмента;
|
||
- Escape при закрытом меню: очищает candidate/preset и переводит Plan editor в его
|
||
текущий нейтральный инструмент `draw`, как существующий opening tool;
|
||
- смене редактора/пространства или config revision;
|
||
- pointerleave stage, а также pointercancel/lostpointercapture активного pointer gesture;
|
||
- начале pan/pinch;
|
||
- размонтировании карточки.
|
||
|
||
Быстрое движение между стенами не создаёт более одной preview-группы и не оставляет
|
||
DOM-остатков предыдущего типа/позиции.
|
||
|
||
## 11. Совместимость и данные
|
||
|
||
- `OpeningCfg`, сериализация, backend API и model version не меняются; миграции нет.
|
||
- Старые openings рендерятся и редактируются без нормализации.
|
||
- Config/layout revision меняется только существующим Save диалога.
|
||
- Новый opening становится одним именованным geometry command по текущему контракту.
|
||
- Undo/Redo сохранённых openings не меняется.
|
||
- `hide_openings: true` не скрывает placement preview в Plan editor.
|
||
- `show_borders: false` не мешает preview допустимой физической стены.
|
||
- Preview не участвует в площади, Resize, Glow, солнце, barriers и room fill.
|
||
|
||
## 12. Edge-case matrix
|
||
|
||
| Случай | Ожидаемое поведение |
|
||
|---|---|
|
||
| Тонкая физическая стена | Полный preview по оси стены |
|
||
| Толстая физическая стена | Hit по всему телу; preview виден поверх кладки |
|
||
| Общая стена комнат | Один deterministic candidate, без двойного preview |
|
||
| Внешняя стена | Корректный face offset и направление символа |
|
||
| Виртуальная граница | Preview отсутствует, создание запрещено |
|
||
| Перегородка/колонна | Preview отсутствует |
|
||
| 0°/45°/90°/любой угол | Preview и committed geometry совпадают |
|
||
| Угол или Т-стык | Hover и click выбирают один segment |
|
||
| Над existing opening | Редактирование existing, без ghost |
|
||
| Ворота шире segment | Сохраняется текущий permissive-контракт: preview/создание доступны, тип и 300 см не меняются |
|
||
| `hide_openings` | Placement preview всё равно виден |
|
||
| Меню закрыто без выбора | Старый tool/preset не меняется |
|
||
| Смена типа при ghost | Старый ghost удалён, candidate пересчитан |
|
||
| Пространство без комнат | Preview отсутствует, console чистая |
|
||
| Imperial UI | Internal см каноничны, подписи в ft/in |
|
||
| Zoom 0.4×/fit/2.5× | Пропорции и hit-testing согласованы |
|
||
| Mouse/trackpad/pen hover | Preview обязателен |
|
||
| Первый click/tap без hover | Candidate разрешается по координатам события, диалог открывается без pre-tap preview |
|
||
| Pointer покинул stage | Preview и hints немедленно очищены |
|
||
|
||
## 13. План реализации
|
||
|
||
### Этап A — placement state и меню (#76)
|
||
|
||
1. Добавить одну таблицу defaults и session-only preset.
|
||
2. Описать группу **«Проём»** через `_editorToolbarGroups` только для Plan editor.
|
||
3. Превратить текущую кнопку в launcher без прямого переключения `_tool`.
|
||
4. Подключить выбор `window/door/gate`, lifecycle и серийное размещение.
|
||
5. Перевести инициализацию `_openingDialog` на preset/candidate.
|
||
|
||
### Этап B — candidate parity и renderer (#75)
|
||
|
||
1. Вынести pure candidate resolver hover/click.
|
||
2. Учесть полное тело толстой стены в hit-area и проекцию на ось.
|
||
3. Выделить общий renderer видимой архитектурной геометрии.
|
||
4. Заменить `.opghost` полным `.opening-preview` выбранного типа.
|
||
5. Зафиксировать слои, очистку и отсутствие побочных эффектов.
|
||
|
||
Этапы можно разнести для code review, но не выпускать как два пользовательских релиза:
|
||
готовой считается только совместная поставка A+B.
|
||
|
||
## 14. Тестирование
|
||
|
||
По принятому процессу тесты создаются с реализацией, но запускаются перед пре-релизом.
|
||
|
||
### 14.1. Unit
|
||
|
||
- defaults resolver: `window/120`, `door/90`, `gate/300`, flip false;
|
||
- candidate parity hover/click;
|
||
- rejection virtual span, partition, column и точки вне hit-envelope;
|
||
- permissive regression: широкий opening на коротком segment не меняет type/length;
|
||
- click без предварительного hover;
|
||
- deterministic junction selection;
|
||
- hit по оси и обеим видимым половинам толстой стены;
|
||
- candidate → dialog draft без потери координат.
|
||
|
||
### 14.2. Browser smoke
|
||
|
||
Расширить или создать `demo/smoke_opening_preview.mjs`:
|
||
|
||
- launcher открывает `kind-group`, не меняет tool и не создаёт ghost;
|
||
- один shared host, stage не меняет высоту;
|
||
- каждый тип создаёт правильный preset;
|
||
- `.opening-preview` содержит полный symbol выбранного типа;
|
||
- opacity/color/pointer-events/aria и отсутствие committed contract;
|
||
- preview видим на тонкой, толстой и диагональной стене;
|
||
- pointer над обеими половинами толстого wall body даёт preview;
|
||
- при одинаковом input preview/click/dialog/Save совпадают по type, width,
|
||
x/y/angle/flip/face offset;
|
||
- первый click/tap без hover создаёт тот же dialog draft;
|
||
- invalid target и existing opening не показывают ghost;
|
||
- Save/Cancel сохраняют serial preset;
|
||
- lifecycle удаляет preview;
|
||
- keyboard/outside-dismiss/focus restore соответствуют общей суб-панели.
|
||
|
||
Регрессионный набор: `smoke_opening_measure`, `smoke_inert_openings`,
|
||
`smoke_grid_snap`, `smoke_wall_thickness`, `smoke_opening_tunnel_fill`, editor tray и
|
||
geometry history.
|
||
|
||
### 14.3. Golden
|
||
|
||
Добавить детерминированную сцену Plan editor:
|
||
|
||
- dark theme;
|
||
- толстая физическая стена;
|
||
- полупрозрачная дверь на 45° с hints;
|
||
- окно и ворота отдельными состояниями либо в одной композиционной сцене;
|
||
- семантический assertion подтверждает цветные пиксели preview поверх wall-body, а не
|
||
только существование DOM-узла.
|
||
|
||
Golden обязан падать, если preview снова окажется полностью закрыт толстой стеной.
|
||
|
||
### 14.4. Ручная проверка
|
||
|
||
- светлая/тёмная тема и узкий desktop viewport;
|
||
- mouse и trackpad;
|
||
- горизонтальная, вертикальная, диагональная, внешняя и общая толстая стена;
|
||
- серия из трёх объектов и смена типа через меню.
|
||
|
||
## 15. Release-артефакты
|
||
|
||
- `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md` со ссылками на #75/#76;
|
||
- обновление раздела Plan editor/проёмы в `docs/USER-GUIDE.ru.md`;
|
||
- RU/EN i18n parity для launcher, группы и пунктов;
|
||
- принятый golden baseline;
|
||
- закрытие обоих issue только после совместной проверки acceptance contract.
|
||
|
||
## 16. Критерии приёмки
|
||
|
||
### Меню и поток
|
||
|
||
- [ ] **«Проём»** только открывает общую суб-панель и не начинает placement.
|
||
- [ ] В меню три локализованных пункта: окно, дверь, ворота.
|
||
- [ ] Выбор создаёт preset 120/90/300 см без записи данных.
|
||
- [ ] Меню не меняет высоту toolbar/stage и использует shared host.
|
||
- [ ] Save/Cancel позволяют продолжить серию с явно выбранным preset.
|
||
- [ ] Возвращение в Plan editor не запускает placement автоматически.
|
||
|
||
### Preview и геометрия
|
||
|
||
- [ ] Полный symbol соответствует выбранному типу; ворота открываются наружу двумя
|
||
створками на 10°.
|
||
- [ ] Preview имеет opacity `0.5`, editor accent и не перехватывает события.
|
||
- [ ] Hover и click используют один resolver; при одинаковом input preview, dialog и
|
||
committed opening не прыгают, а click без hover работает самостоятельно.
|
||
- [ ] На всём теле толстой стены candidate доступен и preview виден поверх кладки.
|
||
- [ ] Face offset, jamb depth, створки и дуги совпадают с committed opening.
|
||
- [ ] Measurement hints контрастны и используют тот же candidate.
|
||
- [ ] Invalid target не показывает preview, который click затем отклоняет.
|
||
|
||
### Безопасность и совместимость
|
||
|
||
- [ ] До Save нет config/layout write, history, wall cut, tunnel, Glow/sun/area/Resize.
|
||
- [ ] Virtual span, partition и column остаются недопустимыми.
|
||
- [ ] Existing openings, их dialog, drag и Undo/Redo не регрессируют.
|
||
- [ ] Модель данных, migrations и backend API не меняются.
|
||
- [ ] Lifecycle, keyboard, focus и touch-gesture проверки выполнены.
|
||
- [ ] Smoke и golden падают при удалении preview или переносе под тело толстой стены.
|