Files
houseplan-card/docs/specs/484-pdf-exterior-dimension-chain.md
T
2026-09-07 20:37:04 +03:00

265 lines
22 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.
# #484 — достаточная внешняя размерная цепь ступенчатого фасада в PDF
Issue: [#484](https://github.com/Matysh/houseplan-card/issues/484)
## 1. Сценарий
Персона — **Home admin** из `docs/SCOPE.md`. В View администратор экспортирует
уже поддерживаемое пространство с прямоугольным выступом фасада в одностраничный
PDF и передаёт лист монтажнику, подрядчику или страховщику. По внешней размерной
цепи получатель должен однозначно восстановить положение и глубину выступа без
доступа к интерактивному плану.
## 2. Что человек увидит до и после
До исправления PDF показывает только высоту выступа; после исправления рядом со
ступенчатым фасадом видны расстояния сверху и снизу, высота выступа и один размер
его глубины, без диагоналей и линий сквозь посторонние стены.
## 3. Проблема и подтверждённая причина
После #482 кандидаты внешних размеров проходят две корректные стадии:
1. `dedupeOppositeDimensionEdges()` оставляет один из двух одинаковых размеров
глубины выступа и не объединяет разные вертикальные участки;
2. `groupCollinearDimensionEdges()` раскладывает независимые линии фасада по
собственным полосам.
Регрессия появляется на следующей стадии в `src/pdf/pdf-scene.ts`.
`externalPlacementTouchesArchitecture()` проверяет размерную линию, обе
выносные линии (extension lines) и shelf через общий
`pdfSegmentTouchesGeometry()`. Параметр `allowStartBoundary` разрешает только
касание в самой начальной точке. Любое коллинеарное наложение и любое следующее
пересечение границы считаются коллизией.
На обычном выпуклом прямоугольном углу выносная линия сразу уходит наружу. На
ступени перпендикуляр от конца измеряемого ребра может сначала совпасть с
примыкающим участком фасада или пройти через тело соединённой в этом же узле
стены. Это не столкновение с посторонней архитектурой, а неизбежный выход из
собственного измеряемого угла. Текущая проверка отвергает его на каждой полосе
0…80 mm, и группа размеров молча не печатается.
## 4. Цели
- восстановить минимально достаточную внешнюю размерную цепь ортогонального
ступенчатого фасада;
- сохранить точный запрет на повторный вход выносной линии в архитектуру;
- сохранить локальную дедупликацию #482 и отсутствие диагональных размеров;
- закрепить регрессию unit-, browser- и визуальным свидетельствами.
## 5. Не входит в задачу
- изменение геометрии плана, модели стен, `model_version` или сохранённого
конфига;
- новые настройки PDF или изменение диалога экспорта;
- общий CAD-размерный движок, цепи между произвольными опорными осями,
автоматические этажные размерные сетки;
- изменение внутренних размеров комнат, площадей, масштаба, компаса, штриховки,
проёмов или других результатов #482;
- разрешение размерным линиям проходить сквозь несвязанную архитектуру;
- принятие golden-эталонов без полного Linux-артефакта и явного визуального
ревью.
## 6. Контракт внешней цепи
### 6.1 Минимально достаточный набор для прямоугольного выступа
Для внешнего ring
`(0,0) → (10,0) → (10,3) → (12,3) → (12,6) → (10,6) → (10,10) → (0,10)`
PDF выводит четыре осевых размера, необходимые для положения выступа:
1. вертикальное расстояние от верхней границы здания до начала выступа;
2. вертикальную высоту выступа;
3. вертикальное расстояние от конца выступа до нижней границы здания;
4. одну горизонтальную глубину выступа.
Две горизонтальные ступени одинаковой глубины образуют локальную
противоположную пару, поэтому печатается ровно один из этих двух размеров.
Детерминированный placement score #482 выбирает сторону с безопасной полосой и
свободным местом; при полной ничьей действует существующий stable source key.
Равные по числу, но топологически разные вертикальные участки не объединяются.
Порядок обхода ring и его winding не меняют множество напечатанных размеров.
### 6.2 Разрешённый начальный выход из собственного узла
Послабление применяется **только** к двум внешним extension lines конкретного
`DimensionEdge`. Полная печатная линия по-прежнему начинается в фактической
точке `sourceA`/`sourceB`; визуальный разрыв или перенос начала за стену не
допускается.
Для проверки коллизии extension line делится точными параметрами пересечений с
архитектурными rings на последовательные интервалы. Разрешён только её
непрерывный начальный префикс от source point до первого строго внешнего
интервала. В этот префикс могут входить:
- общая начальная точка измеряемой и примыкающей граней;
- коллинеарное движение вдоль границы стены, инцидентной source point;
- выход через тело физической стены, непрерывно примыкающее к source point.
После того как линия впервые вышла в свободное пространство, любое более
позднее касание, пересечение, коллинеарное наложение или вход в физическую
архитектуру снова считается коллизией. Это включает другую стену, другой
участок того же connected component, колонну и возвращение в исходную стену.
Касания в одной и той же точке перехода «собственный префикс → свободное
пространство» группируются с физическим epsilon и не считаются повторным входом.
Невалидные координаты, неоднозначная классификация интервала и линия, которая
так и не получает свободного участка, обрабатываются fail closed: кандидат
считается столкнувшимся.
### 6.3 Остальные коллизии
Без изменений остаются:
- collision label box с архитектурой и ранее размещёнными подписями;
- collision основной dimension line и shelf с архитектурой;
- пересечения extension lines с чужими label boxes;
- 1 mm clearance, общий lane для коллинеарной группы и поиск полос 0…80 mm;
- безопасный отказ от конкретной группы, если после узкого исключения ни одна
полоса не подходит.
Послабление не становится default общего `pdfSegmentTouchesGeometry()` и не
используется внутренними размерами, декором либо другими PDF-примитивами.
## 7. Геометрические и визуальные инварианты
- Размеры остаются только horizontal/vertical и near-axis в действующем допуске
#482; диагональная хорда не создаётся и не проецируется.
- Прямоугольный внешний ring по-прежнему получает одну горизонтальную и одну
вертикальную величину после дедупликации противоположных сторон.
- Внешние подписи центрированы на измеряемом участке; весь group lane сдвигается
целиком, а не отдельными фрагментами.
- Допустимый начальный префикс влияет только на collision decision. Цвет,
толщина, начало и конец напечатанной extension line не меняются.
- Если посторонняя архитектура перекрывает все полосы, небезопасный размер не
печатается. Исправление не заменяет fail-closed поведение на overlap.
## 8. Модель, совместимость и миграция
`config_version`, `model_version`, space schema и PDF option storage не
меняются. Экспорт остаётся read-only и работает поверх временной печатной сцены.
Миграция и downgrade converter не нужны; старые и новые карточки читают один и
тот же план.
## 9. UX, i18n и touch
Новых элементов управления и строк интерфейса нет; ключи i18n не добавляются.
Исправляется только содержимое скачанного PDF при уже включённой опции размеров.
Экспорт вызывается из View и остаётся полностью поддерживаемым на touch. Диалог,
tap/pointer-поведение и скачивание не меняются. Результат PDF одинаков для
desktop, touch и HA Companion при одинаковом plan/config.
## 10. Производительность и безопасность
Проверка собственного начального выхода использует конечный набор сегментов
уже подготовленных `architectureRings`; сетевой ввод, DOM и пользовательский
HTML не появляются. Она выполняется только при явном PDF-экспорте с включёнными
размерами.
Действующий budget #482 — построение large-house scene `<200 ms` на референсной
Node-среде — сохраняется. Initial View graph и lazy boundary PDF не меняются.
Детерминированность PDF bytes для одинаковых `now`, plan/config и options
сохраняется.
## 11. План реализации
1. Расширить точный segment/ring collision helper в
`src/pdf/pdf-collision.ts` отдельным opt-in режимом начального выхода:
собрать и отсортировать intersection parameters, классифицировать открытые
интервалы и разрешить только непрерывный source-connected prefix.
2. В `src/pdf/pdf-scene.ts` включить этот режим только для двух внешних
extension lines. Dimension line, shelf, labels и внутренние размеры оставить
на строгом пути.
3. Добавить чистые unit-матрицы: обычный выход наружу; начальное коллинеарное
наложение; начальный проход через инцидентное тело; касание в точке выхода;
повторный вход; касание посторонней стены; invalid/fail-closed.
4. Добавить scene fixture ступенчатого фасада в прямом и обратном winding и
проверить полный набор четырёх внешних величин, единственность глубины,
отсутствие диагонали и неизменный rectangle contract.
5. Добавить focused PDF browser/golden fixture либо расширить
`pdf-export-polish-light` так, чтобы выступ и все четыре размера были видимы
и проверялись semantic guard, а не только пиксельным снимком.
6. Обновить канонические документы, changelog и screenshot fingerprints по
правилам процесса.
## 12. Acceptance criteria и доказательства
| AC | Критерий | Обязательное доказательство |
|---|---|---|
| AC1 | Детерминированный stepped-ring fixture печатает верхний вертикальный участок, высоту выступа, нижний вертикальный участок и одну глубину | `test/pdf-scene.test.mjs`: точные значения и позиции внешних text/line commands для обоих winding |
| AC2 | Из двух равных глубин остаётся ровно одна; rectangle сохраняет одну H и одну V сторону; диагональных размеров нет | unit matrix `pdf-dimensions` + scene assertions на axis/количество |
| AC3 | Extension line может выйти из собственного узла через начальное коллинеарное/solid-примыкание, печатается полностью от source point и не получает визуального разрыва | `test/pdf-collision.test.mjs` + scene command endpoints |
| AC4 | После первого выхода наружу любое касание/пересечение/overlap архитектуры остаётся запрещённым; неразмещаемая группа fail closed | отрицательная unit/scene matrix + mutation witness, глобально разрешающий post-exit collision |
| AC5 | Label, dimension line, shelf, внутренние размеры, 1 mm clearance и lane/group contract #482 не меняются | существующие `pdf-collision`/`pdf-scene` tests + код-ревью по call sites |
| AC6 | Результат не зависит от winding/порядка ring, PDF остаётся deterministic и large-house scene укладывается в `<200 ms>` | unit equality + существующий deterministic writer/scene perf test |
| AC7 | В rasterized focused PDF визуально присутствуют все четыре элемента цепи, подписи не лежат в кладке и никакая extension line не пересекает постороннюю архитектуру | browser PDF smoke + Linux golden/semantic probe с явной приёмкой полного артефакта |
| AC8 | Канонические и пользовательские документы описывают достаточную цепь выступа; оба changelog ссылаются на #484; docs/golden fingerprints актуальны | `node scripts/check-docs.mjs`, changelog/provenance gates, reviewed Linux docs/golden artifacts |
### 12.1 Обязательные отрицательные свидетели защитных AC
| Защитный AC | Чем доказывается | Чем обязан краснеть |
|---|---|---|
| AC3 | stepped source-exit unit/scene fixture | мутант `pdf-own-exit-prefix-disabled` возвращает строгий `allowStartBoundary` и теряет минимум один обязательный размер |
| AC4 | post-exit re-entry и foreign-wall fixtures | мутант `pdf-post-exit-collision-ignored` разрешает всю extension line и печатает заведомо небезопасный размер |
Мутанты добавляются в `scripts/mutation-gate.mjs`, потому что защитные AC живут
в продуктовом коде, а визуальный/browser witness дорог для повторного ручного
снятия на код-ревью.
## 13. Release artifacts
- `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md`: короткий bugfix со ссылкой на
#484;
- `docs/PDF-EXPORT.md`: правило достаточной внешней цепи прямоугольного выступа
и узкий source-exit contract;
- `docs/USER-GUIDE.md` и `docs/USER-GUIDE.ru.md`: пользовательское обещание
размеров ступенчатого фасада;
- `docs/ARCHITECTURE.md`: ownership source-aware collision режима внутри lazy
PDF runtime;
- `docs/STATUS.md`: текущая beta-line после фактического попадания изменения;
- focused Linux golden и semantic contract; принимается только
`npm run golden:accept -- --reviewed --from=<полный Linux artifact>`;
- поскольку меняется `src/**`, полный Linux docs screenshot artifact обязателен
для обновления fingerprint. Пиксельные изменения десяти кадров не ожидаются;
любое изменение кадра разбирается отдельно, а не принимается автоматически.
Backend, HA API и security model не меняются; backend pytest не является целевым
доказательством.
## 14. Риски и снижение
| Риск | Снижение |
|---|---|
| Узкое исключение превратится в общее разрешение линиям проходить через стены | state machine «initial prefix → outside → any later hit rejects» и отрицательный мутант AC4 |
| Касание нескольких граней в одной вершине ошибочно станет re-entry | группировка intersection parameters с единым physical epsilon и corner fixtures |
| `isSolid` по-разному классифицирует точку на границе | интервалы проверяются по внутренним midpoint, коллинеарные интервалы учитываются отдельно |
| Дедупликация снова удалит непарные равные участки | существующая topology matrix #482 плюс exact stepped counts AC1/AC2 |
| Исправление будет видно только в unit, но не в настоящем PDF | browser export + rasterized focused golden + semantic probe AC7 |
| Перебор пересечений замедлит крупный экспорт | используется уже подготовленная архитектура, без повторного model build; сохраняется `<200 ms` budget AC6 |
## 15. Откат
Откат — возврат lazy PDF chunk к версии до #484. Данные и настройки не
мигрируют, поэтому отдельный rollback данных не нужен. При откате возвращается
только известная потеря размеров ступенчатого фасада; обычный экспорт остаётся
совместимым.
## 16. Принято предположительно, можно менять на ревью
Следующие решения технические и не требуют продуктового ответа владельца:
- точный helper может расширить `pdfSegmentTouchesGeometry()` opt-in опцией или
быть отдельной функцией, если default строгого helper остаётся неизменным;
- intersection sweep может хранить пары `tStart/tEnd` либо нормализованные
breakpoints; внешний контракт задаётся §6.2, а не формой API;
- focused visual witness может быть новым golden scenario либо расширением
`pdf-export-polish-light`, если semantic guard однозначно отличает наличие
каждого размера;
- название и число внутренних тестовых helpers свободны при полном покрытии
AC и двух обязательных мутантов.