mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-02 04:38:55 +00:00
@@ -0,0 +1,211 @@
|
||||
# Issue #293 — активная рукоятка Resize обязана выполнять pointer-жест
|
||||
|
||||
- **Issue:** https://github.com/Matysh/houseplan-card/issues/293
|
||||
- **Статус:** первая редакция для внешнего ревью; канонический статус задаётся метками issue
|
||||
- **Тип / приоритет:** bug / P1
|
||||
- **Область:** production pointer pipeline safe Resize, live preview transaction,
|
||||
pointer capture, commit/cancel/Undo и browser regression smoke
|
||||
- **Модель данных:** schema и model version не меняются
|
||||
- **Связано:** #277, #289, #292; правила eligibility не расширяются
|
||||
|
||||
## 1. Подтверждённый дефект
|
||||
|
||||
На `test/fixtures/real-plan-second-floor.json` общая стена `room-a` edge 2 /
|
||||
`room-b` edge 2 имеет одинаковые endpoints и разрешена
|
||||
`resolveSafeResize`. Обе рукоятки активны. `pointerdown` создаёт корректный plan
|
||||
для двух комнат, но настоящие `pointermove` на 2–60 px не дают видимого preview,
|
||||
а `pointerup` не меняет geometry/history.
|
||||
|
||||
Чистая математика на том же plan исправна: `validateSafeResize`,
|
||||
`clampSafeResize` и `applySafeResize` принимают ненулевые шаги в обе стороны и
|
||||
изменяют только две комнаты. Значит, дефект находится в production integration
|
||||
между pointer event и live overlay/commit.
|
||||
|
||||
Текущее наблюдение `d=0, moved=false` не доказывает, что ошибка только в
|
||||
screen-to-SVG conversion: `_rszMove` также молча возвращает старое `d`, если
|
||||
`_rszApplyPreview` отвергает persistence metadata. Реализация обязана сначала
|
||||
зафиксировать точную причину отказа на полноценно инициализированной карточке, а
|
||||
затем исправить её. Маскировать ошибку прямым вызовом private methods нельзя.
|
||||
|
||||
## 2. Пользовательский результат
|
||||
|
||||
Если рукоятка выглядит активной, drag по нормали стены немедленно показывает
|
||||
live preview. Общая стена и обе комнаты движутся синхронно; стены, проёмы,
|
||||
virtual spans и thickness records следуют за своей геометрией. Pointerup делает
|
||||
ровно один commit и один Undo-шаг.
|
||||
|
||||
Если runtime preflight всё же обнаруживает конфликт, рукоятка не может молча
|
||||
остаться активной и инертной: предсказуемый конфликт должен быть отражён в
|
||||
eligibility до жеста, а непредсказуемый отказ во время жеста останавливает
|
||||
preview на последней безопасной позиции и один раз показывает локализованную
|
||||
причину. Нулевая позиция без объяснения запрещена.
|
||||
|
||||
## 3. Pointer contract
|
||||
|
||||
1. `pointerdown` на enabled handle сохраняет immutable geometry snapshot,
|
||||
`pointerId`, plan, options и начальную точку в SVG coordinates.
|
||||
2. Последующие события того же pointer вычисляют signed displacement как
|
||||
проекцию **дельты от начальной pointer point** на `plan.n`; место click вдоль
|
||||
самой стены не влияет на результат.
|
||||
3. Новая ось стены проходит grid snap один раз, затем `clampSafeResize`.
|
||||
4. Gesture получает pointer capture на `currentTarget` handle либо эквивалентный
|
||||
общий listener, поэтому продолжает работать за пределами круга.
|
||||
5. Pointer другого id, hover с `buttons=0`, pinch/pan и unrelated stage gesture
|
||||
не изменяют resize drag.
|
||||
6. `pointerup` коммитит только уже показанный валидный preview. `pointercancel`,
|
||||
`lostpointercapture` до нормального release и Esc отменяют overlay без записи.
|
||||
7. Один physical shared wall может иметь две owner circles, но started gesture
|
||||
привязан к одному immutable plan и не переключается на circle после rerender.
|
||||
|
||||
## 4. Live overlay contract
|
||||
|
||||
Preview строится из immutable pre-drag geometry и записывается только в
|
||||
`_rszPreview`; `_serverCfg` до pointerup не меняется. На каждом candidate:
|
||||
|
||||
- room polygons и openings берутся из одного `applySafeResize` result;
|
||||
- wall/open-span rekey выполняется из исходных spans, не из предыдущего frame;
|
||||
- число wall/open-span records и multiset `wall.cm` сохраняются;
|
||||
- общий physical geometry preflight проверяет exact overlay;
|
||||
- reject возвращает структурированную причину, а не просто `false`.
|
||||
|
||||
Предсказуемые metadata/preflight условия должны использоваться и eligibility
|
||||
#292. Непредсказуемый reject не сбрасывает успешно показанный preview. При первом
|
||||
кандидате он оставляет `moved=false`, но показывает локализованный toast; повтор
|
||||
того же reject в рамках жеста не спамит уведомления.
|
||||
|
||||
## 5. Реальный browser smoke
|
||||
|
||||
Regression test загружает production bundle и настоящую tracked fixture через
|
||||
полноценный `_serverCfg`/space path. Он входит в Plan через штатный mode path,
|
||||
выбирает Resize и отправляет browser pointer events по фактическим screen
|
||||
coordinates handle. Тесту запрещено вызывать `_rszMove`, `_rszApplyPreview`,
|
||||
`applySafeResize` или менять rooms после начала жеста.
|
||||
|
||||
Private state можно читать только как дополнительную диагностику; pass/fail
|
||||
основан на DOM preview, persisted config, history и invariants. Smoke обязан
|
||||
сначала доказать, что fixture space действительно является текущим объектом
|
||||
`_serverCfg.spaces`, чтобы неполный setup не выдавал ложный no-op.
|
||||
|
||||
## 6. Scope
|
||||
|
||||
### Входит
|
||||
|
||||
- точная локализация и исправление enabled pointer no-op;
|
||||
- pointer origin/capture и устойчивость к rerender;
|
||||
- структурированный reject live preview и пользовательская обратная связь;
|
||||
- реальный fixture browser smoke на 10 grid steps и 60 px;
|
||||
- cancel, commit, Undo, metadata и geometry invariants;
|
||||
- RU/EN i18n для runtime reject, если существующего текста недостаточно.
|
||||
|
||||
### Не входит
|
||||
|
||||
- изменение правил, какие стены считаются eligible (#292/#289/#290);
|
||||
- разрешение partial-shared, mixed-role, diagonal или >2-room Resize;
|
||||
- новый resize UX, новая кнопка подтверждения или новый режим;
|
||||
- изменения schema/storage;
|
||||
- исправление геометрии real fixture через прямую мутацию теста.
|
||||
|
||||
## 7. Acceptance criteria
|
||||
|
||||
### AC1. Реальный enabled drag работает
|
||||
|
||||
Production-bundle smoke на `room-a` edge 2 / `room-b` edge 2 выполняет
|
||||
pointerdown, движение ровно на 10 grid steps и pointerup. До release DOM/live
|
||||
geometry показывает 10 шагов, persisted geometry неизменна; после release обе
|
||||
комнаты изменены ровно на 10 шагов, shared endpoints совпадают.
|
||||
|
||||
### AC2. Движение за пределами handle не теряется
|
||||
|
||||
Отдельный drag проходит не менее 60 screen px по нормали. Все intermediate
|
||||
events одного pointer доходят до жеста, preview монотонно следует safe clamp.
|
||||
Mutation, удаляющая capture/common listener, убивается этим smoke.
|
||||
|
||||
### AC3. Pointer delta не зависит от click вдоль стены
|
||||
|
||||
Три старта — midpoint и точки у обоих концов hit area — при одинаковой дельте
|
||||
по нормали дают один `d`. Тангенциальное движение даёт ноль; reversed owner
|
||||
copy даёт тот же physical target, а не противоположный результат.
|
||||
|
||||
### AC4. Один commit и Undo
|
||||
|
||||
Pointerup после ненулевого preview создаёт одну history command и одну config
|
||||
write. Undo восстанавливает byte-equivalent pre-drag room/opening/wall/open-span
|
||||
geometry. Synthesized click не меняет selection и не создаёт второй commit.
|
||||
|
||||
### AC5. Esc и interruption безопасны
|
||||
|
||||
Esc, pointercancel и lost capture до release удаляют overlay, оставляют config
|
||||
и history неизменными и не запускают отложенную запись. Событие другого
|
||||
`pointerId` игнорируется.
|
||||
|
||||
### AC6. Preview reject не молчит
|
||||
|
||||
Targeted test заставляет persistence/preflight отвергнуть первый candidate.
|
||||
Geometry не меняется, `_serverCfg` не мутируется, пользователь получает ровно
|
||||
один локализованный toast. Если reject детерминирован до pointerdown, handle
|
||||
disabled через #292 вместо runtime toast.
|
||||
|
||||
### AC7. Metadata и topology сохраняются
|
||||
|
||||
После реального drag проходят room simplicity/orientation, shared ownership,
|
||||
wall keys, mixed-role records, opening host/fit и physical render preflight.
|
||||
Vertex counts/order, число wall/open-span records и multiset `wall.cm`
|
||||
неизменны; затронуты не более двух комнат.
|
||||
|
||||
### AC8. Harness не может дать ложную зелень
|
||||
|
||||
Smoke падает, если fixture не находится в текущем `_serverCfg`, handle disabled,
|
||||
preview отсутствует, persisted config меняется до release или движение сделано
|
||||
private method. Mutation, возвращающая `d` к нулю либо заставляющая
|
||||
`_rszApplyPreview` отвергать candidate, убивает smoke.
|
||||
|
||||
### AC9. Существующие сценарии не регрессируют
|
||||
|
||||
Outer wall, synthetic exact pair, clamp у угла, disabled handle, opening move,
|
||||
preflight failure и cancellation в `smoke_room_resize` остаются зелёными.
|
||||
|
||||
### AC10. Локальные гейты
|
||||
|
||||
- `npm run typecheck`;
|
||||
- `npm test`;
|
||||
- `npm run build` и bundle parity;
|
||||
- `node scripts/check-docs.mjs`;
|
||||
- targeted real-plan pointer smoke и mutation.
|
||||
|
||||
Полные golden, smoke, performance и Linux HA harness выполняются перед beta.
|
||||
|
||||
## 8. Совместимость, touch, security и performance
|
||||
|
||||
Schema/storage не меняются. Desktop mouse является обязательным контрактом.
|
||||
Touch editor остаётся best effort, но safety floor обязателен: pinch/pan и
|
||||
pointercancel не коммитят resize, а active single-pointer drag не теряется.
|
||||
|
||||
Новых HA actions/security boundaries нет. Pointermove сохраняет существующий
|
||||
bounded `clampSafeResize` и snapshot cache; нельзя сериализовать config или
|
||||
пересчитывать весь model сверх текущего preview path более одного раза на
|
||||
candidate. Safe Resize performance budget из `docs/RESIZE.md` сохраняется.
|
||||
|
||||
## 9. Ожидаемые файлы
|
||||
|
||||
- `src/houseplan-card.ts`, при необходимости `src/resize.ts`;
|
||||
- `src/i18n/en.json`, `src/i18n/ru.json`, если добавляется runtime reason;
|
||||
- `demo/smoke_room_resize.mjs` либо отдельный real-plan pointer smoke;
|
||||
- unit/production-path/mutation tests;
|
||||
- `docs/RESIZE.md`, `docs/TESTING.md`, при необходимости `docs/ARCHITECTURE.md`;
|
||||
- `docs/CHANGELOG.md`, `docs/CHANGELOG.ru.md`.
|
||||
|
||||
## 10. Release
|
||||
|
||||
Implementation-коммит имеет `Issue: #293`, `User-Visible: yes` и обновляет оба
|
||||
changelog. Issue не закрывается вручную: она закрывается выпуском beta.
|
||||
|
||||
## 11. Принятые предположения
|
||||
|
||||
1. Скриншот владельца и реальная fixture имеют приоритет над ограничением
|
||||
первоначального программно собранного стенда; новый smoke обязан устранить
|
||||
это различие setup.
|
||||
2. Structured preview reject — часть внутреннего API, а не новый persisted
|
||||
формат.
|
||||
3. Если root cause окажется только в неполной инициализации старого smoke, это
|
||||
не считается исправлением пользовательского бага без воспроизведения и
|
||||
зелёного pointer path на конфигурации, эквивалентной реальному приложению.
|
||||
Reference in New Issue
Block a user