One word was mistyped while transferring the file: "на каждый HA state update" instead of "на каждом". The moved document must match the original byte for byte. Issue: #142 User-Visible: no
31 KiB
Ревью ТЗ docs/specs/089-isometric-view-stage1.md
Дата ревью: 2026-08-11
Issue: #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 не требуется.
Что уже зафиксировано хорошо
- Labs не меняет backend, schema/config/layout и не попадает в backup.
- Плоский вид остаётся default и fallback; редакторы остаются плоскими.
- CSS 3D, WebGL и многократное клонирование SVG явно запрещены.
- Источник wall geometry —
wallBodiesGeometry(), а не новый параллельный контур. - Проёмы должны быть настоящими разрывами masonry geometry.
- Glow сохраняет один регион на источник и один blur на слой по
LIGHT.md. - Кэш не должен зависеть только от
_cfgEpoch. - У 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() не включает поднятые
верхние грани и начнёт обрезать стены.
Что добавить в ТЗ:
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;
Нормативно определить:
projectPointвозвращает scene, а не screen/client coordinates.unprojectFloorPointинвертирует только плоскостьz=0; высотная грань не имеет единственной plan-точки.- Pivot — одна фиксированная plan-space константа (рекомендуемо
[NORM_W / 2, NORM_W / 2]), а не центр viewport/content frame и не положение курсора. Иначе появление far marker или переключение_showFarсдвинет уже построенные стены без изменения их геометрии. - Wall height сначала переводится из общей константы в plan units, затем
применяется
zScale; выбранные значения фиксируются ADR. fit, pan clamp, home arrow, far-object hint и initial view используютprojectedFrame, включающий floor content и поднятые wall faces.- 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 и
фокус» сейчас не имеет алгоритма.
Добавить нормативный алгоритм:
- Перед сменой проекции получить логический центр пола:
- flat: центр
_viewуже является plan point; - iso: центр
_viewпропустить черезunprojectFloorPoint.
- flat: центр
- Построить target frame и target fit.
- Сохранить тот же scalar zoom.
- Спроецировать логический центр в target scene и вызвать
_applyView()с этим scene center. - Не переиспользовать raw
x/y/w/hмежду видами. - Вход в 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 рекомендуется закрепить простой вариант:
- Кнопка существует только в обычном View под активным Labs.
- Kiosk читает последнее per-space предпочтение этого браузера.
hp-labs=-isoилиhp-labs=off— обязательный аварийный путь: kiosk сразу становится flat и не может восстановить iso из warm memo.- В kiosk нет новой панели/диалога stage 1.
- 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.
Предлагаемый точный контракт:
- База — валидный набор из storage.
- Query operations применяются слева направо, затем hash operations слева направо; hash сильнее, потому что именно он реактивен внутри Lovelace.
idдобавляет,-idудаляет,offочищает набор в этой позиции; следующие токены снова могут добавлять.- Повторные
hp-labsобрабатываются в порядке появления. - Если в URL был хотя бы один известный operation или
off, итог пишется в storage. Неизвестные значения сами по себе storage не переписывают. - URL никогда не переписывается механизмом Labs. Из §2.3 убрать слова об
очистке URL:
offочищает effective set и storage, но остаётся видимым. - Hash разбирается общим helper вместе с
space; оба порядка параметров и percent-encoding тестируются._hashSpace()не остаётся вторым regex parser. 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:
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:
- desktop dark: mixed walls + openings + Glow/sun + devices;
- desktop light: theme/shading/filter parity;
- mobile portrait или узкий kiosk: fit, marker/label alignment, no clipping;
show_borders:false: стены не нарисованы, room fill/Glow сохраняются;- 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|isoper 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 этапа.
Рекомендуемая новая структура нормативных разделов
Чтобы не раздувать основной текст, достаточно добавить четыре подраздела:
- §4.4 Coordinate spaces and viewport — B1, B2.
- §5.1 Wall-face topology and visual tokens — B7, M1.
- §7.1 Native hit testing and warm continuity — B3, B4, B5.
- §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 с высоким риском, но работа станет декомпозируемой и проверяемой; менять выбранную продуктовую концепцию не нужно.