Files
houseplan-card/docs/specs/132-partition-openings.md
T
2026-08-19 00:35:59 +03:00

521 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Issue #132 — проёмы в независимых стенах
- **Issue:** https://github.com/Matysh/houseplan-card/issues/132
- **Связанный bug в том же scope:** https://github.com/Matysh/houseplan-card/issues/185
- **Статус документа:** актуализировано после #173 и #157; готово к ревью ТЗ
- **Приоритет:** P2
- **Тип:** feature, обычный трек
- **Пользовательское изменение:** да
## 1. Сценарий
Администратор рисует незамкнутую цепочку инструментом «Стены», завершает её
сменой инструмента, редактора или этажа и размещает в одном сохранённом сегменте
дверь, окно, ворота либо открытый проём тем же инструментом «Проём», которым
работает со стенами комнат. Домочадцы затем видят корректный разрыв, а для
stateful-типов — тот же live state и действия в View/kiosk.
## 2. Что человек увидит до и после
До изменения завершённая независимая стена остаётся сплошной и не принимает
проём; после изменения выбранные дверь, окно, ворота или открытый проём вырезают
её тело, перемещаются вместе с выбранным отрезком стены и ведут себя как тот же
тип проёма в обычной стене. При этом стена с проёмом по #185 остаётся полноценным
ребром для распознавания замкнутой комнаты.
## 3. Проблема и связь со scope
`partitions[]` — канонические независимые физические стены, но current opening
placement/index принимает только derived room walls. Physical union специально
добавляет partitions после opening cuts, поэтому проём не может вырезать их.
#173 заменил отдельный публичный инструмент «Перегородка» одной непрерывной
цепочкой «Стены». Активные сегменты crash-safe живут в `room_drafts`; явное
завершение превращает каждый сегмент в отдельный `partition` со stable `id`.
Face graph использует сохранённые partitions и может создать room wall на той же
оси, не удаляя исходный объект. Это делает composite room-wall/partition overlap
штатным, а не ошибочным случаем.
#185 фиксирует более новый продуктовый контракт: проём является свойством стены,
а не разрывом её структурного ребра. Поэтому opening cuts влияют на физическую и
световую геометрию, но room-face detection должен уметь замыкать область через
room wall или partition с любым поддержанным проёмом. `open_spans` остаются
реальным отсутствием стены и в topology не возвращаются.
Функция закрывает J4: администратор может без внешнего SVG точно воспроизвести
план. Правильный итоговый View также поддерживает J1/J2/J3: физическая геометрия,
contact/lock status и безопасные actions не расходятся с домом.
## 4. Решения владельца
1. На независимой стене разрешены все существующие типы: `door`, `window`,
`gate`, `passage`. #132 не добавляет новый opening type.
2. Проём следует за host при движении перегородки.
3. Удаление перегородки с проёмами требует confirmation со списком и удаляет их
только после явного согласия.
4. Contact, lock, badges и actions идентичны одноимённому проёму в room wall;
меняется только host geometry.
5. Физический passage в перегородке участвует в световой геометрии по тем же
type-specific правилам, что passage в стене.
6. При точном совпадении saved partition со стеной комнаты они считаются одним
составным физическим барьером без дополнительного host chooser. Explicit host
остаётся partition; hosted interval прорезает оба совпадающих тела. После
движения partition вычисляемый room-wall cut исчезает, room config не
переписывается.
7. По #185 проём любого поддержанного типа не разрывает структурное ребро для
room-face detection. Контур через стену с проёмом замыкается, а проём и его
host сохраняются.
## 5. Scope
- явная persisted host identity для partition opening;
- placement/hover/drag/edit/delete door, window, gate и passage на partition;
- full-depth cut, jamb/tunnel/symbol geometry с толщиной partition;
- composite cut совпадающих partition + derived room wall без persisted room
provenance;
- движение/удаление host и единая Undo/Redo команда;
- structural wall axes для #185 отдельно от physical opening gaps; regression
контракта #173 для endpoint/T/X/collinear faces и реальных `open_spans`;
- Flat Plan/View/kiosk/static и hidden Isometric renderers;
- clean floor, Glow barrier/source guard и действующие sun semantics;
- existing opening HA state/actions/security;
- backend validation, import/export/optimization compatibility;
- i18n, user/canonical docs, unit/backend/smoke/golden/performance evidence.
## 6. Не входит в задачу
- новый opening type или новая семантика существующего `passage`;
- проёмы в unfinished room drafts или columns;
- несколько host segments на один opening;
- автоматическая конверсия старого room-wall opening в partition opening;
- новый способ завершения Walls chain либо auto-resume finished partition;
- новая light model либо изменение window/door/gate semantics;
- sun rays, источником которых становится внутреннее partition window;
- полная touch parity Plan editor;
- свободное удаление host с неявной потерей его openings.
## 7. Модель данных и host identity
`OpeningCfg` получает optional host discriminator:
```ts
host?: {
kind: 'partition';
id: string;
t: number;
}
```
- `host` отсутствует — current legacy/current room-wall association по `x/y/
angle/length`; старые конфиги не мигрируют.
- `kind='partition'` — `id` обязан ссылаться на partition того же space.
- Один finished Walls segment создаёт один partition id. Бывшая multi-segment
chain не становится составным host: соседние segments двигаются и удаляются
независимо.
- `t` — нормализованное положение центра вдоль направленного `a→b`, `[0,1]`.
- `x`, `y`, `angle` остаются materialized compatibility projection и обновляются
атомарно из host; новый frontend считает valid host+t authority.
- `length` остаётся normalized physical length и существующий dialog показывает
cm/in по `cell_cm`.
Если implementation выбирает flat fields вместо object, semantics и
compatibility остаются теми же. Host kind расширяем discriminated union, но #132
добавляет только `partition`.
Backend new writes проверяет: existing partition id, finite `t`, finite geometry,
length fits host с jamb safety margin и total limits. Referentially invalid full
config write отклоняется, чтобы старый client не мог тихо удалить host и оставить
opening.
## 8. Canonical resolved opening
Один pure resolver строит immutable `ResolvedOpeningHost` для всех consumers:
- kind/id;
- directed centreline and normalized unit vector;
- center from `a + t(b-a)`;
- angle modulo current opening convention;
- full length and host depth from partition `cm`;
- derived room-wall intervals, collinear with and covering the hosted opening
interval within the canonical wall epsilon;
- validity/orphan reason;
- adjacent floor samples/type-specific passage policy.
Placement preview, symbol, body/composite cut, tunnel, hit test, light barriers,
Iso panel, move/delete и tests используют этот resolver. Запрещён render-only
nearest-wall fallback для explicit partition host. Coincident room-wall coverage
— вычисляемая projection valid partition host, а не второй persisted host.
Если endpoints хоста представлены в обратном порядке при canonical rewrite,
rewrite одновременно заменяет `t → 1-t`, сохраняя physical center и hinge/flip
orientation. Простое rigid translation сохраняет `t`, length и flip.
## 9. Placement UX
Активный инструмент «Проём» рассматривает два host kind:
- solid derived room-wall intervals;
- saved independent partition segments.
Hover preview показывает тот же translucent door/window/gate и привязывается к
partition axis; `passage` использует свою действующую symbol-less preview. Center
квантуется по действующему wall-bound grid step, length целиком помещается между
endpoints. Click повторно разрешает candidate и открывает существующий opening
dialog.
При близких candidates выбирается минимальная perpendicular distance, затем
полное покрытие opening length. Точное collinear overlap room-wall/partition,
покрывающее reserved interval, считается одним composite barrier и выбирает
explicit partition host без дополнительного dialog. Не совпадающая геометрически
tie остаётся неразрешимой и отклоняется с локализованной причиной. Existing
opening hit имеет приоритет над placement.
Toast `opening_no_wall` обновляется до «стены или перегородки». Virtual room span,
room draft и column остаются невалидными host. Touch placement — best effort;
pointercancel/multi-touch не сохраняют draft по safety floor.
## 10. Толщина, cut и symbol
Partition body строится из centreline и `cm`, half-depth с каждой стороны.
Hosted opening:
- вырезает body на полную глубину ровно по своему reserved interval;
- получает два flat jamb returns на концах cut;
- door swing начинается с выбранной face по `flip_v`;
- window glass располагается в середине partition depth;
- gate leaves используют current compact gate geometry;
- passage остаётся symbol-less full-depth cut по контракту #157;
- overlapping hosted openings не могут резервировать один interval дважды;
- junction patches после cut не должны заново закрыть passage.
Для zero/invalid thickness new partition невозможна по current validation
(1–100 cm). Malformed legacy host fail-dark: cut не применяется. Partition cut
выполняется по explicit host identity до единого physical union. Он дополнительно
вырезает только derived room-wall bodies, которые collinear и покрывают тот же
hosted interval в пределах canonical epsilon. Случайное пересечение, nearby
parallel wall или второй independent body не прорезаются.
## 11. Room topology и bug #185
Room-face graph получает два разных представления одной архитектуры:
- **physical/presentation:** opening interval вырезан из кладки и остаётся
проходом для изображения, hit geometry и type-specific света;
- **structural room topology:** axis валидной стены непрерывен через
door/window/gate/passage, потому что проём не отменяет принадлежность стены
контуру комнаты.
`buildPlanSnapGeometry()` либо следующий pure collector обязан сохранить
structural room/partition segment через opening interval для face traversal, не
возвращая этот interval в physical body. Действующий opening cut может оставаться
snap boundary/measure input, но не удаляет connectivity для room detection.
Контракт применяется одинаково к legacy room-wall opening и новому
partition-hosted opening. Контур, который последним segment опирается на такую
стену endpoint/T/X/collinear-примыканием, предлагает room по обычному #173 flow;
opening config, host, entities и materialized geometry не мутируют. `open_spans`
и отсутствующий/invalid host остаются настоящими gaps. Пассивные HA state ticks,
редактирование opening state и render не запускают room dialog.
## 12. Floor fill и tunnel
Partition не является границей room ownership. После вычитания hosted opening
видна уже существующая clean-floor/fill geometry под body. Дополнительный
нейтральный paper tunnel не рисуется.
Если partition пересекает две room fills, каждый side сохраняет свою canonical
fill ownership; cut не смешивает alpha и не создаёт новую room. Glow/sun layers
остаются выше base fill. Opening symbol скрывается по `hide_openings`, но physical
cut, HA state и barrier semantics остаются активны, как у room-wall opening.
## 13. Glow и light barriers
Решение владельца «проём пропускает свет» реализуется через type-specific
существующий контракт:
- door/gate/passage — transparent passage только там, где по обе стороны host
есть floor; `passage` не получает state или створку;
- exterior/no-floor с одной стороны остаётся opaque, чтобы Glow не уходил наружу;
- window сохраняет текущую политику: glass виден, но indoor Glow через него не
проходит;
- virtual boundary к partition не относится;
- source внутри valid interior door/gate/passage допустим; source внутри window
tunnel или invalid/orphan cut fail-dark.
Barrier geometry получает joined partition body **после вычитания только его
hosted passages**. Cache fingerprint включает host id/t, partition endpoints/cm,
opening length/type и geometry validity. HA-only state tick не rebuild-ит barriers.
## 14. Sun semantics
Partition window не является exterior room-boundary window и не создаёт новый
sun wedge. Existing exterior windows и their wedges сохраняются. Если canonical
sun/physical clipping использует independent body set, hosted cut не должен
восстанавливать opaque partition поверх уже разрешённого passage; никаких новых
direction/source rules #132 не вводит.
## 15. HA state, actions и security
Contact/invert, lock, badges, open amount, info card, lock/unlock confirmation и
`resolveHaBindingStatus()` не зависят от host kind.
- Door/gate lock action остаётся единственной sanctioned opening surface.
- Marker tombstone не отключает exact opening entity reference.
- Disabled/orphaned/unverified entity не выполняет service call.
- Window не получает lock control, если current type contract его не допускает.
- Passage остаётся inert: без contact/lock/invert/flip, badge, info card и service
call, по тому же контракту #157 на room wall.
- Перемещение host не меняет entity ids и runtime state.
Новый action resolver или отдельный partition status model запрещён.
## 16. Move, edit, delete и Undo
### Move/edit
Rigid drag одного partition segment обновляет endpoints и materialized
`x/y/angle` всех его hosted openings в одном preview/commit. `t`, length,
entities and flips сохраняются. Соседние segments бывшей Walls chain не
двигаются. Одна Undo/Redo command восстанавливает partition и все hosted
projections, включая computed composite cut.
Editor operation, которая сократила бы host меньше opening+jamb margin, блокирует
commit с причиной. Import/legacy invalid geometry не clamped молча.
### Delete
Если hosted openings нет, действует current delete path. Если есть:
1. dialog перечисляет openings в порядке `t`: локализованный type + length;
2. Cancel ничего не меняет;
3. Confirm атомарно удаляет partition и ровно его hosted openings;
4. одна Undo восстанавливает host и весь список с entities/geometry;
5. attachments отсутствуют у opening model, file cleanup не вызывается.
Удалять hosted openings без confirmation через generic cleanup запрещено.
## 17. Orphan и failure policy
Explicit partition opening с отсутствующим/invalid host:
- не вырезает physical body и не создаёт transparent light path;
- не рендерится как рабочий opening в View/kiosk/static/Iso;
- в Plan editor показывается диагностический orphan affordance в materialized
`x/y`, позволяющий удалить или перепривязать объект;
- entity action из orphan affordance не выполняется;
- production console не спамится; diagnostic reason доступна test/support hook.
Boolean failure по одному cut возвращает непрорезанный opaque partition
(fail-dark), не удаляет config и не делает все independent bodies прозрачными.
## 18. Migration и compatibility
- Старые openings без `host` читаются и пишутся по current room-wall contract.
- Existing room-wall opening около partition не начинает прорезать partition.
- Room-wall opening, существовавший до #132, сохраняет implicit host и участвует
в structural room topology по #185 без migration/write-on-read.
- New optional fields добавляются в frontend/backend schema и
`docs/CONFIG-COMPATIBILITY.md`/field registry.
- Export/import/backup сохраняют host object и referential order; counts остаются
в общем числе openings.
- Align/optimize переносит partition и hosted opening согласованно, не re-snaps
explicit host к nearby room wall.
- Older frontend may preserve unknown host fields, but backend referential
validation prevents destructive orphan write; backward visual support не
обещается до версии, реализующей #132.
Schema migration существующих данных не нужна: capability появляется только у
новых/явно перепривязанных openings.
## 19. i18n и accessibility
Новые en/ru strings:
- `toast.opening_no_wall` — обновлённая guidance/error «стена или независимая
стена»;
- `opening.host_partition` — host kind «Независимая стена» в
properties/diagnostic;
- `confirm.delete_partition_openings_title`,
`confirm.delete_partition_openings_body` и
`confirm.delete_partition_openings_item` — confirmation и строки списка с
параметрами `{count}`, `{type}`, `{length}`;
- `opening.partition_orphan` и `opening.rebind_partition` — orphan reason и
доступное действие перепривязки по §17.
Opening dialog, list и confirmation используют существующий `hp-dialog`, focus
trap, Escape и restore focus. List доступен screen reader; confirmation сообщает
точное количество. Canvas preview не является единственным объяснением host:
properties/diagnostic имеет текстовое accessible name.
**Touch editor: best effort / intentionally degraded.** Desktop остаётся
reference surface. На touch обязательны только safety floor: tap повторно решает
candidate на commit, а pinch/second pointer/pointercancel не создают opening, не
удаляют host и не завершают Walls chain. Hover parity не обещается; View и kiosk
не получают новых interactions.
## 20. Acceptance criteria
1. **AC1 — Walls workflow и placement.** После завершения multi-segment цепочки
«Стены» каждый saved segment является отдельным candidate host. На partition
размещаются door/window/gate/passage; active draft, column и virtual span не
принимаются. **Доказательство:** resolver/lifecycle unit + desktop smoke.
2. **AC2 — geometry и composite overlap.** Opening cut full-depth с корректными
jamb/symbol для 1/15/100 cm и diagonal partition. При точном покрывающем
room-wall overlap partition выбирается explicit host и весь composite barrier
прорезается; crossing/nearby/второй independent body не затрагиваются.
**Доказательство:** geometry units + reviewed golden.
3. **AC3 — host lifecycle.** Rigid move одного segment переносит его openings и
убирает computed room-wall cut; delete показывает точный список и атомарно
удаляет после Confirm; Undo/Redo восстанавливает host/openings/composite.
**Доказательство:** command units + browser smoke.
4. **AC4 — light.** Interior door/gate/passage пропускают Glow через полный
composite cut; exterior/no-floor варианты и window остаются opaque по current
policy; invalid host/boolean failure fail-dark. **Доказательство:** light
visibility units + smoke/golden.
5. **AC5 — #185 room closure.** Room-face detection считает wall axis
непрерывным через door/window/gate/passage как на legacy room wall, так и на
partition host. Endpoint/T/X/collinear closure предлагает room и сохраняет
opening bit-equivalent; `open_span` остаётся gap. **Доказательство:** pure
topology unit + production-bundle smoke, который краснеет на `origin/dev`.
6. **AC6 — no passive topology mutation.** HA state, opening edit/render,
reload и existing closed face не показывают room dialog и не меняют geometry;
только последний accepted Walls segment может инициировать предложение по
#173. **Доказательство:** wall-face graph unit + smoke.
7. **AC7 — render parity.** Plan/View/kiosk/static/Iso используют один resolved
host и не расходятся по center/angle/depth/open state; passage остаётся
symbol-less. **Доказательство:** cross-render smoke + golden.
8. **AC8 — HA/security parity.** Door/window/gate contact/lock/badge/actions
совпадают с room-wall contract, secure/disabled guards не меняются; passage
остаётся inert. **Доказательство:** unit matrix + smoke.
9. **AC9 — compatibility.** Legacy openings unchanged; host fields survive
save/export/import/optimize; invalid reference rejected or fail-dark; schema
принимает ровно четыре уже существующих type. **Доказательство:**
frontend/backend round-trip tests.
10. **AC10 — accessibility/touch safety.** Dialog/confirmation keyboard
доступны, touch cancel/multi-touch не создают opening и не завершают Walls
chain. **Доказательство:** browser smoke.
11. **AC11 — cache/performance.** HA tick и pointermove не rebuild-ят host,
composite или face topology; caches bounded и инвалидируются geometry host /
opening type/length. **Доказательство:** counters + performance smoke.
12. **AC12 — regression #173/#157.** Walls finish/no-auto-resume, room queue,
passage field restrictions, room-wall opening placement и планы без hosted
openings остаются совместимыми. **Доказательство:** existing targeted suites
+ named #132 smoke.
## 21. План автотестов
### Unit
- host serialization/resolution, t and reversed endpoints;
- room-wall vs partition candidate, composite/non-composite tie and length fit;
- horizontal/vertical/diagonal, 1/15/100 cm cut and jambs;
- overlap reservation, junction patch, coincident derived wall vs independent body;
- move/delete/Undo atomic snapshots;
- interior/exterior/window/passage light policy and fail-dark;
- structural-vs-physical collector: all four openings preserve room-face axes,
while `open_span` remains a gap; endpoint/T/X/collinear #185 matrix;
- only the latest accepted segment may cause room proposal; opening/HA updates do not;
- orphan, missing host and cache fingerprint;
- HA state/action host-kind parity.
### Backend
- host discriminator/id/t and referential validation;
- config size/count limits, export/import/unknown fields;
- canonical `passage` field restrictions from #157 with partition host present;
- malformed/missing partition rejection;
- full HA harness authority — Linux CI/WSL.
### Browser smoke
- finish multi-segment Walls chain; place/edit/drag door, window, gate and passage
on one resulting partition without resuming the chain;
- host move, delete Cancel/Confirm and Undo/Redo;
- contact opens, lock confirmation/action, hide_openings;
- Glow across interior passage and blocked by window/exterior passage;
- reproduce #185 on legacy room wall and hosted partition for all four types,
including T closure; verify `open_span` still blocks closure and opening config
is unchanged;
- exact composite overlap selects partition host without an extra chooser and
leaves no opaque wall layer across the opening;
- Plan/View/kiosk/static/Iso parity;
- touch cancel/pinch safety and keyboard confirmation;
- import/optimize round trip.
### Golden
- thick/diagonal partition with all four types, light/dark;
- coincident room-wall/partition composite before and after hosted cut;
- Glow before/after door passage and opaque window;
- hidden Iso panels/cuts and static-card parity;
- expected diffs accepted only from reviewed full Linux artifact.
### Performance
- cold structural build within current large-house budget;
- HA state tick geometry build count 0;
- repeated render cache growth 0/bounded caps;
- no per-opening repeated full wall index build.
## 22. Затронутые поверхности
- `OpeningCfg`, backend validation and config compatibility registry;
- opening placement/resolver, wall-thickness/physical geometry and command stack;
- `plan-snap-overlay`/wall-face structural collector and #173 room queue;
- Flat/static/Iso opening renderers;
- Glow/light and current sun physical consumers;
- Plan dialog/i18n, import/export/align/optimize;
- unit/backend/smoke/golden/performance fixtures;
- `docs/ARCHITECTURE.md`, `docs/CANVAS.md`, `docs/UX-MODES.md`,
`docs/WALL-THICKNESS.md`, `docs/LIGHT.md`, `docs/SUN.md`,
`docs/USER-GUIDE.ru.md`, `docs/TESTING.md`.
## 23. Риски и откат
| Риск | Мера |
| --- | --- |
| Host inference режет не ту стену | explicit partition id+t |
| Composite overlap режет nearby/crossing body | collinear full-interval coverage + negative units |
| Junction union закрывает passage | cut-aware physical union tests |
| #185 возвращает физическую кладку в проём | отдельные structural и physical projections |
| Opening начинает пассивно открывать room dialog | topology-delta и no-passive smoke #173 |
| Delete теряет entities | atomic command snapshot + confirmation |
| Light проходит через window/outside | existing type/floor-side policy |
| Older writer оставляет orphan | backend referential validation |
| Geometry cost растёт per opening | single immutable host/index snapshot |
Откат запрещает создание новых partition-host openings и возвращает body union
after room cuts. Уже сохранённые host objects должны либо оставаться
read-preserved/diagnostic, либо требовать downgrade warning; автоматически
преобразовывать их в room-wall openings нельзя.
## 24. Release-артефакты
Implementation commit имеет `User-Visible: yes` и одновременно обновляет:
- `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md`;
- `docs/USER-GUIDE.ru.md` — Walls finish, placement/types/move/delete/light и
closure через стену с проёмом;
- `docs/ARCHITECTURE.md`, `docs/CANVAS.md`, `docs/UX-MODES.md`,
`docs/WALL-THICKNESS.md`, `docs/LIGHT.md`, `docs/SUN.md`,
`docs/CONFIG-COMPATIBILITY.md`;
- `docs/TESTING.md`.
Нужны reviewed Flat/Iso/Glow golden artifacts, targeted browser/performance
reports and exact Linux backend evidence. Visual baselines принимаются только
через `golden:accept -- --reviewed` по полному Linux artifact.
## 25. Принятые технические предположения
- partition host хранится explicit id+t, materialized x/y/angle сохраняются;
- room-wall openings остаются implicit для backward compatibility;
- exact covering room-wall/partition overlap выбирает partition host как
owner-approved composite; прочие ambiguous ties отклоняются;
- structural room topology may use uncut wall axes, while all physical/light
consumers continue using type-specific cuts; shared persisted geometry не
создаётся;
- partition window не становится exterior sun source;
- invalid host/cut fail-dark and hidden from ordinary View;
- точная форма discriminator fields может меняться на ревью без изменения
persisted identity/lifecycle semantics.