Files
houseplan-card/docs/specs/329-junction-limits.md
T
Codex 2b1f03be2d test: the Resize channel of the junction limits, driven by a real pointer (#329 AC7a)
Measured what Resize itself already forbids: a room cannot be squeezed below
30 cm (two 15 cm walls), so П3 and П5 are unreachable through shrinking and
the gate merely fails closed there. П4 IS reachable on a fine grid, so the
smoke drags a real handle on a 2 cm grid: two rooms 10 cm apart, a 6 cm pull
would leave 4 cm between foreign nodes, the wall stops at 6 cm and exactly one
toast names the 5 cm rule.

Spec revision 6 records both the measurement and the two corrections it forces
on AC7a: a dimmed handle cannot express a per-step limit, and the plan is NOT
byte-unchanged — the allowed part of the gesture is a legitimate edit.

Issue: #329
User-Visible: no
2026-08-27 23:31:42 +03:00

221 lines
20 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 #329 — ограничения стыков стен и честная острая вершина
Статус: ревизия 6 (r1: M1/M2/L1; r2: M1 — AC7; поправки владельца 2026-08-27: §4 — острая вершина без фаски и без зазубрин на гранях; П3 меряет СТЕНУ, а не атом — кейс компенсации перепада толщин). Решения владельца (чат, 2026-08-27, зафиксированы в issue):
пять ограничений приняты; проверки действуют на записи, легаси читается как
есть; для остаточных острых вершин легаси-планов рендер закрывает остриё
честной фаской вместо «трезубца».
## 1. Сценарий и пользовательский результат
Сегодня комната-треугольник с вершиной ≈9.9° (стены 15 см) рендерится
«трезубцем»: три пика и два V-выреза (репродукция в issue, фикстура
`houseplan-space-test2-2026-08-27_16-14-48.json`, комната `rmtbq3k5e-0`).
Причина класса: при остром угле внутренние грани стен смыкаются на
`полутолщина/tan(θ/2)` от вершины (86 см при 10°/15 см) — весь участок выше
физически перекрыт телами обеих стен, и объединение тел с вершинными
правилами #309/#310 распадается.
После задачи: (а) нарисовать такое больше нельзя — запись отклоняется с
точным тостом; (б) уже существующие такие планы рендерятся прилично — остриё
закрыто одной фаской, без пиков и вырезов.
## 2. Нормативные ограничения (решения владельца)
Все проверки выполняются над кандидатом структурной записи (фронт — в общем
барьере перед записью; бэкенд — семантическая дельта-валидация §5). Нарушение
= отказ всей записи, план не изменён. Канал обратной связи — штатный канал
каждой поверхности (M1 spec-ревью r1):
- **рисование/завершение контура, черновики, независимые стены, merge/
split** — тост с конкретным правилом и значением (текущий канал отказов
рисования);
- **Resize** — существующий контракт ручек (USER-GUIDE «Изменение размеров»):
движение упирается в последнюю допустимую позицию; если безопасного шага
нет, ручка приглушена и объясняет запрет по hover/focus/нажатию — текст
объяснения называет нарушенное правило П1–П5; тост не используется;
- **инструмент «Толщина»** — отказ значением в трее по образцу существующего
отказа нуля для независимой стены (#313): значение не применяется, причина
показывается тостом с названием правила (П3/П5).
- **П1. Минимальный угол:** угол между СОСЕДНИМИ по азимуту стенами одного
узла ≥ **15°**. Считается по осям сегментов, для каждого узла кандидата.
- **П2. Валентность узла:** в одном узле сходятся ≤ **6** стен (контурные
атомы, независимые стены и перегородки считаются вместе).
- **П3. Минимальная длина стены:** длина ≥ **max(20 см, толщина
сегмента)**. Для нулевых стен (#306) — ≥ 20 см. Меряется НЕ отдельный атом
каталога, а вся стена: максимальный коллинеарный (±1°) прогон соседних
атомов ОДНОЙ толщины, склеенных через общие узлы. Иначе правило запрещало
бы законный приём — короткий доборный участок, компенсирующий перепад
толщин соседних стен (владелец, 2026-08-27, фикстура
`houseplan-space-2-2026-08-27_18-24-23.json`: атомы 5 см = (30−20)/2 и
14 см, каждый — продолжение стены той же толщины в 349 см). Такой атом
короче 20 см законен ровно потому, что стена, частью которой он является,
длиннее 20 см.
- **П4. Минимальная дистанция:** несмежные узлы — не ближе **5 см**; узел —
не ближе 5 см к стене, которой он не принадлежит (касание конца о середину
чужой стены остаётся T-стыком и валидно — правило про «почти-касание» без
инцидентности). Порог абсолютный, в сантиметрах, и НЕ зависит от
`cell_cm` пространства.
- **П5. Просвет комнаты:** после вычета тел стен внутренняя область комнаты
непуста и её площадь ≥ **25 см²**. Порог абсолютный и не зависит от
`cell_cm`.
## 3. Граница применения
- Проверяются только записи, ИЗМЕНЯЮЩИЕ соответствующую геометрию: рисование
и завершение контура, черновики, независимые стены/перегородки, Resize,
инструмент «Толщина» (толщина участвует в П3/П5), merge/split.
- Существующие данные наследуются: запись, не трогающая нарушающий элемент,
проходит; миграция (#306/#316), импорт и восстановление из бэкапа не
блокируются никогда.
- Optimize (выравнивание/канонизация) не обязан чинить легаси-нарушения и не
имеет права создавать новые (пост-условие прогона).
## 4. Рендер остаточных острых вершин (легаси)
**Поправка владельца (чат, 2026-08-27):** плоского среза вершины быть не
должно — у остриё обычная нормальная острая вершина.
Для корня «трезубца» доказано исполнением: у вершины ≈9.85° внешний контур
падал на плоскую фаску (лимит mitre 4·h против выноса 87 см), а внутренний
контур складывался «бабочкой» — вырезы рождались вычитанием этой складки.
Контракт:
- вырожденная вершина (угол < 15° и точка смыкания внутренних граней внутри
обеих стен) даёт **одну точку внешнего контура ровно в вершине плана**:
ни двухточечной фаски, ни иглы mitre, уходящей на десятки сантиметров за
стены, которые её образуют;
- внутренний контур сходится в СВОЮ вершину (одна точка mitre) вместо
двухточечной фаски, поэтому «бабочки» не возникает вовсе: вычитать нечего,
V-вырезов нет, кладка выше сплошная, штриховка непрерывна;
- на внешних гранях между внутренней и внешней вершиной не остаётся ступенек
(вторая поправка владельца): кольцо комнаты — ровно треугольник, каждая
сторона длиннее полутолщины, микро-вершин и «щепок» нет;
- обычные и просто острые пары (#310, вершина вне зоны перекрытия) не
меняются: их полный mitre сохраняется.
## 5. Бэкенд
Семантическая дельта-валидация по образцу существующих (например, jamb
margin): правила П1–П5 применяются к элементам, которых касается дельта
записи (новые/изменённые сегменты, узлы, комнаты); унаследованные нарушения
нетронутых элементов проходят. Ошибки — стабильные коды
`junction_limit_<rule>` в общем формате `vol.Invalid`/семантических ошибок.
Питоновское зеркало обязано давать решения, идентичные фронту.
## 6. Затронутые поверхности и артефакты
- `src/wall-thickness.ts` / `src/wall-segment-model.ts` — геометрия проверок
П1–П5 (чистые функции) и фаска §4 в построении тел/вершинных правил.
- `src/houseplan-card.ts` — вызовы проверок в общем барьере записи, тосты.
- `src/i18n/en.json`, `src/i18n/ru.json` — по ключу на правило:
`junction.limit_angle`, `junction.limit_valence`, `junction.limit_length`,
`junction.limit_distance`, `junction.limit_clearance` (с параметрами
значений) + заголовок отказа.
- `custom_components/houseplan/validation.py` — дельта-правила §5;
`tests_backend` — кейсы принятия унаследованного и отказа нового.
- Тесты фронта: юниты на каждую формулу П1–П5 (границы: 14.9°/15°, 6/7 стен,
19/20 см, 4/5 см, пустой/непустой просвет), мутанты в гейте на П1 и §4;
смоки каналов отказа: текст объяснения ручки Resize и текст тоста
«Толщины» (AC7a/AC7b).
- Смок: репро-фикстура #329 — новая попытка нарисовать «шпиль» отклоняется;
легаси-конфиг рендерится по §4.
- Golden: новая сцена `sharp-apex-legacy-dark` по фикстуре issue (рендер §4);
существующие сцены не должны измениться — `golden:verify` обязан
подтвердить (в матрице нет вершин острее 15° — проверить прогоном).
- `docs/USER-GUIDE.md`/`.ru.md` — раздел об ограничениях рисования (таблица
П1–П5); CHANGELOG RU+EN (`User-Visible: yes`).
- `docs/CONFIG-COMPATIBILITY.md` — абзац: ограничения действуют на записи,
унаследованные данные валидны на чтение/миграцию.
- Новых полей конфига нет — `config-field-registry` не меняется.
## 7. Производительность и touch
Проверки П1–П4 — O(узлы+сегменты) на запись, вне горячего пути рендера. П5
использует уже вычисляемые физические тела (кэш) на кандидате записи —
допускается только на commit, не на каждый жест курсора. Touch-контракт не
затрагивается (правки — не жесты).
## 8. AC
- **AC1 (П1).** Завершение контура с вершиной 14° отклоняется, тост называет
минимум 15° и узел; 16° — проходит. Юнит на границе 15.0°.
- **AC2 (П2).** Седьмая стена в узел — отказ; шесть — проходит.
- **AC3 (П3).** Сегмент 19 см — отказ; 20 см — проходит; сегмент 25 см при
толщине 30 см — отказ (длина < толщины).
- **AC3b (П3, компенсация перепада толщин).** Атом 5 см, коллинеарный
продолжению стены той же толщины, проходит: правило меряет прогон, а не
атом. Проверяется юнитом на фикстуре владельца — 0 нарушений П3.
- **AC4 (П4).** Узел в 4 см от несмежного узла или чужой стены — отказ; 5 см
— проходит; T-стык (конец на чужой стене) — проходит.
- **AC5a (сквозной сценарий фикстуры).** Повторение «шпиля» из фикстуры
issue рисованием отклоняется первым же нарушенным правилом (П1, вершина
≈9.9° < 15°) — сквозной смок поверх AC1.
- **AC5b (П5 независим от П1).** Равносторонний треугольник со стороной
60 см и стенами 33 см: все углы 60° ≥ 15°, но inradius ≈ 17.3 см меньше
полутолщины 16.5 см плюс минимального просвета — внутренняя область после
вычета тел < 25 см², запись отклоняется по П5. Тот же треугольник со
стороной 120 см (inradius ≈ 34.6 см) проходит. Юнит с этими числами.
- **AC6 (§4).** Легаси-фикстура issue рендерится без «трезубца»: у вершины
ровно одна точка внешнего контура, стоящая в вершине плана; внутренний
контур — треугольник (ровно 3 точки, не «бабочка»); внешнее кольцо тела —
ровно 3 различные вершины без ступенек и микро-сегментов; golden-сцена +
юниты на контурах и на кольце.
- **AC7a (Resize, канал §2).** Resize, приводящий к нарушению любого из
П1–П5, упирается в последнюю ДОПУСТИМУЮ позицию: нарушающий шаг не
превьюится и не коммитится, а отказ ровно один раз за жест называет
нарушенное правило тем же каналом, которым Resize уже объясняет
отклонённый предпросмотр (`resize.preview_failed` → `resize.limit_stopped`).
Смок ведёт настоящий pointer-жест по ручке: сетка 2 см, две комнаты в 10 см
друг от друга, тяга на 6 см (оставила бы 4 см между чужими узлами, П4)
останавливает стену на 6 см и показывает ровно один тост с «5 см».
> Ревизия 6, отступление от первоначальной формулировки («ручка приглушена,
> тост не показывается, план байт-неизменен»). Приглушение ручки считается
> статически в `_rszResolution`, до жеста, а нарушение стыка зависит от
> КОНКРЕТНОГО шага — статически его не разрешить, не перебирая все позиции.
> Молчаливый отказ при этом прямо противоречит уже действующему контракту
> #293/#295 («reject не молчит»), закреплённому мутантом
> `resize-preview-reject-silent`. И байт-неизменность плана неверна как
> общее требование: разрешённая часть жеста — это нормальная правка,
> которую пользователь и просил; неизменным план остаётся только когда
> допустимого шага нет вовсе.
>
> Замер собственных ограничителей Resize (2026-08-27): комнату не дают
> сузить ниже 30 см (две толщины по 15 см), поэтому П3 (20 см) и П5 через
> сужение комнаты недостижимы — гейт там страхует, а не работает. Реально
> достижимо П4 на мелкой сетке, этот случай и закреплён смоком.
- **AC7b («Толщина», канал §2).** Значение толщины, нарушающее П3/П5, не
применяется; показывается тост с названием правила (образец отказа нуля
#313). План байт-неизменен. Смок проверяет текст тоста.
- **AC8.** Миграция/импорт/restore фикстуры с нарушениями проходят.
- **AC9 (§5).** Бэкенд: эхо-запись унаследованного нарушения проходит;
запись, добавляющая новое нарушение, отклоняется стабильным кодом.
- **AC10.** Идемпотентность: повторный commit валидного конфига байт-иден-
тичен; Optimize на легаси-фикстуре не создаёт новых нарушений (AC-пост-
условие §3).
## 9. Принято предположительно (поменять свободно)
- Угол П1 считается между соседними по азимуту осями в узле (не все пары).
- Порог просвета 25 см² и способ его вычисления по кэшу физических тел.
- Дельта-семантика §5: «элемент дельты» = сегмент/узел/комната, чьи данные
изменились относительно previous.
- Имена i18n-ключей и коды ошибок бэкенда.
- Фаска §4 перпендикулярна биссектрисе в точке смыкания внутренних граней.
## 10. Риски
- Ложные отказы на живых планах у границы правил (особенно П4 при плотной
геометрии) — смягчается точными тостами и тем, что унаследованное не
блокируется.
- П5 на больших пространствах — стоимость union; смягчается кэшем тел и
проверкой только на commit.
- §4 меняет пиксели вырожденных вершин — по построению матрицы golden таких
сцен нет (verify обязан подтвердить), новая сцена фиксирует контракт.
## 11. Откат
Реверт ветки: ограничения исчезают, рендер возвращается к текущему
поведению. Данные фикс не переписывает (валидация и рендер, не миграция).