diff --git a/docs/specs/400-beta-polish.md b/docs/specs/400-beta-polish.md new file mode 100755 index 00000000..018646e2 --- /dev/null +++ b/docs/specs/400-beta-polish.md @@ -0,0 +1,191 @@ +# ТЗ #400 — Полиш v1.70.0-beta.1: ручки мебели, вес помощи, мёртвое исключение + +- Issue: https://github.com/Matysh/houseplan-card/issues/400 +- Приоритет: P3, polish; полный трек — три несвязанные поверхности (SVG-рамка + редактора декора, состав ленивых чанков, направляющие выравнивания), критерий + «одна поверхность» для `small` не выполняется — прецеденты #369, #385 +- Ревизия: 1 (2026-08-31) + +## Сценарий + +Три шероховатости свежей беты, каждая мала по отдельности. Владелец расставляет +мелкую мебель (тумба 40 см) и замечает, что за угол рамки ухватиться труднее, +чем за бок. Пользователь на медленном канале грузит карточку чуть тяжелее, +чем нужно: тексты подсказок редактора приезжают вместе с холодным View. +Разработчик читает код направляющих и видит исключение, которое ничего не +исключает. + +## Что человек увидит до и после + +**До**: (1) на мебели уже примерно 80 см осевая ручка лежит поверх угловой и +забирает часть её зоны — угол остаётся доступен, но площадь попадания меньше; +(2) 38 строк помощи (EN 3,0 КБ + RU 6,3 КБ сырого текста) едут в initial-чанке, +хотя видны только в редакторе; (3) видимой разницы нет — это внутренняя правка. +**После**: угол мелкой мебели тянется так же уверенно, как бок; вес initial не +несёт редакторских текстов (либо решение зафиксировано осознанно); направляющие +опираются на актуальный источник перетаскивания. + +## Проблема и контракты по пунктам + +### (1) M5 — осевые ручки перекрывают угловые на мелкой мебели + +`src/houseplan-card.ts:8439` задаёт единый радиус хита +`hr = max(view.w, view.h) * 0.018`, одинаковый для угловых (`:8467`) и осевых +(`:8471`) ручек. Осевые рисуются **после** угловых, поэтому в зоне пересечения +хит забирают они. + +**Замер, а не расчёт** (браузер, режим декора, `.dtframe` на реальной мебели): +радиус обеих ручек — **14 px** на экране; перекрытие возникает, когда +полуширина объекта меньше `2·hr`, то есть примерно до 80 см. + +**Уточнение серьёзности против формулировки аудита.** В отчёте было сказано, +что «пропорциональный ресайз углом становится недоступен». Это неверно: центр +угловой ручки лежит **вне** круга осевой (для 40 см — 20 единиц плана против +радиуса 18), попасть в угол можно, сужается лишь зона. Поэтому пункт остаётся +полишем, а не багом. Честная формулировка: **на мебели до ~80 см зона угловой +ручки урезана примерно вдвое, потому что осевая лежит поверх неё**. + +**Контракт**: у угловой ручки приоритет хита над осевой. Достигается порядком +отрисовки (угловые кладутся последними) и/или скрытием осевых, когда сторона +меньше `4·hr` — на такой стороне осевая ручка всё равно неотличима от угловых. +Выбор — за реализацией; критерий один: на мебели 40 см клик в геометрический +угол рамки достаётся угловой ручке. + +### (2) Low «б» — тексты помощи в холодном чанке + +38 ключей помощи #86 лежат в `dist/houseplan-assets/houseplan-card-*.js` +(проверено grep'ом по собранному дереву), а не в ленивом чанке редактора. Это +не дефект #86: словари `en` и `ru` по архитектуре ленивой поставки всегда в +initial, лениво грузятся только `de` и `fr`. Но текст, который виден +исключительно в редакторе, оплачивается каждым холодным просмотром: запас +бюджета сейчас 15 945 Б при потолке 300 000, а за сутки беты израсходовано +6,1 КБ. + +**Контракт**: решение принимается явно и записывается. Два честных исхода: + +1. вынести `*.help*`-ключи в ленивый чанк редактора (он и так грузится, когда + помощь может понадобиться) — тогда initial худеет на величину, которую + нужно замерить и указать в отчёте; +2. зафиксировать, что `en`/`ru` неделимы, и планировать бюджет с этим знанием + — тогда в `docs/ARCHITECTURE.md` появляется строка о том, что стоимость + редакторских текстов входит в initial по построению. + +Исход (1) предпочтителен, если механика ленивых словарей допускает раздельные +секции без второго сетевого запроса на каждое открытие подсказки; если нет — +честнее (2), чем ломать поставку ради 9 КБ. + +### (3) Low «г» — исключение по мёртвому источнику + +`src/houseplan-editor-runtime.ts:10970` в ветке `_mode === 'devices'` +пропускает перетаскиваемый маркер по `this.host._drag?.id`. Но перетаскивание +устройств после #74 живёт в отдельном состоянии `_deviceDrag` +(`houseplan-card.ts:2548`, заполняется на `:6736`), а `_drag` (`:2547`) в этом +режиме остаётся `null` — проверено чтением обоих объявлений и всех присваиваний. + +Следствие: маркер, который тянут, попадает в собственный список кандидатов +выравнивания. Видимого сбоя нет только потому, что `alignGuides` +(`src/logic.ts:1981`) сравнивает точку с кандидатами по допуску, и точка +совпадает сама с собой — направляющая рисуется «от себя к себе» и визуально +неотличима от честной. `demo/smoke_align_guides.mjs` после `a6b0850a` +проверяет `guides() >= 1` и такую подмену не различает. + +**Контракт**: исключение опирается на актуальный источник перетаскивания +(`_deviceDrag?.id` в режиме устройств), а смок различает направляющую от +другого маркера и направляющую от самого себя. + +## Скоуп / не-скоуп + +**В скоупе**: `_renderTextFrame` в `src/houseplan-card.ts` (порядок и/или +видимость ручек), состав ленивых чанков для help-ключей либо запись решения, +`_alignCandidates` в `src/houseplan-editor-runtime.ts`, +`demo/smoke_align_guides.mjs`, тесты и мутанты. + +**Не в скоупе**: радиус ручек (`hr`, решение владельца 2026-08-05 «уменьшить в +4 раза»), механика ресайза мебели (#383), содержание текстов помощи (#86), +архитектура ленивой поставки словарей (#352–#355). + +## UX + +Пункт (1) видим: угол мелкой мебели становится так же надёжен, как бок. +Пункты (2) и (3) видимого поведения не меняют. + +## Модель данных и миграция + +Не применимо: ни конфиг, ни layout не затрагиваются. + +## i18n + +Новых строк нет. При исходе (1) пункта (2) ключи не меняются — меняется чанк, +в котором они лежат. + +## Критерии приёмки + +- **AC1**. На мебели 40 см клик в геометрический угол рамки достаётся угловой + ручке, а не осевой. Доказательство: браузерный смок с синтетическим + `pointerdown` в точку угла и перехватом обработчика — `elementFromPoint` + через shadow root недостаточно (проверено: возвращает саму карточку). +- **AC2**. На крупной мебели (160 см) поведение не меняется: доступны и + угловые, и осевые ручки. Доказательство: тот же смок, вторая ветка. +- **AC3**. Пункт (2) закрыт явным решением: либо help-ключи не попадают в + initial-чанк (доказательство: `grep` по собранному дереву + замер бюджета до + и после), либо решение «`en`/`ru` неделимы» записано в + `docs/ARCHITECTURE.md` с причиной. +- **AC4**. В режиме устройств перетаскиваемый маркер исключён из кандидатов + выравнивания по `_deviceDrag`. Доказательство: юнит на `_alignCandidates` — + при активном `_deviceDrag` список не содержит позицию тянущегося маркера. +- **AC5**. Смок направляющих различает «от другого маркера» и «от себя»: + проверка падает, если исключение убрать. Доказательство: отрицательный + прогон, зафиксированный в документе ревью. +- **AC6**. Бюджет initial не растёт: при исходе (1) он уменьшается, при + исходе (2) остаётся прежним. Доказательство: `npm run bundle:budget` до и + после. + +## План автотестов + +**Unit** (`test/align-candidates.test.mjs` либо существующий файл): + +1. `_alignCandidates` в режиме `devices` при активном `_deviceDrag` не + возвращает позицию тянущегося маркера (AC4). +2. Тот же вызов без активного перетаскивания возвращает все маркеры + пространства — исключение не «съедает» лишнего. + +**Browser smoke** (`demo/smoke_furniture_polish.mjs` — дополнение, +`demo/smoke_align_guides.mjs` — правка): + +3. Мебель 40 см: `pointerdown` в геометрический угол рамки вызывает + обработчик угловой ручки (AC1). +4. Мебель 160 см: доступны обе ручки, поведение прежнее (AC2). +5. Направляющие: при перетаскивании маркера направляющая строится от ДРУГОГО + маркера, а не от самого себя (AC5). + +**Мутанты** (`scripts/mutation-gate.mjs`): + +- `furniture-edge-handles-steal-the-corner`: вернуть прежний порядок + отрисовки → смок AC1 красный. +- `align-guides-exclude-dead-source`: вернуть `_drag?.id` в ветке устройств → + юнит AC4 и смок AC5 красные. + +## Риски + +- **Порядок отрисовки меняет и видимую рамку.** Бусины (`.dtknob`) рисуются + рядом с хит-кругами; перестановка не должна менять картинку. Смягчение: + golden-кадры рамки мебели — если они есть в матрице, они это и покажут; при + расхождении кадр пересматривается осознанно. +- **Скрытие осевых ручек ниже порога может удивить.** Пользователь, привыкший + тянуть за бок, на мелкой мебели его не найдёт. Смягчение: предпочтителен + порядок отрисовки, а не скрытие; скрытие — только если порядок не решает. +- **Вынос help-ключей ломает ленивую загрузку словаря.** Второй сетевой запрос + на открытие подсказки хуже, чем 9 КБ в initial. Смягчение: исход (2) + остаётся честным ответом, если механика не позволяет. + +## Откат + +Три независимые правки. Пункт (1) — порядок двух блоков в шаблоне; пункт (2) +при исходе (1) — состав чанка, при исходе (2) — строка документации; пункт (3) +— одно поле в условии. Данные пользователя не затрагиваются. + +## Release-артефакты + +- `docs/CHANGELOG.md` / `docs/CHANGELOG.ru.md`: один пункт про угол мелкой + мебели (User-Visible: yes). Пункты (2) и (3) — `User-Visible: no`. +- Скриншоты: пересматриваются, только если golden покажет расхождение рамки.