44 KiB
ТЗ #89, этап 1 — объёмный вид за флагом Labs
- Issue: https://github.com/Matysh/houseplan-card/issues/89
- Приоритет: P1, feature
- Статус ТЗ: готово к реализации (ревизия 2, ревью 2026-08-11). До ADR (§13.1) задача ведётся как spike/implementation-prep: renderer не считается выбранным, пока решения ADR не записаны.
- Исследование и продуктовое обоснование:
089-isometric-view.md(контекст; нормативен настоящий файл) - Ревью ревизии 1:
SPEC-REVIEW-089-isometric-view-stage1.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() (houseplan-card.ts:3995) не знает про поднятые верхние грани и начнёт их обрезать |
§4.4 — plan/scene/client, pivot-константа, projectedFrame |
| B2 | Нет алгоритма переноса viewport между проекциями. _view и _viewModeSnap (:982) хранят прямоугольник и cx/cy в координатах текущей сцены; требование «не менять фокус» было недоказуемо |
§4.5 — алгоритм из шести шагов |
| B3 | Несовместимость с docs/WARM-REMOUNT.md: warmBoot (:287) переносит _view/_viewModeSnap/mode, а один и тот же ViewRect теперь имеет два разных смысла |
§7.1 — projection в warm-состоянии, усыновление только при совпадении |
| B4 | Правило «все попадания через unproject» технически неверно: SVG сам хит-тестит содержимое трансформированного <g>, HTML-маркер получает клик как обычный DOM-узел; плюс смок про перетаскивание маркера противоречил «редакторы всегда плоские» |
§7.1 — нативный hit-test, смок про drag убран |
| B5 | Кнопка «в киоске рядом с шапкой» невозможна: .hdr.kioskhide { display: none } (styles.ts:987) |
§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 |
1. Цель и границы этапа
Получить работающий объёмный вид на реальных планах, не показывая его никому, кроме тех, кто явно включил флаг.
Входит: механизм 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(), houseplan-card.ts:1290, слушатель hashchange на :1422) и
он реактивен внутри 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 Камера
Один зафиксированный ракурс; константы выбираются на spike и после этого меняются только вместе с пересъёмкой golden. Свободное вращение и tilt в этап 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:11). Не центр viewport и не центр содержимого: иначе появление дальнего объекта или переключение_showFar(houseplan-card.ts:806) сдвинет уже построенные стены без изменения их геометрии. - Высота стены задаётся одной константой, переводится в 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()(:4101) с этим центром. - Сырые
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 исследования, она нормативна. Дополнительно как запреты:
- существующее дерево карточки не оборачивается в 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 и не создаёт записей в сторе.
7.1 Hit-test, непрерывность и киоск (B3, B4, B5)
Попадания. SVG сам хит-тестит содержимое трансформированного <g>, а
HTML-маркер получает клик как обычный DOM-узел. Ручной unproject для них — двойное
преобразование.
- интерактивные потомки SVG и HTML используют нативный hit-test;
- цепочка
client → scene → unprojectFloorPointприменяется только там, где событие сцены действительно должно дать координату плана; - на этапе 1 объёмный вид ничего не создаёт, не редактирует и не перетаскивает,
поэтому
_svgPoint()(:5018) остаётся плоским; - тесты кликают реальные 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 диалог, отдельным
решением.
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).
11.2 Смоки
Контракт «ничего не изменилось» (T1) разбит на проверяемые части внутри одной сборки:
- без параметра и с неизвестным параметром: в DOM нет узлов объёмного вида, нет дополнительных ws/http-запросов и записей в config/layout;
- чтение собственного ключа Labs не считается изменением, но при отсутствии операций в URL storage не перезаписывается;
- нулевой diff существующих плоских golden-сцен между ревизиями гарантирует остальное.
Дальше: кнопка появляется только под флагом и только в обычном View; переключает
и возвращает вид; вход в редактор показывает плоский вид, выход восстанавливает;
флаг переживает смену вида дашборда, off его снимает; iso → remount → тот же
кадр; iso → снять флаг → remount → плоский кадр без вуали; загрузка киоска с
сохранённым iso и возврат в плоский через URL.
11.3 Golden (M7)
Минимальная матрица этапа: desktop dark (смешанные толщины, проёмы, Glow и
солнце, устройства); desktop light (тема, затенение граней, паритет фильтров);
узкий мобильный или киоск (fit, совпадение маркеров и подписей, отсутствие
обрезки); show_borders: false (стены не нарисованы, заливка и Glow на месте).
Ветка активации флага добавляется в 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
делает её мгновенной).
12. Acceptance criteria
- При выключенном флаге карточка идентична текущей: DOM, запросы, сторы, производительность в пределах допуска на шум, все существующие golden.
- Флаг включается из query и из хэша по грамматике §2.2.1, переживает переход
между видами и снимается через
off. - Флаг с достигнутым
expiresне включается никаким способом. - Под флагом в обычном View доступен объёмный вид с фиксированным ракурсом; плоский вид остаётся значением по умолчанию, в том числе при первом запуске.
- Физические стены, перегородки и колонны имеют непрерывные верх и боковые грани; виртуальные границы плоские.
- Проём является разрывом на всю высоту: внутри разрыва нет полосы верха или бока, а две вертикальные грани откосов на границах разрыва — ожидаемая часть объёма.
- Glow, spill, солнце, заливки и hover сохраняют текущую семантику; второго слоя света не появилось.
- Маркеры, подписи и попадания курсора используют одну проекцию и не расходятся с планом; переключение вида не меняет зум и не сдвигает логический центр.
- Редакторы всегда плоские; переключение не пишет в сторы и не создаёт записей в истории; warm-remount не подменяет проекцию.
- Исключение в renderer даёт плоский fallback с защёлкой, кнопка отражает фактический вид, предпочтение не перезаписывается.
- Перф-профиль §8.2 выполнен на полном workflow по точному SHA.
- Формат конфигурации не изменился;
houseplan-space-cardостался плоским.
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/ISOMETRIC.md— внутреннее описание координат, проекции, топологии граней и ограничений;- запись про Labs в
docs/DEVELOPMENT.mdиAGENTS.md: как включать, как заводить флаг, когда он умирает; docs/STATUS.md— строка о скрытом этапе;- issue #89: ссылка на это ТЗ как на нормативное, синхронизация scope и
acceptance criteria, Project остаётся в
Todoдо фактического старта. Issue не закрывается после ADR — только после всех AC этапа (T5).
Не трогаются: README.md, README.ru.md, docs/USER-GUIDE.ru.md,
docs/CHANGELOG.md, docs/CHANGELOG.ru.md.
14. Вне этапа 1
- Кнопка и любые пользовательские упоминания вне флага.
- Пресеты камеры, свободное вращение и tilt.
- Полировка дверей, окон и ворот, мягкие тени, floor edges (этап 2).
- Геометрическая окклюзия маркеров стенами.
- Объёмный вид в
houseplan-space-card. - Редактирование в перспективе, пользовательская высота, 3D-мебель, WebGL.
- YAML-опция карточки для Labs.