[fix] roadmap #15 second drop: properties UX, toasts, stars, PWA, selection/outline (K) - #84
Merged
Merged
Conversation
…w resize Roadmap #15 batch B (plan: cloud repo plans-core/roadmap-15, specs inline): - B1 ThemedSelect: the portaled popup copied the TRIGGER's width, and the trigger is sized to the SELECTED label - picking a short option ("Box") ellipsised every longer name on reopen. The trigger width is now a MINIMUM; the popup sizes to its content (capped at 28rem / the viewport) and is clamped back on screen when that overflows the right edge. Fixes all ~22 call sites at once. - B2 duplicate: a multi-select member wears an emissive highlight, and the clone's fresh material baked it in - the FIRST clone stayed selection-blue forever (its tinted value got recorded as its "original"). Clones now restore the source's recorded emissive (stripSelectionTint), and duplicateSelection no longer selects each clone mid-loop (which collapsed the set and restored sources one at a time); it selects the whole clone set once at the end. - B3 packs: double-click / Enter / "Place in scene" now show the same dismissible loading toast the viewport DROP path shows (shared holdLoadingToast helper) - a slow CDN fetch used to look like nothing happened. - B4 Connect: the chevron badges the toast count the CLOSED drawer is holding when toasts are routed drawer-only (mirrors the drawer tab badge; amber when approvals are pending). - B5 Sessions: naming a session (save + rename) is an inline textbox instead of window.prompt - Enter commits, Esc cancels, focus is taken automatically. - B6 modals: app modals are non-modal <dialog>s, so the page behind them is NOT inert and window key handlers still fired (WASD flew the camera behind Settings). New derived `anyModalOpen` gates shortcuts.js, editorNavigation (keydown AND the per-frame fly, which also clears held keys) and inputRuntime; only Settings was guarded before. - B7 windows: dragWindow gains an opt-in `resizable` corner grabber that persists {w,h} in the same win:<key> record and re-clamps on viewport resize; What's New opts in and goes full-screen below the Connect bar at <=640px (the .tp-modal-frame treatment). - B8 context menu: Edit mesh / Sculpt hide for a multi-selection - they are single-object modes and silently acted on the last-picked object only. Verification: new tests/e2e/ui-fixes-15b.test.cjs (22 checks covering B2/B4/ B5/B6/B7/B8), themed-select extended for B1; sessions + whats-new + the new suite green; build green; svelte-check baseline 435/62 held. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Roadmap #15 batch C. svelte-awesome-color-picker 3.x -> 4.1.3 (deps housekeeping) is a Svelte-5 runes rewrite with NO component events, so every `on:input` handler in the Inspector silently stopped firing - the swatch still tracked the drag (bind:hex) while nothing was applied. That is the reported "changing color in mesh properties does nothing". - C1 mechanism: all five pickers (material, light, hemisphere ground, scene background, fog) move to the v4 `onInput` PROP and read `c.hex` (v4 passes {color,hsv,rgb,hex} directly, not event.detail). - C1 binding: the pickers now take `hex` ONE-WAY. v4's ColorPicker writes its own snapshot back through a binding, which CLOBBERS external writes - a plain `backgroundColor.set(...)` was reverted while the picker was mounted. onInput is the input channel, so a binding buys nothing and costs correctness. (Also drops 14 svelte-check errors: 435 -> 421, gate updated.) - C1 sky: background/fog edits wrote scene.background / scene.fog directly and the next applyEnvironment() restored the preset values - so even with the handler alive the color snapped back. New `editEnvSky()` in environment.js detaches a live custom preset (the editRigComponent precedent), so sky edits stick, persist and replicate as {type:'environment'} instead of the old point-fix {type:'color', uuid:'background'} messages. - C1 material: the typed hex box routes through the shared materialsHandler.setObjectColor (undo + replication); the picker drag keeps its 600ms gesture debounce so a drag is ONE undo entry, not one per frame. - C2: the picker's hex/rgb/hsv text inputs are enabled everywhere (isTextInput + textInputModes), with the built-in mode cycle button - in both mesh properties and configure scene. Verification: new tests/e2e/color-picker-15c.test.cjs (12 checks - modes render, the picker applies + replicates + records one debounced undo entry, undo steps back, the scene background reaches three.js); inspector, environment, environment-v2 and ui-fixes-15b green; build green; svelte-check 421/62 (down from 435, release.yml gate ratcheted). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Roadmap #15 second drop, Opus batches L / M / N / O. O1 material readouts: Roughness/Metalness/Opacity showed the PRE-drag value. `material` was $derived from $selectedObject, but setMaterialParam mutates the material in place and pokes objectsGroup - and $derived compares with ===, so returning the same THREE material never propagated. It now returns a fresh SNAPSHOT per poke, which fixes every material row at once. O2 pin: the properties panel gains a pin toggle (persisted). Pinned it stays open and follows you - object selected shows its properties, nothing selected falls back to the Scene's (deselect no longer closes it). O3 opening it: a plain viewport click now only SELECTS (it used to force the panel open on every click - raycastSelect passed openProperties=true). The panel opens on DOUBLE-click, from the new context-menu "Properties" entry, or from the object list - and always when pinned. O4 menu indicator: the "● " prefix on Configure Scene shifted the label as it appeared and duplicated what the open panel already shows; the row carries an `active` highlight instead. L1/L2 info toasts: "Restore previous session?" and the first-run open-source notice were hand-rolled flowbite <Toast> blocks - which is exactly why they looked nothing like the other cards AND never appeared in the Connect drawer's Toasts tab. They are now sticky INFO entries in the normal toast store (teal accent vs the blue default and amber approvals), mirrored from their source stores so they still appear/disappear with them and keep their dismiss side effects. autoDismiss skips sticky entries. L3 the standalone hex textboxes under the colour pickers are gone - 15-C2 gave every picker its own hex/rgb/hsv field, so they were duplicate UI. L4 the "+N more" overflow line is a button that opens the drawer on Toasts. M GitHub stars: the Welcome overlay's GitHub button shows the star count, and the cloud profile dropdown gains "★ Star on GitHub" ABOVE Support (cloud repo, separate commit). Cached 12h in localStorage (unauthenticated GitHub allows 60 req/h/IP) and HIDDEN on any failure - never a 0. N PWA: manifest + icon set (rasterized from logo.svg) + iOS meta, and a service worker that is a pure network passthrough. That satisfies Chrome's installability criteria without ever serving a stale build - version.json polling stays the update path. Verification: new tests/e2e/ui-fixes-15lmno.test.cjs (26 checks); inspector, whats-new, themed-select, color-picker-15c, ui-fixes-15b, connect-states all green; build green; svelte-check 421/62 held. panels + roadmap-13-notifications failures verified PRE-EXISTING on clean HEAD (git stash). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…banner Follow-up to the 15-L toast work (user review 2026-08-02). - The two z-tiers were BOTH `absolute; top:65px; left:50%`, i.e. pinned to the same coordinates - approval cards physically covered the info toasts. They now live in ONE wrapper and stack. The wrapper centres with auto margins on purpose: a transform would create a stacking context and trap the children's z-index, which is what keeps approvals above modals and ordinary toasts below them. - Connection requests wear the shared .tp-toast card (amber = needs a decision) instead of their own .cxreq chrome, keeping the role-coloured buttons. A rush of joiners folds like any other burst: 3 cards + a "+N more requests…" line into the drawer's Toasts tab. - "Receiving objects" wears the card too (green, progress bar inside). - STICKY prompts can no longer be evicted: only transient toasts are capped, the "+N more" count reflects just those, and sticky cards render last so they hold a stable spot. (A burst used to fold the restore prompt away.) - Spectating is a MODE, not a notification: "Watching X" is now a pinned banner at the head of the stack - red framing, live dot, prominent Exit - so incoming toasts can never shove it (the reported jumping) and it stays centred. It is not `fixed` at the top because that collides with the Connect pill. - Configure Scene: dropped the left accent bar from the active row (tint only). Verification: ui-fixes-15lmno extended to 37 checks (stacking, request folding, sticky survival, banner stability) - all green; svelte-check 421/62 held (the dead .cxreq rules were removed rather than left as unused-selector warnings); build green. Also fixed a STALE assertion in roadmap-13-notifications-notes that expected "Connection request from peer" - text the UI never had; that suite is down to its one pre-existing unread-badge failure. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- "Receiving objects: N/N" never disappeared: the old visibility machinery
(showToast + toastStatus + a re-arming trigger()) fired its "done" branch on
EVERY effect run once $loading emptied - and $objectsGroup pokes on every
scene mutation, so the finished card kept re-showing forever. Replaced with
one state and one rule: visible while a transfer runs, a 2.5s grace so the
user sees "N/N", then gone until the next transfer starts.
- The share-or-stash prompt ("Share your N objects with X, or stash...") was a
15s action toast with a 14s auto-share fallback - it could expire underneath
the user and silently publish a scene they meant to stash. It is now a
STICKY prompt: the joiner's handshake reply waits until it is answered;
closing it with the X takes the safe old default (share).
e2e: ui-fixes-15lmno grows to 45 checks (progress shows/lingers/hides and is
NOT resurrected by scene mutations; the gate prompt is sticky, offers both
choices, resolves on answer); share-stash + sessions suites green (the
Share/Stash buttons match the shared card's action buttons). The spectator
banner-stability check now samples a STABLE baseline first - its own intro fly
can outlive a fixed wait under machine saturation. svelte-check 421/62 held.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… Ctrl+D Roadmap #15 batch K (viewport selection & outline) - the three reported viewport symptoms shared one cause: the outline read `selectedObject`, and selection state disagreed between paths. - K1: the outline is driven by `selectedObjects` (the SET). `selectedObject` deliberately KEEPS the last object after deselect (the open inspector binds to it) so it could never signal "no outline" - deselecting now clears the outline. NOTE for future work: never read selectedObject for presence-of-selection; that is the documented sticky-primary trap. - K2: every uuid in the set is TRAVERSED and each child mesh outlined - postprocessing's OutlineEffect only renders meshes, so adding a Group outlined nothing (the "selecting a group shows nothing" symptom) and only the primary of a multi-selection was ever added. Peer-locked uuids get the same traversal (a locked GROUP had the same blind spot). - K3: createGeometry/createLight/createGroup and the import/animated-import paths now populate the selection SET alongside the sticky primary (they attach the gizmo, so the object IS selected - the set just never said so). Direct store writes, sceneStore only - no new import edges, no lock broadcast (replication unchanged). - K4: Ctrl+D with an empty SET toasts "Nothing selected to duplicate" instead of resurrecting the sticky last object. Safe only now that K3 makes the set authoritative; the one legitimate empty-set-with-primary state - VIEWING a peer-locked object - still falls through and duplicates an editable copy. Verification: new tests/e2e/viewport-selection.test.cjs (14 checks: create outlines + populates the set, deselect clears, trio outlines 3, a GROUP outlines its 3 child meshes, locked-group traversal, duplicate-after-create, empty-duplicate toast, locked-view duplicate). Green: multi-select, undo (two-peer), inspector, delete-object, ui-fixes-15b, ui-fixes-15lmno, vr-mesh-undo. Build green; svelte-check dropped 421 -> 419 (the rework removed two legacy errors; release.yml gate ratcheted). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- $derived compares with === : a derived returning the same in-place-mutated THREE object never propagates - return a fresh SNAPSHOT (the O1 material readout bug; adding the store as a dependency alone does not fix it) - color-picker 4.x: onInput PROP (no component events), one-way `hex` (the binding writes back and clobbers external writes), sky edits via editEnvSky (applyEnvironment reverts direct scene.background/fog writes) - selection truth is the SET since #15-K (creation populates it, the outline traverses set members' meshes, empty-set Ctrl+D toasts; locked-VIEW is the one deliberate empty-set-with-primary state); #15-O click semantics (plain click selects, dbl-click/menu/pin opens properties) - toast system: showToast vs sticky showInfoToast/dismissToastById, the .toasts-stack wrapper (auto-margin, transform would trap z-index), request cards + fold, the spectator MODE banner; anyModalOpen gate - PWA: no-cache SW rule (never cache without wiring the version poll); dragWindow `resizable` - e2e skill: baseline 419/62; port-shadow trap grew the "curl a source file, grep your symbol" proof + lane-ui moved to 5186; PEER_CONFIG reminder for two-peer on localhost (the Approve-button timeout); sticky-toast + __outlineDebug test notes - peer-feature skill: sticky-prompt guidance (safe-default on dismiss) - status entry for the #15 PRs Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review call (15-P2 follow-up): the prompt exists because auto-deciding was wrong, and a ✕ that silently picks Share is the same trap smaller. Sticky info toasts gain an optional `noClose` flag - no ✕ on the card, no Dismiss row in the drawer - and share-or-stash uses it. The restore prompt and the first-run notice keep their ✕ (dismissing those has a sensible meaning). e2e: ui-fixes-15lmno asserts the fork renders no ✕ (46 checks). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-filter Roadmap #15-Q (user-approved layout + type-to-filter). Functionality is unchanged - every action, guard and submenu survives; only structure, metadata and styling change. All three menus (object / viewport / Selected) now read as one family. - HEADER strip: what the menu acts on - object name + type badge, or "N objects selected"; a peer lock shows "locked by X" here instead of scattered tooltips. - SECTIONS: quiet uppercase labels (Edit / Physics & effects / Share) over thin rules; Delete sits last after a plain divider, red, with a Del hint. - ICONS: lucide kebab names in the item data, resolved via ui/Icon.svelte (registry extended); a level reserves the icon column only when a sibling has one, so small data-driven menus (packs, node cards) are untouched. - HINTS: shortcut hints (F, Ctrl+D, Del, Ctrl+Z/Y) render as a dimmed monospace right column instead of hiding in tooltips. - HOVER-INTENT: submenus open after 120ms and close after 150ms - a diagonal pass no longer flickers them; entering the submenu cancels the close. The container deliberately keeps NO role attribute (the menu suites locate submenus by "fixed div without a role"). - TYPE-TO-FILTER (dense menus only, >=8 leaf actions): an autofocused row - typing flattens every leaf command-palette style with its submenu path ("Snapping > Snap to surface"), Enter runs the top hit, Esc clears then closes. Focus is deferred one frame: the parent's use:portal moves the menu into <body> AFTER the child action runs, and moving a focused element blurs it. - Show/Hide REMOVED from the object menu (user call): hiding from the menu was a one-way door - you cannot right-click an invisible object to bring it back; the object list's eye toggle owns visibility. - 'Selected >' label loses its baked-in arrow (the renderer draws the chevron). Verification: new tests/e2e/context-menu-redesign.test.cjs (16 checks); context-menu-v2, context-menu-overflow, multiselect-menu (two-peer), add-menu(+cursor), object-search, packs-explorer, mobile-hud all green (multiselect-menu updated: header/section rows carry no label). Build green; svelte-check 419/62 held. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
[feat] roadmap #16: menus, grid & scene cameras (P1-P6)
[feat] context menu redesign: header, sections, icons, hints, type-to-filter
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 #15 second drop — now the full set: batches K / L / M / N / O + the toast-system rework and two review fixes (plan: cloud repo
plans-core/roadmap-15…#second-drop-2026-08-02).K — viewport selection & outline (Fable)
Three reported symptoms, one cause: the outline read
selectedObject, and selection state disagreed between paths.selectedObjectdeliberately keeps the last object for the inspector, so it could never signal "no outline")./create, lights, groups, imports) — it attached the gizmo but never told the set.Toast system (review follow-ups)
.toasts-stackwrapper now stacks them (auto-margins on purpose: a transform would trap the z-index that keeps approvals above modals).objectsGrouppoke once loading emptied, so the finished card re-showed forever. Now: visible during transfer → 2.5s grace at N/N → gone.O — properties panel UX
Pin toggle (pinned = follows selection, deselect falls back to Scene) · plain click only selects (it used to open the panel on every click); opens on double-click / new context-menu Properties / object list · material slider readouts fixed (a
$derivedreturning the same in-place-mutated material never propagates —===equality; snapshot per poke) · Configure Scene●→ row highlight (no left bar, per review).L / M / N
Info-kind sticky toasts in the drawer · redundant hex textboxes removed · "+N more" opens the drawer on Toasts · GitHub stars on Welcome + cloud profile (cloud
ec87928; 12h cache, hidden on failure) · PWA manifest + icons + no-cache SW (version.jsonstays the update path; dev/self-signed HTTPS can't show the install prompt — verify on the deployed site).Docs
CLAUDE.md + e2e-verify/peer-feature skills updated with the #15 learnings (
$derived ===snapshot rule, picker v4, selection-set truth, toast API, port-shadow proof, baseline 419/62 — C dropped 14, K dropped 2 more; release gate ratcheted).Verification
ui-fixes-15lmno(45 checks),viewport-selection(14 checks);themed-selectextended.npm run buildgreen;npx svelte-check419/62.roadmap-13-notifications-notes(expected "Connection request from peer" — text the UI never had); that suite is down to its one pre-existing unread-badge failure.🤖 Generated with Claude Code