diff --git a/docs/SCOPE.md b/docs/SCOPE.md index 771a40f0..434c0130 100755 --- a/docs/SCOPE.md +++ b/docs/SCOPE.md @@ -104,6 +104,14 @@ the file — and if a future version wants to reclaim that space, it asks. [#89](https://github.com/Matysh/houseplan-card/issues/89) is a deterministic 2.5D presentation of the existing canonical plan: the same J1/J2/J3 state, actions and geometry, with no second model and no free camera. +- General CAD, architectural drafting and construction-document tooling → + specialised applications. A narrow read-only exception approved on + 2026-08-15 and scoped on 2026-09-07 for + [#53](https://github.com/Matysh/houseplan-card/issues/53) exports the current + canonical space as a single print-ready PDF. It introduces no second geometry + model, drawing tools or all-spaces document workflow; its purpose is to hand + the already maintained House Plan geometry to an installer, insurer or + renovation contractor. - A general dashboard framework (menus, popups, theming engine) → Bubble Card, Dwains and friends. diff --git a/docs/specs/053-pdf-export.md b/docs/specs/053-pdf-export.md index c9bb65b6..385cc15e 100755 --- a/docs/specs/053-pdf-export.md +++ b/docs/specs/053-pdf-export.md @@ -143,7 +143,11 @@ суммарной цепочки нет (её даёт сумма). Несколько несвязных наружных контуров (пристройка, отдельное строение в пространстве) — каждый со своими внешними размерами. Виртуальные (нулевые) границы наружного -контура длину не получают. +контура длину не получают. Порог 30 см для внутренних рёбер здесь не +применяется: каждая физическая наружная грань, включая грань короче 30 см, +получает числовое значение. Если число не помещается над размерной линией, +оно выносится наружу на детерминированную полку с линией-указателем и не +скрывается. **Единицы и точность** — `formatLength`/`formatArea`, metric/imperial из HA, масштаб сетки `cell_cm`; числа совпадают с живой линейкой ресайза на @@ -151,9 +155,17 @@ HA, масштаб сетки `cell_cm`; числа совпадают с жив **Коллизии** — детерминированно: приоритет внешние размеры → площади → внутренние рёбра длинные → короткие; подпись, чей прямоугольник -пересекает уже размещённый или выходит за поле листа, скрывается (кроме -случая «непрямоугольный контур» — там уменьшается кегль до 6 pt прежде -чем скрыться). Никакая подпись не печатается вверх ногами. +пересекает уже размещённый или выходит за поле листа, скрывается только в +разрешённых выше случаях: дубликат направления у прямоугольной комнаты или +короткое (< 30 см) внутреннее ребро с засечкой. У непрямоугольного контура +числовое значение ребра из-за коллизии не исчезает: сначала кегль уменьшается +до 6 pt и подпись детерминированно сдвигается вдоль ребра внутрь комнаты; если +свободного места всё равно нет, у ребра ставится компактная метка `R` с +выносной линией, а полное значение печатается в нумерованном блоке +«Внутренние размеры» в свободном поле листа. Нумерация стабильна: комнаты — +по `room.id`, рёбра — от лексикографически минимальной вершины по часовой +стрелке. Блок входит в bounding box печатного содержимого при выборе масштаба +§7.5. Никакая подпись не печатается вверх ногами. ### 7.4. Мебель и другой декор (галочка, по умолчанию — Q1) @@ -276,7 +288,7 @@ PDF идентичны: даты `CreationDate`/`ModDate` берутся из п Новые ключи (en/ru/de/fr): `title.export_pdf`, `pdf.title`, `pdf.dimensions`, `pdf.decor`, `pdf.room_names`, `pdf.backdrop`, `pdf.save`, `pdf.saving`, `pdf.failed`, `pdf.too_large`, `pdf.asset_failed`, -`pdf.scale` («Масштаб 1:{n}»), `pdf.north`, `pdf.legend.wall`, +`pdf.internal_dimensions`, `pdf.scale` («Масштаб 1:{n}»), `pdf.north`, `pdf.legend.wall`, `pdf.legend.partition`, `pdf.legend.virtual`, `pdf.legend.door`, `pdf.legend.window`, `pdf.legend.gate`. Тест `i18n-dead-keys` требует потребителя у каждого — все используются диалогом или подвалом. @@ -301,8 +313,9 @@ View. `localStorage` — единственное новое состояние, - `scripts/bundle-manifest.mjs`, `scripts/bundle-budget.mjs` — роль `pdf`, `lazyPdfFiles`, токен retry-URL; - `src/i18n/*.json`; `docs/PDF-EXPORT.md` (новый), `docs/USER-GUIDE*.md`, - `docs/ARCHITECTURE.md` (ленивая граница), `docs/SCOPE.md` (исключение - 2026-08-15 — уже записано, проверить формулировку), `docs/CHANGELOG*`; + `docs/ARCHITECTURE.md` (ленивая граница), `docs/SCOPE.md` (узкое + read-only исключение #53, принятое 2026-08-15 и суженное 2026-09-07), + `docs/CHANGELOG*`; - тесты §13; golden-сцена; смок `demo/smoke_pdf_export.mjs`; `scripts/mutation-gate.mjs` — свидетели §13. @@ -314,8 +327,8 @@ View. `localStorage` — единственное новое состояние, | AC2 | Галочка «подложка» присутствует только у пространства с `bg`; состояние галочек переживает перезагрузку страницы | smoke | | AC3 | «Сохранить» скачивает файл `houseplan--<дата>.pdf`; в файле ровно одна страница A4 (MediaBox 595.28×841.89 или наоборот) с ориентацией по пропорциям | smoke (перехват download) + unit на writer | | AC4 | Геометрия §7.1: стены с толщиной залиты, нулевые — штрих, проёмы с разрывами, общая стена один раз, ни одного маркера/состояния/цвета комнаты | golden по растру PDF (§13) + unit по операторам контент-потока | -| AC5 | Внутренние размеры: у каждой комнаты подписано каждое ребро чистого контура (грань-в-грань), значения совпадают с `formatLength` живой линейки на том же ребре, площадь — с карточкой комнаты; по этим числам комната вычерчивается автономно (тест восстанавливает контур из напечатанных длин и сверяет с моделью) | unit (фикстуры: прямоугольник, L, комната с выступом, общая толстая стена, виртуальная граница, партиция внутри комнаты) | -| AC6 | Внешние размеры: у каждой прямой наружной грани планировки своя размерная линия снаружи контура с выносными линиями; несвязные контуры — каждый свои; виртуальные границы без размера; текст не вверх ногами; коллизии детерминированы; рёбра < 30 см — засечка без числа | unit на раскладку + golden | +| AC5 | Внутренние размеры: каждое ребро чистого контура комнаты (грань-в-грань) имеет числовую подпись, нумерованную выноску либо разрешённую для ребра < 30 см засечку; значения не исчезают из-за коллизий, совпадают с `formatLength` живой линейки на том же ребре, площадь — с карточкой комнаты; по этим данным комната вычерчивается автономно (тест восстанавливает контур и сверяет с моделью) | unit (фикстуры: прямоугольник, L, комната с выступом, плотный непрямоугольный контур с выноской, общая толстая стена, виртуальная граница, партиция внутри комнаты) | +| AC6 | Внешние размеры: у каждой прямой физической наружной грани планировки, включая грань < 30 см, своя размерная линия и числовое значение снаружи контура; несвязные контуры — каждый свои; виртуальные границы без размера; текст не вверх ногами; тесные значения не скрываются, а детерминированно выносятся | unit на раскладку + golden | | AC7 | Галочки реально управляют содержимым: без «размеров» в PDF нет ни одной размерной строки; без «названий» — ни одного названия; без «декора» — ни одного декор-объекта; без «подложки» — ни одного XObject Image | unit по извлечённому тексту/операторам | | AC8 | Кириллица и латиница в названиях печатаются встроенным сабсетом; текст извлекается из PDF (`pdfjs-dist` в тесте) как Unicode — работает ToUnicode | unit | | AC9 | Подвал: масштаб из ряда §7.5, линейка соответствует масштабу (1 м на бумаге = 1000/N мм ± 0,1 мм), стрелка севера при компасе, легенда только присутствующих элементов | unit | @@ -329,7 +342,9 @@ View. `localStorage` — единственное новое состояние, ## 13. План автотестов - `test/pdf-writer.test.mjs` — объекты/xref валидны (парсится `pdfjs-dist`), шрифт CIDFontType2 + ToUnicode, изображение DCTDecode, детерминизм. -- `test/pdf-dimensions.test.mjs` — пролёты и площади на общих фикстурах, раскладка, коллизии, порог 40 см, «не вверх ногами». +- `test/pdf-dimensions.test.mjs` — пролёты и площади на общих фикстурах, + раскладка, коллизии, нумерованные выноски, порог 30 см только для + внутренних рёбер, короткая внешняя грань с числом, «не вверх ногами». - `test/pdf-scene.test.mjs` — таблица §7.1 по операторам, галочки (AC7), подвал (AC9), ориентация и ряд масштабов. - `demo/smoke_pdf_export.mjs` — кнопка, диалог, `localStorage`, перехват `download`, MediaBox, отказ чанка, лимит растра. - golden: сцена `pdf-export-geometry-light` — PDF рендерится `pdfjs-dist` в canvas в браузере golden-harness и сравнивается как PNG (детерминизм §8.4 делает это устойчивым); при отклонении — обычная приёмка эталона. @@ -341,14 +356,20 @@ View. `localStorage` — единственное новое состояние, ## 15. Производительность и безопасность -Генерация — синхронный расчёт сцены (< 200 мс на `large-house` фикстуре, 60 комнат; проверяется в unit как порог) плюс асинхронная растеризация подложки. Ничего не уходит в сеть, кроме загрузки чанка и получения подложки тем же путём, что и на экране. PDF не содержит ссылок, скриптов, метаданных HA (только название пространства, дома, дата, версия). Шрифт — лицензия Apache 2.0, файл лицензии в репозитории. +Генерация — синхронный расчёт сцены (< 200 мс для одного текущего +пространства `large-house`: 20 комнат и относящиеся к этому пространству +перегородки, колонны и декор; проверяется в unit как порог) плюс асинхронная +растеризация подложки. Ничего не уходит в сеть, кроме загрузки чанка и +получения подложки тем же путём, что и на экране. PDF не содержит ссылок, +скриптов, метаданных HA (только название пространства, дома, дата, версия). +Шрифт — лицензия Apache 2.0, файл лицензии в репозитории. ## 16. Риски и меры | Риск | Мера | |---|---| | Собственный writer даёт PDF, который открывается не везде | тест парсером `pdfjs-dist` + ручная проверка в Acrobat Reader, Chrome, macOS Preview, Android (AC12); PDF 1.4 без потоков-объектов и без сжатия текста (только FlateDecode контента при наличии `CompressionStream`, иначе без сжатия) | -| Размерные подписи налезают друг на друга на плотных планах (по 4+ числа на комнату) | детерминированная коллизия §7.3: внешние → площади → длинные рёбра; у прямоугольников дубликаты опускаются первыми; проверено на `large-house` фикстуре в golden | +| Размерные подписи налезают друг на друга на плотных планах (по 4+ числа на комнату) | детерминированная коллизия §7.3: внешние → площади → длинные рёбра; у прямоугольников дубликаты опускаются первыми, обязательные значения переходят в выноски; проверено на текущем 20-комнатном пространстве `large-house` в golden | | Ядро на потолке | логика целиком в `src/pdf/**`, ядро — кнопка и вызов; счёт строк в дифе | | Растры делают файл огромным | лимит 25 МБ, JPEG q=0.85, тост вместо молчаливого 100-МБ файла | | Мобильное приложение не скачивает Blob | AC12 вручную до закрытия; при отказе — запасной путь `data:`-URL в новой вкладке, решение по факту |