22 KiB
#186 — Безопасный остаток стены у торцов партиционного проёма
Статус: готово к ревью ТЗ
Issue: #186
Связано: #132
1. Сценарий пользователя
Администратор размещает или перемещает дверь, окно, ворота либо открытый проём в независимой стене. Редактор оставляет у каждого торца стены физически правдоподобный участок, поэтому проём не может закончиться вровень с торцом.
2. Проблема
Редактор и backend считают валидным проём, край которого совпадает с торцом независимой стены. У такой geometry нет физического остатка стены под откос, хотя контракт #132 требует jamb safety margin; frontend и backend одинаково пропускают это нарушение.
До / после
До: frontend считает валидным любой проём, целиком находящийся между математическими endpoints host. Backend повторяет эту границу с одним лишь floating-point epsilon. Край проёма можно сохранить прямо на торце стены.
После: у обоих торцов host резервируется остаток, равный половине фактической толщины этой стены. Новое или геометрически изменённое размещение использует одну границу во frontend и backend. Существующая пограничная запись не исчезает, не сдвигается сама и может без изменений пройти через постороннее сохранение.
3. Решения владельца
- Jamb safety margin у каждого endpoint равен половине фактической толщины конкретной partition.
- Правило одинаково для
door,window,gateиpassage. - Между соседними проёмами новый зазор не вводится: остаётся только действующий запрет пересечения интервалов.
- Уже сохранённые проёмы, нарушающие новый предел, остаются видимыми и рабочими; silent clamp и write-on-read запрещены.
- Новая граница обязательна при создании, перемещении самого проёма, изменении его длины и перепривязке. Отказ имеет локализованное объяснение.
- Полный backup/restore всегда сохраняет legacy near-end проёмы: поверх текущего плана, на пустой и на другой инсталляции. Full import проверяет структурное попадание внутрь host, но не применяет новый jamb margin.
4. Техническая база и единицы
Сейчас resolvePartitionOpening() уже принимает jambMargin, но default равен
нулю и production call sites ненулевое значение не передают. Placement и drag
ограничивают центр только половиной длины проёма. Backend
_space_geometry_invariants() также проверяет лишь попадание внутрь endpoints.
Единый физический контракт:
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;
- совместимый full import/restore без нового jamb margin, в том числе без trusted previous на пустой или другой инсталляции;
- совместимое чтение и рендер уже сохранённых нарушающих записей;
- 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 всегда использует compatibility-границу: structural validation по-прежнему требует, чтобы opening целиком находился внутри host, но jamb margin не применяется ни поверх текущего плана, ни без trusted previous на пустой или другой инсталляции. Поэтому неизменённый legacy backup остаётся восстановимым. Осознанная цена решения владельца: вручную изменённый полный backup тоже может содержать near-end geometry, которую обычный UI создать уже не позволит; следующий direct geometry edit потребует исправления.
Для strict config/set/optimize отказ возвращает стабильный 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 сохраняется |
| Full restore с legacy near-end opening | импортируется без jamb-проверки, в том числе на пустой/другой инсталляции |
| 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 того же нарушения отклоняется, а полный backup/restore сохраняет запись без jamb-проверки поверх текущего плана и без trusted previous.
Доказательство: frontend render/cut regression unit + backend delta tests для ordinary write и full-import compatibility tests поверх текущего, пустого и другого config.
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. Full import остаётся на zero-margin
structural boundary и не вызывает этот jamb validator.
Доказательство: backend validator tests и websocket tests обоих strict write paths плюс full-import compatibility regression.
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 |
| Full restore ошибочно становится strict | отдельный compat import path и tests с/без текущего config |
| Негеометрическое редактирование требует миграции | сравнивать только 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. Принятые технические предположения
- Rigid translation host не является geometry change относительно jamb rule: относительные shoulders не меняются, поэтому legacy opening переносится без принудительного ремонта.
- Изменение
typeбез изменения длины не влияет на jamb geometry и само по себе не включает strict-проверку. - Имена helper, typed reason и i18n key не являются публичным API; ревьюер может изменить их при сохранении AC и стабильного backend error code.
- Для malformed
cm/cell_cmсохраняется действующий schema/fail-dark contract; #186 не вводит fallback физической толщины.