mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
docs: specify partition opening jamb margin
Issue: #186 User-Visible: no
This commit is contained in:
@@ -0,0 +1,313 @@
|
||||
# #186 — Безопасный остаток стены у торцов партиционного проёма
|
||||
|
||||
Статус: готово к ревью ТЗ
|
||||
Issue: [#186](https://github.com/Matysh/houseplan-card/issues/186)
|
||||
Связано: [#132](https://github.com/Matysh/houseplan-card/issues/132)
|
||||
|
||||
## 1. Сценарий пользователя
|
||||
|
||||
Администратор размещает или перемещает дверь, окно, ворота либо открытый проём
|
||||
в независимой стене. Редактор оставляет у каждого торца стены физически
|
||||
правдоподобный участок, поэтому проём не может закончиться вровень с торцом.
|
||||
|
||||
## 2. До / после
|
||||
|
||||
**До:** frontend считает валидным любой проём, целиком находящийся между
|
||||
математическими endpoints host. Backend повторяет эту границу с одним лишь
|
||||
floating-point epsilon. Край проёма можно сохранить прямо на торце стены.
|
||||
|
||||
**После:** у обоих торцов host резервируется остаток, равный половине фактической
|
||||
толщины этой стены. Новое или геометрически изменённое размещение использует одну
|
||||
границу во frontend и backend. Существующая пограничная запись не исчезает, не
|
||||
сдвигается сама и может без изменений пройти через постороннее сохранение.
|
||||
|
||||
## 3. Решения владельца
|
||||
|
||||
1. Jamb safety margin у каждого endpoint равен половине фактической толщины
|
||||
конкретной partition.
|
||||
2. Правило одинаково для `door`, `window`, `gate` и `passage`.
|
||||
3. Между соседними проёмами новый зазор не вводится: остаётся только действующий
|
||||
запрет пересечения интервалов.
|
||||
4. Уже сохранённые проёмы, нарушающие новый предел, остаются видимыми и рабочими;
|
||||
silent clamp и write-on-read запрещены.
|
||||
5. Новая граница обязательна при создании, перемещении самого проёма, изменении
|
||||
его длины и перепривязке. Отказ имеет локализованное объяснение.
|
||||
|
||||
## 4. Техническая база и единицы
|
||||
|
||||
Сейчас `resolvePartitionOpening()` уже принимает `jambMargin`, но default равен
|
||||
нулю и production call sites ненулевое значение не передают. Placement и drag
|
||||
ограничивают центр только половиной длины проёма. Backend
|
||||
`_space_geometry_invariants()` также проверяет лишь попадание внутрь endpoints.
|
||||
|
||||
Единый физический контракт:
|
||||
|
||||
```text
|
||||
wallDepth = wallCmToUnits(partition.cm, cellCm, gridPitch)
|
||||
jambMargin = wallDepth / 2
|
||||
requiredShoulder = openingLength / 2 + jambMargin
|
||||
valid = along >= requiredShoulder
|
||||
&& along <= hostLength - requiredShoulder
|
||||
```
|
||||
|
||||
Равенство границе валидно с общим geometry epsilon. Для backend в хранимых
|
||||
normalized units та же формула имеет вид
|
||||
`partition.cm / cell_cm / 240 / 2`; литерал `240` должен быть общей именованной
|
||||
константой масштаба canvas, а не новым настраиваемым параметром. При отсутствии
|
||||
`cell_cm` используется действующий default 5 cm.
|
||||
|
||||
## 5. В scope
|
||||
|
||||
- pure helper/режим проверки jamb margin в `partition-openings`;
|
||||
- размещение и preview на independent partition;
|
||||
- drag hosted opening вдоль partition;
|
||||
- изменение длины в opening dialog и explicit rebind;
|
||||
- frontend commit validation и отдельная причина `does-not-fit-jamb` либо
|
||||
эквивалентный typed result;
|
||||
- backend semantic delta validation для config/set и optimize, а также строгая
|
||||
проверка импортируемого полного документа;
|
||||
- совместимое чтение и рендер уже сохранённых нарушающих записей;
|
||||
- RU/EN сообщение, unit/backend/browser coverage и документация.
|
||||
|
||||
## 6. Не в scope
|
||||
|
||||
- дополнительный зазор между двумя проёмами;
|
||||
- изменение формы jamb returns, symbols, tunnel, light cut либо room topology;
|
||||
- новый persisted field, настройка размера запаса или migration конфига;
|
||||
- автоматический перенос/уменьшение старого проёма;
|
||||
- resize partition как новый жест; действующее перемещение segment остаётся
|
||||
rigid translation;
|
||||
- полная touch parity редактора.
|
||||
|
||||
## 7. Frontend-контракт
|
||||
|
||||
### 7.1 Два явных режима resolver
|
||||
|
||||
Все consumers `resolvePartitionOpening()` должны осознанно выбрать режим:
|
||||
|
||||
- **compat/read:** прежняя проверка `jambMargin=0`; используется для render,
|
||||
cuts, symbols, hit-test, static/Iso и materialized projection уже принятого
|
||||
конфига;
|
||||
- **strict/write:** margin равен половине resolved depth; используется для
|
||||
нового placement, direct opening drag, rebind и сохранения изменённой
|
||||
geometry.
|
||||
|
||||
Нельзя менять default так, чтобы старый near-end opening стал orphan/fail-dark
|
||||
только из-за обновления frontend. Именованный helper/policy должен исключить
|
||||
копирование формулы по call sites.
|
||||
|
||||
### 7.2 Размещение
|
||||
|
||||
Для partition target resolver:
|
||||
|
||||
- считает target eligible, только если
|
||||
`hostLength >= openingLength + 2 * physicalHalfWidth`;
|
||||
- после grid/center magnet ограничивает `along` диапазоном
|
||||
`[openingLength/2 + physicalHalfWidth,
|
||||
hostLength - openingLength/2 - physicalHalfWidth]`;
|
||||
- preview, ruler и следующий click используют один resolved candidate;
|
||||
- room-wall placement сохраняет прежнюю границу без нового jamb margin;
|
||||
- физически слишком короткая partition не получает preview, а click рядом с
|
||||
ней показывает новое локализованное сообщение, не generic orphan.
|
||||
|
||||
На точной границе preview и click валидны. Размеры ruler измеряют фактические
|
||||
плечи от края проёма до endpoints и потому показывают минимум, равный margin.
|
||||
|
||||
### 7.3 Drag и dialog
|
||||
|
||||
Direct drag hosted opening к торцу ограничивает центр тем же strict-диапазоном;
|
||||
preview не может показать и commit не может сохранить меньше margin. Если host
|
||||
слишком короток для текущей длины, drag не меняет config.
|
||||
|
||||
При сохранении dialog strict rule применяется, когда изменены `host.id`,
|
||||
`host.t` или `length`, либо создаётся новая запись. Изменение только type,
|
||||
binding, invert/flip или другого негеометрического поля у legacy near-end
|
||||
opening разрешает прежнюю geometry без автоматического исправления.
|
||||
|
||||
Dialog различает отсутствующий host и недостаточный остаток стены. Для второго
|
||||
случая показывает status banner и при попытке сохранения сообщение:
|
||||
|
||||
- RU: `Оставьте от края проёма до торца стены минимум {distance}`;
|
||||
- EN: `Leave at least {distance} between the opening and the end of the wall`.
|
||||
|
||||
`{distance}` форматируется действующим `formatLength()` в системе единиц HA.
|
||||
Rebind на слишком короткую partition использует ту же строку. Missing partition
|
||||
продолжает использовать `opening.partition_orphan`.
|
||||
|
||||
### 7.4 Перемещение host
|
||||
|
||||
Rigid translation partition сохраняет `t`, length, thickness и относительные
|
||||
плечи, поэтому не создаёт нового нарушения и совместимо переносит даже legacy
|
||||
near-end opening. Изменение длины host или его `cm`, способное создать либо
|
||||
усилить нарушение, должно пройти strict backend boundary; будущий frontend
|
||||
resize обязан использовать тот же helper.
|
||||
|
||||
## 8. Backend и compatibility
|
||||
|
||||
Структурная `SPACE_SCHEMA` сохраняет прежний zero-margin fit check, чтобы
|
||||
полученный ранее near-end config мог быть прочитан и провалидирован перед
|
||||
semantic delta comparison. Новый jamb contract реализуется в semantic validator
|
||||
рядом с `validate_partition_opening_hosts()` либо в его расширении.
|
||||
|
||||
Для обычного write с `previous`:
|
||||
|
||||
- новая hosted opening проверяется strict;
|
||||
- существующая проверяется strict, если изменились `host.id`, `host.t`,
|
||||
`length`, длина host либо `cm`;
|
||||
- неизменённая relative geometry допускается, включая rigid translation обоих
|
||||
endpoints на один вектор и посторонние изменения config;
|
||||
- удаление opening вместе с host остаётся валидным;
|
||||
- существующий downgrade guard удаления `host` сохраняется;
|
||||
- overlap rule не меняется.
|
||||
|
||||
Полный import/restore без trusted previous рассматривает все hosted openings как
|
||||
новые и проверяет strict. Отказ возвращает стабильный public code
|
||||
`invalid_partition_opening_jamb_margin` (или расширенный typed reason того же
|
||||
уровня), включая `space`, `opening` и требуемый margin в diagnostic message.
|
||||
Backend ничего не нормализует и не переписывает.
|
||||
|
||||
## 9. UX-состояния
|
||||
|
||||
| Состояние | Результат |
|
||||
|---|---|
|
||||
| Новая opening на длинной partition | preview/click clamp оставляют по половине толщины у торцов |
|
||||
| Partition короче `opening + thickness` | preview отсутствует, click даёт jamb guidance |
|
||||
| Drag к endpoint | opening останавливается на точной допустимой границе |
|
||||
| Увеличение длины в dialog нарушает margin | banner + локализованный отказ, config не меняется |
|
||||
| Legacy near-end opening без geometry edit | видим, интерактивен и losslessly сохраняется |
|
||||
| Legacy opening получил direct move/length/rebind | новая geometry обязана соответствовать margin |
|
||||
| Два соседних opening касаются, но не пересекаются | допустимо по прежнему overlap contract |
|
||||
| Missing partition | прежний orphan/fail-dark contract #132 |
|
||||
|
||||
## 10. Модель данных, i18n и доступность
|
||||
|
||||
Модель данных и schema полей не меняются. Добавляются синхронные RU/EN ключи для
|
||||
jamb guidance/banner; один и тот же текст используется мышью и клавиатурным
|
||||
сохранением. Banner имеет действующий `role="status"`; цвет/preview не является
|
||||
единственным объяснением отказа.
|
||||
|
||||
**Touch editor: best effort / intentionally degraded.** Новых жестов нет.
|
||||
Обязателен safety floor: tap повторно разрешает candidate, а pinch, второй
|
||||
pointer и `pointercancel` не сохраняют opening. Hover parity не обещается.
|
||||
|
||||
## 11. Критерии приёмки
|
||||
|
||||
### AC1 — единая физическая граница
|
||||
|
||||
Для partition толщиной `D` каждый endpoint сохраняет плечо не меньше `D/2` для
|
||||
door/window/gate/passage. Точная граница валидна, значение меньше неё — нет.
|
||||
Frontend и backend дают одинаковый результат минимум для `cm=1/15/100`, default
|
||||
и нестандартного `cell_cm`, horizontal/diagonal/reversed host.
|
||||
|
||||
**Доказательство:** pure unit matrix + backend parametrized tests с exact
|
||||
boundary и boundary-minus-epsilon.
|
||||
|
||||
### AC2 — placement и drag не создают плохую geometry
|
||||
|
||||
Hover/click на partition clamp к strict range; слишком короткий host не создаёт
|
||||
candidate. Direct drag останавливается на той же границе. Preview, rulers и
|
||||
сохранённые `host.t/x/y` соответствуют одному resolved center.
|
||||
|
||||
**Доказательство:** `opening-placement`/`partition-openings` units + browser
|
||||
smoke placement и drag у обоих endpoints.
|
||||
|
||||
### AC3 — dialog и rebind объясняют отказ
|
||||
|
||||
Недопустимое изменение length/host получает jamb-specific RU/EN status/toast с
|
||||
форматированным физическим расстоянием; missing host остаётся отдельным orphan.
|
||||
После отказа config/history не изменены. Rebind на валидный host проходит.
|
||||
|
||||
**Доказательство:** source/i18n contracts + browser smoke dialog/rebind и
|
||||
отсутствия config write.
|
||||
|
||||
### AC4 — legacy compatibility
|
||||
|
||||
Сохранённый near-end opening остаётся видимым и рабочим во всех прежних
|
||||
consumers, не получает silent clamp и проходит backend round-trip при
|
||||
постороннем edit или rigid host translation. Direct geometry edit и full import
|
||||
того же нарушения отклоняются.
|
||||
|
||||
**Доказательство:** frontend render/cut regression unit + backend delta/import
|
||||
tests с previous и без него.
|
||||
|
||||
### AC5 — overlap и остальные host не меняются
|
||||
|
||||
Между соседними openings не появляется новый gap; пересечение по-прежнему
|
||||
запрещено. Room-wall opening, orphan policy, cut/symbol/tunnel/light и delete
|
||||
semantics #132 не меняются.
|
||||
|
||||
**Доказательство:** существующие tests #132/#157/#193 + новые negative units.
|
||||
|
||||
### AC6 — server parity и error code
|
||||
|
||||
`config/set` и optimize применяют один semantic validator. Новый/изменённый
|
||||
invalid record возвращает стабильный jamb error code, а structural schema не
|
||||
ломает совместимый unchanged round-trip.
|
||||
|
||||
**Доказательство:** backend validator tests и websocket tests обоих write paths.
|
||||
|
||||
## 12. План проверок
|
||||
|
||||
В implementation loop:
|
||||
|
||||
- `npm run typecheck`;
|
||||
- `npm test`;
|
||||
- `npm run build`;
|
||||
- целевые backend tests для validation/websocket.
|
||||
|
||||
Перед `S7-code-review`:
|
||||
|
||||
- затронутый browser smoke placement/drag/dialog;
|
||||
- golden capture/verify только если реализация меняет видимые pixels.
|
||||
|
||||
Визуальный стиль не должен меняться, поэтому новый baseline не ожидается. Если
|
||||
появится новый banner в зафиксированной golden-сцене, candidate обязан пройти
|
||||
обычный Linux review, а не приниматься автоматически. Полные golden, smoke и
|
||||
performance остаются pre-beta gate по release runbook.
|
||||
|
||||
## 13. Риски и производительность
|
||||
|
||||
| Риск | Мера |
|
||||
|---|---|
|
||||
| Default strict скрывает старые openings | явные compat/read и strict/write call sites |
|
||||
| Frontend/backend расходятся в масштабе | shared named formula и boundary matrix |
|
||||
| Structural schema блокирует round-trip раньше delta validator | zero-margin schema + semantic comparison с previous |
|
||||
| Негеометрическое редактирование требует миграции | сравнивать только relative geometry contract |
|
||||
| Rigid translation ошибочно считается resize | сравнивать span, `cm`, host identity/t, а не absolute endpoints |
|
||||
| Room-wall placement получает новый отступ | margin только при `partitionHost` |
|
||||
| Новый gap появляется между openings | не менять `hostedOpeningIntervalsOverlap` |
|
||||
|
||||
Расчёт O(1) на candidate/opening; новых geometry passes, observers, animation и
|
||||
network calls нет. Backend остаётся O(openings + partitions) на space при index
|
||||
partition по id. Отдельный performance profile не требуется.
|
||||
|
||||
## 14. Откат
|
||||
|
||||
Откат удаляет strict jamb policy, semantic delta validator, новые i18n строки и
|
||||
tests. Persisted schema не менялась, поэтому downgrade/migration не нужны;
|
||||
поведение возвращается к zero-margin границе.
|
||||
|
||||
## 15. Release-артефакты
|
||||
|
||||
- user-visible implementation commit обновляет `docs/CHANGELOG.md` и
|
||||
`docs/CHANGELOG.ru.md`;
|
||||
- `docs/USER-GUIDE.md` (либо актуальный раздел Plan editor) фиксирует минимум у
|
||||
торца независимой стены;
|
||||
- `docs/CONFIG-COMPATIBILITY.md` описывает delta-validation и tolerant legacy
|
||||
round-trip без нового поля;
|
||||
- `docs/TESTING.md` перечисляет unit/backend/smoke proof;
|
||||
- синхронные `dist`, integration frontend и demo bundle;
|
||||
- новых screenshots/golden baselines нет, если pixels не меняются;
|
||||
- issue остаётся открытой до выпуска беты.
|
||||
|
||||
## 16. Принятые технические предположения
|
||||
|
||||
1. Rigid translation host не является geometry change относительно jamb rule:
|
||||
относительные shoulders не меняются, поэтому legacy opening переносится без
|
||||
принудительного ремонта.
|
||||
2. Изменение `type` без изменения длины не влияет на jamb geometry и само по
|
||||
себе не включает strict-проверку.
|
||||
3. Имена helper, typed reason и i18n key не являются публичным API; ревьюер может
|
||||
изменить их при сохранении AC и стабильного backend error code.
|
||||
4. Для malformed `cm/cell_cm` сохраняется действующий schema/fail-dark contract;
|
||||
#186 не вводит fallback физической толщины.
|
||||
@@ -100,6 +100,7 @@ GitHub Issues и GitHub Projects (v2) остаются единственным
|
||||
| [#174](https://github.com/Matysh/houseplan-card/issues/174) Связанный виртуальный источник следует реальному контроллеру | [174-linked-virtual-light-controller.md](174-linked-virtual-light-controller.md) |
|
||||
| [#178](https://github.com/Matysh/houseplan-card/issues/178) Выбор сущности для действия «Переключить состояние» | [178-toggle-entity.md](178-toggle-entity.md) |
|
||||
| [#197](https://github.com/Matysh/houseplan-card/issues/197) Один junction-патч не гасит кладку всего плана | [197-junction-patch-fail-dark.md](197-junction-patch-fail-dark.md) |
|
||||
| [#186](https://github.com/Matysh/houseplan-card/issues/186) Безопасный остаток стены у торцов партиционного проёма | [186-partition-opening-jamb-margin.md](186-partition-opening-jamb-margin.md) |
|
||||
|
||||
## P3
|
||||
|
||||
|
||||
Reference in New Issue
Block a user