19 KiB
Issue #200 — одинаковая геометрия подписи комнаты в View и Plan editor
- Дата: 2026-08-19
- Тип: feature / polish · приоритет P3 · ценность 5/10 · сложность/риск 4/10
- Issue: #200
- Ветка:
issue/200-room-label-parity
Канонические документы: docs/SCOPE.md, docs/UX-MODES.md,
docs/TOUCH-SUPPORT.md, docs/STYLING-HOOKS.md, docs/TESTING.md.
1. Сценарий и персона
Администратор дома на десктопе расставляет подписи комнат в Plan editor, затем возвращается в View и проверяет результат. Это штатная часть J4/J6: сохранённая позиция должна означать одну и ту же визуальную точку на обеих поверхностях, без подгонки вслепую при каждом переключении режима.
Задача касается комнат, связанных с HA Area и имеющих обычное имя. Именно у них View сейчас добавляет к имени иконку перехода, которой нет в Plan editor.
2. Что человек увидит до и после
До: при входе в Plan editor иконка HA Area исчезает, строка имени становится уже и видимый центр имени слегка сдвигается относительно сохранённой позиции.
После: в Plan editor подпись выглядит как в View — с той же иконкой HA Area и теми же строками состояний — и при переключении не сдвигается относительно своей точки. В редакторе вся подпись, включая иконку, по-прежнему служит для перетаскивания; переход в HA Area выполняется только в View.
3. Проблема и подтверждённая причина
.roomlabel в обоих режимах получает одну позицию от _labelPos() и одну
проекцию через _scenePoint(). .rlmetrics уже вынесен из centering math и
имеет одинаковый состав и положение. Различается .rlname: условие
!this._markup && r.area скрывает .rlgo именно в Plan editor. В остальных
режимах, где _markup выключен и подписи видимы, иконка уже входит в DOM; это
уточнение Low из зелёного ревью ТЗ r1.
На demo-карточке Living room подтверждено:
- View: ширина
.rlname86,69 CSS px, иконка есть, центр текста смещён от anchor на 6,62 px влево; - Plan: ширина
.rlname71,13 CSS px, иконки нет, центр текста совпадает с anchor; .rlmetricsв обоих режимах показывает22.4° / 175 / 1 of 2и остаётся центрированным на anchor.
Изменение высоты stage при открытии редактора перемещает весь план и не является дрейфом самой комнатной карточки. Сравнивать абсолютную координату страницы между режимами поэтому нельзя.
4. Scope
- показывать
.rlgoу комнаты с HA Area и в View, и в Plan editor; - сохранить навигацию по иконке в View;
- в Plan editor сделать иконку не отдельной кнопкой, а частью drag-зоны комнатной карточки;
- обеспечить одинаковые размеры и offsets core-карточки (
.rlnameи.rlmetrics) относительно room-label anchor в установившихся View и Plan; - сохранить drag, resize handles, placeholder безымянной комнаты и отдельную кнопку настроек Plan editor;
- обновить автоматические проверки room-link и пиксельной parity;
- обновить пользовательские changelog и тестовый контракт.
5. Non-scope
- выравнивание абсолютной координаты страницы или высоты stage между View и Plan editor;
- изменение сохранённых
layout.rl_<roomId>координат, масштаба подписи, grid/clamp или модели данных; - изменение источников, значений, формата или порядка room metrics;
- добавление перехода в HA Area из Plan editor или других новых действий;
- изменение поведения комнаты без HA Area: иконки у неё нет в обоих режимах;
- унификация placeholder безымянной комнаты с View;
- перенос или редизайн resize handles, кнопки настроек, tooltip комнат, SVG-подписей, device/background editors;
- компенсация произвольного пользовательского CSS, меняющего layout внутренних
.rlname,.rlgoили.rlmetrics; - schema/backend/API, migration, i18n-тексты, touch policy, isometric projection и performance pipeline.
6. Контракт поведения
6.1. Состав core-карточки
Для именованной комнаты с непустым area core-карточка в обоих режимах
содержит:
.rlnameс тем же текстом;- одну
.rlgoпосле имени; - тот же
.rlmetricsи те же.rlm, если они разрешены настройками и данными.
Иконка участвует в layout .rlname одинаково в обоих режимах. Режим не должен
добавлять к core-карточке padding, margin, gap или transform, меняющий её
геометрию. Для комнаты без HA Area .rlgo отсутствует в обоих режимах.
6.2. Геометрический инвариант
После завершения update/layout и исчезновения переходной анимации для одной и той же комнаты, конфигурации, viewport и zoom:
- центр
.roomlabelостаётся на одном сохранённом/projected room-label anchor; left,top,widthиheightbounding box.rlnameотносительно anchor различаются между View и Plan не более чем на 0,5 CSS px;- при наличии
.rlmetricsте же четыре относительные величины различаются не более чем на 0,5 CSS px; - контракт выполняется при DPR 1 и DPR 2 в light и dark theme.
Абсолютный getBoundingClientRect() относительно viewport не является
контрактом: editor chrome вправе изменить stage целиком. Resize handles и
кнопка настроек исключаются из измеряемого core.
6.3. События и affordance
- В View
.rlgoсохраняетpointer-events: auto, pointer cursor, доступный title и вызов существующего_clickRoom(); клик открывает HA Area. - В Plan editor
.rlgoне вызывает_clickRoom(), не останавливаетpointerdown/clickи не получает отдельный button affordance. Pointerdown всплывает в.roomlabelи запускает существующий_labelDown(). - Drag, начатый на тексте или на иконке, проходит один и тот же threshold, pointer capture, grid/clamp и сохранение layout.
- Обычный click по иконке без drag в Plan не меняет HA route, режим, room selection или persisted config.
6.4. Editor-only элементы
Четыре .rlhandle, placeholder безымянной комнаты и отдельная кнопка настроек
сохраняют текущее отображение и события. Они могут отличаться от View, но не
должны участвовать в centering/layout .rlname и .rlmetrics.
7. Данные, миграция и совместимость
Persisted-формат, layout, RoomCfg, backend validation и WebSocket API не
меняются. Миграция не нужна: после обновления существующая сохранённая позиция
автоматически получает одинаковую core-геометрию в двух режимах. Downgrade
возвращает прежний визуальный сдвиг, но не повреждает данные.
Внутренние styling hooks .roomlabel, .rlname, .rlgo, .rlmetrics и .rlm
сохраняются. Новый DOM wrapper или второй renderer ради этой задачи не нужен.
8. UX, i18n, accessibility и touch
Новых строк и переводов нет. В View существующий title и навигационная семантика иконки сохраняются. В Plan иконка визуальна, но не обещает отдельное действие: она наследует drag cursor карточки и не получает новый title/role/tab stop.
Keyboard/focus order не меняется. На touch Plan editor остаётся best effort по
docs/TOUCH-SUPPORT.md; pointercancel не выполняет навигацию и использует
существующее завершение drag без лишней записи.
9. Acceptance criteria и доказательства
| AC | Критерий | Обязательное доказательство |
|---|---|---|
| AC1 | Именованная комната с HA Area имеет одну .rlgo в View и одну в Plan; комната без HA Area не имеет её в обоих режимах. |
Расширенный demo/smoke_room_link.mjs. |
| AC2 | В View клик по .rlgo сохраняет существующую навигацию в /config/areas/area/.... |
demo/smoke_room_link.mjs. |
| AC3 | В Plan клик по .rlgo не навигирует, а pointerdown/drag с иконки использует room-label drag и сохраняет новую позицию. |
Browser smoke с перехватом route и фактическим pointer drag. |
| AC4 | Bounding boxes .rlname и .rlmetrics относительно anchor совпадают между View и Plan с допуском ≤0,5 CSS px при DPR 1 и 2. |
Новый/расширенный targeted Playwright smoke с числовым отчётом offsets. |
| AC5 | AC4 выполняется в light и dark theme; room settings button и resize handles не входят в core math и сохраняются в Plan. | Targeted smoke + focused light/dark golden review. |
| AC6 | Placeholder безымянной комнаты, отсутствие иконки у area-less room и room metrics сохраняют текущий контракт. | Browser regression cases и существующие room-card smokes. |
| AC7 | Пользовательский и тестовый контракты обновлены синхронно. | Обе записи changelog и правка docs/TESTING.md. |
| AC8 | Реализация проходит рабочие gates. | npm run typecheck, npm test, npm run build, targeted smoke до code review. |
| AC9 | Две ключевые регрессии доказаны mutation gate. | node scripts/mutation-gate.mjs --check и отдельные запуски mutants §10.3 с ожидаемым non-zero. |
10. План автотестов
10.1. Unit
Новая pure-логика не требуется. Если условие состава/событий будет вынесено в
отдельный helper, добавить table-driven unit для View/Plan × with/without area; иначе DOM и pointer-контракт проверяется browser smoke без искусственного
дублирования template в unit.
10.2. Browser smoke и golden
Расширить demo/smoke_room_link.mjs либо вынести числовую parity в отдельный
targeted smoke:
- включить room labels и выбрать именованную комнату с HA Area и metrics;
- в View записать offsets
.rlname/.rlmetricsот центра.roomlabel; - проверить View click-navigation;
- перейти в Plan, дождаться settled layout и повторить offsets;
- проверить допуск по каждой координате, отсутствие navigation от click и
успешный drag, начатый на
.rlgo; - повторить измерение в light/dark при DPR 1 и DPR 2;
- проверить area-less и unnamed editor cases, handles/settings button и
отсутствие
pageerror/необработанного console error.
Focused golden должен показывать одну и ту же комнатную карточку в View и Plan для light/dark. Ревью проверяет состав и визуальное выравнивание; числовой smoke остаётся каноническим доказательством допуска. Полные golden/smoke/performance выполняются перед бетой по runbook.
10.3. Executable mutation gate
Реализация регистрирует два entries в scripts/mutation-gate.mjs; точные
find/replace адаптируются к итоговому коду, но якорь встречается ровно один
раз, --check проходит, а каждый guard на мутанте завершается non-zero.
| Mutant id | Обязательная поломка | Guard |
|---|---|---|
plan-room-area-icon-hidden |
Вернуть прежнее условие, которое не рендерит .rlgo при активном Plan editor. |
Targeted room-link/parity smoke обязан провалить AC1/AC4. |
plan-room-area-icon-navigates |
Подключить в Plan к .rlgo View-обработчики _clickRoom() и остановку pointerdown. |
Targeted pointer smoke обязан провалить отсутствие route change и drag с иконки из AC3. |
11. Риски и меры
| Риск | Мера |
|---|---|
| Иконка появилась, но стала отдельной кнопкой и ломает drag. | AC3, реальный pointer sequence и отдельный mutant. |
| Навигацию случайно отключили и в View. | Независимая проверка View navigation AC2. |
| Сравнивается viewport, а не anchor, и тест флапает из-за editor chrome. | Измерять offsets только от центра .roomlabel после settled layout. |
| Исправлено DPR 1, но округление возвращает дрейф на HiDPI. | Один и тот же числовой контракт при DPR 1 и 2. |
| Handles или settings button расширяют core bounding box. | Измерять именованные дочерние элементы; AC5 проверяет editor controls отдельно. |
| Изменился контракт area-less/unnamed room. | Явные negative cases AC1/AC6. |
Performance-риск пренебрежимо мал: на комнату с HA Area добавляется только уже существующий DOM-узел в ещё одном режиме, без новых timers, subscriptions, network calls или расчётов по размеру плана. Security/privacy boundary не меняется.
12. Rollback
Frontend-изменение откатывается одним коммитом вместе с тестами и документацией. Данные и schema не меняются; rollback возвращает прежний визуальный сдвиг, но не требует миграции или восстановления конфигурации.
13. Release-артефакты
- пользовательские записи в
docs/CHANGELOG.mdиdocs/CHANGELOG.ru.mdв том же implementation-коммите (User-Visible: yes); - обновлённый контракт
docs/TESTING.mdдля room-link icon в Plan; docs/USER-GUIDE.md/docs/USER-GUIDE.ru.mdне требуют нового сценария: действие в View и drag в Plan не меняются; при реализации повторно проверить отсутствие противоречащей формулировки;- targeted browser smoke и focused light/dark golden evidence;
- синхронные build-артефакты по действующему D-контракту;
- отдельные backend, migration, performance и security artifacts не нужны;
- перед бетой выполняются общие golden, smoke и performance gates, Linux CI остаётся каноном полного HA harness.
14. Принятые решения и предположения
Решения владельца
- В Plan editor иконка присутствует визуально, но не открывает HA Area; с неё можно начинать drag комнатной карточки.
- «Не двигается ни на пиксель» измеряется относительно сохранённого/projected room-label anchor с допуском 0,5 CSS px при DPR 1 и 2, а не относительно viewport.
- Drag, четыре resize handles, placeholder безымянной комнаты и отдельная кнопка настроек сохраняются и не участвуют в layout core-карточки.
Принято предположительно, поменять свободно при ревью
- В Plan у иконки нет отдельного title/role/tab stop, чтобы визуальный элемент не обещал недоступную навигацию.
- Существующие
.roomlabelи внутренние styling hooks переиспользуются без нового renderer или wrapper. - Числовой browser smoke является главным доказательством геометрии; golden нужен как понятная человеку визуальная проверка, а не вместо измерений.