docs: specify open passage behavior

Issue: #157
User-Visible: no
This commit is contained in:
Sergey Matyunin
2026-08-17 16:25:15 +03:00
parent bc478b1756
commit 91c8b01d7d
2 changed files with 628 additions and 0 deletions
+627
View File
@@ -0,0 +1,627 @@
# #157 — тип проёма «Открытый проём»
- Issue: [#157](https://github.com/Matysh/houseplan-card/issues/157)
- Приоритет: P2
- Ветка: `issue/157-open-passage`
- Статус документа: полное ТЗ подготовлено; issue остаётся в `S3-spec`, review не запущено по прямому указанию владельца
- Основание: issue владельца от 2026-08-14 и аналитика от 2026-08-15
## 1. Пользовательская проблема и результат
На реальном плане часто есть арка или обычный открытый дверной проём: стена
сохраняется по сторонам, но полотна двери и связанных с ним устройств нет.
Сейчас такой объект приходится изображать дверью либо виртуально открытым
участком стены. Дверь добавляет ложную створку и предлагает датчик/замок, а
виртуальный участок означает отсутствие кладки и отмечается пунктиром в
редакторе.
После #157 пользователь получает четвёртый тип обычного проёма —
`passage` / «Открытый проём». Он:
- вырезает реальный участок кладки;
- не рисует створку, дугу, пунктир или самостоятельную рамку;
- продолжает пол через существующую геометрию opening tunnel;
- всегда пропускает свет между двумя распознанными комнатами;
- не имеет датчика, замка и параметров створки;
- доступен в существующем потоке размещения и редактирования проёмов.
## 2. Подтверждённая техническая база
На момент подготовки ТЗ:
- `OpeningCfg.type` и backend schema знают только `door`, `window`, `gate`;
- Full card уже вырезает из wall body все сохранённые opening geometry и рисует
room-coloured tunnel fill;
- `renderOpeningVisibleGeometry()` имеет fallback-ветку двери, поэтому простого
расширения union недостаточно: `passage` ошибочно получил бы дверную створку;
- Plan сохраняет невидимый hitbox и hover outline отдельно от видимого символа;
этот механизм можно переиспользовать для выбора `passage`;
- свет уже считает `door`/`gate` внутренним проходом только при наличии пола с
обеих сторон и оставляет внешний проём непрозрачным;
- скрытая изометрия вырезает opening geometry из стены отдельно от построения
полотен, но fallback в `buildIsoOpeningBasis()` также превращает неизвестный
тип в дверь;
- Static card сейчас не рисует opening symbols и не подаёт opening cuts в свою
wall geometry;
- `CONFIG_SCHEMA` сохраняет неизвестные sibling-поля opening благодаря
`extra=vol.ALLOW_EXTRA`, а marker validators уже задают принятый образец
change-aware проверки «старое битое можно прочитать и пронести без изменения,
новое битое записать нельзя»;
- full-card и static-card snapshots сейчас подписываются на `contact`/`lock`
каждого проёма без проверки типа.
## 3. Нормативные продуктовые решения
1. Сохраняемый literal нового типа — только `passage`.
2. Пользовательское название:
- RU: `Открытый проём`;
- EN: `Open passage`.
3. Новый проём размещается тем же инструментом, на тех же физических стенах и
с теми же правилами snap/центрирования/измерений, что дверь.
4. Default ширины нового проёма — **90 см**, как у двери.
5. В покое `passage` не имеет собственного архитектурного символа. Видим только
физический разрыв кладки и пол внутри него.
6. В Plan допускается временный editor chrome: hover/selection outline, hitbox,
центральная точка preview и размерные подсказки. Это не часть итогового
условного обозначения и не показывается в View/Static.
7. `contact`, `lock`, `invert`, `flip_h`, `flip_v` для `passage` неприменимы.
8. При переходе существующего проёма в `passage` сохранение удаляет эти пять
известных полей. Если были непустые `contact` или `lock`, до сохранения
показывается явное предупреждение.
9. Обратный переход из сохранённого `passage` не восстанавливает удалённые
привязки или флаги.
10. Для света `passage` прозрачен только когда проба по обе стороны стены
попадает на пол комнаты. Внешний либо неопределённый край остаётся
fail-dark.
11. `passage` отличается от open span наличием кладки по сторонам и физических
откосов. Open span по-прежнему означает отсутствие кладки на всём участке и
показывается пунктиром только в Plan.
12. Публичность скрытого изометрического режима не меняется.
## 4. Scope
### 4.1 Модель и backend
- расширить TypeScript union и backend enum литералом `passage`;
- добавить change-aware semantic validator неприменимых полей;
- подключить его ко всем путям записи и импорта;
- сохранить lossless round-trip незатронутых legacy/future данных.
### 4.2 Plan editor
- добавить `passage` в submenu инструмента проёмов;
- добавить четвёртый radio-вариант в диалог;
- применить default 90 см;
- скрывать binding/flip/invert controls;
- показывать предупреждение о снятии существующих привязок;
- корректно создавать, выбирать, перетаскивать, менять тип и удалять объект;
- сохранять обычный geometry history/undo contract.
### 4.3 Рендер и свет
- Full 2D Plan/View;
- Static card;
- скрытая изометрия;
- общий wall cut и opening tunnel fill;
- Glow/light barrier geometry;
- исключение stale binding-полей `passage` из runtime подписок и UI.
### 4.4 Совместимость и документация
- full/space export-import;
- downgrade note;
- RU/EN i18n;
- пользовательское руководство, LIGHT, ISOMETRIC и CONFIG-COMPATIBILITY;
- unit/backend tests, smoke и golden evidence.
## 5. Non-scope
- моделирование криволинейной арки, верхней перемычки или высоты проёма;
- выбор формы проёма, материала откосов или декоративной обналички;
- датчик присутствия либо иной entity binding для `passage`;
- изменение прозрачности по состоянию сущности;
- публичное включение изометрии;
- общая parity-задача по отображению `door`/`window`/`gate` в Static card;
- автоматическое преобразование существующих дверей или open spans;
- изменение формата open spans;
- изменение существующей семантики дверей, окон и ворот.
В этой задаче «арка» — пользовательское название открытого проёма. Геометрически
это прямоугольный полноразмерный разрыв стены в текущей 2.5D-модели.
## 6. Контракт данных
### 6.1 TypeScript
```ts
export interface OpeningCfg {
id: string;
type: 'door' | 'window' | 'gate' | 'passage';
x: number;
y: number;
angle: number;
length: number;
contact?: string | null;
lock?: string | null;
invert?: boolean;
flip_h?: boolean;
flip_v?: boolean;
}
```
Общие optional-поля остаются в интерфейсе ради совместимости чтения. Для
канонической записи `type: 'passage'` допустимы только `id`, `type`, `x`, `y`,
`angle`, `length` и неизвестные текущей версии extension-поля.
Пример канонической записи:
```json
{
"id": "o-passage-hall",
"type": "passage",
"x": 0.417,
"y": 0.286,
"angle": 90,
"length": 0.09
}
```
### 6.2 Defaults и ограничения
- `lengthCm = 90` при новом размещении;
- существующие ограничения диалога `20..600 см`, step 5 см сохраняются;
- `x`, `y`, `angle`, `length`, id generation и нормализация не меняются;
- версия модели и migration step не добавляются;
- существующие три литерала и их сериализация не меняются.
### 6.3 Канонизация из UI
При сохранении `passage` frontend:
1. записывает актуальные id/type/geometry;
2. удаляет `contact`, `lock`, `invert`, `flip_h`, `flip_v`, а не записывает
`null`/`false`;
3. при редактировании существующего opening сохраняет неизвестные sibling-поля;
4. не меняет другие openings и их порядок.
Явное редактирование `passage` может канонизировать перечисленные известные
неприменимые поля. Несвязанное сохранение не должно переписывать объект.
## 7. Backend validation и broken-read contract
### 7.1 Schema
Inline opening schema в `validation.py` принимает четвёртый enum literal. Поля
`contact`, `lock`, `invert`, `flip_h`, `flip_v` остаются syntactically readable,
чтобы загрузка и round-trip старых/будущих данных не ломались до semantic stage.
### 7.2 Semantic validator
Добавляется чистая функция, например:
```py
validate_opening_passages(
config: dict,
previous: dict | None = None,
*,
validate_all: bool = False,
) -> None
```
Она сопоставляет пространства по `space.id`, openings внутри пространства по
`opening.id` и применяет правила:
| Ситуация | Результат |
|---|---|
| Новый `passage` без запрещённых ключей | принять |
| Смена другого типа на `passage`, запрещённый ключ остался | отклонить |
| В существующем `passage` добавлен или изменён любой запрещённый ключ | отклонить |
| Неизменённый некорректный `passage` проходит с несвязанной правкой | принять без переписывания |
| Из некорректного `passage` удалены запрещённые ключи | принять |
| Full/space import содержит любой запрещённый ключ у `passage` | отклонить |
Запрещённым считается само наличие ключа, включая `contact: null` и
`flip_h: false`: каноническая новая запись не содержит неприменимых полей.
Если старый объект имел другой `type`, смена контекста на `passage` считается
новой семантикой и проверяется полностью, даже когда значение binding-поля
текстово не изменилось.
Стабильный public error code: `invalid_passage_fields`. Сообщение содержит id
пространства, id проёма и отсортированный список запрещённых полей, но не
значения entity ids.
### 7.3 Точки подключения
Одна и та же проверка обязательна для:
- `houseplan/config/set` с предыдущей конфигурацией;
- `houseplan/plan/optimize` с предыдущей конфигурацией;
- merge-import с предыдущей конфигурацией;
- replace/full import с `validate_all=True`;
- space import с `validate_all=True` для входящего/перенумерованного content.
Ошибка преобразуется в существующий websocket/import error contract; partial
write запрещён.
## 8. UX редактора
### 8.1 Палитра и размещение
В submenu «Проём» после двери добавляется пункт:
- label: `Открытый проём` / `Open passage`;
- icon: `mdi:arch`;
- id/type: `passage`;
- default: 90 см, `flipH=false`, `flipV=false`.
Порядок пунктов: окно, дверь, открытый проём, ворота. Hover/click resolver,
physical-wall eligibility, запрет на open span, center magnet, shoulder rulers и
commit flow полностью общие с остальными openings.
Preview нового типа не рисует дверной symbol. Он показывает только существующие
временные элементы размещения: центральную точку, размерные подписи и нейтральный
selection footprint. Пунктир open span и створка не используются.
### 8.2 Диалог
Диалог содержит четвёртый radio option. При `type === 'passage'`:
- поле ширины остаётся;
- contact selector скрыт;
- invert скрыт;
- lock selector скрыт;
- flip horizontal и flip vertical скрыты;
- icon диалога — `mdi:arch`;
- Save и Delete работают как для остальных openings.
Если draft содержит непустой `contact` или `lock`, после выбора `passage`
показывается видимый inline warning:
- RU: `При сохранении датчик открытия и замок будут удалены.`;
- EN: `Saving will remove the open/close sensor and lock.`
Текст может грамматически перечислять только реально заполненные поля, но смысл
и наличие предупреждения обязательны. Warning имеет `role="status"` и не
полагается только на цвет или icon.
Переключение radio само по себе не изменяет config. Пока диалог не сохранён,
скрытые значения остаются в draft: если пользователь вернулся к исходному типу,
поля снова видны. Save с `passage` удаляет их. Отдельный confirm modal не нужен:
явное предупреждение в открытом commit-диалоге удовлетворяет продуктовой
формулировке и не добавляет второй уровень подтверждения.
Если прочитан уже сохранённый некорректный `passage` со stale bindings, диалог
показывает то же предупреждение, а Save канонизирует запись. Cancel ничего не
меняет.
### 8.3 История и взаимодействие
- create/edit/type change/delete используют существующие history labels;
- drag меняет только geometry и остаётся одним history step;
- undo/redo восстанавливает целиком прежний opening, включая type и bindings;
- в Plan невидимый opening выбирается существующим hitbox и показывает hover
outline;
- в View и Static `passage` inert и не открывает info card;
- stale `contact`/`lock` у прочитанного `passage` не создают lock badge,
opening info или live entity subscription.
## 9. Нормативная матрица рендера
| Surface/state | Кладка | Пол/tunnel | Собственный symbol | Editor chrome |
|---|---|---|---|---|
| Full View | разрыв по `length` | есть | нет | нет |
| Full Plan, покой | разрыв по `length` | есть | нет | только при hover/selection |
| Full Plan, placement | будущая позиция читается по preview/размерам | preview не коммитит geometry | нет створки/пунктира | да |
| Static card | разрыв по `length` | есть | нет | нет |
| Скрытая изометрия | полноразмерный вертикальный разрыв, видимые откосы | существующий floor | нет полотна и его тени | нет |
### 9.1 Full 2D
- `renderOpeningVisibleGeometry()` обязан иметь явную ветку `passage`,
возвращающую пустую visible geometry; fallback неизвестного типа не должен
превращать его в дверь.
- `openingVisibleMetrics()` продолжает выдавать размеры hitbox/outline по длине
и толщине стены.
- wall cut и opening tunnel используют существующий общий pipeline без отдельной
декоративной рамки.
- `openingAmount('passage', ...)` всегда возвращает `1` и не читает contact.
- lock badge разрешён только для `door` и `gate`, а не по условию
`type !== 'window'`.
- `hide_openings` скрывает symbols старых типов, но не заделывает физический
разрыв `passage`.
### 9.2 Static card
Static должен показать новый тип, но #157 не меняет исторический визуал трёх
старых типов. Поэтому static wall fingerprint и wall builder получают только
нормализованные cuts с `type === 'passage'`; door/window/gate по-прежнему не
добавляют Static symbols/cuts в рамках этой задачи.
Для плана без `passage` output Static и cache fingerprint остаются прежними. При
наличии `passage` canonical wall geometry и tunnel/floor layer должны дать
чистый разрыв и пол, в том числе при default wall thickness. Никаких HTML
hit-targets или entity subscriptions для passage Static не создаёт.
### 9.3 Изометрия
- `IsoOpeningType` расширяется `passage`;
- structural basis для `passage` содержит пустой список leaves;
- opening остаётся во входных cuts wall boolean geometry;
- projection и bounds безопасно обрабатывают пустой basis;
- не рисуются panel, leaf shadow и live-state animation;
- откосы являются частью общей extrusion geometry стены;
- высота/форма арки не моделируются: cut идёт на полную текущую высоту стены.
## 10. Свет и Glow
Классификация проходов должна стать явной, а не зависеть от условия
`type !== 'window'`:
- `door`, `gate`, `passage` — кандидаты interior passage;
- `window` — не passage;
- каждый кандидат проходит одинаковую проверку пола по обе стороны;
- только прошедшие кандидаты вычитаются из light masonry и добавляются в cuts;
- `passage` не зависит от HA state, `contact` и `invert`, даже если эти поля
присутствуют в старой битой записи;
- наружный `passage`, passage у пустой стороны или на нераспознанной геометрии
остаётся непрозрачным для light;
- sun/window rays по-прежнему используют только windows;
- ширина прозрачного участка и Glow footprint совпадают с открытой дверью той
же geometry при одинаковом floor test.
Fingerprint/cache key света должен зависеть от geometry passage так же, как от
других interior passages. Изменение type door↔passage при неизменной geometry не
обязано перестраивать barriers, если итоговая классификация та же; correctness
важнее этой оптимизации.
## 11. Import, export и совместимость
### 11.1 Текущая версия
- full export и space export сохраняют literal `passage` без remap;
- import remap меняет id пространства/opening по существующим правилам, но не
type и geometry;
- valid passage проходит full/space import;
- passage с запрещённым ключом отклоняется до commit;
- неизвестные sibling-поля сохраняются backend schema и при несвязанном
round-trip;
- оптимизация не удаляет и не превращает passage.
### 11.2 Старые конфигурации
Конфигурация без `passage` не мигрируется, не переписывается и не меняет
визуальное или runtime-поведение. Нового top-level capability flag не требуется.
### 11.3 Downgrade
Перед реализацией текущий pre-feature frontend фактически попадает для
неизвестного opening type в дверную fallback-ветку. Поэтому ожидаемый
best-effort downgrade для frontend v1.64.0:
- загрузка и рендер не падают;
- passage может выглядеть как дверь, то есть визуальная точность не гарантируется;
- старый диалог не знает radio option и не должен считаться поддерживаемым
редактором этого типа;
- старый backend со строгим enum может отклонить последующую запись всей
конфигурации, содержащей `passage`.
Эти ограничения явно записываются в `docs/CONFIG-COMPATIBILITY.md`. Реализация
должна подтвердить отсутствие runtime crash source-аудитом/fixture-тестом
pre-feature поведения; поддержка редактирования на старой версии не требуется.
## 12. I18n, accessibility и touch
Минимальный набор новых RU/EN ключей:
- `opening.passage`;
- `opening.passage_binding_warning`;
- при необходимости отдельный help/description key для отличия от open span.
Требования:
- RU/EN key parity test проходит;
- radio имеет обычную label association и доступен с клавиатуры;
- warning читается screen reader и не кодируется одним цветом;
- toolbar item получает локализованный accessible label через существующий
menu contract;
- новый тип не уменьшает существующие touch targets;
- невидимый passage hitbox в Plan сохраняет существующий минимум; во View его
pointer events отключены по общему contract openings.
## 13. Performance, security и observability
- отдельного runtime animation/state resolver для `passage` нет;
- stale bindings не попадают в full/static snapshot entity sets;
- wall, light и iso caches используют существующую geometry/fingerprint
архитектуру; новый unbounded cache не добавляется;
- change-aware validator работает линейно по spaces/openings с map по id;
- error не логирует entity ids и не раскрывает больше существующей config API;
- права websocket/import не меняются;
- отдельного benchmark profile не требуется; перед бетой проходит канонический
performance gate всего продукта.
## 14. Acceptance criteria
1. В toolbar и opening dialog доступен `Открытый проём` / `Open passage` с
default 90 см.
2. Для passage показывается только ширина; contact, lock, invert, flip_h,
flip_v скрыты и не записываются.
3. При смене объекта с непустым contact/lock на passage до Save виден явный
warning; Cancel сохраняет исходник, Save удаляет bindings, обратная смена
после Save их не восстанавливает.
4. Full View/Plan показывает чистый физический разрыв стены и продолжение пола,
без створки, дуги, пунктира или рамки.
5. Passage остаётся выбираемым, перемещаемым и удаляемым в Plan через временный
editor chrome, но inert в View/Static.
6. Static card показывает passage как разрыв с полом; план без passage и старые
opening types сохраняют прежний Static output.
7. Скрытая изометрия показывает полноразмерный cut и откосы, без leaf/panel
shadow и без crash.
8. Внутренний passage пропускает light/Glow независимо от state; наружный или
неопределённый passage остаётся fail-dark.
9. Backend принимает канонический passage и отклоняет новую/изменённую запись с
любым из пяти запрещённых ключей кодом `invalid_passage_fields`.
10. Неизменённый legacy-broken passage проходит несвязанное сохранение; его
исправление принимается; import валидирует весь входящий content.
11. Full/space export-import сохраняют type/geometry и обычный id remap; partial
write при ошибке отсутствует.
12. Конфиги с door/window/gate не мигрируют и проходят regression suite без
изменений.
13. Старый frontend v1.64.0 на passage не падает; точный fallback и ограничение
старого backend задокументированы.
14. RU/EN parity, unit/backend tests, build, smoke и новые golden evidence
проходят в предусмотренный процессом момент.
## 15. Тест-план
### 15.1 TypeScript unit
- `OpeningPlacementType`/preset: passage, 90 см, false flips;
- placement на physical wall, запрет на open span, center magnet и measurements
не расходятся с door;
- `openingAmount('passage', null/on/off/unavailable, invert)` всегда 1;
- `renderOpeningVisibleGeometry(passage)` не содержит leaf/arc/glass/gate path;
- metrics/hitbox остаются конечными и положительными;
- full renderer не создаёт lock badge/info/binding subscription для stale
passage fields;
- light: две комнаты пропускают, внешний край не пропускает, footprint равен
door при одинаковой geometry;
- Static fingerprint/output меняется для passage и не меняется для старых типов;
- iso basis passage имеет zero leaves, projection/bounds безопасны;
- RU/EN parity.
### 15.2 Backend unit
- schema принимает canonical passage и прежние три types;
- новый/сменивший тип passage с каждым запрещённым ключом, включая null/false,
отклоняется;
- одновременный список полей даёт стабильный отсортированный error;
- неизменённый broken passage + unrelated config edit принимается;
- изменение geometry при неизменных broken fields принимается как несвязанное с
этими полями изменение;
- удаление broken fields принимается;
- изменение/добавление broken field отклоняется;
- config/set и optimize вызывают validator;
- merge, full replace и space import покрыты success/failure/atomicity cases;
- valid export-import round-trip сохраняет type и geometry.
### 15.3 Smoke/manual
В реализации, а перед бетой — каноническим smoke gate:
1. создать passage через toolbar на общей толстой стене;
2. убедиться в preview без створки и в default 90 см;
3. сохранить, hover/select, перетащить, undo/redo, удалить;
4. создать дверь с contact+lock, выбрать passage, увидеть warning, Cancel;
5. повторить и Save; убедиться, что bindings удалены и не восстановились;
6. проверить Full View и Static в light/dark themes;
7. включить Glow: внутренний проход светится насквозь, наружный — нет;
8. проверить скрытую изометрию;
9. full и space export/import;
10. загрузить downgrade fixture старым frontend и подтвердить отсутствие crash.
### 15.4 Golden
Добавить детерминированную сцену с двумя комнатами, толстой общей стеной,
внутренним passage и источником Glow:
- Full 2D, dark theme;
- Static, dark theme;
- скрытая изометрия, dark theme;
- при необходимости отдельный exterior fail-dark кадр.
Новые baselines предпочтительнее переписывания несвязанных эталонов. Любое
изменение существующего `smoke_opening_*` baseline требует объяснения в PR/code
review evidence.
### 15.5 Команды и момент запуска
В цикле реализации:
```text
npm run typecheck
npm test
npm run build
python -m pytest tests_backend
```
Golden, browser smoke и performance выполняются перед бетой согласно release
runbook. Полный HA harness канонически выполняется в Linux CI; невозможность
локального Windows `fcntl` не считается заменой CI.
## 16. План реализации
1. Расширить types/schema и добавить чистый semantic validator со всеми call
sites и backend tests.
2. Расширить placement preset, toolbar, dialog draft/save canonicalization,
warning и i18n.
3. Добавить явные passage branches в 2D symbol/amount/locks/subscriptions.
4. Подключить passage cut/tunnel к Static без изменения output старых типов.
5. Сделать light classifier явным и покрыть interior/exterior cases.
6. Добавить zero-leaf passage basis в скрытой изометрии.
7. Покрыть import/export и downgrade fixture.
8. Обновить документацию и release artifacts, пройти implementation gates.
## 17. Release-артефакты
При реализации #157 в том же user-visible commit обязательны:
- `docs/CHANGELOG.md`;
- `docs/CHANGELOG.ru.md`;
- `docs/USER-GUIDE.ru.md` — таблица passage/open span и поток редактора;
- `docs/LIGHT.md` — explicit interior-passage rule;
- `docs/ISOMETRIC.md` — zero-leaf/full-height-cut поведение скрытого режима;
- `docs/CONFIG-COMPATIBILITY.md` — enum, broken-read validator и downgrade;
- при необходимости `docs/TESTING.md` — новая smoke/golden сцена;
- новые reviewed golden baselines и ссылка на evidence в issue/PR.
Коммит продуктового кода должен иметь терминальные trailers:
```text
Issue: #157
User-Visible: yes
```
## 18. Риски и меры
| Риск | Мера |
|---|---|
| Fallback рисует passage как дверь | явные branches и отрицательные renderer tests |
| Невидимый объект нельзя выбрать | сохранить отдельные metrics/hitbox/hover tests |
| Stale binding продолжает влиять на UI/runtime | type allowlist для locks/info/subscriptions, tests с broken fixture |
| Строгая схема блокирует несвязанные сохранения | change-aware semantic validator вместо cross-field schema reject |
| Import обходит правило | подключить validator к обоим import flows и optimize |
| Static меняет старые планы | fingerprint/cuts только для passage, regression snapshots без passage |
| Свет утекает наружу | сохранить двусторонний floor probe и exterior test |
| Iso создаёт дверное полотно | zero-leaf basis и golden |
| Старый frontend портит новый тип | документировать downgrade как read-only best effort |
## 19. Откат
Код можно откатить обычным revert, но уже сохранённый literal `passage` станет
неизвестен старому backend. Поэтому безопасный rollback релиза требует либо
предварительно вернуть такие openings к `door`/`gate`, либо сохранить frontend и
backend read support для `passage`, отключив только создание в UI. Автоматически
превращать passage в дверь при rollback запрещено: это создаёт ложный symbol и
может предложить небезопасные lock bindings.
## 20. Принятые технические предположения
Следующие мелкие решения приняты без дополнительного продуктового вопроса и
могут быть изменены при реализации, если не нарушаются acceptance criteria:
- `mdi:arch` — рабочая icon; при отсутствии в поставляемой версии MDI допустим
локализованно нейтральный `mdi:door-open` без изменения persisted contract;
- passage расположен между door и gate в submenu;
- inline warning достаточен, отдельный confirm modal не нужен;
- draft временно хранит скрытые bindings, чтобы смена radio до Save была
обратимой;
- unknown sibling-поля сохраняются при явном edit, запрещённые известные поля
удаляются;
- наличие запрещённого ключа, даже с null/false, неканонично для новой записи;
- Static получает только passage cuts, чтобы не расширять #157 до общей opening
parity;
- «арка» в этой версии — полноразмерный прямоугольный cut без кривой перемычки;
- отдельный performance benchmark не нужен, если общий pre-beta gate не
показывает регрессию.
+1
View File
@@ -89,6 +89,7 @@ GitHub Issues и GitHub Projects (v2) остаются единственным
| [#123](https://github.com/Matysh/houseplan-card/issues/123) Split из вершины не меняет наружную геометрию стен | [123-corner-split-wall.md](123-corner-split-wall.md) |
| [#137](https://github.com/Matysh/houseplan-card/issues/137) Узлы и линии привязки в редакторе Плана | [137-plan-snap-overlay.md](137-plan-snap-overlay.md) |
| [#141](https://github.com/Matysh/houseplan-card/issues/141) Бесшовные стыки перегородок и открытых контуров | [141-wall-junctions.md](141-wall-junctions.md) |
| [#157](https://github.com/Matysh/houseplan-card/issues/157) Тип проёма «Открытый проём» | [157-open-passage.md](157-open-passage.md) |
## Правило актуализации