60 KiB
ТЗ #89, этап 1 — объёмный вид за флагом Labs
- Issue: https://github.com/Matysh/houseplan-card/issues/89
- Приоритет: P1, feature
- Статус ТЗ: на независимом ревью (ревизия 3, решения владельца 2026-08-13).
Ревизия 2 предшествует действующему процессу и новым визуальным референсам;
эта редакция обязана получить независимое ревью до
S5-ready. - Исследование и продуктовое обоснование:
089-isometric-view.md(контекст; нормативен настоящий файл) - Историческое ревью ревизии 1 не является доступным артефактом репозитория и
не заменяет новое ревью по
PROCESS.md. - Связано: #82 (анимация zoom/fit), #73 (визуальная непрерывность), #85 (тесты должны уметь падать), #50 (перенос конфигурации)
0. Решения
0.1 Зафиксировано изначально
| # | Решение | Почему |
|---|---|---|
| D1 | Этап 1 не показывает пользователю ничего. Ни кнопки, ни строки в настройках, ни записи в changelog — только под явно включённым флагом Labs | Ракурс, читаемость подписей и поведение на больших планах проверяются на реальных данных; до этого любая кнопка — обещание, которое нечем закрыть |
| D2 | Механизм флагов вводится один раз и переиспользуется. Следующие кандидаты — #82, #83, #52 | Иначе каждая сырая фича заводит свой одноразовый способ включения |
| D3 | Renderer — SVG-проекция координат. CSS perspective/preserve-3d и WebGL запрещены |
Карточка уже использует filter: brightness() на .zoomwrap (day/night), mix-blend-mode: screen (Glow) и <filter> с feGaussianBlur: 3D-контекст либо ломается, либо уплощается |
| D4 | Предпочтение вида не хранится в конфигурации плана — только в localStorage |
Запись в стор создаёт revision, конфликты совместного редактирования и попадает в экспорт #50 |
| D5 | Кэш геометрии — по контентному отпечатку, не по _cfgEpoch |
Эпоха отстаёт от правок in-place; на этом уже ломался кэш барьеров света |
| D6 | Spike входит в этап 1 и заканчивается ADR до имплементации renderer | Отдельная поставка ради двух прототипов не нужна, но выбор должен быть письменным |
| D7 | Документация этапа 1 — внутренняя | Пользовательской функциональности не появилось |
0.2 Что изменено ревью 2026-08-11
| # | Находка | Куда внесено |
|---|---|---|
| B1 | Проекция точек описана, а три реальные системы координат — нет. IsoCamera без pivot и zScale: проекция вокруг (0,0) сдвинет план, а _baseVb() не знает про поднятые верхние грани и начнёт их обрезать |
§4.4 — plan/scene/client, pivot-константа, projectedFrame |
| B2 | Нет алгоритма переноса viewport между проекциями. _view и _viewModeSnap хранят прямоугольник и cx/cy в координатах текущей сцены; требование «не менять фокус» было недоказуемо |
§4.5 — алгоритм из шести шагов |
| B3 | Несовместимость с docs/WARM-REMOUNT.md: warmBoot переносит _view/_viewModeSnap/mode, а один и тот же ViewRect теперь имеет два разных смысла |
§7.1 — projection в warm-состоянии, усыновление только при совпадении |
| B4 | Правило «все попадания через unproject» технически неверно: SVG сам хит-тестит содержимое трансформированного <g>, HTML-маркер получает клик как обычный DOM-узел; плюс смок про перетаскивание маркера противоречил «редакторы всегда плоские» |
§7.1 — нативный hit-test, смок про drag убран |
| B5 | Кнопка «в киоске рядом с шапкой» невозможна: .hdr.kioskhide { display: none } в src/styles.ts |
§3, §7.1 — кнопка только в обычном View, аварийный выход из киоска через URL |
| B6 | Грамматика Labs противоречива: неясен приоритет query и хэша, смысл off,iso, повторного параметра, и §2.2 против §2.3 про очистку URL |
§2.2.1 — точный контракт операций |
| B7 | Не определена топология граней. wallBodiesGeometry().geom — MultiPolygon с внешними и внутренними rings после union и вырезов; «граничных рёбер» недостаточно. Формулировка «без торцов в проёме» запрещала бы корректные jamb faces |
§5.1, §12 AC 6 |
| B8 | Fallback «вернуться в flat на этом кадре» не отвечает, что будет на следующем рендере и что показывает кнопка | §9 — state machine с latch |
| M1 | ADR требовался, но список обязательных решений не задан | §13.1 |
| M2 | Отпечаток перечислял не все входы геометрии | §8.1 |
| M3 | since/expires без версионной семантики, а зависимости semver в проекте нет |
§2.4 |
| M4 | Не указан владелец механизма в рантайме и форма window.__hpLabs |
§2.5 |
| M5 | Не сказано про вторую карточку — src/space-card.ts |
§10.3 |
| M6 | Перф-контракт не совпадает с существующей инфраструктурой: demo/performance/ использует профильные бюджеты (budgets-*.json) и fail-closed сверку окружения |
§8.2 |
| M7 | Двух golden-картинок мало для новой системы координат | §11.3 |
| M8 | Контракт доступности кнопки неполон | §11.5 |
| T1 | «Сравнить innerHTML до и после ветки» невоспроизводимо обычным тестом | §11.1 |
| T2 | Мутанты сформулированы так, что часть ловится только текстовым поиском | §11.4 |
| T3 | Формулировка AC про проём конфликтовала с B7 | §12 AC 6 |
| T4 | Не определено поведение при первом запуске и при истёкшем флаге с сохранённым iso |
§10.2 |
| T5 | Issue и файл ТЗ расходились в статусе | §13.2 |
0.3 Решения владельца 2026-08-13
| # | Решение | Следствие для этапа 1 |
|---|---|---|
| O1 | Детерминированный 2.5D допускается как узкое исключение из прежнего абсолютного запрета в docs/SCOPE.md |
Это альтернативное представление J1/J2/J3, не отдельная модель, interior editor или свободная 3D-камера |
| O2 | Новые изображения задают обязательное направление Stage 2 | Stage 1 сразу использует почти верхнюю ортографическую камеру без диагонального yaw, но не обещает материал/фотореализм референса |
| O3 | Labs — общий переиспользуемый механизм | Одноразовый флаг #89 запрещён; пользовательской настройки Labs нет |
| O4 | #89 заканчивается полностью реализованным Stage 1 | Stage 2/public rollout получает отдельную feature issue только после зелёного Stage 1 |
| O5 | Вертикальные створки и светлые оконные элементы отложены | Двери, окна и ворота сохраняют текущие символы на плоскости пола; стеновые разрывы полноценны |
| O6 | Все текущие live-эффекты пола сохраняются | Glow/spill, солнце, room fills/hover, decor/backdrop и vacuum не заменяются новым оконным светом |
1. Цель и границы этапа
Получить работающий объёмный вид на реальных планах, не показывая его никому, кроме тех, кто явно включил флаг.
Job и персона. Этап служит J1 «показать весь дом и происходящее сейчас»; на целевой поверхности View домашний администратор оценивает скрытый прототип, а household member/guest в обычной установке продолжает видеть неизменный плоский план. Touch View и kiosk остаются обязательными поверхностями даже для скрытого режима: включивший Labs администратор не должен получить сломанный ежедневный план на планшете.
До → после, без терминов реализации. До этапа явно включённый экспериментальный режим отсутствует; после этапа тестировщик может одним переключателем увидеть тот же живой дом с низкими объёмными стенами и вернуться к прежнему плоскому виду без потери состояния, фокуса и действий.
Входит: механизм Labs (§2); системы координат и проекция (§4); объём стен, перегородок, колонн и разрывы в проёмах (§5); поведение слоёв (§6); взаимодействие, непрерывность и киоск (§7); кэш и производительность (§8); fallback (§9); данные (§10); тесты (§11).
Не входит: §14.
2. Labs — механизм скрытых фич
2.1 Реестр
Единственный источник истины — src/labs.ts:
export interface LabsFlag {
id: string; // 'iso'
issue: number; // 89
since: string; // версия, в которой флаг появился
expires: string; // версия, начиная с которой флаг МЁРТВ
summary: string; // одна строка для консоли, только EN, не i18n
}
export const LABS_FLAGS: readonly LabsFlag[] = [ /* … */ ];
Запись без issue, since, expires или с since >= expires — ошибка сборки
(тест §11.1). summary намеренно не проходит через i18n: это диагностика.
2.2 Источники активации
Читаются и query-строка, и хэш. Хэш — потому что карточка уже использует его в
_hashSpace() и реактивно слушает hashchange внутри Lovelace; query — потому
что его удобно давать тестировщику.
?hp-labs=iso #hp-labs=iso включить
?hp-labs=iso,foo несколько значений
?hp-labs=-iso выключить один
?hp-labs=off очистить набор
2.2.1 Приоритет и семантика операций (B6)
- База — валидный набор из storage.
- К базе применяются операции query слева направо, затем hash слева направо. Хэш сильнее: он реактивен и переживает переходы Lovelace.
idдобавляет,-idудаляет,offочищает набор в этой позиции; следующие токены снова могут добавлять. Поэтомуoff,isoдаёт{iso}, аiso,-iso— пустой набор.- Повторяющиеся параметры
hp-labsобрабатываются в порядке появления. - Неизвестный идентификатор игнорируется молча и сам по себе не переписывает
storage. Storage обновляется, только если в URL была хотя бы одна известная
операция или
off. - Механизм никогда не переписывает URL.
offочищает эффективный набор и storage, но остаётся видимым в адресной строке: пользователь должен видеть, почему карточка выглядит так. - Хэш разбирается общим helper вместе с
space:#space=x&hp-labs=isoи обратный порядок работают одинаково, percent-encoding поддержан. Второй regex-парсер в_hashSpace()при этом удаляется — источник разбора один. hashchangeприменяется без перезагрузки;popstateперечитывает query и хэш, если URL действительно изменился без reload.
2.3 Персистентность
localStorage['houseplan_card_labs_v1'] (соседи: houseplan_card_layout_v1,
houseplan_card_cfg_v1). Недоступное или сломанное хранилище (приватный режим,
квота) — не ошибка: набор живёт до конца жизни страницы.
2.4 Версии и срок жизни (M3)
Зависимости semver в проекте нет, строковое сравнение недопустимо.
Нормативно: собственный parser major.minor.patch[-prerelease].
- Сравнивается числовое ядро
major.minor.patch, prerelease-суффикс игнорируется. Следствие:1.65.0-beta.1уже достигаетexpires: 1.65.0, и мёртвый флаг не уезжает в новый релизный цикл. - Флаг с достигнутым
expiresне включается ничем: ни URL, ни storage. - Некорректная запись реестра или неразбираемая версия карточки — fail closed: флаг считается выключенным.
- Тесты:
1.64.9,1.65.0-beta.1,1.65.0, malformed,since >= expires.
2.5 Владелец в рантайме и диагностика (M4)
- Доступность флагов — состояние загруженного JS-модуля, одно на страницу:
один resolver, одна подписка на
hashchange/popstate, а не по слушателю на каждый рендер карточки. - Эффективный вид (
flat|iso) — состояние конкретной карточки и пространства. window.__hpLabs— замороженный отсортированный массив (Object.freeze(['iso'])). При изменении URL свойство заменяется новым замороженным массивом; внутреннийSetнаружу не отдаётся. За это свойство цепляются смоки.- При непустом наборе — одна строка в консоль при первом рендере:
HOUSEPLAN LABS: iso (#89, expires 1.65.0). При пустом — тишина. - Ничего в UI: ни бейджа, ни тоста, ни строки в «Общих настройках».
2.6 Границы механизма
Флаг Labs — только presentation. Запрещено гейтить им миграции данных, схему конфигурации, записи в сторы и любые сетевые вызовы. Бэкенд о флагах не знает.
2.7 Чего механизм не делает на этапе 1
YAML-опция карточки (labs: [iso]) не вводится: конфигурация уезжает в
скриншоты, в поддержку и в экспорт #50. Добавляется позже одной строкой в том же
парсере.
3. Что видно под флагом iso
- Кнопка
mdi:cube-outlineс accessible name «Объёмный вид» / «Volumetric view» только в обычном режиме просмотра. В киоске её нет и быть не может:.hdr.kioskhide { display: none }(B5). - Строки i18n добавляются в оба словаря сразу, но существуют только в этой ветке разметки.
- Больше ничего: ни в «Общих настройках», ни в настройках пространства, ни в диалогах устройства.
4. Координаты и проекция
4.1 Чистый модуль
src/iso-projection.ts — без Lit, без DOM, без hass.
4.2 Камера
Один зафиксированный почти верхний ортографический ракурс, следующий новым референсам владельца: исходные горизонтали и вертикали плана остаются параллельны экранным осям, диагонального yaw/поворота плана нет, перспектива и схождение линий отсутствуют, высота стен заметна, но не закрывает комнаты.
Нормативная семья проекции для spike: rotDeg = 0; наклон от вида строго
сверху находится в диапазоне 18–22°; логическая высота и zScale должны
давать низкую видимую боковую грань. Точное значение внутри диапазона выбирает
ADR по golden/performance, после чего оно меняется только вместе с пересъёмкой
новых iso-эталонов. Свободное вращение, пользовательский tilt и пресеты камеры
в Stage 1 не входят.
4.3 Одна проекция на всё
SVG-геометрия и HTML-оверлеи (маркеры, подписи комнат, карточки комнат) проецируются одной и той же функцией. Отдельная «примерно такая же» формула в CSS для оверлеев запрещена: расхождение маркера и плана — главный риск исследования.
4.4 Системы координат, pivot и projected frame (B1)
type PlanPoint = readonly [number, number]; // модель: room, wall, marker
type ScenePoint = readonly [number, number]; // координаты SVG viewBox (_view)
interface IsoCamera {
rotDeg: number;
tiltDeg: number;
xyScale: number;
zScale: number;
origin: PlanPoint; // pivot
}
projectPlanPoint(p: PlanPoint, zUnits: number, cam: IsoCamera): ScenePoint;
unprojectFloorPoint(p: ScenePoint, cam: IsoCamera): PlanPoint; // только z = 0
clientToScenePoint(client: readonly [number, number],
stageRect: DOMRectReadOnly, view: ViewRect): ScenePoint;
projectedFrame(input: IsoFrameInput, cam: IsoCamera): ViewRect;
Нормативно:
projectPlanPointвозвращает scene, а не client/screen координаты. Client получается существующим путём через_viewи.stage.unprojectFloorPointинвертирует только плоскостьz = 0: точке на вертикальной грани не соответствует единственная точка плана.- Pivot — фиксированная константа плана, рекомендуется
[NORM_W / 2, NORM_W / 2](NORM_W = 1000вspace-geometry.ts). Не центр viewport и не центр содержимого: иначе появление дальнего объекта или переключение_showFarсдвинет уже построенные стены без изменения их геометрии. - Высота стены задаётся одной константой, переводится в plan units и только
потом умножается на
zScale. Значения фиксируются ADR. fit, ограничение pan, «стрелка домой», подсказка о дальних объектах и начальный вид используютprojectedFrame, который включает и пол, и поднятые верхние грани._baseVb()в объёмном виде не применяется.projectedFrameне зависит от текущего zoom/pan и входит в кэш геометрии (§8.1).- Round-trip
unprojectFloorPoint(projectPlanPoint(p, 0)) ≈ pс точностью 1e-9 на всём диапазоне холста (±5000,docs/CANVAS.md).
4.5 Переключение viewport между проекциями (B2)
_view и _viewModeSnap хранят прямоугольник в координатах текущей сцены;
переносить их между проекциями напрямую нельзя. Алгоритм смены:
- Получить логический центр пола: в flat центр
_viewуже является точкой плана; в iso — пропустить центр_viewчерезunprojectFloorPoint. - Построить целевой
projectedFrameи целевой fit. - Сохранить тот же скалярный zoom.
- Спроецировать логический центр в целевую сцену и вызвать
_applyView()с этим центром. - Сырые
x/y/w/hмежду видами не переиспользуются никогда. - Вход в редактор выполняет тот же переход iso → flat, выход — обратный к предыдущему виду. Смена пространства внутри редактора сбрасывает старый снимок по существующему правилу.
Предпочтение вида и viewport — разные сущности: в localStorage пишется только
предпочтение (§10.2) и существующий скалярный zoom, сырой viewport остаётся
runtime/warm-состоянием.
5. Объём
- Источник — канонические тела стен (
wallBodiesGeometry), те же, что питают модель света. Второй геометрии не заводится. - Число боковых граней O(рёбер); копирование контура слоями (как в демо) запрещено.
- Перегородки и колонны экструдируются из своих текущих геометрий; высота у всех одна, в сантиметрах пользователю не показывается.
- Виртуальные границы остаются линиями на полу и высоты не получают.
show_borders: falseне меняет семантику: невидимые стены остаются невидимыми, но продолжают участвовать в площади и в модели света; кэш при этом не инвалидируется (§8.1).
5.1 Топология граней (B7)
- Грани строятся из rings канонического MultiPolygon после union, стыков и вырезов проёмов. Исходные рёбра комнат для экструзии не используются.
- Winding нормализуется один раз; внешняя нормаль учитывает, внешнее это кольцо или дырка.
- Видимость грани определяется знаком скалярного произведения нормали и фиксированного направления взгляда.
- Для фиксированной камеры задаётся детерминированный порядок отрисовки.
- Проём — разрыв на всю высоту. Две вертикальные грани откосов по краям разрыва (jamb faces) — ожидаемая часть объёма, а не дефект.
- Запрещены: полоса верха или бока, пересекающая сам разрыв, и «крышка» на полу тоннеля проёма.
- Дверь, окно и ворота на этапе 1 — осознанно разрывы на всю высоту: модель не хранит высоту подоконника.
- Проём никогда не вырезает совпадающую перегородку или колонну — сохраняется текущий порядок объединения дополнительных тел после вырезов.
- Верхняя поверхность рисуется целиком с
fill-rule: evenodd. - Юнит-фикстуры: внешнее кольцо, дырка, multipolygon, T- и X-стык, проём у угла, совпадающее независимое тело.
6. Слои
Поведение слоёв — таблица §6 исследования, она нормативна. Дополнительно как обязательный контракт Stage 1:
- floor plane сохраняет все нынешние live-слои и их порядок: room fills/hover, Glow/spill, солнце, backdrop/decor, furniture и vacuum path/outline; состояния HA продолжают обновлять их без перестройки граней;
- дверь, окно и ворота сохраняют нынешний floor-plane symbol и live state; вертикальные створки, светлые оконные вставки и новый оконный свет не появляются;
- room/device actions, badges, live text и screen-facing HTML остаются теми же
пользовательскими поверхностями; объёмный renderer не создаёт новых service
paths и не ослабляет lock invariant из
docs/SCOPE.md; - слои пола проецируются одной матрицей с нижними точками стен; HTML overlay использует те же projected anchors, но сам остаётся screen-facing;
show_borders: falseне рисует top/side faces, но не выключает room fill, Glow/spill, sunlight или их барьерную семантику;
Запреты:
- существующее дерево карточки не оборачивается в CSS 3D-контекст;
perspective,rotateX/Z,preserve-3dне появляются ни на.zoomwrap, ни на её предках; - day/night
filter: brightness()и Glowmix-blend-mode: screenне трогаются; - слой света остаётся одним регионом на источник с одним размытием на весь слой
(
docs/LIGHT.md); второго слоя света объёмный вид не добавляет — это прямо запрещено ассертомtest/golden-matrix.test.mjs.
7. Взаимодействие
- Zoom/pan работают как сейчас и не пересчитывают модель (§8); якорь зума считается в scene-координатах.
- Редакторы всегда плоские. Вход показывает плоский вид, выход восстанавливает предыдущий; переключение не пишет в command stack и не создаёт записей в сторе.
- Touch editor: не exposed. Объёмной геометрии внутри Plan/Devices/Backdrop нет; редакторы используют прежний desktop-first flat contract.
- Touch View и kiosk: полностью поддерживаются. Pan/pinch, space switching, room/device dialogs, безопасные действия, orientation/background/remount и kiosk gestures проходят тот же safety floor, что flat View. Переключение проекции атомарно и само не считается жестом #82.
7.1 Hit-test, непрерывность и киоск (B3, B4, B5)
Попадания. SVG сам хит-тестит содержимое трансформированного <g>, а
HTML-маркер получает клик как обычный DOM-узел. Ручной unproject для них — двойное
преобразование.
- интерактивные потомки SVG и HTML используют нативный hit-test;
- цепочка
client → scene → unprojectFloorPointприменяется только там, где событие сцены действительно должно дать координату плана; - на этапе 1 объёмный вид ничего не создаёт, не редактирует и не перетаскивает,
поэтому
_svgPoint()остаётся плоским; - тесты кликают реальные DOM-узлы комнат, устройств и проёмов и проверяют
действие; отдельный юнит проверяет цепочку
client → scene → floor.
Warm-remount (#73). Один и тот же ViewRect теперь имеет два смысла,
поэтому:
- warm-состояние хранит
projection: 'flat' | 'iso'и логический центр; - сырой
_viewусыновляется только при совпадении пространства, проекции и активного контракта Labs; - при несовпадении восстанавливаются скалярный zoom и логический центр по алгоритму §4.5, а не чужой прямоугольник;
- отпечаток кадра включает эффективную проекцию и отпечаток геометрии объёма;
- снятие или истечение флага никогда не воскрешает iso-DOM из memo;
- смоки: iso → remount → тот же кадр; iso → снять флаг → remount → корректный плоский кадр без вуали и вспышки.
Киоск. Кнопки в киоске нет (§3). Киоск читает последнее предпочтение этого
браузера для пространства. Аварийный путь — ?hp-labs=-iso или off: киоск
немедленно становится плоским и не может восстановить iso из warm-памяти.
Новых панелей и диалогов этап 1 в киоске не добавляет. Если переключатель в
киоске понадобится, его место — существующий long-press диалог, отдельным
решением.
Touch/Companion. На coarse pointer toggle в обычном View остаётся минимум
44×44 CSS px; после переключения pinch midpoint и tap/long-press попадают в
видимый объект, а не в его flat-позицию. Orientation change, background →
foreground и Companion warm-remount используют §4.5/§7.1 и не показывают
полуплоский смешанный кадр. Эти случаи release-blocking по
docs/TOUCH-SUPPORT.md.
8. Производительность и кэш
8.1 Отпечаток (M2)
Геометрия объёма пересчитывается только при изменении контентного отпечатка,
который включает: комнаты и их контуры; стены и толщины; room_drafts с
толщинами сегментов; нормализованные openCuts и виртуальные интервалы;
канонические вырезы проёмов; перегородки; колонны с формой, углом и диаметром;
cell_cm, шаг сетки и масштаб координат; константы камеры и высоты стены;
версию алгоритма проекции и построения граней.
Массивы сериализуются детерминированно, числа нормализуются как в существующих
отпечатках геометрии. Ключ вида ${space.id}|${fingerprint}; _cfgEpoch в
ключе запрещён (D5).
Состояние отображения (hover, состояния HA, show_borders) кэш не
инвалидирует.
Pan/zoom меняют только transform/viewBox и ничего не пересчитывают.
8.2 Перф-контракт (M6)
Существующая инфраструктура (demo/performance/) использует профильные бюджеты
(budgets-*.json), допуск на шум и fail-closed сверку окружения. Поэтому:
- заводится отдельный профиль
large-house-isometric-v1: тот же harness запускает candidate-бандл сhp-labs=isoи переключением вида, а базовый бандл игнорирует неизвестный флаг и остаётся плоским; - идентификатор профиля в обоих отчётах одинаков, runtime/browser/fingerprint проверяются существующим контрактом;
- отдельный reviewed-бюджет задаёт 20 % относительного допуска плюс абсолютный допуск на шум;
- обязательные метрики: первый устойчивый кадр iso, переключение вида, pan/zoom, обновление состояния HA, смена пространства, количество/сумма/максимум long-task, рост heap, размер и рост кэша геометрии, число отрисованных устройств;
- завершение этапа подтверждается полным performance-workflow на точном SHA, а не локальным сравнением с другой машиной.
Требование к плоскому виду формулируется проверяемо: при выключенном флаге геометрия, кэш и DOM объёмного вида отсутствуют, дополнительного прохода по коллекциям комнат и устройств нет, а тайминги укладываются в допуск на шум.
9. Fallback (B8)
Состояние:
desiredView— сохранённое предпочтение;effectiveView— то, что реально нарисовано.
Правила:
- исключение в чистой геометрии или в шаблоне сцены ловится на границе
renderIsoScene(); для пары(space, geometryFingerprint)ставится защёлка на сессию; - при защёлке
effectiveView = 'flat', и геометрия объёма больше не вызывается на каждом обновлении состояния HA; - конфигурация, layout и сохранённое предпочтение не меняются автоматически;
- кнопка отражает
effectiveView(aria-pressed="false"); явное повторное нажатие или новый отпечаток геометрии снимает защёлку и даёт одну повторную попытку; - в консоль — одна строка на защёлку: issue, пространство, отпечаток и короткая причина, без конфигурации и entity id;
- ошибка проекции HTML-оверлеев входит в ту же границу, иначе получится «стены плоские, маркеры объёмные».
10. Данные и совместимость
10.1 Формат
Формат конфигурации не меняется: ни одного нового поля в spaces, markers,
settings. Записей в config/layout сторы объёмный вид не делает. Экспорт и
импорт #50 не затрагиваются.
10.2 Предпочтение вида (T4)
- Ключ
houseplan_card_view_v1, значение — по пространству,flat | iso. - Без записи для пространства эффективный вид всегда плоский, даже при активном флаге.
- Переключение в обычном View пишет предпочтение для текущего пространства.
- При неактивном или истёкшем флаге сохранённое
isoигнорируется: DOM не меняется и в warm-память не попадает. - Вход и выход из редактора предпочтение не перезаписывают.
- Fallback (§9) предпочтение не перезаписывает.
10.3 Вторая карточка (M5)
houseplan-space-card (src/space-card.ts, src/space-render.ts) остаётся
плоской: флаг iso на неё не влияет. Её поддержка — отдельный будущий scope;
иначе этап 1 сразу требует второй композиции сцены.
11. Тесты
11.1 Юниты
iso-projection: round-trip §4.4.7, детерминированность, вырожденная камера,projectedFrameвключает поднятые грани и не зависит от zoom/pan.- Грани: число = числу видимых граничных рёбер; разрыв проёма даёт две грани откосов; полоса через разрыв и крышка на полу тоннеля отсутствуют; фикстуры §5.1.
- Labs: грамматика §2.2.1 целиком (
off,iso,iso,-iso, повторный параметр, приоритет хэша над query, неизвестное значение не переписывает storage, недоступныйlocalStorage), общий разбор#space=…&hp-labs=…в обоих порядках, версии §2.4, валидность реестра. - Отсутствие вызова построителя геометрии объёма при выключенном флаге — проверяется шпионом (T1).
- Camera contract:
rotDeg = 0, диапазон tilt 18–22°, отсутствие perspective term и сохранение параллельности plan axes; поднятая высота не меняет floor round-trip. - Layer contract: state-only HA updates не меняют geometry fingerprint/cache;
show_borders: falseубирает только faces, а не floor layers; door/window/gate Stage 1 geometry не содержит вертикальных leaf/window panels.
11.2 Смоки
Контракт «ничего не изменилось» (T1) разбит на проверяемые части внутри одной сборки:
- без параметра и с неизвестным параметром: в DOM нет узлов объёмного вида, нет дополнительных ws/http-запросов и записей в config/layout;
- чтение собственного ключа Labs не считается изменением, но при отсутствии операций в URL storage не перезаписывается;
- нулевой diff существующих плоских golden-сцен между ревизиями гарантирует остальное.
Дальше: кнопка появляется только под флагом и только в обычном View; переключает
и возвращает вид; вход в редактор показывает плоский вид, выход восстанавливает;
флаг переживает смену вида дашборда, off его снимает; iso → remount → тот же
кадр; iso → снять флаг → remount → плоский кадр без вуали; загрузка киоска с
сохранённым iso и возврат в плоский через URL.
Отдельный smoke live-layer parity последовательно меняет room fill/hover, несколько Glow/spill sources, солнце, backdrop/decor, furniture и vacuum state; между flat и iso совпадают resolved source count/state, service-action outcome и логические floor anchors, а geometry build counter не растёт от HA-only update. Проверяются текущие opening symbols/states без vertical leaf/window DOM.
Touch/Companion smoke: toggle имеет ≥44×44 CSS px; pinch и space switching в
iso; tap/long-press по реальному DOM room/device/opening; orientation resize;
background/foreground и warm-remount; kiosk с сохранённым iso и аварийным
off. В каждом случае нет phantom click, unintended edit/service call,
смешанного flat/iso frame или permanently stuck View.
11.3 Golden (M7)
Минимальная матрица этапа: desktop dark (смешанные толщины, проёмы, Glow и
солнце, устройства); desktop light (тема, затенение граней, паритет фильтров);
узкий мобильный или киоск (fit, совпадение маркеров и подписей, отсутствие
обрезки); show_borders: false (стены не нарисованы, заливка и Glow на месте).
Каждая iso-сцена показывает rotDeg = 0: стены, параллельные plan X/Y, не
получают диагонального поворота. Референсы владельца не являются baseline
Stage 1 и не принимаются как обещание фотореалистичных материалов.
Ветка активации флага добавляется в demo/golden/harness.mjs,
GOLDEN_MATRIX_VERSION поднимается. Существующие плоские сцены не принимаются
заново: ненулевой diff — регресс. Новые эталоны принимаются только из полного
Linux-артефакта CI по действующему контракту HP-QA-01.
11.4 Мутанты (T2, контракт #85)
Каждый мутант исполним, и для каждого сохраняются patch/команда и имя краснеющего теста:
- объёмный вид включается без флага → падает смок §11.2;
- в отображение HTML-оверлеев вносится контролируемый сдвиг → смок видит расхождение якоря больше 1 CSS px;
- кэш геометрии ключуется по
_cfgEpoch: тест правит геометрию in-place, грани обязаны обновиться; expiresигнорируется → падает юнит версий;- боковые грани строятся слоями → падает проверка верхней границы числа граней
как
O(E).
11.5 Доступность (M8)
aria-pressed по effectiveView; стабильное имя «Объёмный вид» /
«Volumetric view»; фокус остаётся на той же кнопке после переключения; активное
состояние не кодируется только цветом; порядок обхода устройств и действий
комнаты совпадает с плоским видом; анимации переключения этап 1 не добавляет —
свап атомарный (если анимация появится через #82, prefers-reduced-motion
делает её мгновенной).
11.6 Матрица доказательств AC
| AC | Доказательство до code review | Обязательная проверка ревьюера |
|---|---|---|
| AC1 | unit spy + flat smoke + существующие golden + performance profile | убедиться мутантом, что включение iso без флага краснит smoke |
| AC2–AC3 | unit Labs grammar/version + URL smoke | запустить команды и проверить fail-closed malformed/expired |
| AC4 | unit camera + desktop/touch smoke + iso golden | проверить чтением отсутствие perspective/free camera и исполнением fixed view |
| AC5–AC6 | unit face topology + iso golden | применить один geometry mutant из §11.4 |
| AC7 | unit fingerprint/layer contract + live-layer smoke + golden | выполнить HA-only update и проверить cache/source/action parity |
| AC8 | projection unit + DOM interaction/touch smoke | внести контролируемый anchor shift и получить красный smoke |
| AC9 | editor/warm/touch smoke | проверить View → editor → View и Companion remount исполнением |
| AC10 | fallback unit + smoke | инъекция renderer exception, затем повторный HA update |
| AC11 | reviewed performance profile на exact SHA | проверить report provenance/environment fail-closed |
| AC12 | schema/config source-contract + smoke второй карточки | чтение diff и исполнение flat houseplan-space-card |
| AC13 | source-contract + opening/live-layer smoke | проверить отсутствие vertical leaf/window и нового window-light layer |
| AC14 | touch/Companion smoke + mobile/kiosk golden | исполнить coarse-pointer safety matrix |
| AC15 | review документации и provenance | проверить issue ↔ spec ↔ SCOPE и отсутствие user changelog |
12. Acceptance criteria
- AC1 (
unit+smoke+golden+performance): при выключенном флаге карточка сохраняет прежние DOM, запросы, сторы, flat golden и бюджет; geometry/cache объёмного вида не создаются. - AC2 (
unit+smoke): флаг включается query/hash по §2.2.1, реактивно переживает навигацию и снимается через-iso/offбез переписывания URL. - AC3 (
unit): malformed registry/version и достигнутыйexpiresfail-closed; ни URL, ни storage не включают мёртвый флаг. - AC4 (
unit+smoke+golden): Labs View использует один почти верхний ортографический ракурс:rotDeg = 0, tilt 18–22°, без перспективы; план не повёрнут диагонально, комнаты не перекрыты стенами, flat default. - AC5 (
unit+golden): physical walls, partitions и columns имеют непрерывные top/visible side faces O(E); virtual boundaries остаются flat. - AC6 (
unit+golden): door/window/gate opening — full-height gap без top/side strip внутри; jamb faces присутствуют, независимые extra bodies не вырезаются. - AC7 (
unit+smoke+golden): room fills/hover, Glow/spill, солнце, backdrop/decor, furniture и vacuum сохраняют state, source count, порядок, actions и barrier semantics между flat/iso; HA-only update не rebuild-ит geometry; второго light layer нет. - AC8 (
unit+smoke): SVG floor, markers, room labels/cards и native hit targets используют один projection snapshot; anchor error ≤1 CSS px; toggle сохраняет scalar zoom и logical floor center. - AC9 (
smoke): все редакторы flat; View → editor → View, space change и warm-remount не смешивают projections, не меняют config/history/view intent. - AC10 (
unit+smoke): renderer/projection exception даёт latched flat fallback на fingerprint, отражается кнопкой, не стирает preference и не повторяется на каждом HA update. - AC11 (
performance): профиль §8.2 зелёный на exact SHA; pan/zoom и HA update не recompute geometry, flat path остаётся в noise budget. - AC12 (
unit+smoke+ code review): plan schema/backend/import-export не меняются;houseplan-space-cardостаётся flat; Labs не создаёт network calls и не гейтит migrations. - AC13 (
unit+smoke+ code review): Stage 1 сохраняет нынешние floor-plane opening symbols и states; vertical leaf/window panels, floor edges, new window light и reference-grade materials отсутствуют. - AC14 (
smoke+golden): desktop View, touch View и kiosk полностью работоспособны; toggle ≥44×44, pinch/tap/long-press/space/orientation/ background/remount безопасны; touch editors остаются flat/not exposed. - AC15 (code review):
SCOPE, issue, spec, internal Labs/ISO docs иSTATUSсогласованы; en+ru i18n ключи toggle перечислены; README, User Guide и оба changelog не обещают скрытую функцию.
13. Артефакты этапа
13.1 ADR (M1)
До основной реализации ADR закрывает: формулу проекции и pivot; константы
камеры, единицы высоты стены и zScale; заливку, обводку и затенение верха и
боков в светлой и тёмной теме; нормализацию колец, видимость граней и порядок
отрисовки; z-порядок «пол → Glow/солнце/декор → грани и верх → экранные
HTML-оверлеи»; осознанное правило этапа 1 «маркеры и карточки комнат всегда выше
граней и не получают геометрической окклюзии»; projectedFrame и конверсию
viewport; результат проверки SVG filter/clip/mix-blend в Chromium, Firefox и
WebKit; причины отказа от проигравшего прототипа.
13.2 Документы
docs/SCOPE.md— узкое owner-approved исключение для детерминированного 2.5D без расширения в photorealistic/free-camera/interior editor;docs/ISOMETRIC.md— внутреннее описание координат, проекции, топологии граней и ограничений;- запись про Labs в
docs/DEVELOPMENT.mdиAGENTS.md: как включать, как заводить флаг, когда он умирает; docs/STATUS.md— строка о скрытом этапе;- issue #89: ссылка на это ТЗ как на нормативное, синхронизация scope и
acceptance criteria, Project остаётся в
Todoдо фактического старта. Issue не закрывается после ADR — только после всех AC этапа (T5). - После зелёного Stage 1 — отдельная feature issue
S1-newдля Stage 2/public rollout; в ней хранятся новые референсы владельца и решения о vertical doors/windows, materials, shadows/floor edges и публичном UX.
Не трогаются: README.md, README.ru.md, docs/USER-GUIDE.ru.md,
docs/CHANGELOG.md, docs/CHANGELOG.ru.md.
13.3 Предполагаемые файлы реализации
Точный список подтверждается ADR, но code review ожидает как минимум:
- новый pure geometry/projection слой:
src/iso-projection.tsиsrc/iso-walls.tsлибо эквивалентные модули без Lit/HA; - reusable Labs resolver
src/labs.ts; - интеграцию только в
src/houseplan-card.ts,src/styles.ts,src/i18n/en.json,src/i18n/ru.json;src/space-card.tsиsrc/space-render.tsразрешены только для negative/source-contract tests, не для iso-rendering; - unit: новые
test/iso-*.test.mjs,test/labs.test.mjsи узкие source-contract/regression additions; - browser: целевые
demo/smoke_isometric_*.mjs, touch/warm/live-layer paths; - golden matrix/harness и новые iso baselines, принимаемые отдельно только по reviewed Linux artifact; существующие flat baselines не обновляются;
- performance profile/budget
large-house-isometric-v1и workflow wiring; docs/ISOMETRIC.md,docs/DEVELOPMENT.md,AGENTS.md,docs/STATUS.md.
Backend Python, manifest/HACS metadata, config schema, import/export и package dependencies не меняются. Новая runtime dependency требует возврата в S3 и отдельного архитектурного решения.
13.4 i18n, compatibility и security
- Новые ключи только для toggle:
view.volumetricиview.flatлибо эквивалентные согласованные имена, одновременно EN/RU. Текст берётся из действующей терминологии: «Объёмный вид» / «Плоский вид» и “Volumetric view” / “Flat view”. - Config/schema migration отсутствует; новые localStorage keys namespaced и fail-soft, удаление keys полностью откатывает пользовательское состояние.
- Labs parser принимает только известные registry ids, не исполняет и не логирует произвольный input; diagnostics не содержат entity ids, URLs, config или персональные данные.
- Новый renderer не создаёт service calls. Все safe actions и единственная sanctioned lock surface остаются прежними.
13.5 Откат
Пользовательский аварийный откат без новой сборки: ?hp-labs=-iso или
?hp-labs=off; flat View немедленно становится effective, сохранённый iso не
воскрешается warm-state. Полный code rollback — revert одного принятого
behavior commit/серии #89 и удаление iso из Labs registry; plan/config/backend
данные не требуют обратной миграции. Если Stage 1 ломает flat path, Labs flag
не считается достаточным смягчением: issue возвращается в S6 и beta блокируется.
14. Вне этапа 1
- Кнопка и любые пользовательские упоминания вне флага.
- Пресеты камеры, свободное вращение и tilt.
- Полировка дверей, окон и ворот, мягкие тени, floor edges (этап 2).
- Геометрическая окклюзия маркеров стенами.
- Объёмный вид в
houseplan-space-card. - Редактирование в перспективе, пользовательская высота, 3D-мебель, WebGL.
- YAML-опция карточки для Labs.
15. Принятые предположения — можно изменить без пересмотра продукта
- Имена pure modules и i18n keys из §13.3–13.4 предварительные; review может выбрать более согласованные имена без изменения поведения.
- Точная wall height,
zScale, palette и side-face contrast выбираются ADR внутри ограничений §4.2 и требуют новых iso golden, но не нового owner decision. - LocalStorage keys версионируются суффиксом
_v1; внутренняя форма может меняться до публичного Stage 2, если чтение malformed/old данных fail-soft. - Stage 2 issue создаётся после зелёного code review/интеграции Stage 1, не обязательно после beta; закрытие самой #89 всё равно следует beta-процессу.