Волна 5 эпика #674, перенос ТЗ (класс C). Из 240 файлов `docs/specs/` в `legacy/specs/` уехали 219: на них не ссылается ни один живой файл (код, тесты, скрипты, workflow, документы вне архива и ревью). Остались 21 ТЗ — на которые ссылаются код, ADR, ISOMETRIC, SUN, RADAR, LIGHT (`docs/specs/067`), DECOR-EDITOR, support-relay, и те, на которые ссылаются они сами; README каталога объясняет, где искать остальное. Открытых issue с файлом ТЗ среди перенесённых нет. Относительные ссылки перенесённых файлов переписаны (`../X` → `../../docs/X`, соседние оставшиеся ТЗ → `../../docs/specs/…`) — все 26 резолвятся. Попутно: битая ссылка в `089-isometric-view-stage1.md:8` на удалённый `089-isometric-view.md` — теперь команда `git show` по истории. Строка в `legacy/README.md`. Issue: #682 User-Visible: no Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
41 KiB
Executable File
ТЗ #53 — Экспорт пространства в PDF: чистый архитектурный план
Частично переопределено #482. Актуальный контракт цвета и штриховки стен, отбора/дедупликации/размещения размеров, выбора листа и масштаба, компаса и отсутствия легенды находится в
482-pdf-export-polish.md. При противоречии он имеет приоритет; остальные требования этого ТЗ сохраняются.
- Issue: https://github.com/Matysh/houseplan-card/issues/53
- Приоритет: P3 · Тип: feature · Трек: полный (новый UX, новый ленивый чанк, новый формат вывода)
- Оценка: пользовательская ценность 7/10 («распечатал план для электрика/страховой»), уникальность против конкурентов 9/10 (у них нет геометрии для печати); сложность 6/10; риск 4/10 (шрифты и растр в PDF, детерминизм)
- Решения владельца: 2026-08-15 (исключение из
docs/SCOPE.md, задача остаётся в очереди); 2026-09-07 (#52 объединён сюда, слой размеров во View не делается; кнопка-принтер, маленький диалог, только текущее пространство, без устройств, подложка — если есть у пространства) - Связанные контракты:
docs/specs/052-view-dimensions.md(§ Площади — контракт площадей; § Длины на бумаге не применяется, см. §7.3),docs/WALL-THICKNESS.md,docs/BACKDROP.md,docs/FURNITURE.md,docs/SCOPE.md, #456 (образец диалога у пространства), #476 (диалог с кнопкой)
1. Сценарий
Персона — администратор дома, desktop-браузер (touch — best effort по
docs/TOUCH-SUPPORT.md, но диалог обязан работать пальцем). Он открывает
нужное пространство, нажимает иконку принтера в панели карточки, в
маленьком диалоге оставляет или снимает три-четыре галочки и нажимает
«Сохранить». Через секунду браузер скачивает houseplan-<пространство>-<дата>.pdf
— один лист A4 с планом этого пространства: стены с толщиной, перегородки,
виртуальные стены, проёмы, размеры и площади, названия комнат — без
устройств, состояний, подсветок и цветов. Лист можно отдать электрику,
страховой, подрядчику.
2. Что человек увидит до и после
До: план существует только на экране; «бумажный» вариант — скриншот с маркерами и свечением.
После: в панели карточки между шестерёнкой и знаком вопроса — иконка принтера. Диалог «Сохранение в PDF» с галочками и одной кнопкой. Файл скачивается без диалога печати браузера, одинаково на десктопе и в мобильном приложении HA. На листе — векторная штриховая графика, читаемые подписи на языке плана, масштаб и масштабная линейка в подвале.
3. Проблема
Экспорт JSON (#50) переносит план между установками, но не годится как документ для человека без Home Assistant. SVG на экране рисует всё сразу — состояния, свечение, маркеры — и не имеет ни масштаба, ни листа. Размеры стен и площади уже считаются (ресайз, карточка комнаты), но нигде не печатаются. Контракт измерений принят в #52 и не реализован; печатный лист — первое место, где он нужен целиком.
4. Решения владельца (2026-09-07)
- Точка входа — иконка принтера в основной панели между настройками и знаком вопроса.
- Диалог маленький: заголовок «Сохранение в PDF», галочки и кнопка «Сохранить».
- Экспортируется только текущее пространство; «все пространства» — не делаем.
- На листе только архитектура: стены, перегородки, виртуальные стены, проёмы, размеры. Устройства не нужны совсем — ни галочки, ни маркеров.
- Галочки: «Отображать размеры» (по умолчанию включена), «Отображать мебель и другой декор», «Отображать названия комнат», «Отображать подложку» — последняя есть только если у пространства задана подложка.
- Слой размеров на экране (View) не делается — #52 закрыт.
5. Скоуп
- Кнопка в панели и диалог (§6).
- Печатная сцена пространства из канонической модели (§7): геометрия, подписи, размеры по контракту #52, подложка и декор по галочкам.
- Лист A4, автоориентация, вписывание, подвал с масштабом (§7.5).
- Генерация PDF целиком на клиенте, ленивым чанком, без внешних сервисов (§8); скачивание файла.
- Шрифт с кириллицей и латиницей, встроенный в PDF (§8.3).
- i18n четырёх языков (§9), документация, тесты (§12–§13).
6. Не-скоуп
- Устройства, маркеры, состояния, свечение, солнце, следы робота, топология Zigbee — не печатаются никогда.
- Экспорт всех пространств одним файлом; титульный лист; выбор формата листа (A3, Letter) и масштаба вручную — возможные следующие задачи.
- Слой размеров на экране; изометрический вид (печатается только плоский план независимо от текущей проекции).
- Печать через
window.print()/диалог печати браузера — отвергнуто в §8.1. - Изменение модели данных, конфига, схемы бэкенда — нет.
houseplan-space-card— кнопки нет (статичная карточка без панели); отдельная задача при необходимости.
7. Контракт: что на листе
7.1. Геометрия (всегда)
Источник — spaceModels(cfg) для текущего пространства, та же
нормализованная модель, что у View: комнаты, wall_segments с толщиной,
partitions, wall_columns, проёмы (door, window, gate, passage).
| элемент | изображение |
|---|---|
| стена с толщиной | замкнутый контур по физическим граням, заливка серым (#555), обводка чёрная 0,25 мм; общая стена — один раз |
| стена нулевой толщины (виртуальная / открытая граница) | штриховая линия 0,35 мм, штрих 3 мм / пробел 2 мм, без заливки |
| перегородка | как стена, своей толщиной; тоньше 4 см на бумаге — сплошная линия 0,5 мм |
| колонна | контур по cm (квадрат/круг), заливка серым |
| дверь | разрыв в стене + створка и дуга открывания в сторону и с петлями, как на экране (flip_v/петли уважаются) |
| окно | разрыв в стене с двойной линией по граням |
| ворота | как дверь, без дуги, с диагональной штриховкой створки |
проход (passage) |
разрыв без линий |
Никаких заливок комнат, свечения, цветов стен из настроек: лист
монохромный. Толщина линий — в миллиметрах бумаги, не зависит от масштаба
(аналог vector-effect: non-scaling-stroke).
7.2. Названия комнат (галочка, по умолчанию — см. §18 Q1)
Название — как хранится в плане (язык пользователя), в сохранённой позиции подписи либо во внутренней точке комнаты (тот же резолвер, что у View). Кегль 9 pt; если название не помещается в комнату при масштабе листа — уменьшается до 7 pt, ниже — скрывается (tooltip'а на бумаге нет, поэтому комната остаётся без подписи, а не с обрезанной).
7.3. Размеры и площади (галочка, по умолчанию включена)
Решение владельца 2026-09-07: внутренние размеры каждой комнаты,
достаточные, чтобы начертить её независимо от остальной планировки, и
внешние размеры каждой наружной грани планировки. Осевые пролёты стен из
052-view-dimensions.md § Длины на бумаге не используются; из #52
переиспользуется только § Площади.
Площадь — чистый пол через geometryArea (тот же резолвер, что у
карточки комнаты), formatArea, единицы из HA. Печатается под названием
комнаты кеглем на 1 pt меньше; без названия — одна строка площади в той же
точке.
Внутренние размеры комнаты. Контур — внутренние грани стен комнаты
(чистый пол, тот же полигон, что у площади), после компакции коллинеарных
рёбер. Каждое ребро контура получает длину formatLength(segmentCm) —
расстояние от грани до грани, а не по оси стены. Подпись — внутри комнаты,
параллельно ребру, отступ 2 мм от грани, читается слева направо или снизу
вверх. Для комнаты-прямоугольника печатаются все четыре ребра; если при
коллизии места не хватает, второе ребро той же длины и того же направления
может быть опущено — комната остаётся вычерчиваемой (по одной длине на
направление). Для непрямоугольных контуров опускать нельзя: каждое ребро —
свой размер. Рёбра короче 30 см на бумаге не подписываются, но их длину
можно восстановить из соседних — такие рёбра помечаются короткой засечкой
без числа. Проёмы на длину ребра не влияют (ребро — грань стены целиком).
Внешние размеры планировки. Внешний контур — объединение физических стен пространства по наружным граням (тот же силуэт, что у контура «вписать всё»), после компакции. Каждая прямая наружная грань получает размерную линию снаружи контура на расстоянии 6 мм: выносные линии перпендикулярно грани, засечки, число над линией. Грани одной прямой (сдвиг по наружной стене из-за выступа) — отдельные размеры, каждая своя; суммарной цепочки нет (её даёт сумма). Несколько несвязных наружных контуров (пристройка, отдельное строение в пространстве) — каждый со своими внешними размерами. Виртуальные (нулевые) границы наружного контура длину не получают. Порог 30 см для внутренних рёбер здесь не применяется: каждая физическая наружная грань, включая грань короче 30 см, получает числовое значение. Если число не помещается над размерной линией, оно выносится наружу на детерминированную полку с линией-указателем и не скрывается.
Единицы и точность — formatLength/formatArea, metric/imperial из
HA, масштаб сетки cell_cm; числа совпадают с живой линейкой ресайза на
том же ребре и с площадью в карточке комнаты.
Коллизии — детерминированно: приоритет внешние размеры → площади →
внутренние рёбра длинные → короткие; подпись, чей прямоугольник
пересекает уже размещённый или выходит за поле листа, скрывается только в
разрешённых выше случаях: дубликат направления у прямоугольной комнаты или
короткое (< 30 см) внутреннее ребро с засечкой. У непрямоугольного контура
числовое значение ребра из-за коллизии не исчезает: сначала кегль уменьшается
до 6 pt и подпись детерминированно сдвигается вдоль ребра внутрь комнаты; если
свободного места всё равно нет, у ребра ставится компактная метка R<n> с
выносной линией, а полное значение печатается в нумерованном блоке
«Внутренние размеры» в свободном поле листа. Нумерация стабильна: комнаты —
по room.id, рёбра — от лексикографически минимальной вершины по часовой
стрелке. Блок входит в bounding box печатного содержимого при выборе масштаба
§7.5. Никакая подпись не печатается вверх ногами.
7.4. Мебель и другой декор (галочка, по умолчанию — Q1)
Всё содержимое decor[]: мебель (арт из furniture-art-runtime, штрих
чёрный), линии/прямоугольники/эллипсы (цвет игнорируется — чёрный штрих,
без заливки), текстовые подписи (кегль по width_cm, минимум 6 pt),
изображения (kind: 'image') — растром по правилу §7.6. Порядок слоёв —
как в модели.
7.5. Лист, подвал, легенда (всегда)
- A4 (210 × 297 мм), поля 12 мм; ориентация — по пропорциям bounding box контента (ширина > высоты → landscape).
- Контент — геометрия ± внешние размерные линии (при включённой галочке) ± подложка/декор (при включённых) — вписывается в поле листа с сохранением пропорций; масштаб округляется вниз до стандартного ряда 1:20, 1:25, 1:50, 1:75, 1:100, 1:150, 1:200, 1:250, 1:500, чтобы линейкой на бумаге можно было мерить; при экстремальных планах — ближайший больший.
- Шапка: название пространства (кегль 14 pt), справа — название дома
(
titleкарточки), если задано. - Подвал: «Масштаб 1:N», масштабная линейка 1 м (metric) / 5 ft (imperial)
с делениями, стрелка севера при заданном компасе пространства или общем
north_deg(тот же резолвер, что у солнца), дата (локальная,YYYY-MM-DD), «House Plan vX.Y.Z», легенда: стена · перегородка · виртуальная стена · дверь · окно · ворота (только присутствующие на плане элементы). - Текст шапки/подвала — на языке карточки (
_t).
7.6. Подложка (галочка, есть только при заданной подложке)
Растровая подложка (PNG/JPEG/WebP) встраивается как XObject с исходными
байтами (JPEG — DCTDecode без перекодирования; PNG/WebP — через canvas в
JPEG q=0.85, чтобы не тащить декодер PNG в writer), в сохранённой позиции
(plan_x/plan_y/plan_scale*, поворот) под геометрией, с прозрачностью 60 %
— иначе штриховая геометрия тонет в скане. SVG-подложка растеризуется через
canvas при 150 dpi по размеру на листе. Изображения декора — по тем же
правилам. Суммарный размер встроенных растров ограничен 25 МБ: превышение —
отказ с тостом pdf.too_large до генерации, галочки остаются.
Внешние URL подложки идут через тот же подписанный доступ, что и на
экране; недоступная картинка — отказ с тостом pdf.asset_failed, файл не
пишется (лист без обещанной подложки молча — хуже, чем честный отказ).
8. Контракт: как делается PDF
8.1. Выбор способа
| вариант | плюсы | минусы | решение |
|---|---|---|---|
window.print() + print-CSS |
нет зависимостей, любой шрифт | диалог печати вместо скачивания; в мобильном приложении HA (WebView) печать недоступна или калечит лист; пагинация и масштаб не под контролем | отвергнут |
| jsPDF + svg2pdf | готовые примитивы | ~200 КБ gzip ленивого кода плюс шрифт всё равно нужен свой (стандартные 14 шрифтов PDF без кириллицы); конвертация SVG → PDF с потерями (маски, non-scaling-stroke) |
отвергнут |
| собственный минимальный PDF-writer | вектор, прямое скачивание, ~15 КБ gzip кода, одинаково на десктопе и в приложении; рисуем из модели, а не из экрана — без потерь | писать самим: страницы, контент-поток (m l c h f S), встраивание TrueType (CIDFontType2, Identity-H, ToUnicode), XObject Image, xref |
принят |
8.2. Ленивый чанк pdf-export
src/pdf/ — новый модуль, динамически импортируемый из ядра через
EditorRuntimeLoader (отпечаток сборки, повтор с нонсом, терминальный отказ
для чужой сборки — образец iso-scene-render). В стартовый граф не входит
ничего, кроме кнопки, обработчика и вызова загрузчика; манифест получает
роль pdf и lazyPdfFiles, bundle:budget требует непустой список и
отсутствие пересечения с initial (как у lazyIsometricFiles).
Состав: pdf-writer.ts (объекты, потоки, xref, шрифт, изображения),
pdf-scene.ts (модель → примитивы листа: геометрия, подписи, размеры,
подвал), pdf-dimensions.ts (контракт #52: пролёты, площади, раскладка),
pdf-export.ts (оркестрация: опции → сцена → байты → Blob → скачивание),
hp-pdf-dialog.ts (диалог).
8.3. Шрифт
Один шрифт, встроенный в PDF как FontFile2: Roboto Regular, лицензия
Apache 2.0 (файл лицензии в assets/fonts/), сабсет, собираемый на этапе
генерации (scripts/generate-pdf-font.mjs, devDependency subset-font):
Basic Latin, Latin-1 Supplement, Latin Extended-A, Cyrillic, Cyrillic
Supplement, General Punctuation, знаки ° ² ′ ″ × ≈. Результат —
src/pdf/pdf-font.generated.ts (base64, ожидаемо 60–80 КБ raw), только в
ленивом чанке; --check-режим генератора, как у мебели. Глиф вне сабсета
→ .notdef (пустой прямоугольник) — документировано; язык плана
ограничен четырьмя поддерживаемыми, поэтому в практике не встречается.
Ширины глифов берутся из hmtx сабсета в момент генерации и кладутся в
тот же файл — writer не парсит TrueType в браузере.
8.4. Скачивание и детерминизм
Blob → URL.createObjectURL → <a download> → revokeObjectURL. Имя:
houseplan-<slug названия пространства>-<YYYY-MM-DD>.pdf. В мобильном
приложении HA скачивание идёт штатным путём WebView — проверяется вручную
на Android-приложении (AC12).
При одинаковых входах (модель, опции, единицы, язык, версия, дата) байты
PDF идентичны: даты CreationDate/ModDate берутся из переданного
now, объекты нумеруются детерминированно, растры — исходные байты. Это
основа golden-проверки (§13).
8.5. Диалог и состояния
hp-pdf-dialog на базе hp-dialog (модальность и центрирование — #463).
Заголовок pdf.title («Сохранение в PDF»), галочки в порядке: размеры,
мебель и другой декор, названия комнат, подложка (только если
space.bg); кнопка pdf.save («Сохранить»), закрытие крестиком/Esc/кликом
вне — как у остальных диалогов. Состояние галочек запоминается в
localStorage (hp.pdf.options, схема {v:1,...}); при отсутствии —
значения по умолчанию §18 Q1.
Нажатие «Сохранить»: кнопка блокируется, текст pdf.saving
(«Формируем…»), загружается чанк (первый раз — до 1 с), формируется файл,
диалог закрывается после начала скачивания. Отказ (чанк не загрузился,
растр не удалось получить, лимит) — тост с причиной, диалог остаётся
открытым с теми же галочками. Повторное нажатие во время формирования
игнорируется.
Кнопка в панели: mdi:printer-outline, title/aria-label =
title.export_pdf, стоит между «Общие настройки» и «Помощь и обратная
связь» в том же блоке (_canEdit), в киоске не показывается, в
изометрии — показывается (печатается плоский план). Минимальная зона
нажатия 44×44 px как у соседей.
9. i18n
Новые ключи (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.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 требует
потребителя у каждого — все используются диалогом, содержимым листа или
подвалом.
10. Модель данных, миграция, совместимость
Конфиг, схема, бэкенд — без изменений; экспорт только читает. Планы любой
поддерживаемой версии модели печатаются через ту же нормализацию, что и
View. localStorage — единственное новое состояние, per-браузер.
11. Затронутые файлы и модули
src/pdf/pdf-writer.ts,pdf-scene.ts,pdf-dimensions.ts,pdf-raster.ts,pdf-export.ts,hp-pdf-dialog.ts,pdf-font.generated.ts— новые;scripts/generate-pdf-font.mjs,assets/fonts/Roboto-Regular.ttf+LICENSE— новые;package.json(devDependencysubset-font, скриптыpdf-font:generate/pdf-font:check);src/houseplan-card.ts— только кнопка, exact-build loader, открытие диалога и передача уже рассчитанной геометрии; после реализации 13 643 строки при потолке 13 659. Writer, сцена, шрифт и UI диалога остаются вне ядра;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(узкое read-only исключение #53, принятое 2026-08-15 и суженное 2026-09-07),docs/CHANGELOG*;- тесты §13; golden-сцена; смок
demo/smoke_pdf_export.mjs;scripts/mutation-gate.mjs— свидетели §13.
12. Критерии приёмки
| AC | Критерий | Доказательство |
|---|---|---|
| AC1 | Кнопка-принтер между шестерёнкой и «?» у _canEdit, отсутствует в киоске; открывает диалог с заголовком, галочками §8.5 и кнопкой |
smoke |
| AC2 | Галочка «подложка» присутствует только у пространства с bg; состояние галочек переживает перезагрузку страницы |
smoke |
| AC3 | «Сохранить» скачивает файл houseplan-<slug>-<дата>.pdf; в файле ровно одна страница A4 (MediaBox 595.28×841.89 или наоборот) с ориентацией по пропорциям |
smoke (перехват download) + unit на writer |
| AC4 | Геометрия §7.1: стены с толщиной залиты, нулевые — штрих, проёмы с разрывами, общая стена один раз, ни одного маркера/состояния/цвета комнаты | golden по растру PDF (§13) + unit по операторам контент-потока |
| 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 |
| AC10 | Детерминизм: два вызова с тем же now дают идентичные байты; изменение любой галочки — другие |
unit |
| AC11 | Ленивость: чанк pdf не в initial, initial View растёт только на кнопку, exact-build loader и передачу контекста (≤ 1 200 Б gzip; замер реализации относительно origin/dev — 983 Б); отказ загрузки чанка → тост, карточка жива; чужая сборка → терминальный отказ без повтора |
bundle:budget + smoke с перехватом чанка (образец #474) |
| AC12 | Скачивание работает в мобильном приложении HA (Android) — файл появляется в загрузках | вручную владельцем на даче до закрытия, запись в issue |
| AC13 | Растровая подложка встроена под геометрией с прозрачностью, лимит 25 МБ даёт pdf.too_large без файла |
unit + smoke |
| AC14 | Диалог модален и центрирован после переподключения HA (контракт #463) | smoke_pdf_export повторяет reconnect-проверку из smoke_dialog_modal_recovery на реальном hp-pdf-dialog |
| AC15 | Свидетели §13 в реестре, каждый «поймано 1 из 1» | отрицательные прогоны |
13. План автотестов
test/pdf-writer.test.mjs— объекты/xref валидны (парситсяpdfjs-dist), шрифт CIDFontType2 + ToUnicode, изображение DCTDecode, детерминизм.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 делает это устойчивым); при отклонении — обычная приёмка эталона. - Свидетели:
pdf-shared-wall-twice(общая стена дважды),pdf-dimensions-ignore-toggle(галочка размеров не влияет),pdf-room-edge-dropped(у непрямоугольной комнаты пропущено ребро — контур невосстановим),pdf-outer-face-inside(внешний размер нарисован внутри контура),pdf-virtual-wall-solid(нулевая стена сплошная),pdf-font-no-tounicode(текст без ToUnicode — не извлекается),pdf-backdrop-over-geometry(подложка поверх геометрии),pdf-devices-leak(маркеры попадают на лист),pdf-scale-not-standard(масштаб не из ряда).
14. Release-артефакты
User-Visible: yes. Changelog en/ru: «Экспорт текущего пространства в PDF: чистый архитектурный план с размерами, площадями и названиями комнат, по желанию — мебель и подложка (#53)». docs/PDF-EXPORT.md (что печатается, правила измерений — ссылка на 052, ограничения растров, шрифт и лицензия), раздел в USER-GUIDE/USER-GUIDE.ru, скриншот диалога в документации (пересъёмка по правилу §8 PROCESS).
15. Производительность и безопасность
Генерация — синхронный расчёт сцены из уже построенных видимой карточкой
кэшей стен, чистых контуров и чистых площадей комнат (< 200 мс CPU для одного текущего
пространства large-house: 20 комнат и относящиеся к этому пространству
перегородки, колонны и декор; проверяется в unit как порог) плюс асинхронная
растеризация подложки. Ничего не уходит в сеть, кроме загрузки чанка и
получения подложки тем же путём, что и на экране. PDF не содержит ссылок,
скриптов, метаданных HA (только название пространства, дома, дата, версия).
Шрифт — лицензия Apache 2.0, файл лицензии в репозитории.
16. Риски и меры
| Риск | Мера |
|---|---|
| Собственный writer даёт PDF, который открывается не везде | тест парсером pdfjs-dist + ручная проверка в Acrobat Reader, Chrome, macOS Preview, Android (AC12); простой PDF 1.4 без потоков-объектов и без сжатия контент-потока |
| Размерные подписи налезают друг на друга на плотных планах (по 4+ числа на комнату) | детерминированная коллизия §7.3: внешние → площади → длинные рёбра; у прямоугольников дубликаты опускаются первыми, обязательные значения переходят в выноски; проверено на текущем 20-комнатном пространстве large-house в golden |
| Ядро на потолке | логика целиком в src/pdf/**, ядро — кнопка и вызов; счёт строк в дифе |
| Растры делают файл огромным | лимит 25 МБ, JPEG q=0.85, тост вместо молчаливого 100-МБ файла |
| Мобильное приложение не скачивает Blob | AC12 вручную до закрытия; при отказе — запасной путь data:-URL в новой вкладке, решение по факту |
| Детерминизм ломается плавающей точкой раскладки | координаты округляются до 0,01 pt перед записью |
17. Откат
Revert одного коммита: кнопка и чанк исчезают, конфиг и данные не
затрагивались, localStorage-ключ становится бесхозным (безвреден).
18. Принятые предположения и вопросы к ревью ТЗ
- Q1 — значения галочек по умолчанию. Владелец задал только «размеры — включена». Принято: названия комнат — включена (архитектурный план без названий комнат нечитаем), мебель и другой декор — выключена («чистый план»), подложка — включена, если есть (формулировка владельца «подложка — да, если она включена на этом пространстве»).
- Q2 — кому видна кнопка. Принято: тем же, кому видны шестерёнка и «?»
(
_canEdit), поскольку владелец поместил её между ними; не-админы экспорт не получают. Альтернатива — всем, кто видит карточку. - Q3 — формат листа и масштаб. Принято автоматическое решение §7.5 (A4, автоориентация, стандартный ряд масштабов); выбор в диалоге — не-скоуп.
- Шрифт Roboto Regular как единственный (без жирного): заголовок — тот же шрифт большим кеглем.
- Изометрическая проекция на печать не влияет: всегда плоский план.