mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-04 21:58:56 +00:00
180 lines
16 KiB
Markdown
180 lines
16 KiB
Markdown
# Issue #329 — ограничения стыков стен и честная острая вершина
|
||
|
||
Статус: ревизия 3 (r1: M1/M2/L1; r2: M1 — AC7 доказывает каналы §2). Решения владельца (чат, 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 см.
|
||
- **П4. Минимальная дистанция:** несмежные узлы — не ближе **5 см**; узел —
|
||
не ближе 5 см к стене, которой он не принадлежит (касание конца о середину
|
||
чужой стены остаётся T-стыком и валидно — правило про «почти-касание» без
|
||
инцидентности). Порог абсолютный, в сантиметрах, и НЕ зависит от
|
||
`cell_cm` пространства.
|
||
- **П5. Просвет комнаты:** после вычета тел стен внутренняя область комнаты
|
||
непуста и её площадь ≥ **25 см²**. Порог абсолютный и не зависит от
|
||
`cell_cm`.
|
||
|
||
## 3. Граница применения
|
||
|
||
- Проверяются только записи, ИЗМЕНЯЮЩИЕ соответствующую геометрию: рисование
|
||
и завершение контура, черновики, независимые стены/перегородки, Resize,
|
||
инструмент «Толщина» (толщина участвует в П3/П5), merge/split.
|
||
- Существующие данные наследуются: запись, не трогающая нарушающий элемент,
|
||
проходит; миграция (#306/#316), импорт и восстановление из бэкапа не
|
||
блокируются никогда.
|
||
- Optimize (выравнивание/канонизация) не обязан чинить легаси-нарушения и не
|
||
имеет права создавать новые (пост-условие прогона).
|
||
|
||
## 4. Рендер остаточных острых вершин (легаси)
|
||
|
||
Для пары стен вершины, чьи внутренние грани смыкаются на расстоянии `d` от
|
||
вершины, меньшем длины обеих стен (эквивалентно: угол мал настолько, что
|
||
существует зона полного перекрытия тел):
|
||
|
||
- остриё обрезается **одной плоской фаской**, перпендикулярной биссектрисе,
|
||
проходящей через точку смыкания внутренних граней;
|
||
- выше фаски тел нет вовсе (никаких пиков), ниже — обычная кладка; V-вырезы
|
||
запрещены; штриховка непрерывна;
|
||
- правило дополняет #309/#310 и включается только в вырожденной зоне
|
||
(`d` внутри длины стен); обычные острые пары (#310, остриё вне зоны
|
||
перекрытия) не меняются.
|
||
|
||
## 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 см — отказ (длина < толщины).
|
||
- **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 рендерится без «трезубца»: одна фаска,
|
||
ноль V-вырезов; golden-сцена + пиксельная проверка смоком (нет разрывов
|
||
тела выше фаски).
|
||
- **AC7a (Resize, канал §2).** Resize, приводящий к нарушению любого из
|
||
П1–П5, упирается в последнюю допустимую позицию; при отсутствии
|
||
безопасного шага ручка приглушена, и её объяснение (hover/focus/нажатие)
|
||
называет нарушенное правило. План байт-неизменен; тост не показывается.
|
||
Смок проверяет и байт-неизменность, и текст объяснения ручки.
|
||
- **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. Откат
|
||
|
||
Реверт ветки: ограничения исчезают, рендер возвращается к текущему
|
||
поведению. Данные фикс не переписывает (валидация и рендер, не миграция).
|