Пятнадцать расхождений из #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
54 KiB
Геометрия: стены, проёмы, комнаты, холст
Приложение к
docs/TESTING.md: перенесено оттуда дословно (#634). Индекс всех приложений —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.jsonproduces 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,-3suffixes. - 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-modelmaterialises 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 throughtests_backend/test_wall_segment_model.pyandtests_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 noroom_draftsin 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 v9room_draftswrite over v10. demo/smoke_unified_wall_tool.mjsaccepts 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 andnode scripts/check-docs.mjs --external. - Mutation
current-rejected-physical-write-keeps-optimistic-wallproves the browser rollback scenario fails when rejection recovery is bypassed. - Mutation
room-accept-leaves-coincident-partitionsproves the real editor smoke fails if accepted room carriers stop being consumed atomically.
Current-writer fixed point (#477)
test/writer-fixed-point.test.mjsproves 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.mjsfinishes 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.mjskeeps 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.mjsproves Optimize leaves complete furniture/image transforms byte-equivalent while an ordinary decor rectangle remains grid-bound.- The
writer-*mutations inscripts/mutation-registry.mjsremove 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, sixsmoke_edit_walkruns; 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_vvalues, door/window flips change only their direction, and gateflip_vreverses 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: centerfor 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
actualSha256for HEAD andorigin/devunder the same Chromium build; baseline status alone is not used becausedevalready 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:acceptremains 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)
floorresolves 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
floorproperty 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/devhas 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
tand 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_spansstill 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)
ResizeControlleris 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)andshared(A,B) -> shared(A,C)remains split at the exact role breakpoint, while equal neighbouring atoms inside one role still compact. Optimize onreal-plan-first-floor.jsonis 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-timesimplifyPolyare unreachable fromhouseplan-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 entriesnpm 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-cardand kiosk carousel all behave exactly as on a plan inside0..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: truedoes not RENDER the button at all — checking that the header isdisplay:noneis 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
scrollWidthvsclientWidth] - 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 showstrue/trueand switching back restoresfalse/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/falseandfalse/truecases. Further File ↔ Draw switches change only the source; Save never silently restorestrue/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
.wallbodypath 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/5have 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 / 5node 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_openingsthe 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:0atoms 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 legacyopen_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 includes316×1, excludes316×2and 30° diagonals, and Walls preview/click persist316×0without 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 → 22with 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 → 22profile 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, existingoptimize-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.jsonfixture 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;
wallsschema 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-cardandhouseplan-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 inopen_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: falsechanges 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 выполняются перед бетой.