[feat] roadmap #16: menus, grid & scene cameras (P1-P6) - #86
Merged
Conversation
Roadmap #16 P6. - LOCK LEAK: `applySelectionSet` broadcasts a `lock` message, but a deselect broadcast nothing - and `lock` only ever REPLACES the sender's set (lockGeometry ignores an empty list), so clicking empty space left the object highlighted and "locked by X" on every other peer until the deselecting peer happened to select something else. `broadcastSelectionRelease(uuids)` now sends one `unlock` per released uuid from all three release paths (deselectObject, applySelectionSet([]), and switching to a locked-VIEW). No new message type - `unlock`/`applyUnlock` already existed, and a peer that had asked us for control now gets it, which is right. - "Selected" SUBMENU: the viewport menu gated it on `selectedObject`, which is STICKY by design (it keeps the last object so the open inspector has something to bind to), so the submenu lingered after a deselect. It now gates on the live selection SET, keeping the one legitimate empty-set state - VIEWING a peer-locked object - which retains its view-only actions. - Configure Scene's active nav row drops the `inset 2px` accent bar (it read as a stray border); the tint + accent text stay. Verification: new tests/e2e/deselect-unlock.test.cjs (two-peer, 9 checks: lock replace, release on deselect / on applySelectionSet([]) / on locked-view, sticky primary preserved) + viewport-selection extended with the menu gating. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Roadmap #16 P1. The 15-Q filter row was always visible on dense menus and the menu had no keyboard model beyond Enter; both are fixed without changing any action. - HIDDEN UNTIL TYPING: the filter input stays MOUNTED and FOCUSED (that is what swallows app shortcuts while a menu is open - the input-focus guard) but collapses to height 0 / opacity 0 until there is a query. Not display:none or hidden: the input must remain focusable to own the keyboard. Revealed, it is a normal menu ROW - same padding and font-size as an item (verified 26px vs 26px in the suite) with a tint + bottom rule instead of the old inset pill. - EVERY menu now carries it, so the >=8-leaf threshold is gone: small menus show no filter row (it is collapsed) but still get arrow navigation. - KEYBOARD: Up/Down walk the current level skipping disabled rows (wrapping), Enter runs the highlighted row or OPENS its submenu and moves the cursor inside, Esc unwinds one step at a time (query -> open submenu -> close the menu). Left/Right stay with the textbox caret as asked; with an EMPTY box (no caret to move) they double as open/close submenu. - ONE highlight for mouse and keyboard: hovering a row moves the cursor, so Enter always acts on the row you are looking at. The open submenu keeps its 150ms close-intent delay while the cursor moves - that delay is what lets you reach a submenu diagonally, so pointer hover deliberately does NOT close it immediately (hence openPath for rendering vs navPath for the cursor). - Clicking a row no longer steals focus from the filter (mousedown default prevented) - opening a submenu by click used to hand the keyboard back to the app's global shortcuts. - Submenus now anchor to their own row element instead of a captured mouse rect, so a keyboard-opened submenu places itself exactly like a hovered one (and the stale-rect risk is gone). - New `$lib/menuFilter.js` holds the flatten + ranking (label prefix > path prefix > substring > fuzzy subsequence, 40-row cap) so there is ONE matcher - P2 retires the node editor's private search box onto it. Verification: context-menu-redesign extended (hidden-then-revealed row, row height parity, Up/Down/Enter/Esc, highlight lands INSIDE the submenu); context-menu-v2, context-menu-overflow, add-menu-cursor, object-search, mobile-hud green. add-menu fails at its documented pre-existing flake (right-tap does not open the menu, line 105) - identical with these changes stashed. Build green, svelte-check 419/62 held. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Roadmap #16 P2. The flow pane menu carried its OWN search popup (#node-search-box, a separate fixed box that replaced the menu at the same coordinates, with its own ranking, its own arrow-key handling and its own drag-to-move) and, since 15-Q, the shared filter row on top of it - visibly duplicated. There is now ONE implementation. - Deleted the popup and everything only it needed: openSearch, searchEntries, searchResults, its private subsequence ranker, pickResult, onSearchKeydown, scrollHighlightIntoView, savedMenu, the window keydown that jumped into search, and the rightDragMove/inputContextMenu imports (the box's right-drag move goes with it - the menu opens at the click). - "Search nodes..." stays as the first row but is now a `revealFilter` item: it reveals the shared filter row WITHOUT closing the menu, so the grouped list stays as the browse view it always was. Typing anywhere in the menu does the same (the filter input is always focused), which is how it already behaved. - Matches render as "Group > Node" - the same information the old box showed as "group . label" - ranked by the shared matcher, with arrow-key navigation and shortcut interception for free. - Esc unwinds one step at a time: query -> revealed filter -> submenu -> close. FIX found by the suite: a menu opened near the bottom edge (flipped up) grew straight off the screen when filtering changed its height, because `place()` only ran on open and on window resize. It now re-places on any size change via a ResizeObserver, guarded against a maxHeight write loop. Also repaired two STALE assertions in node-search (a known-failing suite): they asserted context menus never scroll and are never height-capped, which the later cap-and-scroll change deliberately reversed - the suite is fully green now. Verification: node-search rewritten against the shared filter (20 checks, ALL PASS - previously on the known-failure list); context-menu-redesign, context-menu-v2, context-menu-overflow, multiselect-menu, object-search, packs-explorer green. Build green, svelte-check 419/62 held. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Roadmap #16 P3. CONFIGURE SCENE gains two sections (LOCAL per-device prefs, like the clip planes and the render mode that already live there - a grid is a view aid, not scene content, so nothing here replicates): - GRID: show, cell size, "match snapping step" (the drawn cell tracks the translate snap step, so the grid you see is the grid objects land on), major line every N cells, cell + major colours, fade auto/fixed with radius and edge softness, infinite vs a finite extent, follow-camera, and origin axes. Reset restores the defaults. New `$lib/gridSettings.js` holds it (cameraClip idiom, unknown keys fall back to defaults so old payloads keep working); extensions/Grid.svelte reads it and keeps the I4 no-lerp auto-fade math. The axes helper renders in the world rig, never under objectsGroup - anything there would enter GLTF sync and duplicate for peers. - SNAPPING: enable, position/rotation/scale steps as preset chips PLUS a free number each (custom steps were menu-only before), and surface snap. CONTEXT MENU: new `checked` item flag renders the active choice of a group as BOLD WHITE on a brand-tinted pill, replacing the '● ' label prefix that shifted labels sideways as it appeared. The text itself is deliberately not accent-tinted: the app's accent is a salmon (#fe795d), which would sit too close to the red `danger` rows in the same menu. SNAPPING SUBMENU is sectioned (Position / Rotation / Scale / Surface) instead of one flat run of ten steps, the parent row's hint column shows the live setup ("0.5 · 15° · 0.1", or "off"), and a last row jumps to Configure Scene for custom values. The View submenu gets the same treatment (Show grid as a checked toggle + a grid-settings jump). Verification: new tests/e2e/grid-snapping.test.cjs (15 checks: sections, checked marks, no bullets, live hint, click-applies-and-mark-follows, panel toggles reach the store, AxesHelper outside objectsGroup, match-snap-step, persistence across a reload, custom step); context-menu-redesign, context-menu-overflow, vr-snap-menu green. Build green, svelte-check 419/62 held. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Roadmap #16 P4. BOOKMARKS were five anonymous slots where a sixth save silently pushed the oldest out, labelled by save TIME, storing only position + target. - Unlimited NAMED entries (default "View N"), each storing the LENS it was saved with (FOV + near/far); recall restores the whole look, near/far through cameraClip so the orbit zoom clamp stays paired with the far plane. - Management in Configure Scene: rename inline, re-shoot from the current view (keeps the name), reorder (order IS the Shift+N mapping), delete, plus Save current view. Shift+1..5 still recall the first five. - Legacy payloads normalize on read: no name -> "View N", no lens -> recall leaves your lens alone. The viewport menu lists them by name with a ⇧N hint and a "Manage saved views…" jump. CAMERA SETTINGS were a "Camera lens" sub-label buried inside the View section, and the orbit feel was hardcoded in Scene.svelte's <OrbitControls>. Now one Camera section: lens presets + FOV, near/far clip, rotate/zoom/pan speed, damped orbiting, invert vertical orbit (the sign rides rotateSpeed - three has no flag), Frame scene (fit everything along the current view direction), Reset view (the mount defaults), Reset feel, and the saved-view manager. Orbit prefs are LOCAL and re-apply whenever the controls remount (VR exit, spectator mode). Verification: new tests/e2e/camera-bookmarks.test.cjs (14 checks: legacy normalization, lens capture + restore, no cap, panel rename/overwrite/reorder/ delete, Shift+2, orbit prefs reach the live controls + reset); camera-clipping and roadmap-13-viewport-camera green. Build green, svelte-check 419/62 held. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Roadmap #16 P5. Perspective and orthographic CAMERA objects you can place, aim, look through and shoot from. DATA: a camera is a normal replicated marker MESH (`/create Camera` / `/create CameraOrtho`, body from customGeometries - a box with a lens cone down -Z, three's camera convention) whose settings live on `userData.camera`. That is the userData.physics trick, so replication, undo, sessions, autosave, prefabs and GLTF extras all work with ZERO new plumbing, and peers agree because the same /create stamps the same deterministic defaults. Markers are never physics bodies and never cast shadows. Add > Camera > Perspective / Orthographic. WRITES go through `setCameraFor(uuid, patch)` - one props history entry, one `objectParameters` message, one poke that rebuilds viz + preview (the setPhysicsFor precedent); the 'props' history kind and the objectParameters applier both learned `camera`. FRUSTUM VIZ (`cameraHelpers.js`, the colliderHelpers pattern): a wireframe frustum per camera, built from the spec, living at the SCENE ROOT so it stays out of GLTF sync, following its marker per frame, hidden in wireframe view mode and for the camera you are currently inside. On by default with a local `showCameraFrustums` toggle. PREVIEW is a TRUE camera swap: `CameraPreview.svelte` mounts a real Perspective/OrthographicCamera as threlte's default, so an ortho camera renders genuinely orthographic and fov/near/far are exact. Two things this needed: - Outline.svelte re-points its EffectComposer (setMainCamera + N8AO's own camera reference) whenever the active camera changes - every pass baked `camera.current` at construction, so without this you keep seeing the editor view. It tracks `$camera` (the STORE): threlte's CurrentWritable exposes `.current` as a plain property, so reading it inside an $effect registers no dependency and the effect runs exactly once. - the previewed marker hides itself locally (you stand inside its body mesh otherwise), the way spectator mode hides the avatar you watch from. CONTROL (banner button): hands the camera to the normal viewport navigation - WASD + mouse, because the preview camera owns the OrbitControls and Scene's existing per-frame nav call drives whatever is active. Every controlled frame writes the pose back onto the marker (throttled ~10Hz `move` broadcasts), and ending control leaves exactly ONE undo entry for the whole ride (the possess "one undo per ride" precedent). Also: framing guide (letterbox bars for 16:9 / 4:3 / 1:1 / 2.39:1 while previewing), Capture (renders one offscreen frame at the framing aspect and downloads it), Set from current view / Align view to camera, an Inspector Camera section, the two headline actions in the object context menu, and REPLICATED preview presence (`campreview`) - the Users popover shows "previewing <camera>" per peer with a click to join, cleared on disconnect and shared on handshake. Verification: new tests/e2e/camera-objects.test.cjs (27 checks, two-peer: defaults, replication, write+undo replication, scene-root frustums that follow, ortho preview really orthographic, banner, own-frustum suppression, peers seeing the preview, flying writes the marker + replicates + one undo entry, exit restores your camera, Capture at 640x360). collider-viz, inspector, object-properties, viewport-selection, roadmap-13-viewport-camera green; view-mode fails a pre-existing shadow-catcher check (identical with these changes stashed). Build green, svelte-check 419/62 held. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two bugs in 16-P5's Control mode, both reported. 1. Pressing Control SNAPPED the view. A fresh OrbitControls targets the world origin and its update() ends with camera.lookAt(target), so mounting it rotated the preview camera the moment Control was pressed. The controls are now seated behind the camera first - the target goes on the camera's own forward axis, making that lookAt a no-op - and because OrbitControls already ran one update() during construction (before any effect could run), the camera is re-synced from the MARKER first, so the mount rotation cannot get baked into the pose. Measured drift is now 0.0000. 2. After exiting a preview, dragging the transform gizmo also ORBITED the camera. Both sets of controls were bound to the same `orbitControls` store, and threlte clears a bound ref on unmount - so the preview's teardown could land after the editor's controls remounted and leave the store EMPTY. Every suppression site (`$orbitControls.enabled = false` on gizmo/marquee drags) then wrote to nothing, for the rest of the session. The preview camera now publishes its own `previewOrbit` store, a derived `activeOrbit` says which controls are actually steering, and Scene routes its six suppression sites plus the per-frame navigation call through it. Verification: new tests/e2e/camera-preview-control.test.cjs (9 checks: no drift on Control, target seated ahead, ownership handover, editor controls restored live and attached to the active camera, suppression reaches them, two cycles stay clean). svelte-check 419/62 held. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Roadmap #16 second drop, batch Q1 — three reported annoyances in the menu. - SUBMENU CURSOR is remembered per level: stepping out of a submenu lands back on the row you came from instead of jumping to the top. Pointer hovers record the same memory, so mouse and keyboard agree about where you were. - SEARCH IS STICKY. Deleting the query used to snap you back to the grouped menu mid-thought; now the box stays and the list simply widens to EVERY action (capped at 200 rows, scrolling) - the browse list the retired node-search box used to be. Only Esc leaves search, so Esc now unwinds query -> search -> submenu -> menu, one step per press. - THE MENU KEEPS ITS ANCHOR. It picked the roomier side of the click on EVERY re-place, so a long match list re-decided and teleported the menu to the top of the screen. The side is chosen once, the anchored edge stays on the click, and anything that doesn't fit is capped and scrolls (verified: top 140 -> 140 while the list grows from 2 rows to 59). - "Search nodes…" no longer appears among its own search results (`revealFilter` rows are excluded from the leaf collection). Verification: context-menu-redesign +9 checks (sticky search, anchor stability, internal scroll, three-step Esc, cursor memory into and out of a mid-list submenu); node-search updated for the new Esc semantics +1 check for the exclusion; context-menu-v2 and context-menu-overflow green. svelte-check 419/62. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ckboxes Roadmap #16 second drop, batch Q2 — five reported items. - DEEP LINKS: "More snapping settings…", "Grid & axes settings…" and "Manage saved views…" now open Configure Scene, EXPAND the named section (even one you had collapsed) and scroll to it. They went through `showSidebar('scene')`, which TOGGLES - so clicking one while the panel was already open closed it. New `openSceneSection(label)` only ever opens, and Section.svelte watches an `inspectorScrollTo` store, expands, scrolls and clears it. - GRID FOLLOW is a three-way choice: Off / Look-at / Camera. "Follow the camera" tracked your POSITION, which is the wrong thing while you are looking somewhere else; Look-at centres the grid under the orbit target. Both modes stay HORIZONTAL (y = 0 - it is the ground plane, not a flying sheet) and snap the centre to whole cells so the lines keep agreeing with world coordinates instead of sliding under objects. The old boolean migrates to Look-at. - SNAPPING STEPS: Scale gained the missing 0.25 (and Rotation 90°), and a value typed in Configure Scene now joins the presets in the menu, sorted and marked as active - custom steps used to be invisible and unreachable there. - PHYSICS CHECKBOXES: the Sensor, freeze-axis and show-collider boxes were raw <input type="checkbox"> rendering as native controls; they use the themed component now, like everything around them. - ADD opens the new object's properties, even when another panel was showing. Verification: new tests/e2e/panel-deeplinks.test.cjs (16 checks: open from closed, expand a collapsed section, no-close on a second link, look-at follow snapped to cells with y pinned, Off recentres, 0.25 + custom step present and marked, themed checkbox, Add switches the panel); grid-snapping, add-menu-cursor, physics-colliders, collider-viz, inspector green. Build green, svelte-check 419/62. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Roadmap #16 second drop, batch Q3. The transform rows had a drag-to-scrub control; every other number was a plain <input type="number"> that only committed on Enter or blur - so its arrows looked like they did nothing, and the two behaved differently. DragRow is now THE numeric field and the sliders' boxes, the snapping steps, the clip planes, render order and the particle offsets all use it: drag horizontally scrub (Shift = fine, Ctrl = snap) click / type applies LIVE, no Enter needed ArrowUp / Down one MINOR unit (0.01 at 2 decimals), Ctrl x10, Shift x100; integer fields step by 1 / 10 / 100 Esc back to the value you focused it with It is a real <input> the whole time instead of swapping a button for a box: the caret is always there, ids keep working, and touch gets the numeric keypad (inputmode). type="text" on purpose - a native number spinner would fight our own arrow steps. TWO DELEGATION TRAPS, both found by the suites: Svelte's `onkeydown`/`onpointerdown` attributes are DELEGATED, running only once the event reaches the app root, and the panels this field lives in swallow both on the way up (the drawer's drag/resize wiring eats pointerdown, the flowbite dialog eats Escape). Esc-to-revert did nothing, and a drag whose pointerdown never arrived jumped the value by the pointer's absolute X (+22 instead of +2). Both are direct element listeners now. The pointer trio rides the WRAPPER so a scrub can start on the axis label too, and a click there focuses the input for typing. Verification: new tests/e2e/number-fields.test.cjs (13 checks: live typing, the three arrow steps, integer stepping, Esc revert, drag scrub, the same rules in a slider box); inspector updated for the always-input field (its drag check now lands on 2.0 instead of 22.9); camera-clipping, grid-snapping, camera-bookmarks, object-properties, panel-deeplinks, geometry-params, particles(+impact), environment(+v2) green. Build green, svelte-check 419/62. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Roadmap #16 second drop, batch Q4. Selecting a camera object now shows a small LIVE preview bottom-right. It is not a second WebGL context (that duplicates every texture and geometry on the GPU): Outline.svelte draws one extra SCISSORED viewport of the same renderer after the composer pass, into the rect the window publishes - so the DOM part is pure chrome and the cost is one viewport draw, only while a camera is selected. gl clears respect the scissor box, so the inset clears just itself. - Right-drag moves it (left stays free for the viewport), touch-HOLD then drag; it clamps fully on screen and parks bottom-right, LEFT of an open side panel so the two never overlap. - Inside the frame the camera's own marker and the frustum lines are hidden for that draw - you would otherwise be looking at the inside of its body. - A ⤢ button jumps to the full-screen preview; ✕ turns the window off for that camera. On by default, with "Preview window while selected" in Camera properties. - CAPTURE moved onto its own row below Preview / Set from view / Align view, with a camera icon and a note that it saves a PNG at the framing aspect. Two familiar traps, both caught by the suite: `pipTarget` reads the selection SET (the sticky `selectedObject` kept the window open after a deselect) and takes `objectsGroup` as a dependency (the `pip` flag lives on userData, and THREE trees are not reactive - the post-write poke is the only signal a derived store gets). Verification: new tests/e2e/camera-pip.test.cjs (18 checks: targeting, published rect matches the frame, framing aspect, parking clear of the panel, right-drag + clamping, hidden during a full preview and after deselect, the per-camera switch, and the DOM->gl y-flip); camera-objects (banner check hardened against its fly transition), camera-preview-control, viewport-selection, collider-viz green. view-mode still fails its pre-existing shadow-catcher check. Build green, svelte-check 419/62. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… PiP polish Roadmap #16 second drop, batch Q5. THE BUG, properly this time. Nothing in the app ever disabled orbiting during a gizmo drag: threlte's TransformControls disables whatever controls sit in ITS OWN context slot, and a camera preview unmounts + remounts the editor's OrbitControls, after which that slot points at an instance which no longer drives the view. So dragging an object also spun the camera. The `dragging-changed` hook now does the suppression itself, first thing and for every gizmo target, writing through `activeOrbit` - instance-proof. The preview's own controls are disposed when Control ends (three keeps DOM listeners on a merely-dropped instance). Reproduced end to end first: the SAME real drag on the REAL gizmo arrow rotated the view 0.21 after a preview cycle and 0.00 before it; now both read 0.00000 and the object still moves the identical 1.04 units. MENU placement, rewritten to the rules you gave: open AT the cursor preferring downward; when the content does not fit below, shift the WHOLE menu up just far enough that its bottom stays inside (no flipping to the other side of the pointer); a scrollbar appears only when the content is taller than the window. SEARCHING no longer moves anything - the menu keeps the top it opened with, the list gets a sensible height instead of unfolding to the floor, and a corner grip resizes it while searching. GRID follow: 'camera' now uses threlte's own followCamera, which keeps the shader drawing lines at WORLD positions - smooth while you pan. Only 'look-at' snaps the centre to whole cells, which is what you want when it is locked to an object. DEEP LINKS scroll the container with the sticky header's height as an offset, so the section label lands BELOW "Filter properties" instead of under it, and "Manage saved views…" targets the SAVED VIEWS sub-heading (`data-anchor`) rather than the top of the Camera section. SNAP STEPS are clean numbers: the shared numeric field quantizes a drag to its own precision (a scrub used to produce 0.7999999999999999), the position/scale snap fields are 2-decimal, and every menu label and hint prints through one formatter. CAMERA PiP: the title bar drags with the LEFT button too (the body keeps right-drag so a stray left-click cannot move it), the transform gizmo is hidden for the inset draw - it sat on the lens as a giant coloured blob, and hiding the controls was not enough because three keeps the gizmo's visuals in a separate helper object - and the parked window keeps clear of the round HUD buttons. Verification: new tests/e2e/gizmo-orbit-leak.test.cjs (9 checks, A/B real-mouse gizmo drag before and after a preview cycle, so normal left-drag orbiting can never be mistaken for the bug); context-menu-redesign +5 (cursor-anchored open, shift-up with no scrollbar, sticky top while searching, bounded height, grip); panel-deeplinks +4 (three headers clear the sticky header, clean snap hint); camera-pip +4 (left-drag on the bar, body left-drag inert, gizmo restored after each draw). camera-objects, camera-preview-control, node-search, grid-snapping, number-fields, inspector all green. Build green, svelte-check 419/62. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…es, menu placement)
…ght, PiP layer Roadmap #16 second drop, batch Q6 — the six follow-ups. - LOOK-AT POINT no longer jumps to the world origin when you exit a preview. The editor's OrbitControls unmount while the preview owns the view, and the pair that mounts on exit starts from the default target (0, 1.5, 0). The target is now snapshotted when the preview takes over and copied back onto the FRESH instance once it appears (the store still holds the disposed one for a beat). Verified: (6, 2, -3) in, (6, 2, -3) out. - SNAPPING ROWS align: label | chips | field in one grid, so the three numeric boxes share a right edge no matter how many preset chips a row has (4 / 4 / 3), and the chips are tightened enough to stay on ONE line in the 320px panel. - NO CARET during a scrub: pointerdown's default is blocked so a drag can never place or drag the caret through the digits (you could watch it skate about), selection is suppressed while scrubbing, and focus is handed over on RELEASE instead — which is also what makes click-to-type work. - THE SEARCH LIST REMEMBERS the height you drag it to, per menu kind: the viewport menu, the node editor and the object menu each keep their own (`sizeKey` prop → `ctx:searchHeight:<kind>` locally). - DEEP LINKS are deterministic: measure → scroll → re-measure → correct, with the scroller found by real SCROLLABILITY rather than class names, an instant scroll (a `smooth` one was being cancelled by the reflow of the section it had just expanded) and retries while the just-expanded content lays out. All three land 8-20px under the sticky filter header even when the panel starts scrolled to the bottom. NOTE: the previous check passed vacuously — it only asked for "somewhere below the header", which is true without any scrolling at all. - CAMERA PiP frame drops to z-index 2: it is a viewport overlay, not chrome, so panels and HUD now cover it (verified pip z 2 vs panel z 10). Verification: camera-preview-control +2 (look-at round trip), panel-deeplinks +3 (tight landing band from a bottom-scrolled panel), context-menu-redesign +3 (height persisted, restored on reopen, per-kind keys), number-fields +4 (scrub reports itself, no focus, no selection, click still focuses), camera-pip +2 (layer below the panel). camera-objects, gizmo-orbit-leak, grid-snapping, node-search, inspector, object-properties green — 11 suites in one run. Build green, svelte-check 419/62. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
e2e-verify gains an ASSERTION DISCIPLINE section, because the expensive failures in this roadmap were not broken code but checks that could not fail: - position/layout asserts need a tight band AND a start state that forces the behaviour (the deep-link check passed while nothing scrolled, because most sections were collapsed and the label was trivially "below the header"); - isolate a regression with an A/B of the same gesture, and assert the gesture did its job, so a no-op cannot read as a pass; - match the metric to the gesture (OrbitControls: LEFT rotates, RIGHT pans - a quaternion check on a right-drag reads 0.0000 forever); - when a check says pass and the user says fail, re-read the check first. Also in e2e-verify: HMR churn makes runs LIE (a page loaded while vite re-transforms edited modules is half-mounted - three runs "proved" a working feature dead); component-side debug hooks as the cure (__outlineDebug, __cameraPreviewDebug, cameraHelpersDebug, pipDebug); the real-mouse GIZMO drag recipe (find the picker via getHelper().traverse + project, never guess pixels, and remember three keeps the gizmo visuals in that helper); panels scroll so scrollIntoViewIfNeeded before boundingBox; section headers are buttons with a -/+ glyph so match with startsWith; the new numeric-field (.dn-wrap/.dn-input) and context-menu (.ctx-filter-input/.ctx-match/.ctx-grip/[data-ctx-active]) selectors; the new __stores keys; and node-search is OFF the known-failing list (two of its assertions were asserting the old no-scroll contract). peer-feature gains "the cheapest replicated feature: put it on userData" - the full recipe behind userData.camera/physics/particles (deterministic defaults at creation, ONE write path with props history + objectParameters + poke, the applier case that a two-peer suite must cover, scene-root visuals), plus PRESENCE-style state (campreview: one message on change, per-peer map, cleanup in BOTH teardown paths, late joiners piggyback an existing handshake reply) and the lock-RELEASE trap (a `lock` message only replaces the sender's set, so letting go needs explicit `unlock` per uuid). CLAUDE.md: the Configure-Scene deep-link seam (openSceneSection/inspectorScrollTo/ data-anchor and why smooth scrolling and class-name scroller lookups fail), the remembered per-kind menu search height (sizeKey), revealFilter rows, and the PiP frame's deliberate z-index 2 (a viewport overlay, so chrome covers it). svelte-check 419/62 unchanged (docs only). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Roadmap #16 — the 2026-08-04 feedback drops. Stacked on #85 (base =
fix/roadmap15-q-menu); retarget torelease/nextonce #85 lands. Lane../theprototype-lane-ui@5186. Plan + as-built notes: cloud repodocs/plans-core/roadmap-16-menus-grid-cameras.md.First drop — P1…P6
P6
[fix]deselect releases the peer lock; Selected menu follows the set — a deselect broadcast nothing andlockonly ever replaces a peer's set, so clicking empty space left objects highlighted + "locked by X" on every other peer. "Selected ▸" also lingered (it was gated on the STICKY primary), and Configure Scene's active row dropped its inset accent bar.P1
[feat]context menu: filter hidden until typing, arrow-key navigation — the filter input stays mounted + focused (that's what swallows app shortcuts) but collapses to zero height until you type. ↑/↓ walk the level, Enter runs or opens a submenu, Esc unwinds; ←/→ stay with the caret. New$lib/menuFilter.jsholds the flatten + ranking.P2
[feat]node editor: search runs inside the context menu — the private#node-search-boxis gone; "Search nodes…" reveals the shared filter. Found a real bug: a menu opened near the bottom grew off-screen while filtering (place()only ran on open) — now re-places via ResizeObserver. Two stale phase-124 assertions repaired;node-searchis green again.P3
[feat]grid + snapping settings; menu marks the active choice — Configure Scene ▸ Grid and ▸ Snapping;●replaced by a bold-on-tintcheckedstyle; the snapping submenu is sectioned with a live hint.P4
[feat]camera section; named bookmarks with lenses — unlimited NAMED bookmarks storing FOV + clip planes, managed inline; new Camera section (lens, clip, orbit speeds, damping, invert-Y, Frame scene, Reset view).P5
[feat]scene camera objects — perspective/orthographic markers carryinguserData.camera, scene-root frustum wireframes, a true camera-swap preview, Control (WASD fly, one undo), framing guide, Capture, replicated preview presence.Second drop — the follow-up items
[fix]camera control: no view jump, no orbit-controls leak — pressing Control snapped the view to the origin (a fresh OrbitControls targets 0,0,0 and itsupdate()ends withlookAt); the controls are now seated behind the camera and the pose is re-synced from the marker first (drift 0.0000). And after exiting a preview, dragging the gizmo also orbited the camera: both control sets were bound to one store and threlte clears a bound ref on unmount, so every suppression wrote to nothing. The preview publishes its ownpreviewOrbit; a derivedactiveOrbitdrives Scene's six suppression sites and its navigation call.[feat]context menu: sticky search, remembered cursor, fixed anchor — stepping out of a submenu returns to the row you came from; deleting the query keeps the search box (the list widens to every action) and only Esc leaves search; the menu no longer re-decides its side as the list grows, so it stays anchored at the click and scrolls internally; "Search nodes…" is excluded from its own results.[feat]panel deep links, grid look-at follow, snap values, themed checkboxes — the three "…settings" rows open Configure Scene, expand the named section and scroll to it (they used to close an already-open panel); grid follow is Off / Look-at / Camera, horizontal and cell-snapped; Scale gained 0.25 and a custom step from the panel now appears in the menu, marked; the Physics section's raw checkboxes are themed; adding from the Add menu opens the new object's properties.[feat]one numeric field everywhere — DragRow is THE numeric control: drag to scrub, type with LIVE updates, ↑/↓ step one minor unit (Ctrl ×10, Shift ×100; integers 1/10/100), Esc reverts. Two delegation traps found: Svelte'sonkeydown/onpointerdownattributes are delegated and the panels swallow both on the way up — Esc did nothing and a drag jumped by the pointer's absolute X. Both are direct element listeners now.[feat]camera preview window (PiP) + Capture row — a small live view bottom-right when a camera is selected, rendered as a scissored inset of the same renderer (no second WebGL context), right-drag/touch-hold to move, clamped, parked clear of an open panel, per-camera on/off; Capture moved to its own row with an icon.Verification
New suites:
deselect-unlock(9, two-peer),grid-snapping(15),camera-bookmarks(14),camera-objects(27, two-peer),camera-preview-control(9),panel-deeplinks(16),number-fields(13),camera-pip(18). Updated:context-menu-redesign,node-search,viewport-selection,inspector.Green regressions: context-menu-v2/overflow, add-menu-cursor, object-search, mobile-hud, multiselect-menu, packs-explorer, camera-clipping, roadmap-13-viewport-camera, collider-viz, inspector, object-properties, physics-colliders, geometry-params, particles(+impact), environment(+v2), vr-snap-menu.
Known pre-existing failures, confirmed identical with these changes stashed:
add-menu(its documented right-tap flake) andview-mode(shadow-catcher check).npm run buildgreen; svelte-check 419/62 held throughout.Third + fourth drops — Q5 / Q6
[fix]gizmo drags no longer orbit the view — the reported bug, at the root: nothing in the app ever disabled OrbitControls during a gizmo drag. Threlte's TransformControls does it against its own context slot, and a camera preview unmounts + remounts the editor's controls, after which that slot points at an instance that no longer drives the view. Scene'sdragging-changedhook now suppresses orbiting itself throughactiveOrbit, and the preview's controls are disposed on hand-back. Proven by an A/B real-mouse drag on the real gizmo arrow: 0.21 rotation before the fix, 0.00000 after, with the object moving the identical 1.04 units both times.[fix]look-at survives a preview round trip — the remounted editor controls came back with their prop defaulttarget.y = 1.5, recentring the view on the world origin. The target is snapshotted at hand-over and copied onto the fresh instance ((6, 2, −3) in → (6, 2, −3) out).Menu placement, to spec: open at the cursor preferring downward; when the content doesn't fit below, shift the whole menu up so its bottom stays inside (never flip to the other side of the pointer); a scrollbar only when the content is taller than the window. Searching keeps the opened top, bounds the list height, and offers a corner grip — whose height is now remembered per menu kind (viewport / nodes / object).
Grid —
camerafollow uses threlte's ownfollowCamera(smooth panning; the shader keeps lines at world positions); onlylook-atsnaps the centre to whole cells.Deep links — deterministic measure → scroll → re-measure → correct, scroller found by real scrollability, instant scroll (a smooth one was being cancelled by the reflow of the section it had just expanded). All three land 8–20px under the sticky filter header even from a bottom-scrolled panel. The earlier check passed vacuously ("somewhere below the header" is true without any scrolling at all) and was replaced with a tight band from a forcing start state.
Numeric fields — drags quantize to the field's own precision (0.7999999999999999 is gone) and no longer drag a caret through the digits (focus is granted on release, selection suppressed while scrubbing).
Snapping rows align on a label | chips | field grid, with chips tightened so four presets stay on one line.
Camera PiP — the title bar drags with the left button, the transform gizmo is hidden for the inset draw (three keeps its gizmo visuals in a separate helper object, so hiding the controls wasn't enough), the frame sits at z-index 2 so panels and HUD cover it, and it parks clear of the round HUD buttons.
Extra suite:
gizmo-orbit-leak(9 checks). Extended:camera-preview-control,panel-deeplinks,context-menu-redesign,number-fields,camera-pip. Final sweep: 11 suites green in one run, build green, svelte-check 419/62.🤖 Generated with Claude Code