From 8eb4bab7c630571737262f055f893e7eba341f59 Mon Sep 17 00:00:00 2001 From: Matysh Date: Fri, 14 Aug 2026 11:40:26 +0300 Subject: [PATCH] docs: file reviews where reviews live, retire the #89 draft, honest markers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three review documents sat in the repository root, committed before the pipeline existed and before docs/reviews/ did. The directory exists now and the pipeline writes into it, so they move there and the root stops being a second place to look. The #89 spec had a twin: the research draft next to the normative stage1 document, two files for one issue. The draft goes to legacy — it fed the decisions and is worth keeping, but nothing should read it as current. ROADMAP.md carried a live link to the Project v2 board that was dropped yesterday; missed then because the sweep grepped for status-canon wording, not for every link. And docs/README.ru.md said "verified against v1.60.0" as if that were fresh — the line is now an explicit warning naming what to trust instead: USER-GUIDE.ru.md and the changelogs. Issue: #142 User-Visible: no --- docs/README.ru.md | 6 +- docs/ROADMAP.md | 5 +- .../CODE-REVIEW-issue-068-2026-08-12.md | 82 +++ .../CODE-REVIEW-issue-094-2026-08-12.md | 180 +++++++ .../SPEC-REVIEW-089-isometric-view-stage1.md | 503 ++++++++++++++++++ legacy/docs/089-isometric-view-draft.md | 301 +++++++++++ 6 files changed, 1073 insertions(+), 4 deletions(-) create mode 100644 docs/reviews/CODE-REVIEW-issue-068-2026-08-12.md create mode 100644 docs/reviews/CODE-REVIEW-issue-094-2026-08-12.md create mode 100644 docs/reviews/SPEC-REVIEW-089-isometric-view-stage1.md create mode 100644 legacy/docs/089-isometric-view-draft.md diff --git a/docs/README.ru.md b/docs/README.ru.md index f4249758..2762a43f 100644 --- a/docs/README.ru.md +++ b/docs/README.ru.md @@ -1,6 +1,10 @@ # Документация House Plan -Этот каталог — новая точка входа в русскоязычную документацию House Plan. Материалы ниже сверены с исходным кодом и интерфейсом версии **v1.60.0**. +Этот каталог — точка входа в русскоязычную документацию House Plan. + +> **Сверено с версией v1.60.0.** Продукт с тех пор ушёл вперёд (линия 1.64); +> при расхождении этого каталога с `docs/USER-GUIDE.ru.md` и changelog верить им. +> Пересверка каталога — отдельная документационная задача. | Документ | Для кого | Что внутри | |---|---|---| diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index f63e1f78..6232830e 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -5,9 +5,8 @@ not a hardcoded feature. Universal (any house, any language, any plan format), flexible (options instead of assumptions), and measured against the official **Integration Quality Scale** even though custom integrations are not formally graded. Current tasks and priorities live only in -[GitHub Issues](https://github.com/Matysh/houseplan-card/issues) and the linked -[Project v2](https://github.com/users/Matysh/projects/1); this file keeps -historical engineering direction. The original market rationale is archived at +[GitHub Issues](https://github.com/Matysh/houseplan-card/issues) and their labels +(`PROCESS.md` §9); this file keeps historical engineering direction. The original market rationale is archived at `legacy/docs/PRODUCT-2026-07-05.md`. Phases 0–6 of the original plan (server-side config, room markup editor, device diff --git a/docs/reviews/CODE-REVIEW-issue-068-2026-08-12.md b/docs/reviews/CODE-REVIEW-issue-068-2026-08-12.md new file mode 100644 index 00000000..7a08fa39 --- /dev/null +++ b/docs/reviews/CODE-REVIEW-issue-068-2026-08-12.md @@ -0,0 +1,82 @@ +# Code review — issue #68 (contextual help) + +Date: 2026-08-12 + +Branch: `dev` + +Scope: `hp-help`, Houseplan help factory, localization contract, dialog/overlay lifecycle, +keyboard and touch interaction, responsive placement, consumers and regression coverage. + +## Outcome + +The overlay, focus, Escape, outside-click, scroll, Popover/fallback and visual-viewport +paths are internally consistent. The shared dialog overlay registry is used correctly, +the trigger remains reachable in disabled fieldsets through `legend`, and all seven +current call sites have non-empty RU/EN body and ARIA strings. + +Four hardening findings were accepted and fixed locally. No model, saved configuration +or user data contract changed. + +## Findings and resolutions + +### CR68-01 — dead trigger is rendered without help content (P1) + +`hp-help` blocked `_openHelp()` when `text` was empty, but `render()` still returned a +focusable button. The result was the exact reported defect: a visible help glyph that +could not open any explanation. + +Resolution: `hp-help` renders nothing unless both trimmed `text` and `ariaLabel` exist. +The same predicate guards opening and closes an already-open surface if either value is +removed dynamically. + +### CR68-02 — missing translation could be displayed as a key (P1) + +The card factory called `t()` directly. Its intended generic fallback returns the key +name when neither dictionary contains a value, so a future incomplete help pair could +produce a real trigger with implementation text such as `marker.foo.help`. + +Resolution: the factory now checks the localized value and its derived `.aria` value +through `hasTranslation()` before it creates `hp-help`. English fallback remains valid; +a genuinely absent or whitespace-only pair produces no host and no layout gap. + +### CR68-03 — accessible name had a hard-coded English fallback (P1) + +Direct use without `ariaLabel` produced `aria-label="Help"`. This violated the issue +contract requiring a complete localized accessible name and made an incomplete component +look valid to keyboard and screen-reader users. + +Resolution: the fallback was removed. Missing ARIA copy suppresses the affordance just +like missing visible copy. + +### CR68-04 — icon did not follow the product icon system (P2) + +The trigger used a font `?`, whose shape and optical alignment depended on the platform +font and did not visually mean “question in a circle”. + +Resolution: the glyph is now the shared MDI `help-circle-outline` vector inside the same +32/40 px target. It remains decorative because the button already has a full ARIA label. + +### CR68-05 — regression coverage missed incomplete content (P2) + +The smoke covered all open/close and overlay paths but never instantiated an empty or +half-configured component, so CR68-01/03 could pass the release gate. + +Resolution: the #68 smoke now asserts that empty body and empty ARIA copy create no +trigger, and that restoring a complete pair creates the circled-question SVG. + +## Reviewed without changes + +- Mouse hover timing, keyboard focus, touch click and the second-Escape dialog path. +- `aria-describedby` only while open; the visible bubble stays hidden from the + accessibility tree to avoid duplicate announcements. +- Exclusive transient-surface ownership with the colour/opacity picker and toast. +- Popover API path and dialog-owned fallback portal. +- Cached dialog scroll-listener cleanup and disconnect cleanup. +- Visual viewport placement, flipping and edge clamping. +- Existing call sites and RU/EN localization parity. + +## Verification policy + +Per project policy, no tests were run during this local edit. Static type checking, +syntax checking and whitespace validation are recorded in the handoff; the updated +targeted smoke is intended for the next prerelease gate. diff --git a/docs/reviews/CODE-REVIEW-issue-094-2026-08-12.md b/docs/reviews/CODE-REVIEW-issue-094-2026-08-12.md new file mode 100644 index 00000000..7ed3d4df --- /dev/null +++ b/docs/reviews/CODE-REVIEW-issue-094-2026-08-12.md @@ -0,0 +1,180 @@ +# Код-ревью issue #94 — универсальное «Переключить состояние» + +- **Дата:** 2026-08-12 +- **Issue:** https://github.com/Matysh/houseplan-card/issues/94 +- **Проверенная версия:** локальный `dev` после `v1.62.0-beta.3`, включая + незакоммиченные исправления #95–#97 +- **Итог ревью до правок:** changes requested — 2 high, 4 medium, 1 minor +- **Итог после локальных правок:** замечания устранены; проверки отложены до + ближайшего pre-release по принятому правилу владельца + +## 1. Охват + +Проверены: + +1. нормативный алгоритм и acceptance criteria в + `docs/specs/094-universal-state-toggle.md`; +2. pure resolver `src/device-toggle.ts`; +3. target selection через exact binding, device role и `controls`; +4. capability/security/service guards; +5. dialog projection, hint, lossless Save и preview; +6. обычный click, confirmation re-resolve и обработка ошибок; +7. общий cover target для действия и presentation; +8. backend schema и import/export round-trip; +9. unit/smoke-матрица и архитектурная документация; +10. совместимость с визуальной непрерывностью #73 и локальными правками + #95–#97. + +## 2. Найденные и исправленные замечания + +### CR94-01 — High: domain service ошибочно считался capability конкретной entity + +**Было:** `POWER_DOMAINS` разрешал `climate`, `water_heater`, `siren` и +`camera`, если нужный service существовал на уровне domain. Но HA публикует +services для всего domain; неподдерживающая их конкретная entity всё равно +оставалась «исполняемой» в hint, а вызов затем отклонялся Home Assistant. + +Это прямо противоречило §9.1 и mutation gate 6 ТЗ. Home Assistant Core +подтверждает entity-level guards: + +- Climate `TURN_OFF=128`, `TURN_ON=256`: + https://github.com/home-assistant/core/blob/dev/homeassistant/components/climate/const.py +- Water heater `ON_OFF=8`: + https://github.com/home-assistant/core/blob/dev/homeassistant/components/water_heater/__init__.py +- Siren `TURN_ON=1`, `TURN_OFF=2`: + https://github.com/home-assistant/core/blob/dev/homeassistant/components/siren/const.py +- Camera `ON_OFF=1`: + https://github.com/home-assistant/core/blob/dev/homeassistant/components/camera/__init__.py + +**Исправлено:** введён декларативный `POWER_ADAPTERS` с state semantics, +unknown policy и точными feature masks. Feature-gated entity теперь получает +команду только при наличии требуемых bits; service catalog остаётся вторым +guard. Media player и legacy vacuum включены в тот же реестр. + +**Покрытие:** параметрические unit-матрицы для всех базовых power adapters и +для climate, media player, siren, water heater, camera, legacy vacuum — как +разрешённые, так и запрещённые/missing-feature варианты; отдельная матрица +`unknown` проверяет полный/неполный capability mask. + +### CR94-02 — High: click мог использовать target из сохранённого визуального frame + +**Было:** #73 намеренно может некоторое время показывать последний цельный +`_renderDevices` snapshot, но `_clickDevice(ev, d)` разрешал action прямо по +переданному `d`. Если binding/controls изменились до атомарной смены frame, +нажатие без confirmation могло вызвать прежнюю цель. Confirmation уже делал +повторное разрешение, обычный click — нет. + +**Исправлено:** в View действие сначала находит текущий `DevItem` в +`this._devices` по стабильному marker id. Action, binding, controls и command +разрешаются только из него; исчезнувший marker даёт no-op. Локальная +House Plan info-card по-прежнему может использовать видимый snapshot — это +безопасная read-only поверхность и намеренный контракт исправления #96. + +**Покрытие:** smoke сохраняет старый `DevItem`, меняет controls, перестраивает +live devices и проверяет, что click вызывает только новую группу. + +### CR94-03 — Medium: неизвестный persisted action расходил UI и runtime + +**Было:** неизвестный token на light проецировался как default `toggle`, тогда +как `toggleOriginOf()` правильно не признавал его toggle-origin. Селектор мог +показать «Переключить состояние», hint оставался пустым, а click был no-op. + +**Исправлено:** light default применяется только к действительно отсутствующему +token (`null`, `undefined`, пустая legacy-строка). Неизвестное значение fail- +closed проецируется в локальную карточку; backend по-прежнему отклоняет его при +записи. + +### CR94-04 — Medium: legacy cover терял identity после disable в HA + +**Было:** legacy `tap_action: cover` искал cover только в active +`device.entities`, если рядом оставался хотя бы один активный sibling. После +disable cover в HA старое явное намерение превращалось в анонимный no-target и +presentation переставал знать прежнюю cover entity. + +**Исправлено:** legacy-cover origin сначала сохраняет приоритет активной cover, +а при её отсутствии ищет историческую цель в `allEntities`. Общий resolver +возвращает `ha-disabled` и сохраняет тот же cover identity для hint/presentation, +но более ранняя disabled registry row не может заслонить рабочую cover. Новый +device-role toggle по-прежнему исключает disabled rows. + +### CR94-05 — Medium: пустой service catalog считался поддержкой всех services + +**Было:** отсутствие/пустой `hass.services` давало optimistic `true` для любого +service. Это нарушало runtime guard из ТЗ и позволяло построить команду без +доказательства её существования. + +**Исправлено:** отсутствующий catalog/domain/service теперь означает +`unsupported`. После появления актуального HA snapshot resolver автоматически +пересчитывает hint и command. Синтетический HA в `demo/srv/demo.html` теперь +публикует явный service catalog, поэтому smoke-среда проверяет тот же fail-closed +контракт и не создаёт ложные no-op. + +### CR94-06 — Medium: device binding не выбирал первую действительно поддерживаемую entity роли + +**Было:** resolver выбирал первую entity «подходящего domain», а затем мог +остановиться на `unsupported`, хотя следующая равноправная entity той же +functional role имела требуемую capability. Это не соответствовало формулировке +§8.1 «первая поддерживаемая entity». + +**Исправлено:** проверка идёт по уже выбранной shared functional role. +Capability-unsupported peer можно пропустить только внутри неё; missing, +unavailable и secure identity сохраняются без retarget. Config/diagnostic +switch более слабой роли по-прежнему никогда не подставляется. + +### CR94-07 — Minor: статус ТЗ оставался «готово к реализации» + +**Исправлено:** ТЗ, specs index, архитектура, STATUS, TESTING и RU/EN changelog +актуализированы под опубликованную beta.3 и этот локальный hardening pass. + +## 3. Проверенные инварианты без изменений + +- exact `entity:` binding не ищет sibling при unsupported/missing/unavailable; +- raw external controls владеют tap только у explicit toggle и не дают fallback + на собственную entity контроллера; +- passive forced-light marker сохраняет единственное документированное driver- + исключение и дедупликацию; +- partial group вызывает только отображённое доступное подмножество; +- any-on/all-off group semantics соответствует ТЗ; +- lock, alarm и cover classes `garage`/`door`/`gate` остаются secure no-op; +- cover/valve open/close/stop используют одновременно feature bit и service; +- confirmation сравнивает target set, а направление намеренно пересчитывается + по текущему state; +- legacy `cover` и отсутствующий default-light action сохраняются lossless до + явного изменения select; +- backend принимает текущие actions и legacy `cover`, неизвестные tokens + отклоняет; import/export сохраняет action-поля без преобразования; +- отдельного `cover` в текущем UI нет; +- right-click, long press, touch/pinch и confirmation UX этим проходом не + менялись. + +## 4. Изменённые файлы + +- `src/device-toggle.ts` +- `src/houseplan-card.ts` +- `test/device-toggle.test.mjs` +- `demo/smoke_controls.mjs` +- `demo/srv/demo.html` +- `docs/specs/094-universal-state-toggle.md` +- `docs/specs/README.md` +- `docs/ARCHITECTURE.md` +- `docs/STATUS.md` +- `docs/TESTING.md` +- `docs/CHANGELOG.md` +- `docs/CHANGELOG.ru.md` + +## 5. Проверка + +Локально выполнены только read-only/static проверки ревью: + +- `git diff --check`; +- `npm run typecheck`; +- `node --check test/device-toggle.test.mjs`; +- `node --check demo/smoke_controls.mjs`; +- поиск всех consumers `resolveToggleIntent`, `projectedTapAction`, + `toggleCoverEntity`, `sameToggleCommandTargets`; +- сверка backend schema/import-export; +- сверка capability flags с официальным Home Assistant Core. + +Unit, browser smoke, backend tests, build и generated bundles **не запускались** +по правилу проекта: локальные правки делаются без тестов, минимальный целевой +прогон выполняется при следующем pre-release. diff --git a/docs/reviews/SPEC-REVIEW-089-isometric-view-stage1.md b/docs/reviews/SPEC-REVIEW-089-isometric-view-stage1.md new file mode 100644 index 00000000..a3beb294 --- /dev/null +++ b/docs/reviews/SPEC-REVIEW-089-isometric-view-stage1.md @@ -0,0 +1,503 @@ +# Ревью ТЗ `docs/specs/089-isometric-view-stage1.md` + +Дата ревью: 2026-08-11 +Issue: [#89](https://github.com/Matysh/houseplan-card/issues/89) +Проверенная версия ТЗ: локальный `dev`, после `v1.62.0-beta.1`, SHA `2cf5c27` + +## Итог + +Направление выбрано правильно: фиксированная SVG-проекция, отсутствие новой +модели данных, каноническая геометрия стен, плоские редакторы, скрытая поставка +через Labs и обязательные golden/performance gates хорошо соответствуют +текущей архитектуре House Plan. + +Однако статус **«готово к реализации» пока преждевременен**. В ТЗ остаются +восемь блокирующих неоднозначностей. Главная из них — документ описывает +проекцию точек, но не определяет переход между тремя реально существующими +системами координат и не задаёт новый контракт viewport/frame. Если начать +реализацию буквально по текущему тексту, наиболее вероятный результат — +прыжок масштаба при переключении, рассинхронизация HTML-маркеров и SVG, неверный +warm-remount либо двойное обратное преобразование pointer events. + +Рекомендация: внести блокеры B1–B8 и существенные замечания M1–M8 в ТЗ, после +чего документ можно переводить в `approved`. Переписывать продуктовую часть +или менять выбранный renderer не требуется. + +## Что уже зафиксировано хорошо + +1. Labs не меняет backend, schema/config/layout и не попадает в backup. +2. Плоский вид остаётся default и fallback; редакторы остаются плоскими. +3. CSS 3D, WebGL и многократное клонирование SVG явно запрещены. +4. Источник wall geometry — `wallBodiesGeometry()`, а не новый параллельный + контур. +5. Проёмы должны быть настоящими разрывами masonry geometry. +6. Glow сохраняет один регион на источник и один blur на слой по `LIGHT.md`. +7. Кэш не должен зависеть только от `_cfgEpoch`. +8. У stage 1 нет публичного обещания и пользовательской миграции. + +--- + +## Блокирующие замечания + +### B1 — Не определены системы координат, pivot и projected frame + +**Где:** §4, §7, §8, AC 8. +**Критичность:** blocker. + +`projectPoint(p, z, cam)` и `unprojectPoint(screen, cam)` недостаточны для +существующего renderer. Сейчас House Plan различает как минимум: + +- plan/model units (`room`, `wall`, `marker`); +- координаты SVG scene/viewBox (`_view`); +- client pixels внутри `.stage` (`_screenToVb()`). + +В объёмном виде plan units и scene units перестают совпадать. Кроме того, +`IsoCamera` не содержит pivot/origin и масштаба оси Z. Проекция вокруг `(0, 0)` +сместит план при смене пространства, а старый `_baseVb()` не включает поднятые +верхние грани и начнёт обрезать стены. + +**Что добавить в ТЗ:** + +```ts +type PlanPoint = readonly [number, number]; +type ScenePoint = readonly [number, number]; + +interface IsoCamera { + rotDeg: number; + tiltDeg: number; + xyScale: number; + zScale: number; + origin: PlanPoint; +} + +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; +``` + +Нормативно определить: + +1. `projectPoint` возвращает **scene**, а не screen/client coordinates. +2. `unprojectFloorPoint` инвертирует только плоскость `z=0`; высотная грань не + имеет единственной plan-точки. +3. Pivot — одна фиксированная plan-space константа (рекомендуемо + `[NORM_W / 2, NORM_W / 2]`), а не центр viewport/content frame и не + положение курсора. Иначе появление far marker или переключение `_showFar` + сдвинет уже построенные стены без изменения их геометрии. +4. Wall height сначала переводится из общей константы в plan units, затем + применяется `zScale`; выбранные значения фиксируются ADR. +5. `fit`, pan clamp, home arrow, far-object hint и initial view используют + `projectedFrame`, включающий floor content и поднятые wall faces. +6. Projected frame не зависит от текущего zoom/pan и входит в geometry cache. + +Без этого нельзя проверить «не меняет фокус плана скачком» и «не обрезает +объекты». + +### B2 — Не задано преобразование viewport при flat ↔ iso и при входе в редактор + +**Где:** §7, §10, AC 7–9. +**Критичность:** blocker. + +Текущий `_view` хранит прямоугольник именно в координатах текущего SVG. Его +нельзя без преобразования перенести из flat scene в iso scene. Текущий +`_viewModeSnap` также хранит `cx/cy` в flat units. Требование «не менять zoom и +фокус» сейчас не имеет алгоритма. + +**Добавить нормативный алгоритм:** + +1. Перед сменой проекции получить логический центр пола: + - flat: центр `_view` уже является plan point; + - iso: центр `_view` пропустить через `unprojectFloorPoint`. +2. Построить target frame и target fit. +3. Сохранить тот же scalar zoom. +4. Спроецировать логический центр в target scene и вызвать `_applyView()` с + этим scene center. +5. Не переиспользовать raw `x/y/w/h` между видами. +6. Вход в editor выполняет тот же iso → flat переход; выход — flat → прежний + view kind. Смена пространства внутри editor сбрасывает старый snapshot по + существующему правилу. + +Предпочтение вида (`flat|iso`) и viewport — разные сущности. В localStorage +пишется только предпочтение и существующий scalar zoom; raw viewport остаётся +runtime/warm state. + +### B3 — ТЗ не совместимо с `docs/WARM-REMOUNT.md` + +**Где:** §7 «Непрерывность», §10. +**Критичность:** blocker. + +#73 переносит через `warmBoot` точный `_view`, `_viewModeSnap`, mode и +fingerprint кадра. После введения iso один и тот же `ViewRect` имеет два разных +смысла. Если новый экземпляр восстановит iso rectangle в flat mode (например, +флаг снят/истёк) либо наоборот, получится именно тот скачок/пустой кадр, который +#73 устраняет. + +**Добавить:** + +- warm viewport хранит `projection: 'flat'|'iso'` и `logicalCenter`; +- raw `_view` усыновляется только при совпадении space, projection и активного + Labs contract; +- при несовпадении восстанавливаются scalar zoom + logical center через + алгоритм B2, а не чужой rectangle; +- frame fingerprint включает effective projection и iso geometry fingerprint; +- выключение/expiry Labs никогда не может воскресить iso DOM из memo; +- отдельный smoke: iso → remount → тот же iso frame; iso → снять flag → + remount → корректный flat frame без veil/flash. + +### B4 — Правило «все попадания через unproject» технически неверно + +**Где:** §7 Pointer, §11 smoke Pointer. +**Критичность:** blocker. + +SVG сам hit-тестирует элементы внутри трансформированного ``. Room hover и +SVG opening symbols не нужно вручную unproject-ить: это даст двойное +преобразование. HTML marker также получает click как обычный DOM-элемент. +Кроме того, marker drag выполняется в Device editor, а по этому же ТЗ все +редакторы плоские; smoke «перетаскивание маркера в объёмном виде» противоречит +scope. + +**Заменить правило на:** + +- SVG/HTML interactive children используют нативный DOM/SVG hit-test; +- pan и zoom anchor работают в scene coordinates; +- `client → scene → unprojectFloor` применяется только там, где stage event + действительно должен получить plan coordinate; +- в stage 1 iso mode не создаёт/редактирует geometry и не перетаскивает + markers, поэтому editor `_svgPoint()` остаётся flat; +- тесты кликают реальные room/device/opening DOM targets и проверяют action; + отдельный unit проверяет `client → scene → floor` для будущего использования. + +### B5 — Kiosk UX противоречит фактическому DOM + +**Где:** §3, §7, AC 2/4. +**Критичность:** blocker. + +ТЗ обещает кнопку «в режиме просмотра (и в киоске) рядом с шапкой». В текущем +kiosk вся `.hdr.kioskhide` имеет `display:none`; такой кнопки физически не +будет. Одновременно §7 говорит, что скрытая панель не должна лишить пользователя +возврата в flat. + +Для скрытого stage 1 рекомендуется закрепить простой вариант: + +1. Кнопка существует только в обычном View под активным Labs. +2. Kiosk читает последнее per-space предпочтение этого браузера. +3. `hp-labs=-iso` или `hp-labs=off` — обязательный аварийный путь: kiosk сразу + становится flat и не может восстановить iso из warm memo. +4. В kiosk нет новой панели/диалога stage 1. +5. Smoke покрывает загрузку kiosk с сохранённым `iso` и возврат в flat через + URL operation. + +Если владельцу нужен переключатель прямо в kiosk, его надо отдельно поместить +в существующий long-press kiosk dialog; «рядом с шапкой» всё равно неверно. + +### B6 — Грамматика Labs содержит противоречия и ломает комбинированный hash + +**Где:** §2.2–2.4. +**Критичность:** blocker. + +Не определено: + +- кто сильнее при одновременных `?hp-labs=` и `#hp-labs=`; +- является URL полным replacement или операциями над storage; +- что делает `iso,-iso`, `off,iso`, повторный параметр; +- §2.2 требует не удалять параметр из URL, а §2.3 говорит, что `off` «очищает + и то, и другое»; +- как `#space=x&hp-labs=iso` сохраняет существующий deep link; +- что происходит при `history.back()`/`popstate`. + +**Предлагаемый точный контракт:** + +1. База — валидный набор из storage. +2. Query operations применяются слева направо, затем hash operations слева + направо; hash сильнее, потому что именно он реактивен внутри Lovelace. +3. `id` добавляет, `-id` удаляет, `off` очищает набор в этой позиции; следующие + токены снова могут добавлять. +4. Повторные `hp-labs` обрабатываются в порядке появления. +5. Если в URL был хотя бы один известный operation или `off`, итог пишется в + storage. Неизвестные значения сами по себе storage не переписывают. +6. URL никогда не переписывается механизмом Labs. Из §2.3 убрать слова об + очистке URL: `off` очищает **effective set и storage**, но остаётся видимым. +7. Hash разбирается общим helper вместе с `space`; оба порядка параметров и + percent-encoding тестируются. `_hashSpace()` не остаётся вторым regex parser. +8. `hashchange` реактивен; `popstate` перечитывает query/hash, если URL реально + сменился без reload. + +### B7 — Не определена топология side faces и смысл «нет торцов в проёме» + +**Где:** §5, AC 3/5/6. +**Критичность:** blocker. + +`wallBodiesGeometry().geom` — MultiPolygon с внешними и внутренними rings, уже +после union, junction patches и opening cuts. «Граничные рёбра» недостаточно: +нужно определить winding, outward normal, holes, culling и порядок отрисовки. +Фраза «без торцов внутри проёма» двусмысленна. При полном разрыве стены +вертикальные jamb faces по краям проёма являются корректной частью объёма; +запретить их — значит получить визуально обрезанную плёнку вместо стены. + +**Добавить:** + +- faces строятся непосредственно из rings канонического MultiPolygon после + union/cuts; исходные room edges для extrusion не используются; +- winding нормализуется один раз, outward normal учитывает outer/hole ring; +- face видима по знаку dot product normal и фиксированного view direction; +- для фиксированной камеры задаётся детерминированный stable depth order; +- opening slot создаёт две exposed jamb faces по концам разрыва — они нужны; +- запрещены face/полоса, пересекающая сам gap, и cap на floor тоннеля; +- на stage 1 дверь, окно и ворота являются full-height gaps осознанно, так как + модель не хранит высоту подоконника; +- opening никогда не вырезает coincident partition/column — сохраняется + текущий порядок union extras после room opening cuts; +- top face использует whole geometry с `fill-rule:evenodd`; +- unit fixtures включают outer ring, hole, multipolygon, T/X join, opening у + угла и coincident independent body. + +### B8 — Fallback может зациклить exception и оставить кнопку во лжи + +**Где:** §9, AC 10. +**Критичность:** blocker. + +«Вернуться в flat на этом кадре» не отвечает на вопросы: будет ли следующий +Lit render снова падать, что показывает `aria-pressed`, сохраняется ли `iso` в +localStorage и когда разрешён retry. + +**Добавить state machine:** + +- `desiredView` — сохранённое предпочтение; +- `effectiveView` — реально нарисованный `flat|iso`; +- исключение в pure geometry/iso template ловится на границе + `renderIsoScene()`, для `(space, geometryFingerprint)` ставится session latch; +- при latch effective view = flat, iso geometry больше не вызывается на каждый + HA state update; +- конфиг/layout и сохранённое предпочтение не меняются автоматически; +- кнопка отражает `effectiveView` (`aria-pressed=false`), явное повторное + нажатие или новый geometry fingerprint очищает latch и делает один retry; +- console error содержит issue, space, fingerprint и короткий reason, но без + config/entity payload; один раз на latch; +- ошибка HTML overlay projection также входит в эту границу, иначе получится + «стены flat, markers iso». + +--- + +## Существенные замечания + +### M1 — Spike ADR должен фиксировать больше, чем выбор renderer + +Сейчас D6 требует ADR, но его обязательные решения не перечислены. ADR должен +закрыть до основной реализации: + +- формулу и pivot проекции; +- camera constants, wall-height units и zScale; +- top/side fill, stroke, hatch и side shading в light/dark theme; +- ring normalization, face visibility и depth order; +- z-order floor → Glow/sun/decor → faces/top → screen-facing HTML overlays; +- осознанное правило stage 1: markers/room cards всегда выше wall faces и не + получают геометрическую occlusion; +- projected frame и flat↔iso viewport conversion; +- результат проверки SVG filter/clip/mix-blend на Chromium, Firefox, WebKit; +- причины отказа от проигравшего прототипа. + +До ADR issue остаётся в статусе spike/implementation-prep, не renderer-ready. + +### M2 — Fingerprint перечисляет не все входы iso geometry + +В §8 добавить как минимум: + +- `room_drafts` и их segment thickness; +- нормализованные `openCuts`/virtual intervals; +- canonical opening cuts; +- partitions и columns с shape/angle/diameter; +- `cell_cm`, grid pitch, coordinate scale/NORM; +- camera constants и wall-height constant; +- версию алгоритма projection/faces. + +Массивы должны сериализоваться детерминированно, числа — нормализоваться как в +существующих geometry fingerprints. Display state (`hover`, HA states, +`show_borders`) не должен инвалидировать geometry cache. `show_borders:false` +просто не рисует cached top/sides, но physics остаётся прежней. + +### M3 — `since`/`expires` требуют точной version semantics + +В проекте нет зависимости `semver`; строкового сравнения допускать нельзя. +Зафиксировать parser `major.minor.patch[-prerelease]`, fail-closed для +некорректной registry entry и инвариант `since < expires`. + +Рекомендуемое продуктовое правило: сравнивать numeric core, поэтому +`1.65.0-beta.1` уже достигает `expires: 1.65.0` и не тащит мёртвый флаг в новый +release cycle. Добавить тесты `1.64.9`, `1.65.0-beta.1`, `1.65.0`, malformed. + +### M4 — Не определён runtime owner механизма Labs + +Нужно указать, что availability flags глобальны для загруженного JS-модуля, а +effective `flat|iso` остаётся состоянием конкретной карточки/пространства. +Один module-level resolver/subscription не должен создавать по listener на +каждый render. + +`window.__hpLabs` должен иметь нормативную форму, например frozen sorted array: + +```ts +Object.freeze(['iso']) +``` + +При изменении URL property заменяется новым frozen array, все подключённые +карточки получают update. Нельзя отдавать внутренний mutable `Set`. + +### M5 — Scope `houseplan-space-card` не указан + +В репозитории есть второй renderer: `src/space-card.ts` + `src/space-render.ts`. +Текущий текст можно прочитать как требование объёмного вида для обеих карточек. + +Рекомендация для stage 1: явно записать, что `houseplan-space-card` остаётся +flat и Labs `iso` на него не влияет. Его поддержка — отдельный будущий scope. +Иначе придётся сразу заводить вторую композицию сцены, что противоречит цели +скрытого первого этапа. + +### M6 — Performance contract не совпадает с существующей инфраструктурой + +`compare.mjs` использует profile-specific budgets, noise allowance, +relative+absolute thresholds и exact same runner. Просто потребовать «≤20% по +трём полям» недостаточно; `longTask.maxSingleMs` особенно нестабилен около +нуля, а `modelReadyMs` почти не измеряет переключение renderer. + +Добавить отдельный профиль `large-house-isometric-v1`: + +- текущий benchmark harness запускает candidate bundle с `hp-labs=iso` и + переключает view; тот же harness запускает base bundle, который игнорирует + неизвестный flag и остаётся flat; +- profile id в обоих reports одинаков, runtime/browser/fingerprint проверяются + существующим fail-closed контрактом; +- отдельный reviewed budget JSON задаёт 20% relative allowance **плюс** + абсолютный noise allowance; +- обязательные метрики: first stable iso frame, view toggle, pan/zoom, + HA-state update, space switch, long-task count/total/max, heap growth, + iso-cache entries/growth, rendered devices; +- candidate-only prerelease smoke получает абсолютные ceilings; +- перед завершением этапа выполняется exact-SHA full performance workflow, а + не локальное сравнение с другой машиной. + +Фразу «flat не должен подорожать вообще» заменить на проверяемое: при +выключенном флаге iso geometry/cache/DOM отсутствуют и нет дополнительного +прохода по room/device collections; timing находится внутри noise allowance. + +### M7 — Golden coverage слишком мала для новой системы координат + +Две картинки не покрывают заявленный scope. Минимальная матрица stage 1: + +1. desktop dark: mixed walls + openings + Glow/sun + devices; +2. desktop light: theme/shading/filter parity; +3. mobile portrait или узкий kiosk: fit, marker/label alignment, no clipping; +4. `show_borders:false`: стены не нарисованы, room fill/Glow сохраняются; +5. remount/toggle sequence проверяется smoke, а финальный кадр — golden при + необходимости. + +Существующие flat baselines действительно не принимаются заново, если diff не +нулевой. Новые baselines принимаются только из полного Linux CI artifact по +действующему HP-QA-01 контракту. + +### M8 — A11y toggle contract неполон + +Для кнопки добавить: + +- `aria-pressed="true|false"` по `effectiveView`; +- стабильный accessible name «Объёмный вид» / `Volumetric view`; +- focus остаётся на той же кнопке после переключения; +- active visual state не кодируется только цветом; +- DOM/tab order устройств и room actions совпадает с flat; +- stage 1 не добавляет projection animation: swap атомарный. Если анимация + будет добавлена через #82, `prefers-reduced-motion` делает её мгновенной. + +--- + +## Замечания к тестам и формулировкам + +### T1 — «innerHTML до и после ветки» нужно сделать воспроизводимым + +Обычный тест не может сравнить текущий commit с кодом до ветки. Разделить +контракт: + +- в одном candidate build сравнить no-param и unknown-param: нет iso nodes, + нет дополнительных WS/HTTP и config/layout writes; +- golden гарантирует нулевой diff существующих flat scenes между revisions; +- unit spy подтверждает, что iso geometry builder не вызывался; +- чтение собственного Labs localStorage не считать сетевым/сторным изменением, + но при отсутствии URL оно не должно переписывать ключ. + +### T2 — Мутанты должны быть исполнимыми + +Пункт «отдельная формула проекции HTML» нельзя надёжно поймать текстовым +поиском. Нормативный mutant: внести controlled offset только в overlay mapping; +smoke должен увидеть расхождение anchor больше 1 CSS px. Для cache mutant тест +меняет geometry in-place без `_cfgEpoch`; iso faces обязаны обновиться. Для +layer-copy mutant тест проверяет upper bound DOM face count как `O(E)`. + +Для каждого из пяти mutants сохранить команду/patch id и имя краснеющего теста +в PR/issue evidence; ручной тезис «проверено» недостаточен. + +### T3 — Opening wording + +В AC 6 заменить «без швов и торцов внутри проёма» на: + +> Проём является full-height gap. Внутри gap нет wall top/side полосы; +> вертикальные jamb faces на двух границах masonry разрыва являются ожидаемыми. + +Это снимает конфликт с B7 и делает golden однозначным. + +### T4 — Первый запуск и сохранённое предпочтение + +В §9/§10 уточнить: + +- без записи `houseplan_card_view_v1[space]` effective view всегда flat, даже + при активном Labs; +- toggle в обычном View пишет `flat|iso` per space; +- при неактивном/expired flag сохранённое `iso` игнорируется, не меняет DOM и + не попадает в warm memo; +- вход/выход editor не перезаписывает предпочтение; +- fallback B8 не перезаписывает предпочтение автоматически. + +### T5 — Backlog — канонический источник + +Issue #89 сейчас говорит «черновик продуктового и технического решения», тогда +как файл говорит «готово к реализации». По `AGENTS.md` Issue/Project являются +каноническими. После принятия новой редакции: + +- добавить в body issue ссылку на stage 1 spec как нормативную; +- синхронизировать scope/acceptance criteria issue с утверждённой редакцией; +- оставить Project `Todo` до фактического начала, затем перевести в + `In progress`; +- не закрывать #89 после одного spike ADR: закрытие только после всех AC этапа. + +--- + +## Рекомендуемая новая структура нормативных разделов + +Чтобы не раздувать основной текст, достаточно добавить четыре подраздела: + +1. **§4.4 Coordinate spaces and viewport** — B1, B2. +2. **§5.1 Wall-face topology and visual tokens** — B7, M1. +3. **§7.1 Native hit testing and warm continuity** — B3, B4, B5. +4. **§2.2.1 Labs operation precedence and version lifecycle** — B6, M3, M4. + +Остальные замечания можно встроить в §8–§13. + +## Definition of Ready после следующей итерации + +ТЗ можно считать готовым к реализации, когда: + +- [ ] определены plan/scene/client spaces, camera pivot/zScale и projected frame; +- [ ] записан алгоритм flat↔iso viewport conversion; +- [ ] обновлён warm-remount contract; +- [ ] исправлено pointer rule и убран iso marker-drag smoke; +- [ ] выбран однозначный kiosk escape contract; +- [ ] полностью определена Labs grammar и expiry semantics; +- [ ] определены ring/face/jamb/depth rules; +- [ ] определена fallback state machine; +- [ ] ADR имеет обязательный список решений; +- [ ] fingerprint содержит все входы; +- [ ] указан scope `houseplan-space-card`; +- [ ] создан исполнимый performance profile/budget plan; +- [ ] расширена golden/a11y/mutant matrix; +- [ ] issue #89 ссылается на утверждённое ТЗ и не противоречит ему. + +После этого оценка stage 1 остаётся **L/XL с высоким риском**, но работа станет +декомпозируемой и проверяемой; менять выбранную продуктовую концепцию не нужно. diff --git a/legacy/docs/089-isometric-view-draft.md b/legacy/docs/089-isometric-view-draft.md new file mode 100644 index 00000000..8169fdd6 --- /dev/null +++ b/legacy/docs/089-isometric-view-draft.md @@ -0,0 +1,301 @@ +# #89 — Опциональный объёмный 2.5D/изометрический вид плана + +**Статус:** исследование завершено; черновик продуктового и технического решения +**Дата:** 2026-08-11 +**Приоритет:** P1 — высокая продуктовая ценность, высокая стоимость реализации +**Область:** режим просмотра и киоск; редакторы в первую версию не входят +**Исходные референсы:** предоставленные владельцем проекта `isometric-plan-demo.zip` и изображение `photo_2026-08-11_18-30-38.jpg` + +## 1. Краткое решение + +Добавить в Houseplan опциональный **«Объёмный вид»** — стилизованное 2.5D-представление существующего плана с приподнятыми стенами, видимыми боковыми гранями, мягкой тенью и изометрической камерой. + +Это не отдельный редактор и не новая модель плана. Комнаты, стены, проёмы, перегородки, колонны, устройства, Glow и все состояния продолжают использовать текущие данные. Меняется только способ их проекции и визуализации. + +Рекомендуемая первая пользовательская версия: + +- переключаемый плоский/объёмный вид; +- только режим просмотра и киоск; +- фиксированный проверенный ракурс без свободного вращения камеры; +- редакторы всегда открываются в обычном плоском виде; +- без WebGL/Three.js и без 32 копий всего SVG; +- стены строятся из канонической геометрии Houseplan, проёмы действительно вырезаются из объёма; +- Glow, солнечные лучи, заливки и hover остаются функционально теми же эффектами на плоскости пола; +- диалоги и tooltips остаются обычным экранным UI и не наклоняются вместе с планом. + +## 2. Пользовательская ценность + +Ценность оценивается как **очень высокая**: + +- план превращается из утилитарной схемы в визуально сильный центр dashboard; +- различия стен, комнат, проёмов и внешних зон считываются быстрее; +- скриншоты и демонстрации продукта становятся существенно убедительнее; +- Houseplan получает заметное визуальное отличие от обычных floorplan-карточек; +- существующая модель данных уже содержит большую часть необходимой геометрии — пользователь не строит второй план. + +При этом объёмный вид не должен становиться обязательным. Плоский вид точнее для редактирования, плотных планов и слабых устройств и остаётся полностью поддерживаемым. + +## 3. Что показало исследование демо + +Переданный demo — удачный визуальный proof of concept, но не готовая архитектура для Houseplan. + +Он использует: + +- один SVG с вручную заданной геометрией; +- CSS `perspective`, `rotateX`, `rotateZ` и `preserve-3d`; +- 32 копии одного контура стен, поднятые последовательными `translateZ`, чтобы имитировать толщину по высоте; +- отдельный верхний контур стен; +- статические подписи и устройства внутри наклонённой плоскости; +- drag для вращения и wheel для масштаба. + +Для маленькой статической сцены это работает и хорошо показывает продуктовый эффект. Прямой перенос подхода в Houseplan не подходит, потому что: + +1. Геометрия Houseplan динамическая: смешанная толщина, T/X-стыки, перегородки, круглые и квадратные колонны, двери, окна и ворота. +2. В карточке есть тяжёлые динамические слои: Glow, spill через проёмы, солнечные лучи, hover, vacuum overlays и HTML-маркеры. +3. 20–32 копии сложных wall paths заметно увеличат DOM, raster/compositing cost и вероятность швов. +4. CSS 3D-контекст разрушается или уплощается рядом свойств, уже используемых карточкой: `filter`, opacity на предках, `clip-path`, mask и часть blend/compositing-сценариев. +5. Сейчас day/night использует `filter: brightness(...)` на `.zoomwrap`, а Glow — `mix-blend-mode: screen`; слепое помещение существующего дерева в CSS 3D создаёт высокий риск регрессий. +6. Координаты устройств и room labels сейчас считаются для плоского `viewBox` и выводятся HTML-слоем. Их нужно проецировать тем же каноническим преобразованием, иначе они разойдутся с планом. + +Вывод: demo подтверждает **реализуемость и ценность визуального направления**, но production-рендерер нужно строить на геометрии Houseplan. + +## 4. Рекомендуемая архитектура + +### 4.1. Не полноценный 3D, а детерминированная 2.5D-проекция + +Для первой версии рекомендуется обычный SVG с явной 2D-проекцией, а не CSS-стопка и не WebGL: + +- floor/decor/room/glow-слои помещаются в SVG-группу с одной аффинной изометрической матрицей; +- верх стены — каноническое объединённое тело стены, спроецированное и сдвинутое на визуальную высоту; +- боковые грани — четырёхугольники, построенные из граничных рёбер wall body и вектора высоты; +- видимые боковые грани фильтруются по направлению камеры и сортируются по глубине; +- HTML-маркеры получают экранные координаты через ту же чистую функцию проекции; +- tooltips, dialogs и системные controls остаются вне проецируемой сцены. + +Это позволяет сохранить обычный SVG compositor, mask/blend-поведение Glow и предсказуемую деградацию, а число элементов растёт по числу реальных граней, а не умножается на 20–32 слоя. + +### 4.2. Канонические источники геометрии + +Нельзя создавать вторую независимую модель стен. Рендерер обязан использовать: + +- `wallBodiesGeometry()` / объединённые wall bodies из `src/wall-thickness.ts`; +- текущие opening cuts/tunnel geometry; +- существующую модель перегородок и колонн; +- текущие room polygons и decor geometry; +- текущий канонический light resolver и описанную в `docs/LIGHT.md` семантику. + +Возможные новые модули: + +- `src/render/isometric-projection.ts` — чистая математика проекции и обратного hit mapping; +- `src/render/isometric-walls.ts` — top/side faces и depth ordering; +- `src/render/isometric-scene.ts` — композиция слоёв; +- детерминированные fixtures/golden matrix отдельно от основной карточки. + +Кэш геометрии строится по содержательному fingerprint геометрии, а не только по `_cfgEpoch`. + +### 4.3. Высота стен + +В модели сейчас нет полноценной высоты помещений/стен. В первой версии высота — **визуальный параметр представления**, а не архитектурный размер: + +- единое безопасное значение по умолчанию; +- один общий параметр для карточки/пространства, если настройка вообще выводится пользователю; +- одинаковая высота у обычных стен, перегородок и колонн; +- виртуальные стены остаются линиями пола и не получают объём. + +Не следует выводить высоту в сантиметрах: это создаст ложное обещание настоящей 3D-модели. + +## 5. UX первой версии + +### 5.1. Переключение + +- В режиме просмотра появляется компактная кнопка с `mdi:cube-outline` и accessible name «Объёмный вид». +- Повторное нажатие возвращает «Плоский вид». +- Переключение не изменяет геометрию и не создаёт запись в command stack. +- Предпочтение вида хранится отдельно от модели плана; оно не должно создавать конфликтов совместного редактирования. +- В киоске используется настроенный вид по умолчанию; скрытая панель не должна делать возврат в плоский вид невозможным из настроек. +- Вход в любой редактор временно показывает плоский вид. После выхода восстанавливается предыдущий режим просмотра. + +Плоский вид остаётся значением по умолчанию для существующих установок и fallback при ошибке/неподдерживаемом окружении. + +### 5.2. Камера и навигация + +MVP использует один тщательно подобранный ракурс либо 2–3 пресета. Свободное вращение и изменение tilt не входят в первую версию. + +Причины: + +- свободное вращение конфликтует с pan, pinch zoom, long press и кликами устройств; +- оно требует динамической сортировки граней на каждом кадре; +- маркетинговый эффект достигается фиксированным хорошим ракурсом; +- фиксированный ракурс детерминирован для golden image и поддержки. + +Обычный zoom/pan сохраняется. Изменение масштаба должно быть совместимо с #82 и не менять фокус плана скачком при переключении вида. + +### 5.3. Устройства и подписи + +Для первой версии рекомендуется: + +- позиция marker/room label проецируется вместе с планом; +- сама интерактивная карточка устройства остаётся достаточно читаемой и получает мягкую объёмную тень; +- tooltip/dialog всегда экранные и не наклоняются; +- touch target не уменьшается из-за визуального наклона; +- tab order и accessible name совпадают с плоским режимом. + +Полностью «лежащие на полу» подписи красивее, но хуже читаются. Допустим компромисс: лёгкое согласование с ракурсом для подложки и screen-facing содержимое. Точный вариант выбирается по прототипу и a11y-проверке. + +## 6. Поведение слоёв + +| Слой/объект | Поведение в объёмном виде | +|---|---| +| Пол/заливка комнаты | Лежит на нижней плоскости, текущая семантика цвета сохраняется | +| Room hover | Затемняет заливку и подсвечивает внутренние границы без вспышки Glow | +| Физические стены | Верхняя поверхность + видимые боковые грани | +| Перегородки | Как физические стены; комнату автоматически не делят | +| Квадратные/круглые колонны | Экструдируются из своей текущей геометрии | +| Виртуальные стены | Пунктир на плоскости пола, без высоты | +| Дверь | Проём реально разрывает стену; символ состояния сохраняется. Вертикальная створка — polish-этап | +| Окно | Разрыв объёма с отдельным стилизованным оконным элементом; без моделирования реальной высоты подоконника | +| Ворота | Разрыв объёма; две наружные створки сохраняются | +| Opening tunnel fill | Цвет комнаты на полу, без швов; семантика Glow не меняется | +| Glow и spill | На плоскости пола, стеновые барьеры и проёмы работают по `docs/LIGHT.md` | +| Солнечные лучи | На плоскости пола, геометрия и настройки остаются текущими | +| Decor/backdrop | Лежит на floor plane; не создаёт объём автоматически | +| Vacuum path/outline | На floor plane; robot marker остаётся интерактивным | +| Device markers | Проецированная позиция, безопасный z-order и неизменная логика состояний | +| Tooltips/dialogs | Экранный overlay поверх сцены | +| `show_borders: false` | Невидимые стены остаются невидимыми, но продолжают влиять на площадь и свет | + +## 7. Обязательные edge cases + +Реализация и fixtures должны покрыть: + +1. Выпуклые и вогнутые комнаты, отверстия и несколько несвязанных контуров. +2. Смешанную толщину стен, короткие сегменты, T/X-стыки и сложные объединения. +3. Проём у угла, несколько соседних проёмов, дверь/окно/ворота шире толщины стены. +4. Перегородку, примыкающую к стене без визуального шва. +5. Квадратную повёрнутую и круглую колонну. +6. Виртуальную стену между двумя толстыми стенами. +7. Комнату без физических стен и старую конфигурацию без thickness. +8. Glow с несколькими источниками, spill через открытые проёмы и hover комнаты. +9. Day/night brightness, непрозрачные/полупрозрачные заливки и additive blending. +10. Большую подложку, decor, текст, пунктирные линии и мебель. +11. Скрытые/удалённые/недоступные устройства и все режимы device presentation. +12. Touch: pinch не вызывает click, long press не оставляет phantom pan. +13. Возврат на вкладку: сцена не мигает и не пересобирается из пустого состояния. +14. Очень широкий/высокий план, detached terrace/porch и несколько пространств. +15. Светлая/тёмная тема, kiosk, reduced motion и high zoom. + +## 8. Производительность и технические ограничения + +- Не клонировать весь SVG или wall body десятки раз. +- Число side faces должно быть O(числу граничных рёбер), а не O(рёбра × визуальная высота). +- Pan/zoom не пересчитывает модельную геометрию; меняется только transform/viewBox. +- Геометрия стен пересчитывается только при изменении содержательного fingerprint. +- Цель на детерминированном большом fixture: плавный pan/zoom на desktop и не более 20% регрессии frame time относительно плоского вида. +- Переключение вида не должно показывать пустой/чёрный промежуточный кадр. +- При нехватке возможностей или исключении renderer карточка безопасно возвращается в плоский вид и сообщает диагностируемую причину в dev log. +- Новая тяжёлая runtime-зависимость и WebGL не допускаются без отдельного архитектурного решения. + +## 9. Доступность + +- «Объёмный вид» — визуальная альтернатива, не отдельный набор функций. +- Все устройства, комнаты и действия доступны с клавиатуры так же, как в плоском виде. +- Focus ring виден и не обрезается стеной/контейнером. +- Screen reader получает те же имена и состояния. +- `prefers-reduced-motion` отключает переход между проекциями, но не сам режим. +- Минимальные touch targets сохраняются в экранных координатах. +- Плоский вид остаётся fallback для пользователей, которым перспектива мешает чтению. + +## 10. План реализации + +### Этап 0 — технический spike, 2–4 рабочих дня + +- детерминированный fixture со стенами, mixed thickness, проёмами, колонной, Glow и устройствами; +- два прототипа: CSS wall slices и явные side faces; +- замеры Chrome/Edge, Firefox и Safari/WebKit; +- проверка Glow/mask/blend, day/night и HTML overlays; +- ADR с окончательным выбором renderer. + +Spike не включается пользователям и не считается завершением issue. + +### Этап 1 — ship-ready фиксированный объёмный вид, 8–15 рабочих дней + +- чистая математика проекции; +- floor plane, wall top и side faces; +- проёмы, перегородки и колонны; +- проекция markers/labels и pointer mapping; +- переключение view/editor/kiosk; +- кэш, fallback, unit/integration/golden/performance gates; +- RU/EN документация и changelog. + +### Этап 2 — визуальный polish уровня референса, ещё 8–15 рабочих дней + +- улучшенные двери, окна и ворота; +- мягкие тени/ambient depth без дорогого dynamic lighting; +- аккуратные floor/platform edges; +- доводка markers/labels и occlusion; +- дополнительные пресеты камеры и качества. + +### Отдельный будущий этап — свободная камера, ещё 5–10+ рабочих дней + +Свободное вращение/tilt, gesture arbitration и динамический depth sort не входят в базовый scope. Их ценность ниже, а риск заметно выше. + +## 11. Оценка сложности + +| Вариант | Срок | Риск | Оценка | +|---|---:|---:|---| +| Демонстрационный CSS stack на одном fixture | 2–4 дня | Средний | Хорош для spike, не для релиза | +| Production MVP с фиксированным ракурсом | 8–15 дней | Высокий | Рекомендуемый первый релиз | +| Визуально отполированный вариант уровня референса | 16–30 дней суммарно | Высокий | Реалистичная конечная цель | +| Свободная камера поверх polished renderer | +5–10 дней | Высокий | Позже, только по подтверждённой ценности | +| Настоящий WebGL/Three.js 3D renderer | 1–2+ месяца | Очень высокий | Не рекомендуется для этой цели | + +Общая оценка: **L/XL, высокий архитектурный и визуальный риск, но очень высокая продуктовая отдача**. Сам эффект дешёв в статичном demo; дорого стоит сохранение всей существующей интерактивности и световой модели без регрессий. + +## 12. Риски и меры + +| Риск | Вероятность/влияние | Мера | +|---|---|---| +| Glow/blend/filter ломает CSS 3D | Высокие | Обычная SVG 2.5D-проекция вместо вложенного `preserve-3d` | +| Неверные T/X-стыки и проёмы | Высокие | Только канонические union bodies + geometry fixtures | +| Маркеры расходятся с планом | Высокие | Одна pure projection для SVG и HTML overlays | +| Падение FPS на больших планах | Средние/высокие | Side faces вместо десятков slices, кэш и perf gate | +| Нечитаемые подписи | Средние | Screen-facing/гибридный marker prototype и a11y review | +| Touch misclick при навигации | Средние/высокие | В MVP нет свободной камеры; существующие pan/pinch правила сохраняются | +| Режим превращается в второй renderer со своей логикой | Высокие | Общие resolvers/geometry; новый слой отвечает только за projection/presentation | +| Референс обещает больше, чем модель умеет | Средние | Называть «Объёмный вид», не «полноценный 3D»; явно ограничить высоту и окна | + +## 13. Не входит в первую версию + +- редактирование геометрии в перспективе; +- пользовательская высота каждой стены/комнаты; +- 3D-мебель и импорт моделей; +- настоящая физика света и динамические тени; +- свободный orbit camera; +- WebGL/Three.js; +- автоматическое превращение растровой подложки в 3D; +- изменение формата существующих room/wall/opening данных. + +## 14. Acceptance criteria пользовательской версии + +1. Пользователь может переключить текущий план между плоским и объёмным видом без изменения данных. +2. Существующие планы открываются плоскими и не требуют миграции. +3. В объёмном виде физические стены, перегородки и колонны имеют непрерывные top/side faces; виртуальные стены не экструдируются. +4. Двери, окна и ворота образуют реальные разрывы объёма, без тонких швов и торцов внутри проёма. +5. Glow, spill, sunlight, room fill и hover сохраняют текущую семантику и не мигают. +6. Device markers, room controls, tooltips и dialogs остаются кликабельными, читаемыми и доступными с клавиатуры. +7. Открытие редактора показывает плоский вид; выход восстанавливает предыдущий просмотр. +8. Zoom/pan/fit работают без скачка, phantom click и изменения сохранённой геометрии. +9. При ошибке объёмного renderer доступен безопасный плоский fallback. +10. Пройдены unit tests проекции/side faces, smoke interactions, golden matrix и performance comparison на детерминированных fixtures. +11. Обновлены `README.md`, `docs/USER-GUIDE.ru.md`, RU/EN changelog и release screenshots. + +## 15. Рекомендуемая поставка + +Фичу лучше вести отдельным релизным циклом: + +1. внутренняя beta/spike без публичного toggle; +2. beta с фиксированным объёмным видом и полным fallback; +3. beta с doors/windows/markers polish и performance fixes; +4. stable только после golden review на реальных больших планах и проверки мобильного просмотра. + +Главный критерий успеха — не максимальная «трёхмерность», а визуально сильный вид без потери надёжности Houseplan как интерактивной HA-карточки.