mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-30 19:58:50 +00:00
727 lines
49 KiB
Markdown
727 lines
49 KiB
Markdown
# #306 — Нулевые стены вместо виртуальных границ
|
||
|
||
- **Issue:** [#306](https://github.com/Matysh/houseplan-card/issues/306)
|
||
- **Статус документа:** принятый контракт; рабочий статус хранится в метках issue
|
||
- **Приоритет / тип:** P1 / feature
|
||
- **Целевая версия модели:** `PLAN_MODEL_VERSION = 9`
|
||
- **Пользовательское изменение:** да
|
||
|
||
## 1. Сценарий
|
||
|
||
Администратор дома создаёт или исправляет планировку в Plan editor на десктопе.
|
||
В момент рисования новой стены, изменения толщины существующего участка или
|
||
настройки пространства ему нужно обозначить границу без физической толщины, не
|
||
переключаясь на отдельный инструмент с другой логикой.
|
||
|
||
Задача закрывает J4 и J6 из `docs/SCOPE.md`: первоначальная настройка становится
|
||
понятнее, а последующее обслуживание плана использует одну систему стен без
|
||
рассинхронизации площади, света и геометрии.
|
||
|
||
## 2. Что человек увидит до и после
|
||
|
||
**До:** граница без физической стены создаётся отдельным неочевидным инструментом
|
||
и ведёт себя иначе обычной стены. **После:** пользователь рисует и меняет её как
|
||
обычную стену с толщиной 0, а в настройках пространства выбирает, показывать её
|
||
пунктиром с пропусканием света или сплошной линией с блокировкой света.
|
||
|
||
## 3. Цель
|
||
|
||
Убрать из продукта отдельный инструмент **«Граница»** и канонические сущности
|
||
`space.open_spans` / `rooms[].open_to`. Их роль выполняют обычные атомарные
|
||
отрезки стен с явным `cm: 0`.
|
||
|
||
В редакторе остаётся одна модель:
|
||
|
||
- `cm > 0` — стена с физическим телом;
|
||
- `cm = 0` — топологическая стена без физического тела.
|
||
|
||
После #282 каждый contour atom уже хранится в authoritative-каталоге
|
||
`space.wall_segments[]` со стабильным `id`, точными `a/b` и явным `cm`.
|
||
Происхождение нулевого атома не сохраняется и не влияет на результат: прежняя
|
||
физическая ось без тела, бывшая виртуальная граница и новая стена, нарисованная
|
||
с толщиной 0, становятся одной и той же сущностью.
|
||
|
||
Пользователь рисует и изменяет оба вида инструментами **«Стены»** и
|
||
**«Толщина»**. Настройка пространства определяет, показываются ли все нулевые
|
||
стены пунктиром или сплошной линией и пропускают ли они свет.
|
||
|
||
## 4. Зафиксированные продуктовые решения
|
||
|
||
1. Кнопка, сессия и термин **«Граница»** удаляются из UI.
|
||
2. Толщина обычной стены и перегородки принимает `0..100 см`; колонны сохраняют
|
||
диапазон `1..150 см`.
|
||
3. `0` — сохранённое значение, не пустое поле и не команда удаления записи.
|
||
4. Стиль нулевых стен задаётся один раз на пространство:
|
||
|
||
| Значение | Вид | Glow | Солнечные лучи | Общая граница комнат |
|
||
|---|---|---|---|---|
|
||
| `dashed` | пунктир | проходит | проходят | соединяет световые зоны |
|
||
| `solid` | сплошная линия | блокируется | блокируются | не соединяет световые зоны |
|
||
|
||
5. Отсутствующее/неизвестное значение настройки читается как `dashed`.
|
||
Это правило применяется ко **всем** `cm:0`, включая атомы, которые #282
|
||
создала из старых физических осей без толстого тела. В результате часть
|
||
старых планов после обновления неизбежно изменит вид или светопроницаемость;
|
||
это принятое владельцем поведение, а не ошибка миграции.
|
||
6. Нулевая стена в обоих режимах остаётся частью топологии, но не создаёт
|
||
физическое тело, бумагу, тоннель проёма и вычет площади.
|
||
7. Новый проём на нулевой стене запрещён. Перевод участка с проёмом в `0`
|
||
отклоняется до записи; проём автоматически не удаляется и не деактивируется.
|
||
8. Legacy `open_spans`, а при их отсутствии legacy `open_to`, переводят
|
||
покрытые атомы v8 в `cm:0`; все остальные уже существующие `cm:0` остаются
|
||
неотличимыми от них. Простое чтение конфигурации ничего не записывает.
|
||
9. После канонической записи старые поля больше не пишутся. Downgrade на версию,
|
||
не понимающую `cm:0`, не поддерживается; восстановление выполняется из backup.
|
||
10. Plan editor остаётся desktop-first. View и kiosk на touch входят в
|
||
блокирующий acceptance floor.
|
||
|
||
Открытых продуктовых вопросов нет.
|
||
|
||
## 5. Термины и инварианты
|
||
|
||
### 5.1 Атом стены
|
||
|
||
Атом — максимальный коллинеарный интервал с одними и теми же:
|
||
|
||
- координатами носителя;
|
||
- ролью `outer(room)`, `shared(roomA,roomB)` либо `independent(id)`;
|
||
- множеством владельцев;
|
||
- толщиной `cm`;
|
||
- границами, заданными узлами, концами существующих записей и проёмов.
|
||
|
||
Слияние соседних атомов разрешено только при полном совпадении роли, владельцев,
|
||
носителя и `cm`. Нельзя сливать `outer(A)` с `shared(A,B)`, разные пары shared,
|
||
room-wall с independent wall или пересекать узел/проём.
|
||
|
||
### 5.2 Authoritative wall identity from v8; current model v9
|
||
|
||
#282 уже реализовала Stage 1 модели стен:
|
||
|
||
- `space.wall_segments[]` содержит **каждый** contour atom и является
|
||
authoritative для identity и толщины;
|
||
- `rooms[].wall_ids[]` ссылается на эти атомы в порядке обхода `poly`;
|
||
- `space.walls[]` — только compatibility-проекция положительных толщин и не
|
||
является источником истины;
|
||
- `partitions[]` и `room_drafts[].segments[]` сохраняют собственные стабильные
|
||
ID и явный `cm`.
|
||
|
||
#306 не создаёт второй каталог и не возвращает midpoint-key identity. Она
|
||
расширяет уже существующий v8-инвариант: `cm:0` получает единый пользовательский
|
||
смысл и диапазон записи `0..100` на всех segment-based write paths. Функции
|
||
физического тела фильтруют `cm > 0`; функции топологии и редактора сохраняют и
|
||
видят `cm = 0`. При замыкании draft нулевой сегмент наследует ID по lineage-
|
||
правилам #282.
|
||
|
||
Никаких `zero_kind`, `legacy_origin`, скрытых compatibility-флагов или эвристик
|
||
по происхождению сегмента не добавляется. Два `cm:0` с одинаковой ролью и
|
||
геометрией всегда эквивалентны.
|
||
|
||
### 5.3 Единый resolver
|
||
|
||
Новый pure-модуль `src/zero-walls.ts` (имя можно уточнить без изменения
|
||
контракта) является единственной точкой для:
|
||
|
||
- `resolveZeroWallMode(space): { style, transmitsLight }`;
|
||
- проекции legacy `open_spans/open_to` на authoritative v8 atoms;
|
||
- классификации явных нулевых атомов по роли;
|
||
- получения линий световых барьеров и derived room connectivity;
|
||
- миграции v8 candidate в канонический v9 document без смены stable ID там,
|
||
где carrier не разрезается.
|
||
|
||
Glow и солнце не имеют собственных проверок `dashed/solid`. Оба используют
|
||
результат этого resolver. Визуальный renderer получает тот же `style`, но не
|
||
определяет световое поведение через CSS.
|
||
|
||
## 6. Persisted schema
|
||
|
||
### 6.1 Новое поле пространства
|
||
|
||
```ts
|
||
type ZeroWallStyle = 'dashed' | 'solid';
|
||
|
||
interface SpaceConfig {
|
||
zero_wall_style?: ZeroWallStyle; // missing/unknown read fallback = dashed
|
||
wall_segments: Array<{ id: string; a: number[]; b: number[]; cm: number }>;
|
||
walls?: Array<{ key: string; cm: number; a: number[]; b: number[] }>;
|
||
room_drafts?: Array<{ segments: Array<{ id: string; cm: number }> }>;
|
||
partitions?: Array<{ id: string; a: number[]; b: number[]; cm: number }>;
|
||
}
|
||
```
|
||
|
||
Backend принимает `cm: 0..100` для `wall_segments[]`,
|
||
`room_drafts[].segments[]` и `partitions[]`; `wall_columns[].cm` остаётся
|
||
`1..150`. Compatibility `walls[]` генерируется только из `cm>0`, всегда имеет
|
||
точные `a/b` и никогда не содержит zero record.
|
||
|
||
`PLAN_MODEL_VERSION` повышается с `8` до `9` только при фактической канонической
|
||
записи хотя бы одного затронутого пространства. Открытие карточки и
|
||
неструктурные изменения версию не повышают.
|
||
|
||
### 6.2 Deprecated compatibility fields
|
||
|
||
`space.open_spans` и `rooms[].open_to`:
|
||
|
||
- временно принимаются backend/import reader для старых документов;
|
||
- сохраняются без мутации при read-only загрузке;
|
||
- проецируются в runtime поверх v8 catalog: покрытые интервалы имеют `cm:0`, а
|
||
остальные существующие `cm:0` не получают отдельного происхождения;
|
||
- удаляются из затронутого пространства одной транзакцией после успешной
|
||
канонической миграции;
|
||
- никогда не создаются новым frontend;
|
||
- в экспорте канонического v9 отсутствуют;
|
||
- регистрируются в `docs/CONFIG-COMPATIBILITY.md` и
|
||
`scripts/config-field-registry.mjs` существующими статусами
|
||
`deprecated-read` для compatibility-read и `migrate-on-write` для
|
||
документированных structural-write/Optimize/import путей.
|
||
|
||
Если одновременно есть валидные `open_spans` и `open_to`, источником геометрии
|
||
являются `open_spans`; `open_to` не расширяет их. Если `open_spans` отсутствует
|
||
или пуст, `open_to` материализует полный доказанный общий интервал каждой пары.
|
||
|
||
Unknown sibling-поля сохраняются на read/write как сейчас.
|
||
|
||
## 7. UX редактора
|
||
|
||
### 7.1 Основная панель
|
||
|
||
- Кнопка **«Граница»**, её submenu/context tray, подсказки и состояния удаляются.
|
||
- Поле толщины у **«Стен»** принимает локализованный эквивалент `0..100 см`.
|
||
- Пустое, `NaN`, отрицательное или больше максимума значение блокирует начало
|
||
сегмента и показывает существующий range-feedback с новым диапазоном.
|
||
- `0` рисует обычный сегмент текущей wall/draft/partition цепочки. Замыкание
|
||
комнаты, Ctrl+click и Shift 45° работают без отдельной ветки.
|
||
- Pointer cancel, pinch и уход из редактора не записывают незавершённый нулевой
|
||
сегмент.
|
||
|
||
### 7.2 Инструмент «Толщина»
|
||
|
||
- Диалог/поле принимает `0..100 см` для room wall, draft и partition.
|
||
- `positive → 0` атомизирует только выбранный доказанный интервал и сохраняет
|
||
толщину соседних остатков.
|
||
- `0 → positive` восстанавливает физическое тело обычным путём.
|
||
- Пустое поле не означает `0`: Save неактивен/операция отклонена.
|
||
- Если выбранный интервал пересекает размещённый проём, `positive → 0`
|
||
атомарно отклоняется с сообщением удалить проём прежде. Остальной план не
|
||
меняется.
|
||
|
||
### 7.3 Рисование по существующей оси
|
||
|
||
Рисование `cm:0` поверх доказанного участка room wall изменяет/атомизирует
|
||
authoritative `wall_segments[]` через identity barrier #282. Оно не создаёт
|
||
совпадающую `partition`. Повторное рисование того же интервала — semantic no-op
|
||
и не добавляет Undo-команду.
|
||
|
||
На совпадающей independent partition меняется существующий объект только если
|
||
есть ровно один однозначный carrier. T/X-узел, несколько carrier или конфликт
|
||
ролей отклоняются до write с локализованным сообщением. Частичный кандидат не
|
||
сохраняется.
|
||
|
||
### 7.4 Настройки пространства
|
||
|
||
В секции отображения стен добавляется radio/select:
|
||
|
||
- заголовок RU: **«Стены нулевой толщины»**;
|
||
- варианты RU: **«Пунктирные»**, **«Сплошные»**;
|
||
- видимое пояснение RU: **«Пунктирные стены пропускают свет. Сплошные стены
|
||
блокируют свет, даже при нулевой толщине.»**;
|
||
- эквивалентные EN-строки перечислены в §15.
|
||
|
||
Изменение применяется сразу ко всем нулевым стенам пространства одной config
|
||
транзакцией, поддерживает обычную отмену несохранённого диалога и не переписывает
|
||
геометрию.
|
||
|
||
## 8. Геометрия и операции
|
||
|
||
### 8.1 Физическое тело, площадь и пол
|
||
|
||
- `cm:0` никогда не передаётся в wall-body union как полигон и не заменяется
|
||
внутренним epsilon/`1 см`.
|
||
- Чистая площадь комнаты и inset floor игнорируют его физическую толщину, но
|
||
продолжают использовать ось как контур комнаты.
|
||
- Independent `cm:0` не создаёт бумагу/пол и не вычитает площадь.
|
||
- Замкнутая цепочка independent нулевых стен не создаёт комнату сама по себе;
|
||
комната возникает только через существующий flow предложения комнаты.
|
||
|
||
### 8.2 Проёмы
|
||
|
||
- Picker/placement не предлагает нулевой wall interval как host.
|
||
- Изменение host wall в `0` запрещено, пока любой door/window/gate/passage
|
||
пересекает целевой атом.
|
||
- У нулевой стены нет tunnel fill, откосов и opening cut.
|
||
- Legacy-конфликт `open_span/open_to` с проёмом блокирует миграцию всего
|
||
пространства. Runtime остаётся читаемым по legacy projection; Optimize и
|
||
структурная запись показывают конкретный blocker и не удаляют проём.
|
||
|
||
### 8.3 Split, Merge, Delete, Resize
|
||
|
||
- Split переносит `cm:0` на соответствующий новый room-wall atom без подстановки
|
||
fallback thickness.
|
||
- Merge удаляет только доказанно внутренние дубли; нулевые атомы внешнего
|
||
контура и независимые нулевые стены сохраняются.
|
||
- Delete room с сохранением стен не превращает `cm:0` в partition с положительной
|
||
толщиной. Нулевой exclusive interval либо сохраняется как `partition cm:0`,
|
||
если пользователь выбрал сохранить стены, либо удаляется вместе с комнатой.
|
||
- Resize меняет координаты carrier, но сохраняет `cm:0`, роль и breakpoints;
|
||
примкнувшие independent стены по прежнему автоматически не двигаются.
|
||
- Каждая геометрическая операция остаётся одной именованной geometry-history
|
||
командой. Undo/Redo восстанавливает одновременно геометрию, `cm` и legacy-поля,
|
||
если миграция входила в эту транзакцию. `zero_wall_style` следует текущему
|
||
контракту Save/Cancel настроек пространства и не добавляется в geometry stack.
|
||
|
||
### 8.4 Нормализация и Optimize
|
||
|
||
- Identity writer #282 сохраняет stable ID и явные нули; соседние атомы
|
||
объединяются только при совпадении role/owners/cm и по документированным
|
||
lineage-правилам (survivor ID детерминирован, ссылки opening остаются валидны).
|
||
- Ни один helper не должен применять `clampWallCm(0) → 1`.
|
||
- Optimize preview отдельно считает `legacy virtual spans migrated` и
|
||
`zero-wall atoms merged`.
|
||
- После Confirm все пространства мигрируют атомарно; ошибка/лимит в одном
|
||
пространстве отменяет весь candidate и оставляет one-deep Optimize Undo.
|
||
- Повторный Optimize канонического v9 — byte/semantic no-op.
|
||
|
||
## 9. Свет и отображение
|
||
|
||
### 9.1 Свет
|
||
|
||
Для `dashed` нулевые сегменты исключаются из Glow/sun barriers. Shared нулевой
|
||
атом добавляет пару комнат в derived light connectivity. Для `solid` тот же
|
||
сегмент добавляется как **осевая line barrier без площади**, а shared atom не
|
||
соединяет комнаты.
|
||
|
||
Допустим вычислительный epsilon только внутри line-intersection predicate для
|
||
численной устойчивости. Он не попадает в wall body, floor, area, renderer,
|
||
opening depth или persisted data и не зависит от `cell_cm` как фиктивная
|
||
физическая толщина.
|
||
|
||
Смена `zero_wall_style` инвалидирует room connectivity, light regions, Glow
|
||
barriers, sun barriers, wall/render fingerprints и isometric cache. Она не
|
||
зависит от HA state ticks и не требует reload.
|
||
|
||
Outer и independent `cm:0` следуют тому же правилу barrier: dashed не блокирует,
|
||
solid блокирует пересечение луча. Нулевая outer wall сама не создаёт источник
|
||
солнечного света — источник по-прежнему возникает только из валидного окна.
|
||
|
||
### 9.2 Flat, isometric и редакторы
|
||
|
||
- Flat View, kiosk, isometric View и визуальный export используют выбранный
|
||
solid/dashed стиль.
|
||
- Нулевая стена рисуется одной line primitive на оси, без hatch/body/end cap.
|
||
- При `show_borders:false` нулевые стены скрыты в View/kiosk, но их выбранная
|
||
световая семантика сохраняется. Редакторы продолжают показывать их.
|
||
- Plan editor показывает служебные оси и узлы единым overlay во всех режимах;
|
||
overlay не зависит от solid/dashed.
|
||
- Decor editor применяет к нулевым стенам ту же контекстную прозрачность, что к
|
||
остальной планировке.
|
||
- Hover/hit target остаётся доступным минимумом текущего wall editor и не
|
||
ограничивается визуальной толщиной линии.
|
||
|
||
## 10. Алгоритм миграции v8 → v9
|
||
|
||
Миграция pure, deterministic, idempotent и возвращает либо полный candidate,
|
||
либо typed blocker. Документ старее v8 сначала проходит уже реализованный
|
||
identity barrier #282 и только затем этот алгоритм; отдельный второй каталог
|
||
стен не строится.
|
||
|
||
1. Валидировать v8 `rooms[].poly/wall_ids`, `wall_segments`, openings, limits и
|
||
координаты без изменения input. Для pre-v8 input получить полный v8 candidate
|
||
существующим `commitWallSegmentModel()`.
|
||
2. Если есть валидные `open_spans`, использовать их. Иначе построить полные
|
||
shared intervals по `open_to` через существующий `sharedBoundary` resolver.
|
||
3. Нормализовать и clip legacy spans строго к доказанным contour carriers.
|
||
Висящий, неоднозначный или non-finite span — blocker, не silently drop.
|
||
4. Убедиться, что endpoints каждого legacy span являются границами атомов.
|
||
Если v8 catalog уже атомизирован #282, stable IDs сохраняются. Если требуется
|
||
дополнительный split, применяется lineage #282: один доказанный survivor
|
||
сохраняет исходный ID, остальные получают новые ID, а `room.wall_ids` и
|
||
wall-hosted references обновляются атомарно.
|
||
5. Каждый атом, покрытый legacy virtual span, получает `cm:0`. Положительный
|
||
остаток сохраняет точный `cm`. Любой атом, который уже имел `cm:0`, остаётся
|
||
нулевым без отдельной метки происхождения и получает тот же style/light mode.
|
||
6. Проверить проёмы на **всех** итоговых `cm:0`, а не только на бывших virtual
|
||
spans. Zero opening host — blocker всего пространства: opening и исходный v8
|
||
документ сохраняются, молчаливое удаление запрещено.
|
||
7. Role-aware normalization объединяет только эквивалентные соседние нулевые
|
||
атомы и сохраняет валидную identity/host lineage.
|
||
8. Перегенерировать compatibility `walls[]` только из `cm>0`. Удалить
|
||
`space.open_spans` и `rooms[].open_to` только из полностью готового candidate.
|
||
9. Не добавлять `zero_wall_style`, если пользователь его не сохранял: runtime
|
||
fallback `dashed` применяется ко всем `cm:0`. Это намеренно может изменить
|
||
вид и свет старых bodyless-физических осей после обновления.
|
||
10. Прогнать geometry preflight, model invariants и backend schema. При
|
||
превышении `MAX_WALL_SEGMENTS` (сейчас 200 000) или любого другого
|
||
актуального backend-лимита отказать целиком; усечение запрещено.
|
||
11. При записи установить `model_version: 9`. Повторный запуск возвращает no-op.
|
||
|
||
### 10.1 Когда выполняется write
|
||
|
||
- Read adapter всегда строит runtime projection без persistence.
|
||
- Структурная транзакция конкретного пространства (изменение rooms, walls,
|
||
drafts, partitions, openings, Split/Merge/Delete/Resize/Thickness/Draw)
|
||
сначала мигрирует это пространство и пишет всё одной транзакцией.
|
||
- Сохранение marker/layout, HA state, настроек устройства, названия/заливки
|
||
пространства и простое открытие карточки не запускают migration write.
|
||
- **«Оптимизировать планы»** строит preview и мигрирует все legacy-пространства
|
||
одной подтверждённой транзакцией.
|
||
- Full/space import старого документа проецирует и валидирует migration в
|
||
preview; Apply сохраняет canonical v9 candidate либо отказывает целиком.
|
||
|
||
### 10.2 Масштаб и round-trip
|
||
|
||
Координаты переносимых `a/b` проходят существующий lattice write barrier.
|
||
Space-only import с другим `cell_cm` сохраняет нормализованную геометрию и
|
||
сантиметровый `cm:0` без масштабирования значения. Full export/import и
|
||
config/layout round-trip не восстанавливают deprecated fields.
|
||
|
||
## 11. Backend, import/export и concurrency
|
||
|
||
- `custom_components/houseplan/validation.py` принимает новый enum и сохраняет
|
||
существующий v8 диапазон `wall_segments[].cm = 0..100`.
|
||
- Validation v9 запрещает zero opening host, non-finite/negative cm, нарушение
|
||
catalog/room projection и canonical наличие `open_spans/open_to`; legacy
|
||
document допускается только через compatibility read/import path.
|
||
- `import_export.py` включает `zero_wall_style`, умеет preview старой миграции и
|
||
выдаёт локализуемый/машиночитаемый blocker code.
|
||
- Config revision/CAS и атомарная config+layout запись остаются обязательными;
|
||
конфликт ревизии не повторяет migration поверх устаревшего candidate.
|
||
- Permissions, HA service calls, entity registry и security boundary не
|
||
меняются. Новые данные не исполняются и проходят существующие bounds.
|
||
|
||
## 12. Совместимость и откат
|
||
|
||
### 12.1 Forward compatibility
|
||
|
||
Старые `open_spans/open_to` до записи продолжают читаться. Одновременно runtime
|
||
применяет единый zero-wall mode ко всем v8 `cm:0`: при отсутствующей настройке
|
||
это `dashed`, поэтому ранее bodyless-физические оси могут стать пунктирными и
|
||
начать пропускать свет уже после обновления. Этот переход намеренно не является
|
||
lossless по визуалу/свету; координаты, stable IDs, комнаты, положительные
|
||
толщины и сами opening records не меняются. Backend сохраняет compatibility-
|
||
reader минимум весь релизный цикл v1.68.x; удаление reader требует отдельного
|
||
issue и данных telemetry/fixtures.
|
||
|
||
### 12.2 Downgrade / rollback
|
||
|
||
Версия до #306 понимает v8 `cm:0`, но не понимает их новую единую семантику и
|
||
может снова записать `open_spans/open_to`. Поэтому downgrade после canonical v9
|
||
write не поддерживается. До первой структурной записи пользователь может сделать
|
||
полный backup. После Optimize доступен существующий one-deep Undo в текущей
|
||
сессии; надёжный откат между версиями — импорт backup.
|
||
|
||
Релизный rollback до первой beta выполняется revert коммита. После beta нельзя
|
||
возвращать старую schema в stable; исправление выпускается forward-only с
|
||
сохранением v8 reader. Labs-флаг не используется: две одновременно пишущие
|
||
модели создадут больший риск, чем feature flag способен снять.
|
||
|
||
## 13. Производительность
|
||
|
||
- Migration/atomization выполняется только на structural write/Optimize/import,
|
||
не на HA state tick и не на каждом render.
|
||
- Runtime projection, wall-role index, light barriers и render geometry
|
||
fingerprint/cache включают `space id + geometry revision + zero_wall_style`.
|
||
- Смена HA state не пересобирает wall atoms, connectivity или sun barriers.
|
||
- Лимит authoritative-каталога остаётся равен backend-константе
|
||
`MAX_WALL_SEGMENTS` (сейчас 200 000); результат не truncates.
|
||
- Benchmark на large-house fixture сравнивает v8 compatibility projection и v9 canonical:
|
||
p95 построения barrier/first render не должен регрессировать более чем на 10%,
|
||
steady HA-state render — более чем на 5% относительно baseline ветки.
|
||
- Нулевые line barriers не превращаются в полигоны, что ограничивает рост
|
||
polyclip input.
|
||
|
||
## 14. Touch и accessibility
|
||
|
||
- View/kiosk: нулевые стены, свет и `show_borders` обязаны совпадать с desktop;
|
||
pinch/tap не оставляет sticky hover и не меняет стену.
|
||
- Plan editor на touch — best effort / intentionally degraded по
|
||
`docs/TOUCH-SUPPORT.md`, но cancel/pinch/scroll не имеют права совершить write.
|
||
- Select/radio настройки имеет label, keyboard focus и видимое пояснение;
|
||
ошибка проёма/неоднозначности доступна через существующий status/toast flow.
|
||
- Контраст dashed/solid использует текущие wall tokens; смысл не кодируется
|
||
только цветом.
|
||
- `prefers-reduced-motion` не требует отдельного поведения: новая фича не
|
||
добавляет animation.
|
||
|
||
## 15. i18n
|
||
|
||
Добавляются симметрично в `src/i18n/ru.json` и `src/i18n/en.json`:
|
||
|
||
| Key | RU | EN |
|
||
|---|---|---|
|
||
| `space.zero_wall_style` | Стены нулевой толщины | Zero-thickness walls |
|
||
| `space.zero_wall_dashed` | Пунктирные | Dashed |
|
||
| `space.zero_wall_solid` | Сплошные | Solid |
|
||
| `space.zero_wall_help` | Пунктирные стены пропускают свет. Сплошные стены блокируют свет, даже при нулевой толщине. | Dashed walls let light through. Solid walls block light even at zero thickness. |
|
||
| `toast.zero_wall_opening_conflict` | Сначала удалите проём на этом участке стены. | Remove the opening on this wall segment first. |
|
||
| `toast.zero_wall_ambiguous` | Не удалось однозначно выбрать участок стены. Уточните геометрию узла. | The wall segment is ambiguous. Simplify or adjust the junction. |
|
||
| `toast.zero_wall_migration_blocked` | Пространство не преобразовано: {reason}. Данные не изменены. | The space was not converted: {reason}. No data was changed. |
|
||
| `gs.zero_walls_migrated` | Преобразовано виртуальных участков: {n}. | Virtual wall spans converted: {n}. |
|
||
|
||
Существующие `boundary.*`, `toast.boundary_*` и подписи инструмента удаляются
|
||
только после проверки отсутствия consumers. Compatibility import error codes
|
||
локализуются UI, backend возвращает стабильный machine code без текста интерфейса.
|
||
|
||
## 16. Затронутые модули
|
||
|
||
Обязательный минимум; точное разбиение большого `houseplan-card.ts` допускается
|
||
без изменения контракта:
|
||
|
||
- `src/zero-walls.ts` — новый canonical resolver/migration;
|
||
- `src/types.ts`, `src/space-geometry.ts`, `src/logic.ts` — schema/runtime model;
|
||
- `src/wall-thickness.ts`, `src/physical-geometry.ts`, `src/wall-face-graph.ts`,
|
||
`src/wall-merge.ts` — explicit zero, role-aware normalization, no body;
|
||
- `src/open-spans.ts` — оставить compatibility adapter либо заменить им; новый
|
||
production write не импортирует legacy mutators;
|
||
- `src/houseplan-card.ts` — убрать Boundary UX/session; Draw/Thickness/settings,
|
||
render, Undo, Split/Merge/Delete/Resize, cache invalidation;
|
||
- `src/light-visibility.ts`, `src/sun.ts`, `src/iso-walls.ts` — единый light mode
|
||
и visual parity;
|
||
- `src/plan-optimizer.ts`, `src/plan-geometry-preflight.ts`,
|
||
`src/wall-segment-model.ts`, `src/coordinate-canonicalization.ts` — migration
|
||
v9 поверх identity barrier #282, limits/idempotence;
|
||
- `custom_components/houseplan/const.py`, `validation.py`, `import_export.py`,
|
||
`coordinate_canonicalization.py` — model version, schema и import/export;
|
||
- `src/i18n/en.json`, `src/i18n/ru.json`;
|
||
- `docs/WALL-THICKNESS.md`, `docs/LIGHT.md`, `docs/USER-GUIDE.md`,
|
||
`docs/USER-GUIDE.ru.md`,
|
||
`docs/CONFIG-COMPATIBILITY.md`, `docs/ARCHITECTURE.md`, `docs/STATUS.md`, оба
|
||
changelog;
|
||
- unit/backend/smoke/golden/performance fixtures из §17.
|
||
|
||
Противоречащие актуальные ТЗ #148 и #173 получают короткую superseded-note со
|
||
ссылкой на #306; история их acceptance contract не переписывается задним числом.
|
||
|
||
## 17. Acceptance criteria и доказательства
|
||
|
||
### AC1. Один пользовательский инструмент
|
||
|
||
В Plan editor нет кнопки/режима/подсказок «Граница». «Стены» принимают 0 и
|
||
создают нулевой room wall, draft или partition тем же pointer/keyboard flow.
|
||
|
||
**Доказательство:** source-contract unit + `demo/smoke_zero_walls.mjs` desktop;
|
||
golden основной панели до/после.
|
||
|
||
### AC2. Толщина переключается в обе стороны без побочного изменения
|
||
|
||
Для цепочки `15 → 0 → 20` инструмент «Толщина» переводит только выбранный атом
|
||
`0 ↔ positive`; соседи, ownership, coordinates и room topology неизменны.
|
||
Пустое поле не записывает ноль.
|
||
|
||
**Доказательство:** table-driven unit `test/wall-thickness.test.mjs` и smoke с
|
||
Undo/Redo.
|
||
|
||
### AC3. Все `cm:0` имеют одну семантику
|
||
|
||
Для v8 plan с двумя нулевыми contour atoms — один получен из прежней физической
|
||
оси без тела, второй совпадает с `open_spans` — runtime и v9 migration не
|
||
различают происхождение. Оба следуют одному `zero_wall_style`; persisted
|
||
`zero_kind`/compatibility-marker отсутствует. При default `dashed` оба становятся
|
||
пунктирными и пропускают свет.
|
||
|
||
**Доказательство:** migration/runtime unit на смешанной v8 fixture + source-
|
||
contract, запрещающий discriminator происхождения.
|
||
|
||
### AC4. Физическая геометрия и площадь не получают тело
|
||
|
||
Shared, outer и independent `cm:0` дают нулевой wall-body contribution,
|
||
не меняют чистую площадь и не создают paper/hatch/tunnel. Положительные соседи
|
||
сохраняют точные mitre/end geometry.
|
||
|
||
**Доказательство:** `test/physical-geometry.test.mjs`, area unit и golden flat +
|
||
isometric на смешанной цепочке.
|
||
|
||
### AC5. Пунктир пропускает оба вида света
|
||
|
||
В `dashed` Glow и солнечный луч проходят shared/outer/independent zero line;
|
||
shared atom соединяет room light zones. Положительные стены продолжают блокировать.
|
||
|
||
**Доказательство:** unit light matrix (`test/light-visibility.test.mjs`,
|
||
`test/sun.test.mjs`) + deterministic Glow/sun golden.
|
||
|
||
### AC6. Сплошная нулевая стена блокирует оба вида света без фиктивного тела
|
||
|
||
В `solid` та же ось блокирует Glow и солнечные лучи и не соединяет комнаты, но
|
||
area/wall body остаются идентичны dashed-варианту.
|
||
|
||
**Доказательство:** shared fixture matrix и assertions, что physical geometry и
|
||
area byte/number equal между стилями; golden solid.
|
||
|
||
### AC7. Смена настройки работает без reload
|
||
|
||
Переключение `dashed ↔ solid` сразу меняет line style, Glow, sun и connectivity;
|
||
старые barrier/render caches не используются, координаты и `wall_segments[]` не
|
||
меняются.
|
||
|
||
**Доказательство:** unit fingerprint/cache test + browser smoke в одном session.
|
||
|
||
### AC8. Миграция `open_spans` сохраняет данные и idempotent
|
||
|
||
Полный/частичный/соседний span, разные positive residues и shared/outer role
|
||
преобразуются в exact `cm:0` atoms; stable IDs сохраняются по lineage #282,
|
||
deprecated fields удаляются только в candidate, model становится v9. Второй
|
||
запуск no-op. Координаты/комнаты/opening records не теряются; визуальная и
|
||
световая losslessness для прежних bodyless `cm:0` намеренно не обещается.
|
||
|
||
**Доказательство:** `test/zero-wall-migration.test.mjs` fixture matrix и snapshot
|
||
до/после/после второго запуска.
|
||
|
||
### AC9. Legacy `open_to` fallback мигрирует только доказанную общую границу
|
||
|
||
При отсутствии `open_spans` симметричная или односторонняя legacy link создаёт
|
||
полный shared zero interval. При наличии spans link их не расширяет. Missing or
|
||
ambiguous room id блокирует write без partial result.
|
||
|
||
**Доказательство:** migration unit на winding/order/id variants.
|
||
|
||
### AC10. Проёмы защищены от потери
|
||
|
||
Новый проём нельзя разместить на zero atom. Positive→zero и legacy migration с
|
||
пересекающимся opening отклоняются атомарно, сохраняют opening и исходные стены.
|
||
|
||
**Доказательство:** `test/opening-placement.test.mjs`, backend validation и browser
|
||
smoke с RU/EN error lookup.
|
||
|
||
### AC11. Операции геометрии сохраняют ноль и роль
|
||
|
||
Split, Merge, Delete (оба варианта), Resize, room creation, повторное рисование,
|
||
T/X junction, совпадающая partition, Undo/Redo не clamp'ят zero и не создают
|
||
duplicate carrier/mixed-role record.
|
||
|
||
**Доказательство:** pure matrix в wall/resize/room-deletion tests +
|
||
`demo/smoke_edit_walk.mjs` deterministic seeds; ambiguous overlap asserts no write.
|
||
|
||
### AC12. Read-only загрузка ничего не мигрирует
|
||
|
||
Открытие View, HA state update, marker/layout save и изменение неструктурной
|
||
настройки не вызывают config write/model bump. Первая geometry write конкретного
|
||
space мигрирует его в той же CAS transaction.
|
||
|
||
**Доказательство:** mutation-gate unit + browser websocket call log.
|
||
|
||
### AC13. Optimize и import/export атомарны
|
||
|
||
Optimize preview показывает counts и мигрирует все spaces после Confirm; one-deep
|
||
Undo возвращает исходные v8/legacy data в сессии. Full/space v8 или более старый
|
||
import создаёт v9 candidate через последовательные identity и zero-wall barriers.
|
||
Превышение любого актуального backend-лимита, invalid span, opening conflict
|
||
или revision conflict отклоняет весь candidate без truncation/partial apply.
|
||
|
||
**Доказательство:** optimizer unit, `tests_backend/test_ha_import_export.py`,
|
||
`tests_backend/test_validation.py`, browser backup transfer smoke.
|
||
|
||
### AC14. View/isometric/editor/export parity
|
||
|
||
Один и тот же style виден в flat, isometric, Plan/Decor contexts и visual export.
|
||
`show_borders:false` скрывает линию в View/kiosk, но не меняет выбранную световую
|
||
семантику; Plan axes/nodes видны во всех режимах редактора.
|
||
|
||
**Доказательство:** golden matrix desktop + narrow viewport; source assertions и
|
||
smoke `show_borders`.
|
||
|
||
### AC15. Backend и compatibility registry согласованы
|
||
|
||
Backend принимает canonical v9 `wall_segments[].cm:0` с exact endpoints,
|
||
отклоняет negative, non-finite, zero opening host и legacy virtual fields в v9.
|
||
Старый v8/pre-v8 документ читается через compatibility path.
|
||
`docs/CONFIG-COMPATIBILITY.md` описывает оба deprecated поля и downgrade.
|
||
|
||
**Доказательство:** backend parameterized tests + docs review.
|
||
|
||
### AC16. Touch safety floor
|
||
|
||
На touch View/kiosk линия и свет совпадают с desktop. Pinch/cancel в Plan editor
|
||
не записывает нулевой сегмент и не оставляет активную command session.
|
||
|
||
**Доказательство:** Playwright touch smoke; ручное редактирование сверх этого —
|
||
best effort и не блокирует.
|
||
|
||
### AC17. Производительность и кэши
|
||
|
||
Large-house v8 projection/v9 canonical проходят бюджеты §13; HA state tick не
|
||
запускает atomization/barrier rebuild; style toggle запускает ровно одну
|
||
инвалидацию требуемых geometry/light caches.
|
||
|
||
**Доказательство:** benchmark artefact JSON + source-fingerprint unit.
|
||
|
||
### AC18. Документация и релизные артефакты
|
||
|
||
RU/EN UI, changelog и user docs описывают одну систему стен, новый диапазон,
|
||
стиль/свет и backup перед migration. ТЗ #148/#173 помечены superseded в части
|
||
Boundary. Bundle-копии синхронны.
|
||
|
||
**Доказательство:** i18n/docs/bundle tests и ревью артефактов.
|
||
|
||
## 18. Обязательные проверки реализации
|
||
|
||
Минимальный implementation gate до code review:
|
||
|
||
```text
|
||
npm run typecheck
|
||
npm test
|
||
npm run build
|
||
uv run pytest tests_backend/test_validation.py tests_backend/test_ha_import_export.py
|
||
```
|
||
|
||
Code-review/release gate дополнительно выполняет:
|
||
|
||
- `demo/smoke_zero_walls.mjs` desktop + touch safety;
|
||
- Glow и sun deterministic golden matrix;
|
||
- flat/isometric/show_borders golden review;
|
||
- large-house performance capture с бюджетами §13;
|
||
- полный импорт v8 и pre-v8 full/space fixtures и v9 round-trip;
|
||
- сверку `dist/houseplan-card.js` и
|
||
`custom_components/houseplan/frontend/houseplan-card.js`.
|
||
|
||
Golden принимаются только через действующую policy после визуального review;
|
||
изменение baseline ради зелёного CI запрещено.
|
||
|
||
## 19. Release-артефакты
|
||
|
||
В том же коммите, что пользовательское поведение:
|
||
|
||
- `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md`: удаление «Границы», стены `0`,
|
||
настройка пунктир/сплошная и предупреждение о migration backup;
|
||
- `docs/USER-GUIDE.md`, `docs/USER-GUIDE.ru.md`, `docs/WALL-THICKNESS.md`,
|
||
`docs/LIGHT.md`;
|
||
- `docs/CONFIG-COMPATIBILITY.md`, `docs/ARCHITECTURE.md`, `docs/STATUS.md`;
|
||
- RU/EN screenshots/golden: toolbar, space setting, dashed/solid flat и iso,
|
||
Glow/sun matrix;
|
||
- performance JSON и полный review-документ;
|
||
- security artefact не требуется: permissions/service boundary не меняется;
|
||
backend bounds покрываются tests.
|
||
|
||
## 20. Риски и защита
|
||
|
||
| Риск | Защита |
|
||
|---|---|
|
||
| Отсутствующая thickness-запись ошибочно станет open | explicit-presence invariant + AC3 |
|
||
| Потеря positive residue при partial span | breakpoint atomization + snapshot AC8 |
|
||
| Проём исчезнет/переедет | migrate blocker и запрет positive→zero |
|
||
| Glow и sun разойдутся | единый resolver, совместная матрица AC5/AC6 |
|
||
| `0` превратится в `1` старым clamp | отдельный zero-aware parser + AC2/AC11 |
|
||
| Atomization превысит лимит | atomic failure, no truncation |
|
||
| Старый клиент вернёт legacy-поля в v9 | документированный unsupported downgrade + backup |
|
||
| Cache переживёт style toggle | style in fingerprint + AC7/AC17 |
|
||
| Две модели продолжат писаться | source-contract запрещает production legacy writers |
|
||
|
||
## 21. Зависимости и вне скоупа
|
||
|
||
### Зависимости
|
||
|
||
- #33 — compatibility lifecycle;
|
||
- #173 — единый инструмент «Стены»;
|
||
- #199 — geometry preflight;
|
||
- #224/#299 — canonical coordinates и role-aware records;
|
||
- #282 — реализованная обязательная основа: stable `wall_segments[].id`,
|
||
`rooms[].wall_ids` и единый structural identity barrier. #306 не дублирует и
|
||
не обходит этот writer.
|
||
|
||
### Вне скоупа
|
||
|
||
- Stages 2–4 ADR #282 (integer lattice, единый planar graph, closed-form junctions);
|
||
- разный style/light mode у отдельных zero walls;
|
||
- проёмы в нулевых стенах и «неактивные» сохранённые проёмы;
|
||
- создание внешней световой зоны;
|
||
- автоматическое разделение HA area независимой стеной;
|
||
- полноценная гарантия Plan editor на touch сверх safety floor;
|
||
- удаление compatibility reader в том же релизе.
|