mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-28 19:01:34 +00:00
docs: file reviews where reviews live, retire the #89 draft, honest markers
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
This commit is contained in:
+5
-1
@@ -1,6 +1,10 @@
|
||||
# Документация House Plan
|
||||
|
||||
Этот каталог — новая точка входа в русскоязычную документацию House Plan. Материалы ниже сверены с исходным кодом и интерфейсом версии **v1.60.0**.
|
||||
Этот каталог — точка входа в русскоязычную документацию House Plan.
|
||||
|
||||
> **Сверено с версией v1.60.0.** Продукт с тех пор ушёл вперёд (линия 1.64);
|
||||
> при расхождении этого каталога с `docs/USER-GUIDE.ru.md` и changelog верить им.
|
||||
> Пересверка каталога — отдельная документационная задача.
|
||||
|
||||
| Документ | Для кого | Что внутри |
|
||||
|---|---|---|
|
||||
|
||||
+2
-3
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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-тестирует элементы внутри трансформированного `<g>`. 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 с высоким риском**, но работа станет
|
||||
декомпозируемой и проверяемой; менять выбранную продуктовую концепцию не нужно.
|
||||
@@ -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-карточки.
|
||||
Reference in New Issue
Block a user