From d53e4f70d33e12ce55e9029fc700724b8d0c046e Mon Sep 17 00:00:00 2001 From: Matysh Date: Mon, 7 Sep 2026 20:37:04 +0300 Subject: [PATCH] docs: specify stepped PDF dimension chain Issue: #484 User-Visible: no --- .../specs/484-pdf-exterior-dimension-chain.md | 264 ++++++++++++++++++ docs/specs/README.md | 1 + 2 files changed, 265 insertions(+) create mode 100644 docs/specs/484-pdf-exterior-dimension-chain.md diff --git a/docs/specs/484-pdf-exterior-dimension-chain.md b/docs/specs/484-pdf-exterior-dimension-chain.md new file mode 100644 index 00000000..84fb3a65 --- /dev/null +++ b/docs/specs/484-pdf-exterior-dimension-chain.md @@ -0,0 +1,264 @@ +# #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 и двух обязательных мутантов. + diff --git a/docs/specs/README.md b/docs/specs/README.md index 89774dc1..a6056f0d 100644 --- a/docs/specs/README.md +++ b/docs/specs/README.md @@ -28,6 +28,7 @@ GitHub Issues и GitHub Projects (v2) остаются единственным | Issue | ТЗ | |---|---| +| [#484](https://github.com/Matysh/houseplan-card/issues/484) Внешняя размерная цепь ступенчатого фасада в PDF | [484-pdf-exterior-dimension-chain.md](484-pdf-exterior-dimension-chain.md) | | [#482](https://github.com/Matysh/houseplan-card/issues/482) Доводка экспорта пространства в PDF | [482-pdf-export-polish.md](482-pdf-export-polish.md) | | [#471](https://github.com/Matysh/houseplan-card/issues/471) Убрать белые raised plates вокруг маркеров и названий комнат | [471-isometric-overlay-white-plates.md](471-isometric-overlay-white-plates.md) | | [#6](https://github.com/Matysh/houseplan-card/issues/6) Vacuum XCME path segments | [006-vacuum-xcme-path.md](006-vacuum-xcme-path.md) |