Files
houseplan-card/docs/specs/186-partition-opening-jamb-margin.md
T
2026-08-19 15:33:45 +03:00

22 KiB
Raw Blame History

#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. Решения владельца

  1. Jamb safety margin у каждого endpoint равен половине фактической толщины конкретной partition.
  2. Правило одинаково для door, window, gate и passage.
  3. Между соседними проёмами новый зазор не вводится: остаётся только действующий запрет пересечения интервалов.
  4. Уже сохранённые проёмы, нарушающие новый предел, остаются видимыми и рабочими; silent clamp и write-on-read запрещены.
  5. Новая граница обязательна при создании, перемещении самого проёма, изменении его длины и перепривязке. Отказ имеет локализованное объяснение.
  6. Полный 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. Принятые технические предположения

  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 физической толщины.