mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
Пятнадцать расхождений из #667. Бюджет initial View назван одним источником (scripts/bundle-budget.mjs). CONTRIBUTING описывает бандл после #657, HA-харнесс после #630 и релиз через release-contract и release.yml. Мутанты указывают на реестр scripts/mutation-registry.mjs. STATUS и ROADMAP больше не держат PR в HACS «в очереди» и инструкции прежней песочницы. SCOPE называет три редактора, форматы плана и радар #485. README выводят первую комнату через «Стены». Русское руководство сверено с v1.78.0-beta.5, таблица «Источник плана» склеена. UX-MODES и STYLING-HOOKS следуют коду: проёмы в просмотре инертны, space-card рисует проёмы и декор-изображения. Починены якорь в DEVICE-PRESENTATION и пол зума 1/3. ADR 089/122/160 и ISOMETRIC помечают активацию через hp_alpha исторической. Из раскладки ARCHITECTURE убран несуществующий src/data. legacy/README не называет docs/superpowers действующими спецификациями. Паритет английского руководства (п. 8) выделен в #668. Issue: #667 User-Visible: no Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
805 lines
54 KiB
Markdown
805 lines
54 KiB
Markdown
# Геометрия: стены, проёмы, комнаты, холст
|
||
|
||
> Приложение к [`docs/TESTING.md`](../TESTING.md): перенесено оттуда дословно (#634).
|
||
> Индекс всех приложений — [`README.md`](README.md).
|
||
|
||
## Stable wall-segment identity (#282)
|
||
|
||
- [ ] Wall junction limits (#329): drawing refuses an apex under 15°, a seventh
|
||
wall in one node, a wall shorter than 20 cm or than its own thickness,
|
||
nodes closer than 5 cm and a room with under 25 cm² of interior, each
|
||
through the surface's own channel — a toast naming the rule for drawing
|
||
and Thickness, a stopped wall for Resize. A T-joint stays legal, a short
|
||
filler atom compensating a thickness step stays legal (length is measured
|
||
along the collinear same-thickness wall run), and an inherited violation
|
||
never blocks an unrelated edit
|
||
[unit: junction-limits; auto: smoke_junction_limits, smoke_island_rooms;
|
||
mutants: junction-limit-angle-not-enforced,
|
||
junction-limit-write-gate-removed, degenerate-apex-bevelled-again].
|
||
- [ ] A degenerate sharp apex renders as ONE point on both faces — no flat
|
||
chamfer, no bow-tie fold, no jags between the inner and outer vertex; the
|
||
room ring of the #329 fixture triangle has exactly three distinct
|
||
vertices [unit: junction-limits §4].
|
||
- [ ] Shared fixture `test/fixtures/282-wall-identity-parity.json` produces the
|
||
same exact v8 catalog, room references, opening host and draft IDs in
|
||
TypeScript and Python.
|
||
- [ ] Initial v7 migration is deterministic and idempotent; new post-v8 atoms
|
||
use UUIDs. A split/promoted draft keeps one documented carrier ID, while
|
||
reserved/colliding deterministic IDs receive stable `-2`, `-3` suffixes.
|
||
- [ ] The three structural writer families — interactive commit, Undo/Redo
|
||
restore and Optimize — are enumerated by the source guard and each has an
|
||
independent bypass mutant in `scripts/mutation-registry.mjs`. A rejected
|
||
migration changes neither config, Undo history nor revision.
|
||
- [ ] Full/space imports cover v7→v7 (no upgrade), v7→v8 and v8→v8; copy/merge
|
||
remaps every ID and reference together. A byte-equivalent legacy-client
|
||
round-trip of v8 is accepted, while a structural legacy change is rejected.
|
||
- [ ] Resize, Split/Merge, opening edit and Optimize browser smokes retain wall
|
||
thickness and ownership across reload. Performance gate:
|
||
`npm run benchmark:wall-model` materialises 10,000 atoms with p95 below
|
||
500 ms on the reference Windows machine.
|
||
- [ ] Local commands: `npm test`, `npm run typecheck`,
|
||
`npm run benchmark:wall-model`; backend parity/schema tests run through
|
||
`tests_backend/test_wall_segment_model.py` and
|
||
`tests_backend/test_validation.py`. HA import/export coverage runs in the
|
||
normal Linux/CI Home Assistant harness when unavailable natively.
|
||
|
||
## Legacy draft migration and atomic wall-chain writes (#314, #478)
|
||
|
||
- [ ] `demo/smoke_v8_draft_write.mjs` (compatibility filename) proves that a
|
||
model-v9 draft migrates once to ordinary partitions, preserves existing
|
||
edge IDs/thicknesses and leaves no `room_drafts` in model v10.
|
||
- [ ] The same fake-WS smoke proves each accepted current wall reaches storage,
|
||
a later edge preserves earlier identities, Undo removes only the terminal
|
||
wall, and a closed chain creates a durable room without carrier debris.
|
||
- [ ] A rejected in-flight physical write synchronously rolls back its whole
|
||
pending batch: active path, session partition IDs, pending map and command
|
||
history are empty, so no optimistic ghost wall survives.
|
||
- [ ] Frontend/backend model tests consume the same
|
||
`478-room-draft-migration-vectors.json`: missing/null IDs, colliding IDs,
|
||
numeric strings, `null`/boolean thickness and epsilon-length edges must
|
||
produce the same exact partitions or rejection reason in both runtimes.
|
||
Backend coverage also rejects a stale v9 `room_drafts` write over v10.
|
||
- [ ] `demo/smoke_unified_wall_tool.mjs` accepts a room over a partially
|
||
coincident older partition, preserves only its outside tail and an
|
||
unrelated partition, then proves an immediate Optimize reports zero
|
||
reconciliation and no config change. A forced missing post-mutation wall
|
||
model restores the whole room transaction.
|
||
- [ ] Local commands: `npm test`, `npm run bundle:sync`,
|
||
`node demo/smoke_v8_draft_write.mjs`, targeted backend pytest and
|
||
`node scripts/check-docs.mjs --external`.
|
||
- [ ] Mutation `current-rejected-physical-write-keeps-optimistic-wall` proves
|
||
the browser rollback scenario fails when rejection recovery is bypassed.
|
||
- [ ] Mutation `room-accept-leaves-coincident-partitions` proves the real editor
|
||
smoke fails if accepted room carriers stop being consumed atomically.
|
||
|
||
## Current-writer fixed point (#477)
|
||
|
||
- [ ] `test/writer-fixed-point.test.mjs` proves seed-bounded repeated collinear
|
||
merge, safe positive coincident reconciliation/opening rehost, identical
|
||
fail-closed cases, exact room-reference rewrite/restore and the executable
|
||
writer-owner manifest.
|
||
- [ ] `demo/smoke_writer_fixed_point.mjs` finishes a real straight Walls chain
|
||
through the production bundle, then proves the immediate, post-Undo and
|
||
post-Redo Optimize previews are no-ops. The same smoke deletes a room and
|
||
restores/reapplies direct and cross-space vacuum references without
|
||
overwriting unrelated marker fields.
|
||
- [ ] `demo/benchmark_wall_draw_click.mjs` keeps the #461 terminal-click budgets
|
||
and structural counters, then separately proves finish uses one bounded
|
||
physical/junction transaction, one write, no extra history and sublinear
|
||
scaling when unrelated rooms are added.
|
||
- [ ] `test/align-grid.test.mjs` proves Optimize leaves complete furniture/image
|
||
transforms byte-equivalent while an ordinary decor rectangle remains
|
||
grid-bound.
|
||
- [ ] The `writer-*` mutations in `scripts/mutation-registry.mjs` remove one finish
|
||
owner, pre-adoption safety, history normalization, direct/vacuum reference
|
||
rewrite, free-transform exclusion, terminal-click separation, seed merge
|
||
and seed reconciliation; each named witness must turn red.
|
||
|
||
## Resize: реальный pointer pipeline (#293)
|
||
|
||
- [ ] `demo/smoke_resize_pointer_real_plan.mjs` загружает tracked fixture
|
||
второго этажа обычным `houseplan/config/get`, включает Resize кнопкой и
|
||
двигает доступную общую стену только реальными `page.mouse` событиями.
|
||
Прямые вызовы приватных resize-методов в этом smoke запрещены source
|
||
guard-юнитом.
|
||
- [ ] На десятом шаге сетки обе комнаты имеют видимый preview, а server config
|
||
ещё байт-в-байт исходный. Pointerup создаёт одну history-команду и одну
|
||
запись; wall count и набор толщин сохраняются; Ctrl+Z возвращает исходную
|
||
геометрию.
|
||
- [ ] Pointer capture продолжает жест минимум в двух диаметрах и 120 px от
|
||
хэндла, чужой
|
||
pointer id игнорируется, а Escape и `lostpointercapture` возвращают DOM и
|
||
config без дополнительной записи.
|
||
- [ ] Невозможная физическая preview-геометрия останавливает стену на последней
|
||
безопасной позиции и показывает один локализованный toast за жест. Отдельно
|
||
проверяется отказ финального preflight без commit/history.
|
||
- [ ] Мутанты `resize-pointer-delta-zeroed`,
|
||
`resize-shared-seam-not-coalesced`, `resize-pointer-capture-removed`,
|
||
`resize-preview-reject-silent` и
|
||
`safe-resize-commit-preflight-bypassed` обязаны красить соответствующие
|
||
unit/production smoke guards.
|
||
- [ ] Fixed-topology wall records (#298): moving-wall breakpoints translate
|
||
rigidly, side-wall interior endpoints never scale proportionally,
|
||
the exact first-floor 49→52 gesture ends on 17/52/57/101, unrelated
|
||
records remain byte-equivalent, and a full-span carrier/lattice proof
|
||
rejects gaps before preview. Key-only legacy records move only by one
|
||
whole-edge identity; partial midpoint ambiguity produces no preview,
|
||
history or config write [unit: `wall-thickness.test.mjs`; auto:
|
||
`smoke_resize_pointer_real_plan`, `smoke_resize_wall_thickness`, six
|
||
`smoke_edit_walk` runs; mutation:
|
||
`safe-resize-wall-endpoints-affine-scaled`,
|
||
`safe-resize-legacy-midpoint-fail-open`].
|
||
|
||
## Opening symbol centreline (#242, #250)
|
||
|
||
- [ ] Unit and browser checks prove that door/window/gate stay on the wall
|
||
centreline for both `flip_v` values, door/window flips change only their
|
||
direction, and gate `flip_v` reverses the first-leaf 10° turn on shared room walls,
|
||
independent partitions and hidden Iso without translating the gate
|
||
[unit: `opening-symbol.test.mjs`, `iso-openings.test.mjs`; auto:
|
||
`smoke_wall_thickness.mjs`, `smoke_isometric_contract.mjs`; mutations:
|
||
`opening-symbol-flip-restores-edge-offset`,
|
||
`opening-gate-flip-cancels-turn`].
|
||
- [ ] Matrix v37 adds four dedicated semantic scenes. Before PNG comparison
|
||
they assert the saved flip value, wall centreline, visible-group offset,
|
||
full jamb depth, window glass membership and opposite gate turn signs:
|
||
`opening-symbol-room-wall-light`,
|
||
`opening-symbol-diagonal-partition-dark`,
|
||
`opening-symbol-flip-pairs-light`,
|
||
`isometric-opening-symbol-parity-dark`.
|
||
- [ ] #250 reuses those four scenes and requires `offset: center` for every
|
||
door/window/gate entry, including flipped pairs. The semantic guard must
|
||
fail before PNG comparison if any saved flip restores a wall-face offset.
|
||
- [ ] The exact existing golden impact set below contains **67** scenes. It was
|
||
measured by comparing `actualSha256` for HEAD and `origin/dev` under the
|
||
same Chromium build; baseline status alone is not used because `dev`
|
||
already has unrelated pending pre-release candidates. Every listed frame
|
||
uses a shared fixture containing an affected opening or retains that plan
|
||
behind an editor/dialog. No other existing frame changed:
|
||
|
||
`isometric-geometry-view-dark`, `isometric-geometry-view-light`,
|
||
`isometric-live-layers-dark`, `isometric-no-borders-dark`,
|
||
`isometric-touch-kiosk-dark`, `isometric-large-warm-remount-dark`,
|
||
`geometry-view-dark-fit`, `geometry-view-light-fit`,
|
||
`room-label-parity-view-dark`, `room-label-parity-plan-dark`,
|
||
`room-label-parity-view-light`, `room-label-parity-plan-light`,
|
||
`day-cycle-dawn-dark`, `day-cycle-day-dark`, `day-cycle-dusk-dark`,
|
||
`day-cycle-night-dark`, `geometry-plan-editor-dark`,
|
||
`space-tab-drop-before-light`, `space-tab-drop-after-dark`,
|
||
`plan-snap-endpoint-light`, `plan-snap-line-gaps-dark`,
|
||
`junction-patch-resilience-plan-dark`,
|
||
`opening-placement-door-thick-wall-dark`,
|
||
`opening-placement-passage-thick-wall-dark`,
|
||
`opening-placement-passage-thick-wall-light`,
|
||
`geometry-devices-editor-dark`, `geometry-decor-editor-dark`,
|
||
`tray-wide-selection-en`, `tray-wide-tool-ru`,
|
||
`tray-medium-group-en`, `tray-medium-selection-ru`,
|
||
`tray-narrow-palette-en`, `tray-narrow-tool-ru`,
|
||
`geometry-diagonal-45-opening-dark`, `openings-thick-wall-dark`,
|
||
`lighting-glow-sun-dark`, `device-value-badge-positions-dark`,
|
||
`device-icon-state-table-light`, `device-icon-state-table-dark`,
|
||
`device-text-shell-long-light`, `device-text-shell-long-dark`,
|
||
`lighting-sun-window-state-only-dark`,
|
||
`lighting-fill-light-axis-split-dark`,
|
||
`lighting-fill-temp-axis-split-dark`,
|
||
`lighting-fill-lqi-axis-split-dark`, `lighting-temp-glow-dark`,
|
||
`lighting-temp-glow-light`, `lighting-custom-glow-dark`,
|
||
`lighting-opaque-glow-two-doorways-dark`,
|
||
`lighting-custom-glow-light`, `lighting-temp-glow-no-sources-dark`,
|
||
`lighting-temp-glow-room-override-dark`,
|
||
`lighting-manual-auto-spill-overlap-dark`, `hover-over-glow-dark`,
|
||
`hover-nested-room-dark`, `large-house-zoom-040-dark`,
|
||
`large-house-zoom-250-dark`, `large-house-warm-remount-dark`,
|
||
`device-dialog-desktop-en`, `device-help-popover-light-ru`,
|
||
`decor-color-popover-desktop-en`, `general-color-popover-desktop-en`,
|
||
`space-room-color-popover-desktop-ru`,
|
||
`backup-full-preview-desktop-en`,
|
||
`backup-plan-only-export-desktop-en`,
|
||
`optimize-preflight-dialog-dark-en`,
|
||
`optimize-preflight-dialog-light-ru`.
|
||
- [ ] Baselines for the 67 existing and four dedicated scenes are accepted
|
||
only from the reviewed full Linux pre-beta artifact. Local
|
||
`golden:accept` remains forbidden.
|
||
|
||
## Empty-space lifecycle (#113)
|
||
|
||
- [ ] Active selection keeps active-or-first compatibility, while an empty
|
||
model returns `undefined`; exact lookup of a stale saved id never falls
|
||
back to another space [unit: `space-model-selection.test.mjs`].
|
||
- [ ] There are no unguarded `_spaceModel().…` dereferences, explicit-id calls
|
||
use `_spaceModelById()`, and marker/position persistence validates its target
|
||
before config/file/WS side effects
|
||
[unit: `optional-space-model-contract.test.mjs`].
|
||
- [ ] Delete the last space while an editor gesture and debounced write are
|
||
active: the empty card renders, View is restored, pointer/draft/dialog
|
||
state is cleared, the pending write is cancelled and Add space still
|
||
opens Create. Recreate a plan, then receive an empty WS config and repeat
|
||
under a theme/resize/read-only tick [auto: `smoke_optional_space_model`].
|
||
- [ ] Removing the authoritative empty-state cleanup makes that smoke red
|
||
[mutation: `empty-space-cleanup-disabled`].
|
||
|
||
## Fixed card space (#210)
|
||
|
||
- [ ] `floor` resolves exact stable IDs and zero-based finite integer indexes;
|
||
quoted numeric strings stay IDs, and explicit empty, unknown, fractional,
|
||
negative or out-of-range values fail closed [unit: `initial-load.test.mjs`].
|
||
- [ ] Three coexisting instances (fixed ID, fixed index and unpinned) keep
|
||
independent authority while sharing the legacy navigation key. Fixed
|
||
cards ignore hash, tabs, guarded internal transitions, warm remount,
|
||
kiosk swipe/cycle/dots and never read or write saved navigation. Invalid
|
||
config renders an accessible error without a spatial stage, while the
|
||
unpinned card still restores legacy navigation
|
||
[auto: `smoke_fixed_floor.mjs`, `smoke_nav_persist.mjs`, `smoke_kiosk.mjs`].
|
||
- [ ] The GUI offers stable IDs only, preserves an existing numeric YAML value
|
||
during unrelated edits and deletes the `floor` property when cleared
|
||
[unit: `fixed-floor-contract.test.mjs`; auto: `smoke_fixed_floor.mjs`].
|
||
- [ ] Bypassing the shared transition guard makes the focused browser scenario
|
||
red; before-fix evidence also records that `origin/dev` has no fixed
|
||
resolver or guarded transition
|
||
[mutation: `fixed-floor-transition-guard-bypassed`].
|
||
|
||
## Open passage (#157)
|
||
|
||
- [ ] Подменю и диалог показывают четвёртый тип в порядке Окно / Дверь /
|
||
Открытый проём / Ворота; новый проём имеет ширину 90 см. [auto: open-passage-contract, opening-placement]
|
||
- [ ] В Plan/View у passage отсутствуют створка, дуга, рамка и пунктир, но
|
||
сохраняются hitbox, wall cut и room-coloured tunnel. [auto: opening-symbol, smoke_open_passage]
|
||
- [ ] Static вырезает и заполняет тоннель только для passage, не меняя старые
|
||
door/window/gate. [auto: space-geometry, smoke_open_passage]
|
||
- [ ] Внутренний passage пропускает Glow, внешний и неизвестный будущий тип
|
||
остаются fail-dark. [auto: light-visibility, smoke_open_passage]
|
||
- [ ] Passage в скрытой изометрии имеет full-height cut и zero leaves.
|
||
[auto: iso-openings, golden]
|
||
- [ ] Смена типа предупреждает о датчике/замке; Save удаляет пять
|
||
неприменимых ключей, Cancel не меняет config. [auto: open-passage-contract, smoke_open_passage]
|
||
- [ ] Full/space import отвергает forged binding до preview, а старое битое
|
||
значение можно прочитать и очистить. [auto: test_validation, test_ha_import_export]
|
||
- [ ] Пять passage-мутантов из `scripts/mutation-registry.mjs` пойманы своими
|
||
guards до передачи в review. [auto: mutation-gate]
|
||
|
||
## Independent-wall openings and structural axes (#132, #185)
|
||
|
||
- [ ] Door/window/gate/passage placement on a finished independent wall stores
|
||
`host.kind/id/t`; a coincident room wall chooses that explicit host, while
|
||
crossing or duplicate-host ties are rejected. [auto: opening-placement,
|
||
partition-openings]
|
||
- [ ] Every hosted type cuts only its host full-depth in Plan/View/Static/Iso;
|
||
exact composite room masonry is also cut, nearby bodies remain intact,
|
||
and malformed hosts fail dark. [auto: physical-geometry,
|
||
smoke_partition_openings]
|
||
- [ ] Rigid host drag preserves `t` and updates projections atomically; delete
|
||
lists hosted openings, Cancel changes nothing, Confirm cascades in one
|
||
Undo/Redo command. [auto: partition-openings, smoke_partition_openings]
|
||
- [ ] Contact/lock actions keep existing security rules; passage stays inert;
|
||
windows and exterior passages stay opaque to Glow, and partition windows
|
||
produce no sun wedge. [auto: runtime contracts, smoke_glow]
|
||
- [ ] Door/window/gate/passage presentation gaps do not split the structural
|
||
axis used by the Walls face graph; real `open_spans` still do. [auto:
|
||
plan-snap-overlay, smoke_room_autoclose, smoke_partition_openings]
|
||
- [ ] Backend rejects missing host references, out-of-range `t`, non-fitting or
|
||
overlapping hosted openings and stale host stripping; exports round-trip
|
||
the host. [auto: test_validation, test_ha_import_export]
|
||
- [ ] The exact #276 Optimize candidate is shared by frontend and backend tests:
|
||
Python independently proves the removed partition, two-room solid wall,
|
||
envelope, opening identity and non-overlap; config/set and every partial
|
||
or mutated candidate remain rejected. Linux HA WS persists and reloads
|
||
the implicit opening, then Undo restores the partition and explicit host.
|
||
[auto: coincident-partitions, test_validation, test_ha_websocket]
|
||
|
||
## Independent-wall opening jamb margin (#186)
|
||
|
||
- [ ] Strict resolver and placement reserve half the host depth for
|
||
door/window/gate/passage at both endpoints, including exact-boundary,
|
||
diagonal, reversed, thickness and scale matrices; room-wall placement
|
||
keeps its zero-jamb rule. [auto: partition-openings, opening-placement]
|
||
- [ ] Direct drag, dialog length edits and rebind share the same formatted
|
||
RU/EN guidance; a rejected edit writes neither config nor history.
|
||
[auto: smoke_partition_openings]
|
||
- [ ] Backend config/set and optimize reject a new/direct invalid geometry with
|
||
`invalid_partition_opening_jamb_margin`, while unrelated writes, rigid
|
||
translation and full backup restore preserve a legacy near-end record.
|
||
[auto: test_validation, test_ha_websocket, test_ha_import_export]
|
||
|
||
## Room resize (docs/RESIZE.md)
|
||
|
||
- [ ] `ResizeController` is the sole owner of selection, gesture, preview,
|
||
labels and eligibility cache. The card is only the DOM/render/persistence
|
||
adapter; controller state-machine unit tests cover foreign pointers,
|
||
repeated deltas, rejection rollback, cancel and exact commit
|
||
- [ ] One production wall-record preservation helper serves Resize and the
|
||
invariant CLI. Resize checks exact multiplicity for every finite value,
|
||
including `cm: 0`; the CLI keeps positive-value presence semantics
|
||
[unit: wall-record-preservation + resize-controller]
|
||
- [ ] The «Размер» tool appears in the Plan editor toolbar; in EVERY other
|
||
tool (and in Devices/Decor/View) there is not a single `.rszhandle`
|
||
- [ ] Every edge has a finger-sized midpoint handle. Eligible handles capture
|
||
the pointer; ineligible handles remain visible/dimmed, expose a localized
|
||
reason through hover/focus/tap, carry `aria-disabled=true`, and create no
|
||
drag, Undo or write [auto: smoke_room_resize + resize-production-path]
|
||
- [ ] Only a numerically horizontal/vertical wall with perpendicular side
|
||
edges is eligible. Diagonal, partial/unequal shared, coincident physical
|
||
extra and third-owner cases fail closed with the stable reason matrix
|
||
[unit: resize.test]
|
||
- [ ] A non-shared drag changes exactly one room; an exact endpoint-to-endpoint
|
||
shared drag changes exactly two. Both existing endpoints move by one
|
||
vector and every room keeps its vertex count/order [unit + smoke]
|
||
- [ ] An irregular exact pair moves only until the first corner/grid node that
|
||
would change the moving segment or collapse a side. No third room can
|
||
join the gesture. The anonymized private #277 topology stays predictably
|
||
disabled [unit fixture + production pointer smoke]
|
||
- [ ] Side-wall ownership stays atomic (#289): the anonymized 43-step repro is
|
||
disabled before pointer capture in both directions, while an outer side
|
||
reaches but cannot cross the next room's edge. No thickness record can
|
||
become partly shared and partly outer
|
||
[unit: resize.test + fixture 289-mixed-role-resize; auto:
|
||
smoke_room_resize; mutation: safe-resize-side-ownership-bypassed]
|
||
- [ ] Wall compaction preserves physical ownership (#299): equal thickness on
|
||
`shared(A,B) -> outer(A)` and `shared(A,B) -> shared(A,C)` remains split
|
||
at the exact role breakpoint, while equal neighbouring atoms inside one
|
||
role still compact. Optimize on `real-plan-first-floor.json` is immutable,
|
||
invariant-clean and idempotent; real-plan edit-walk seeds 1 and 3 exercise
|
||
Optimize and Delete-room/Keep-walls without producing a mixed-role record
|
||
[unit: wall-thickness + plan-optimizer; auto: smoke_edit_walk seeds 1/3;
|
||
mutation: wall-compaction-owner-role-bypassed]
|
||
- [ ] Live badges while dragging: lengths of the dragged wall + both
|
||
adjacent walls, and the m² area at the room centre; dragging a shared
|
||
wall shows BOTH areas; all numbers update continuously
|
||
- [ ] Stops are contiguous from zero: 30 cm room clearance, first topology
|
||
corner, foreign room/island, partition/column and every side-wall
|
||
opening. The opening jamb includes half the moving wall thickness, and a
|
||
wall cannot jump through an invalid interval to a later valid position
|
||
- [ ] An ordinary door/window/gate ON the moving wall travels exactly once;
|
||
length/type/angle/other fields remain byte-equivalent. A hosted opening
|
||
never transfers to a room wall through Resize
|
||
- [ ] The corner scale frame and its four handles are absent. Source guard
|
||
proves `applyRoomScale`, `clampRoomScale`, partial-shared insertion and
|
||
commit-time `simplifyPoly` are unreachable from `houseplan-card.ts`
|
||
- [ ] Esc, pointercancel and lost capture cancel instantly with original
|
||
persisted geometry, zero Undo and zero config writes
|
||
- [ ] Ctrl+Z / ⌘Z after releasing a handle restores the previous geometry —
|
||
one release = one undo step (rooms AND openings)
|
||
- [ ] The Plan toolbar names the next Undo/Redo operation; Ctrl+Shift+Z and
|
||
Ctrl+Y redo it, and a new geometry edit after Undo clears the redo branch.
|
||
Fifty committed operations remain available in the shared stack
|
||
[auto: command-stack.test]
|
||
- [ ] History shortcuts are layout-independent without conflating physical and
|
||
labelled keys: Cyrillic Ctrl/Cmd+Z works, QWERTZ Ctrl+Z/Ctrl+Y pick the
|
||
labelled command, and AZERTY Ctrl+W never becomes Undo. Focused inputs
|
||
keep native history [auto: smoke_editor_tabs]
|
||
- [ ] Exact eligible wall thickness/open spans re-key losslessly in the same
|
||
overlay; unrelated extras and rooms are byte-equivalent. Any production
|
||
wall/floor failure on the exact final preview cancels before save
|
||
- [ ] `npm run benchmark:safe-resize`: pointer clamp p95 ≤16 ms, ≤20% over the
|
||
same-run historical baseline (with bounded noise); cached pointerup
|
||
preflight p95 ≤75 ms; active-plan delta cache ≤4096 entries
|
||
- [ ] `npm run benchmark:safe-resize-render`: on the 20-room/80-handle floor,
|
||
a warm Resize layer takes one geometry snapshot per frame and stays at
|
||
p95 ≤25 ms
|
||
- [ ] Six Resize mutants are caught: axis eligibility, third-room cascade,
|
||
topology signature, side ownership, physical jamb and controller commit
|
||
preflight
|
||
- [ ] The test-only Resize eligibility audit calls the production resolver,
|
||
pins exact post-Optimize totals/reason counts and per-handle identities
|
||
for both real-plan fixtures, and reports stable handle ids when the
|
||
baseline changes. The known second-floor shared seam has two enabled
|
||
owner handles; raw-vs-optimized classification proves near-axis repair
|
||
removes only false angle reasons [unit: resize-availability-audit.test;
|
||
source: resize-production-path; mutation: resize-audit-resolver-bypassed]
|
||
- [ ] Disabled Resize handles expose the same actionable localized explanation
|
||
through aria-label, click, Enter and Space; pointerdown starts no drag and
|
||
creates no history/write [auto: smoke_room_resize]
|
||
- [ ] Device markers do not move; the room settings gear re-centres itself
|
||
- [ ] Smoke: `node demo/smoke_room_resize.mjs`
|
||
|
||
## Infinite canvas (docs/CANVAS.md, dev)
|
||
|
||
- [ ] **A plan drawn past the old square opens whole**: a space whose rooms
|
||
live at normalised 1.5..3.0 renders complete and centred (it used to
|
||
frame empty canvas with the house off-screen) [auto:
|
||
smoke_infinite_canvas; units: test/canvas.test.mjs]
|
||
- [ ] **Nothing stops at an edge any more**: in the Plan / Devices / Decor
|
||
editors a room, a marker and a decor shape can be drawn, dragged and
|
||
SAVED far outside `0..1`, on any floor; reload keeps them there
|
||
[backend: tests_backend/test_validation.py::test_infinite_canvas_range]
|
||
- [ ] **A typical small plan is visually unchanged** — same framing, same
|
||
room and label positions as before the feature. The ONE intended
|
||
difference is icon size (below) [auto: smoke_infinite_canvas
|
||
(legacyFrameUnchanged); the whole smoke suite is the regression net]
|
||
- [ ] **Icons no longer grow with zoom** (§6, owner is aware): a marker keeps
|
||
the same pixel size at zoom 1, 4 and at the zoom-out floor; the
|
||
per-device size multiplier, kiosk icon/font scales, badges, LQI chips
|
||
and presence rings all still scale from `--dev-size`
|
||
- [ ] **Start view follows the content**: opening a space frames what is
|
||
drawn plus a small margin, on every floor, with and without a backdrop
|
||
image (with one the IMAGE sets the extent — it must not be cropped to
|
||
the outlined rooms)
|
||
- [ ] **What is not drawn does not frame** (audit DEV-2C947-01): tick «hide
|
||
from plan» on a marker standing far from the house and the view snaps
|
||
back to the house — the hidden marker neither renders nor stretches the
|
||
frame, on the full card and on `houseplan-space-card`. Untick it and the
|
||
frame takes it in again; room LQI counted it the whole time
|
||
[auto: smoke_canvas_frame]
|
||
- [ ] **The editor frame does not follow you out** (audit DEV-2C947-02): move
|
||
the only room five canvases away in the Plan editor (the frame grows
|
||
there, deliberately), close the editor — View frames the room where it
|
||
is NOW, not the union with where it was; re-entering the editor starts
|
||
from the current geometry [auto: smoke_canvas_frame]
|
||
- [ ] **A far stray does not inflate the icons either** (audit DEV-2C947-03):
|
||
one ROOM dragged an order of magnitude away is rejected from the frame
|
||
(as before) and the markers of the main plan keep the size they have
|
||
without it; auto-placement spacing goes with them
|
||
[auto: smoke_canvas_frame + unit canvas.test.mjs]
|
||
- [ ] **A far stray does not break the view** (§4.1): a marker dragged an
|
||
order of magnitude away leaves the opening view alone and raises the
|
||
inline chip «Объектов далеко от плана: N» with «Показать». No modal.
|
||
«Показать» fits the plan AND the stray; the chip then disappears
|
||
- [ ] **«Вписать всё»** (middle zoom button): fits the content from any pan
|
||
and any zoom, is never disabled, tooltip en/ru
|
||
- [ ] **Zoom-out floor**: the wheel / the minus button stop at three times the
|
||
content frame; zoom-in still stops at 800 %
|
||
- [ ] **Pan has slack, not walls**: you can pan a full screen past the plan in
|
||
every direction; when the plan is fully off screen a small arrow points
|
||
home and one click fits it back
|
||
- [ ] **Pan at ANY zoom** (owner's report 2026-08-04): dragging empty scene
|
||
moves the view at 100 %, at 50 % and at the zoom-out floor — in View and
|
||
in all three editors, with every plan tool selected. The tools keep the
|
||
pointer they own (a resize handle resizes, a device badge in the Devices
|
||
editor moves the device, an opening slides along its wall — none of them
|
||
pan), two fingers still pinch, and on a kiosk screen a horizontal drag
|
||
is still the floor swipe (a vertical one pans) [auto: smoke_pan_any_zoom]
|
||
- [ ] **Kiosk: a pan stays a pan to the very end** (dev, audit DEV-1DA1-02):
|
||
on a wall tablet at 100 % start a drag with a small VERTICAL lead-in
|
||
(the plan starts following the finger — the gesture is locked as `pan`),
|
||
then curve it far to the left or right and lift. The floor must NOT
|
||
change: the decision taken on the first movement is final, and only a
|
||
gesture locked as a swipe may switch storeys. Mirror check: a horizontal
|
||
lead-in locks the swipe — the plan never slides under it, and if the
|
||
trajectory then bends vertically and no longer qualifies as a swipe, the
|
||
gesture simply does nothing (it does not turn into a pan). Straight
|
||
swipes still switch, straight vertical drags still pan, a motionless
|
||
double tap still resets the zoom [auto: smoke_kiosk_pan_lock]
|
||
- [ ] **Adaptive grid** (§7): in the Plan editor zoomed far out the grid does
|
||
not merge into a grey wash — fine dots thin out, every 5th/10th node
|
||
stays bigger; zoomed in the grid is the usual one and snapping still
|
||
lands on the same nodes as before
|
||
- [ ] **Everything else on a far-out plan**: sun wedges, glow radii, open
|
||
boundaries, room resize handles/rulers, opening rulers, split/merge,
|
||
vacuum trails, the static `houseplan-space-card` and kiosk carousel all
|
||
behave exactly as on a plan inside `0..1`
|
||
- [ ] **Real config regression**: a production config (e.g. the dacha, 3
|
||
floors / 106 markers) frames bit-identically to the previous release —
|
||
no outliers reported, no frame movement
|
||
|
||
## «+» adds a space from anywhere (dev, unreleased)
|
||
|
||
- [ ] **The button is where the floors are, always**: as an admin, open the
|
||
card in View — the «+» sits at the end of the tab row next to the floor
|
||
names, is at least icon-sized and actually hittable (nothing overlaps
|
||
it), and opens the NEW-space dialog. Repeat in all three editors (Plan,
|
||
Devices, Background): the same button in the same place, not only in the
|
||
Plan editor as before [auto: smoke_gear_tabs]
|
||
- [ ] **A kiosk has no «+»**: a card with `kiosk: true` does not RENDER the
|
||
button at all — checking that the header is `display:none` is not
|
||
enough, a hidden node is still clickable from script [auto: smoke_gear_tabs]
|
||
- [ ] **The tab row still fits a phone**: at 390 px the row wraps, nothing
|
||
scrolls sideways out of the card, and the «+» stays inside the card and
|
||
hittable [auto: smoke_gear_tabs measures `scrollWidth` vs `clientWidth`]
|
||
- [ ] **A non-admin never sees it**: the button follows the same rule as the
|
||
per-space gear (`_canEdit`) [manual, needs a non-admin HA user]
|
||
|
||
## Honest new-space display defaults (#204, dev, unreleased)
|
||
|
||
- [ ] **The dialog and Save agree**: open a new space. File begins with room
|
||
borders/names `false/false`; before either control is touched, switching
|
||
to Draw shows `true/true` and switching back restores `false/false`.
|
||
Saving either source and reopening it yields the exact visible pair
|
||
[auto: `space-dialog.test`, `smoke_space_create_display_defaults`,
|
||
`smoke_space_settings`].
|
||
- [ ] **One touch protects both choices**: on Draw change either display
|
||
switch, including the mixed `true/false` and `false/true` cases. Further
|
||
File ↔ Draw switches change only the source; Save never silently restores
|
||
`true/true` [auto: `space-dialog.test`,
|
||
`smoke_space_create_display_defaults`; mutation:
|
||
`space-create-hidden-display-override`].
|
||
- [ ] **Draft state does not leak**: Cancel and a fresh Create return to File
|
||
`false/false`. In Floors/Areas onboarding every next floor also starts
|
||
clean and cannot inherit the preceding floor's touched state
|
||
[auto: `smoke_space_create_display_defaults`].
|
||
|
||
## Wall thickness (docs/WALL-THICKNESS.md, Unreleased)
|
||
|
||
- [ ] **Walls + adjacent draw thickness**: the first Plan-editor tool is named
|
||
“Walls” / «Стены» rather than “Add”; while it is active, its thickness
|
||
field is the immediately following toolbar element, before Merge. The
|
||
field still defaults to 15 cm and disappears when another tool is selected
|
||
[auto: smoke_draw_wall_thickness]
|
||
- [ ] **Tool + hover + input**: Plan editor → Thickness. Hover highlights
|
||
the whole wall; click opens the cm/in field; empty/0 clears; Esc closes
|
||
without applying; «Apply to all walls of this room» fills every allowed
|
||
edge [auto: smoke_wall_thickness]
|
||
- [ ] **Hover width follows wall centimetres (#303)**: on a 30 cm cell a
|
||
50 cm wall's hover fill matches the masonry within 2%; a zero-thickness
|
||
wall keeps the same physical visual minimum across cell sizes, while the
|
||
pointer still hits at five grid pitches from the axis
|
||
[unit: grid-scale.test.mjs; auto: smoke_wallthick_hover_width;
|
||
mutations: wallthick-hover-floor-back,
|
||
wallthick-zero-strip-not-visual, wallthick-hit-narrowed]
|
||
- [ ] **Hatched body, clean-floor area**: after setting thickness a `.wallbody`
|
||
path appears; room-card and tooltip m² both decrease to the same inner-
|
||
contour area
|
||
[auto: smoke_wall_thickness]
|
||
- [ ] **Exact thickness transition after Split**: split one zero-thickness room,
|
||
apply 10 cm to every wall of one child and keep the other child at zero.
|
||
Both halves of the 10 cm facade end at the divider endpoint; no hatch,
|
||
paper or light masonry continues along the zero side. Plan, View, static
|
||
and hidden Iso consume the same stepped geometry
|
||
[auto: smoke_wall_thickness_transition + test/wall-thickness.test.mjs]
|
||
- [ ] **One virtual-junction patch cannot blank the plan (#197)**: load the
|
||
complete 8-room, 25-wall, 3-cut regression fixture. Its ULP-noisy patch
|
||
is stabilised below geometry tolerance; a forced failure of one optional
|
||
patch retains the previous body and later patches still run. Plan, View,
|
||
kiosk, static and hidden Iso keep one non-empty canonical masonry path;
|
||
paper, clean-floor and light/sun consumers stay non-empty, and theme/HA
|
||
ticks neither rebuild topology nor write configuration
|
||
[auto: test/wall-thickness.test.mjs +
|
||
smoke_junction_patch_resilience + junction-patch-resilience golden
|
||
scenarios].
|
||
- [ ] **A bounded T-junction keeps its exterior half-wall (#261)**: in the
|
||
anonymised #197 fixture the measured point `(895.5, 556)` is filled by
|
||
room masonry, final masonry and paper, and excluded from clean floor.
|
||
Plan, View, kiosk, Static, hidden Iso and light/sun barriers agree at that
|
||
point, while the old excessive #249 spike remains absent
|
||
[unit: test/wall-thickness.test.mjs; auto:
|
||
smoke_junction_patch_resilience; golden:
|
||
junction-patch-resilience-plan-dark +
|
||
junction-patch-resilience-view-dark; mutation:
|
||
multi-wall-paper-full-origin-cut].
|
||
- [ ] **Degree-3 repair stops at every finite ray endpoint (#271)**: canonical
|
||
co-directional rays retain separate short-thick and long-thin supports;
|
||
the rebuilt masonry, paper and light barrier contain the real short arm
|
||
but no area after its endpoint. Plan, View, kiosk, Static, hidden Iso and
|
||
clean floor agree, independent of owner order, winding and scale
|
||
[unit: test/wall-thickness.test.mjs; auto:
|
||
smoke_junction_patch_resilience; golden:
|
||
junction-patch-resilience-plan-dark +
|
||
junction-patch-resilience-view-dark; mutation:
|
||
multi-wall-finite-ray-disabled].
|
||
- [ ] **A degree-3+ junction has no enclosed white triangle (#272)**: every
|
||
excessive bevel sector remains empty but is connected to the exterior
|
||
through a finite-width local corridor. Equal/mixed T and X nodes at
|
||
`cell_cm: 1/5` have zero local holes in room/final masonry and paper;
|
||
Plan, View, kiosk, Static, hidden Iso and light/sun agree
|
||
[unit: test/wall-thickness.test.mjs; auto: smoke_multiwall_junction;
|
||
golden: multiwall-junction-bevel-view-dark; mutation:
|
||
multi-wall-exterior-corridor-disabled].
|
||
- [ ] **A perpendicular T/X junction keeps every real wall strip (#275)**:
|
||
every finite ray with a perpendicular partner remains filled through its
|
||
node even when the removed sector is exterior-connected or neighbouring
|
||
node masks overlap. Mixed orthogonal/diagonal nodes protect only the
|
||
qualifying rays; the non-orthogonal #249 wedge stays empty. Raw,
|
||
Optimize preview, applied storage and reload agree at `cell_cm: 5/1`, as
|
||
do Plan, View, kiosk, Static, hidden Iso, paper, clean floor and light
|
||
[unit: test/wall-thickness.test.mjs; auto:
|
||
smoke_multiwall_strip_containment; golden:
|
||
orthogonal-strip-cell-5-view-dark +
|
||
orthogonal-strip-cell-1-view-dark; mutation:
|
||
multi-wall-orthogonal-strip-protection-disabled; exact local gate:
|
||
scripts/wall-strip-containment.mjs].
|
||
- [ ] **A short multi-wall ray cannot erase its attached shared wall (#288)**:
|
||
the real second-floor `349 / 120 / 5` node keeps the 20 cm wall beginning
|
||
at the short ray's far endpoint; both tracked real plans have zero
|
||
undeclared centreline gaps, while the #271 outer phantom remains absent
|
||
[unit: test/wall-thickness.test.mjs; auto:
|
||
smoke_real_plan_masonry; mutation:
|
||
multi-wall-shared-continuation-protection-disabled].
|
||
- [ ] **Openings cut the slab**: a door/window/gate on a thick wall leaves a gap in
|
||
the body; the door swing is offset toward the inner face and gate leaves
|
||
toward the exterior face; with
|
||
`hide_openings` the symbols hide but the cut remains
|
||
[auto: smoke_wall_thickness]
|
||
- [ ] **Opening tunnel repeats the room fill**: check a door, window and gate on
|
||
outer and shared thick walls. An outer opening uses its room colour for
|
||
the complete wall depth; a shared opening with different fills has one
|
||
hard transition exactly on the wall axis and no white/alpha seam. Window
|
||
glass and all architectural symbols remain above it. Repeat with hidden
|
||
opening symbols, hidden wall borders and in the Background editor; Glow
|
||
and sun geometry must not change
|
||
[auto: smoke_opening_tunnel_fill + test/wall-thickness.test.mjs +
|
||
test/logic.test.mjs]
|
||
- [ ] **Opening association and overlap edges**: a parallel room separated by
|
||
an air gap does not colour the outer half; a perpendicular T arm does not
|
||
capture the opening; a 45° wall keeps its local-axis split; nested rooms
|
||
resolve deterministically; a legacy opening beyond an endpoint paints
|
||
only the real wall interval. Exact and partial duplicate openings do not
|
||
stack alpha, and an angle-invalid opening is consistently rejected by
|
||
symbol offset, wall cut and tunnel fill
|
||
[auto: test/wall-thickness.test.mjs]
|
||
- [ ] **Door/gate light uses the clear tunnel**: place an off-centre light beside a
|
||
door or gate in a thick wall. In the neighbouring room the glow is limited by
|
||
sight lines through both the near and far inner-face corners; neither
|
||
side crosses a solid jamb return. Clearing wall thickness restores the
|
||
wider centreline-based sector [auto: test/logic.test.mjs; manual visual]
|
||
- [ ] **Wide gate stays compact**: add a 300–400 cm Gate. It has two equal
|
||
leaves with no swing arc; without a contact they open exactly 10°
|
||
outwards, and a closed/open contact changes the angle between 0° and
|
||
10°. A lock badge and Glow tunnel behave exactly like a door
|
||
[auto: test/logic.test.mjs + test/wall-thickness.test.mjs +
|
||
tests_backend/test_validation.py + smoke_styling_hooks]
|
||
- [ ] **Shared once / zero → line / safe Resize**: one body for a shared wall;
|
||
setting thickness to zero restores the centreline. Resize preserves
|
||
thickness on an eligible uniformly thick exact wall. Partial/unequal
|
||
shared and mixed-thickness walls remain visibly disabled; an attempted
|
||
drag changes no rooms, openings, wall atoms or Undo history
|
||
[auto: smoke_wall_thickness + smoke_zero_walls +
|
||
smoke_resize_wall_thickness + smoke_room_resize +
|
||
test/wall-thickness.test.mjs + mutation-gate]
|
||
- [ ] **Wall key survives storage round-trip (#258)**: exact grid endpoints and
|
||
their nine-decimal stored form produce one midpoint key, including odd
|
||
and even lengths, negative/reversed coordinates and render-space scale.
|
||
Both known persisted key variants resolve the same exact span immediately
|
||
without accepting a parent, child, neighbour or parallel wall. The
|
||
affected T-node stays filled in Plan, View, kiosk, Static and hidden Iso;
|
||
clean-floor and light barriers use the same masonry. Explicit Optimize
|
||
rewrites the stable key and the next in-memory/backend echo is a no-op
|
||
[unit: test/wall-thickness.test.mjs + test/plan-optimizer.test.mjs +
|
||
test/model-invariants.test.mjs; auto: smoke_wall_key_roundtrip; golden:
|
||
wall-key-roundtrip-view-dark; mutation: wall-key-storage-normalization-disabled +
|
||
wall-exact-span-fallback-disabled + invariant-wall-key-storage-normalization-disabled].
|
||
- [ ] **Zero-wall T-junction**: when two positive thick arms from different
|
||
room contours meet at a zero-wall endpoint, the outside corner is a clean
|
||
mitre with no stair-step. Editors paint the complete zero axis above the
|
||
real hatch; View paints it below the body so jambs mask its ends without
|
||
changing stored geometry [auto: test/wall-thickness.test.mjs +
|
||
smoke_zero_walls].
|
||
- [ ] **Zero fragment normalisation**: adjacent or overlapping `cm:0` atoms
|
||
with the same ownership compact without losing the exact breakpoint at a
|
||
positive thickness or owner-role change. Resize transforms their stable
|
||
endpoints without recreating legacy `open_spans/open_to`
|
||
[auto: test/wall-segment-model.test.mjs + test/wall-thickness.test.mjs +
|
||
smoke_zero_walls].
|
||
- [ ] **Near-axis authoring and explicit repair (#290)**: the shared
|
||
`0.25°` classifier includes `316×1`, excludes `316×2` and 30° diagonals,
|
||
and Walls preview/click persist `316×0` without claiming the wrong saved
|
||
endpoint. Optimize deduplicates the tracked shared wall across two room
|
||
owners, reports one wall and an exact physical maximum, rekeys thickness,
|
||
passes production preflight, applies one atomic write, reloads as a no-op
|
||
and restores the original through one Undo. Independent walls use the
|
||
same classifier; unsafe candidates are counted as skipped
|
||
[unit: test/near-axis.test.mjs; auto: smoke_plan_drawing_repairs +
|
||
smoke_near_axis_optimize; mutations: `near-axis-threshold-weakened`,
|
||
`near-axis-inclusive-boundary-disabled`,
|
||
`near-axis-authoring-snap-bypassed`].
|
||
- [ ] **Explicit Optimize cleans only an isolated micro-interval (#198)**:
|
||
`22 → 15 → 22` with a centre shorter than half a grid step and no
|
||
room/opening node becomes one 22 cm run in Preview and Apply; Cancel
|
||
writes nothing and server Undo restores the three exact entries. Exact
|
||
half-step, end, unequal-neighbour, chained and topological cases remain
|
||
lossless at normalized/render scales; a second Optimize is idempotent
|
||
[unit: test/plan-optimizer.test.mjs; auto:
|
||
smoke_optimize_micro_interval; mutation:
|
||
`optimizer-micro-interval-cleanup-disabled`].
|
||
- [ ] **A single T-node does not preserve an artificial thickness island
|
||
(#273)**: the minimized beta.5 `22 → 15 → 22` profile has a 1.381904-unit
|
||
centre beside one perpendicular room edge. Preview/Apply store one 22 cm
|
||
run, the T coordinate and incident room stay unchanged, render probes
|
||
see one continuous outer face, reload is idempotent and server Undo
|
||
restores the exact entries. A second topology endpoint or any open-span
|
||
endpoint still blocks cleanup [unit: test/plan-optimizer.test.mjs; auto:
|
||
smoke_optimize_micro_interval; mutation:
|
||
`optimizer-single-topology-island-blocked`].
|
||
- [ ] **Optimize reconciles only one exact coincident wall (#276)**: the
|
||
anonymized two-room fixture keeps its two 5 cm endpoint offsets while an
|
||
exact independent wall is removed, its hosted door becomes an ordinary
|
||
opening at the same centre/angle with all bindings and unknown fields,
|
||
and the wider centred thickness survives. Direction and room order do
|
||
not matter; three non-overlapping hosted door/window/gate records are
|
||
rehosted atomically, while a hosted-hosted overlap and all other
|
||
partial/extra/unknown/column/opening conflicts fail closed. Preview
|
||
writes nothing, Apply uses one WS transaction,
|
||
reload is idempotent, Undo restores the hosted form, and Boundary plus
|
||
Thickness target the resulting shared wall. Four targeted golden scenes
|
||
retain the 5 cm offset and show before, 10 cm, 30 cm and virtual results;
|
||
the paired large-house benchmark enforces p95 overhead ≤15% and ≤25 ms,
|
||
while source ownership plus an injected counter keep the helper out of
|
||
render/pointer paths [unit:
|
||
test/coincident-partitions.test.mjs + test/plan-optimizer.test.mjs;
|
||
auto: smoke_optimize_coincident_partition; mutations:
|
||
`optimizer-coincident-opening-rehost-disabled`,
|
||
`optimizer-coincident-partial-accepted`, existing
|
||
`optimize-preflight-bypassed`; performance:
|
||
`npm run benchmark:coincident-partitions`; golden:
|
||
`coincident-partition-{before,thin,thick,virtual}-dark`].
|
||
- [ ] **Optimize unlocks only proved zero-range Resize handles (#281)**: three
|
||
exact independent partitions over solid one-room outer boundaries block
|
||
the affected shared-wall Resize before maintenance. Optimize removes all
|
||
three, materializes both hosted windows without changing their fields,
|
||
passes independent backend proof and is idempotent. Afterwards the target
|
||
handle has a non-zero grid step in both directions and one production
|
||
pointer gesture changes exactly the two adjacent rooms. Partial, unknown
|
||
or opening-overlapped outer candidates remain untouched. Every handle
|
||
reported enabled on the anonymized `44.json` fixture has a non-zero
|
||
contiguous range; a zero-range handle stays visible/focusable but disabled
|
||
and captures no pointer [unit: test/resize-optimize.test.mjs; backend:
|
||
tests_backend/test_validation.py + tests_backend/test_ha_websocket.py;
|
||
auto: smoke_resize_outer_reconciliation].
|
||
- [ ] **Unit + backend**: inset/mitre/bevel, key from either end, degrade,
|
||
rekey, cm↔inches; `walls` schema bounds
|
||
[auto: test/wall-thickness.test.mjs + tests_backend/test_validation.py]
|
||
- [ ] **Thin-on-screen parity**: at the same card width a 1 cm wall suppresses
|
||
the hatch (solid fill stays) in both `houseplan-card` and
|
||
`houseplan-space-card`; a 20 cm wall restores the hatch in both
|
||
[auto: smoke_wall_thickness + test/wall-thickness.test.mjs]
|
||
- [ ] **Split through an open span**: split one side of a shared wall through
|
||
the middle of an existing open stretch. Both resulting pieces remain in
|
||
`open_spans`, and both new rooms link to the neighbour in `open_to`
|
||
[auto: smoke_merge_split + test/open-spans.test.mjs]
|
||
|
||
## Wall chains, partitions and columns
|
||
|
||
- [ ] **Persistence and joining**: every accepted wall-chain edge is immediately
|
||
one ordinary partition with its selected thickness and one history/save
|
||
boundary. Switching tools or reloading leaves those walls intact but does
|
||
not resume their chain. A current config never stores `room_drafts`; a v9
|
||
draft migrates edge-for-edge to partitions [auto: smoke_free_walls +
|
||
smoke_unified_wall_tool + frontend/backend wall-segment-model tests].
|
||
- [ ] **Creation limits and validation**: 1/100 cm partitions and 1/150 cm
|
||
columns save exactly. Zero, NaN and 101/151 cm block the final click with
|
||
a range toast and create no history entry. Client caps match backend:
|
||
2000 partitions and 500 columns [auto: backend validation +
|
||
smoke_free_walls].
|
||
- [ ] **Select gestures**: creation tools click through existing physical
|
||
bodies. Select gives at least a 24 px target, cycles overlaps on repeated
|
||
clicks and moves a partition rigidly on the grid. Escape/pointercancel
|
||
restores the pre-drag state. A square column rotates in 5° steps (Shift
|
||
free); a circle has no rotation handle [auto: smoke_free_walls; manual].
|
||
- [ ] **Delete contract**: every accepted open-chain edge is an ordinary
|
||
independently selectable partition. Delete and properties therefore use
|
||
the standard partition confirmation/dialog; there is no whole-outline or
|
||
segment-of-draft action [auto: smoke_free_walls].
|
||
- [ ] **Area and light**: overlapping bodies are subtracted once from clean
|
||
area; closed partition rings keep the enclosed floor; bodies outside all
|
||
rooms create no paper. Glow does not cross a long nearby partition and a
|
||
source inside masonry lights nothing. Window rays are blocked by the same
|
||
bodies. `show_borders: false` changes paint only [auto:
|
||
physical-geometry.test; manual visual].
|
||
- [ ] **Seamless junctions**: partition L corners (right, acute and
|
||
obtuse), unequal thickness, endpoint-on-line T and a branch touching a
|
||
room wall use one bounded joined body; near-miss, X crossing, malformed
|
||
segments and flat free caps keep their documented semantics. The active
|
||
rubber-band has the same contour before and after commit, target records
|
||
are not split, Plan/View/static/hidden-iso paths agree, clean floor and
|
||
light use the joined corner, and preview/render never writes config
|
||
[auto: wall-thickness.test, physical-geometry.test,
|
||
smoke_wall_junctions, wall-junctions golden scenarios; manual golden
|
||
artifact review].
|
||
- [ ] **Lifecycle/performance**: an external config revision cancels live
|
||
move/rotate state before replacing geometry. Drag preview performs no
|
||
polygon boolean work; clean floor and Glow clips are reused until the
|
||
config/space/source changes [auto: editor/preloader smokes; performance
|
||
profile for a dense plan].
|
||
|
||
## Вписывание выбранной комнаты (#152)
|
||
|
||
- [ ] В View чистый mouse click и одиночный touch tap по полу простой,
|
||
вогнутой и вложенной комнаты центрируют именно browser-target и помещают
|
||
её видимый пол вместе с ограничивающими стенами в центральные 80% stage.
|
||
- [ ] В Flat и Iso ограничивающая ось даёт поля `10% ± 1 CSS px`; устройства,
|
||
badges, room label, Glow, солнце, подложка и декор bounds не расширяют.
|
||
- [ ] Device/capsule, opening/lock/action, vacuum и HA Area link выполняют только
|
||
своё действие; pan выше click threshold, pinch и long press не запускают
|
||
room fit, а движение ниже threshold остаётся tap.
|
||
- [ ] В kiosk два tap по комнате не запускают Fit all и не меняют background
|
||
double-tap sequence; два tap по свободному фону сохраняют прежний reset.
|
||
- [ ] Быстрые клики разных комнат retarget-ят один camera controller; повторный
|
||
fit уже вписанной комнаты не создаёт RAF и не пишет `LS_ZOOM`.
|
||
- [ ] Stable resize атомарно пересчитывает активную комнату. Wheel, zoom buttons,
|
||
Fit all/home, pan/pinch, mode/space/projection, hidden/disconnect и новая
|
||
structural snapshot снимают intent.
|
||
- [ ] Видимая подпись имеет один `role=button`/tab stop, локализованное имя и
|
||
`:focus-visible`; Enter/Space вызывают тот же fit и не двигают focus.
|
||
Скрытая/безымянная подпись не создаёт невидимый tab stop.
|
||
- [ ] Pure proofs: `node --test test/room-fit.test.mjs`; полный cycle:
|
||
`npm run typecheck`, `npm test`, `npm run build`. Browser matrix и
|
||
мутации room ownership и session-only zoom persistence выполняются перед
|
||
бетой.
|