Release 1.2.0 — Make it real - #88
Merged
Merged
Conversation
…able lockstep - three ^0.185.1; @threlte/core 8.5.16, extras 9.21.0, xr 1.6.1 (off the year-old -next pins) - guardThrelteXr vite transform + optimizeDeps.exclude DELETED: the stable xr package rewrote input handling (setupInputSources keyed map) - the unguarded handedness indexing behind the Cardboard teardown crash no longer exists in the dist; useHand/useController/component APIs unchanged - all 21 three/examples/jsm imports -> three/addons (10 files) - breaking-change sweep vs the official r171->r185 guide: no PCFSoftShadowMap/ RGBELoader/Clock/ColorManagement-rename/toTrianglesDrawMode usages; CapsuleGeometry uses positional args; FBX imports now auto-convert Z-up to Y-up upstream (r183 behavior change, no code needed) - overrides: camera-controls pinned 3.1.0 (3.1.2 declares node>=22 engines; project is engine-strict on node 20; patch-level diff) - postprocessing 6.39 officially supports three 0.185: npm install no longer needs --legacy-peer-deps Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…oot warm-up) three 0.185 + Chromium <=150 (ANGLE D3D11): any shader program FIRST COMPILED while the N8AO pass is enabled links broken - with AO on from boot the whole scene is black (outline pass only); with AO enabled later, every mesh created afterwards renders invisible (its outline still draws). Chromium 151+ fixed the underlying bug, which is why Playwright chromium (151) passed while real Edge/Chrome/Android (150) broke. Fix: AO is gated OFF on Chromium <=150 (UA major) with a one-time toast - lights/shadows/outlines keep working, AO returns with a browser update. A ~10-frame boot warm-up stays as belt-and-braces for unknown engines. Repro/verify: headed+headless screenshots via the installed msedge channel (boot-black, invisible-new-mesh, and post-fix live orbit + toast). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e-threlte Deps migration A: three 0.171 -> 0.185 + threlte prerelease -> stable
…tible picks) - @xyflow/svelte 0.1.39 + flowbite-svelte 0.47.4 (grouped minor/patch, PR 57) - prettier-plugin-svelte 4.1.1 + prettier-plugin-tailwindcss 0.8.1 (PRs 57/58) - svelte-awesome-color-picker ~4.1.0 (PR 60 wanted 4.1.3, which declares node>=24 engines; 4.1.0 is the svelte-5 rewrite without the engine wall - picker verified rendering in the Inspector; ships types, svelte-check drops to 437/62) - vite-plugin-mkcert stays 1.x (2.1.0 declares node>=22.19; revisit with the node upgrade planned for migration Part D) - closes PR 61 as not-now - @sveltejs/adapter-auto REMOVED instead of bumped (PR 59) - adapter-static is the one in use Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ge missed search-menus and fly-shortcut-keys drive Shift+A and were not updated when the shortcut became opt-in (default off) - they had not run between that change and the Part A full run. Same seed as add-menu/add-menu-cursor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
e2e: seed the Shift+A opt-in in search-menus + fly-shortcut-keys
- flowGraphs.js: new 'flownodes' history kind (create/delete/data ops, one undoable entry per AI call; serialized copies so re-broadcasts hash identically) + recordFlowNodesEntry - physics.js: setPhysicsFor(uuid, patch) shared write path (props history + objectParameters replication); enablePhysicsOnSelection and Inspector setPhysics refactored onto it - new src/lib/ai/flowTools.js: create_flow_nodes / update_flow_nodes (always available) + set_physics / create_joints / control_simulation (gated by a per-provider Physics tools checkbox); curated node-type enum with alias map, editor-identical node/edge construction, implicit-owner leverage - tools.js: schemas + dispatch + repairToolCall (5 new names, invention aliases, shape inference incl. physics-only updates -> set_physics), summarizeScene now carries per-object physics + compact flow, top-level sceneFlow + joints; system prompt gains flow block + gated physics block (spider recipe, world-aligned jointing, auto sim start) - assistant.js: status labels + tally for the new tools (control_simulation counts 0); providers.js physicsTools flag; Settings -> AI checkbox with docs link - e2e: ai-flow-physics.test.cjs (38 checks) - scripted moving-spider scenario over a mocked endpoint, gating + per-item physics-node rejection, invented add_behavior repair, one-undo/redo semantics, broadcast capture - verified: build green, svelte-check 437/62 (baseline held), suites ai-assistant ai-local-model flow-object-graphs physics-joints all pass Co-Authored-By: Claude <noreply@anthropic.com>
… freeze + gravity + viz + custom compound editing (CL-A) - colliderSpec.js: ONE source of truth for collider shapes (measure, hull verts, custom compound pieces) - consumed by BOTH physics and the viz - physics.js: spec-driven collider construction placed relative to the CURRENT body pose; rebuildColliders/physicsShapeChanged rebuild a body's collider set MID-SIM (no restart, joints/velocity survive); liveParamsJson widened to collider/sensor/freeze/material/mass; per-piece mass split - sensors (userData.physics.sensor): setSensor colliders, pass-through, both directions collected per pair; fireObjectEnter/fireObjectExit dispatch in flowRuntime (replicated nodetrigger stamps; nodes land in CL-C) - physics material presets (ice/rubber/wood/metal) + Inspector Material select above the Bounciness/Friction sliders - freeze axes (userData.physics.freeze rx/ry/rz/px/py/pz) via setEnabledRotations/Translations + Inspector lock rows (dynamic only) - scene gravity: scenePhysics.js replicated latest-wins singleton (scenephysics msg + handshake push), applied at world create AND live; Inspector scene Physics gravity slider + reset - collider viz: colliderHelpers.js (lightHelpers pattern) - scene-root wireframe proxies from the SAME spec, green/amber(sensor), global showColliders (local pref) + per-object union, per-frame follow, hidden in wireframe view mode - custom compound colliders: colliderEdit.js proxy session runs the real Edit Mesh tool on a scene-root proxy (edit-proxy lookup fallback in faceEdit/meshEdit; peer messages no-op); + Box/Sphere piece; Done splits SHELLS (shellsOfTris union-find, shared with CL-B) -> colliderVerts/ colliderPieces (1200-float cap) -> one convexHull piece per shell on one body; Inspector collider select gains Custom (edit...) - MeshEditPopup: collider-session banner state (add piece / Done / Cancel) - suites: collider-viz (13), collider-live (11), collider-custom (13) all green; regressions physics-colliders/particles-impact/mesh-edit-popup/ novr-inset/desktop-face-gizmo green (terrain-sculpt = env-gated two-peer) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- new src/lib/vrSleeve.js: flat strip of 6 ghost mini-primitives riding the sleeve-hand forearm (left; mirrors right when the menu owns the left hand), slots wrist->elbow at 0.055m pitch, scene-graph only (never objectsGroup). Trigger on a ghost detaches a held preview that rigid-follows the grabbing controller (stick-Y scales 0.2-5, wrist rotates 1:1); release creates the primitive at the preview pose with grip-drop snap rules (grid rounds x/z, surface rests it) as ONE undo batch. K2: grip-drop an object ONTO the strip to capture a prefab snapshot into a custom slot (LOCAL idb, 8 cap, 5 MB), ghost thumbnail + clear chip on a second row, slot spawn = replicated instantiatePrefab at the release pose. Gated by vrSleeveEnabled (default OFF) via Settings > VR + a VR Settings panel row (settings:sleeve) - src/modules/vrsleeve/: thin core-module shell (register wires the hooks; disabling the module removes the feature entirely) - vrControls.js: generic module-VR hook registries (nav suppressor, panel group provider for the beam/reticle family, trigger start/end/swallow hooks, grip-drop interceptor, per-frame hook) so feature modules plug into the interaction loop without import cycles; onSqueezeEnd release can be consumed by a hook (object pose restored, physics hold still released) - Scene.svelte: onXRSelect swallow guard + selectstart/selectend module-hook dispatch (a sleeve pick/place never falls through to raycastSelect) - e2e: vr-sleeve.test.cjs (29 checks, headless with synthetic poses): strip structure, default-off gating, ghost->create round trip w/ one-undo + broadcasts + grid/surface snap, suppression + swallow predicates, K2 capture/restore/persist-across-reload/spawn/clear/cap - on-device feel (forearm placement/offsets, grab ergonomics) = manual check - verified: build green, svelte-check 437/62 baseline held, suites vr-grab vr-window-grab vr-vertex-hold vr-box-select vr-teleport-toggle vr-snap-menu all pass Co-Authored-By: Claude <noreply@anthropic.com>
…ubdivide/flip/weld/bridge + floating toolbar (CL-B) - B1 inset interaction fix: the seated MOVE gizmo intercepted the next face click (dragging||axis guard) so click 2 of an armed inset RIGID-MOVED the face - the gizmo now seats ONLY for the Move op (Scene click path, setFaceOp arms detach, enterFaceEdit restore gated); regression in mesh-ops suite - B2 face-mode wireframe: shared buildEditWireframe + meshEditWireframe display toggle (local pref, honored by BOTH modes, live mid-session); face overlay rebuilds on every geometry swap (commit/gesture/remote/undo) - B3 granularity Face / Triangle / Shell: 'polygon' RENAMED to triangle (legacy value migrates at read time - VR suites untouched), shell = the connected island via shellsOfTris; setFaceGranularity 3-state + toggleFaceGranularity cycles (VR menu label shows Face/Triangle/Shell) - B4 topology ops: subdivide (each target tri -> 4 via welded edge midpoints), flip (winding reversal), weld (vertex multi-selection -> its centroid, ONE meshgeo undo entry - a verts entry can't hold per-handle befores), bridge (exactly two multi-selected faces: ordered boundary loops, equal-count gate, closest-pair anchor + untwist direction pick, outward-wound wall quads; watertight on separated shells) - B5 toolbar redesign: MeshEditPopup rebuilt as a FLOATING draggable segmented toolbar (dragWindow key meshEditToolbar, runes mode) - mode / granularity+multi / ops / display / done segments, keyboard shortcuts (E I G S B F X, W weld) active only while mounted, contextual amount row, collider-session banner preserved; all suite element ids kept - mesh-ops suite (17 checks): inset regression, wireframe toggle, granularity picks + legacy migration, subdivide 4x, flip, watertight bridge, shell pick, weld + undo; desktop+VR face suites all green Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… toolbar - terrainSculpt gains a MESH mode (sculptMode store): weld by full xyz position (split copies never tear), tri-adjacency neighbors, brush displaces welded verts along their averaged NORMAL - raise/lower along normals, flatten pulls to the hit tangent plane, smooth = Laplacian relax toward neighbor average; same smoothstep falloff constants - entry: object context menu 'Sculpt mesh' on any mesh (Terrain keeps its column brush + 'Sculpt terrain'); 45k-float meshgeo cap gates entry - same netcode as terrain: non-indexed conversion synced on entry, throttled meshgeo previews, ONE snapshot + undo per stroke - weld caches carry the position-attribute identity - a same-tick brush after a remote/undo meshgeo swap can never hit a stale cache (the async rebuild hook alone left a gap) - brush cursor ring orients to the surface normal in mesh mode - SculptToolbar rebuilt as a FLOATING draggable toolbar (dragWindow key sculptToolbar) matching the mesh-edit toolbar language; titles the mesh vs terrain session - mesh-sculpt suite (13 checks): mode flip, floating toolbar, normal raise + falloff, one-undo stroke, smooth relax, welded no-tear, meshgeo replication spy, exit, terrain routing Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…locity readout (CL-C) - collider node (Physics group): shape box/sphere/capsule/cylinder/hull/ custom/object + scale + sensor toggle; consumed by collectParams and WINS over the Inspector pick (the mass precedent); shape 'object' hulls the geometry of the Object Selector wired into its source handle (colliderSpecOf gains sourceObject/scale options); node edits re-apply LIVE mid-sim through the CL-A widened liveParams -> collider rebuild - onenter / onexit nodes (Triggers group): pulse on the CL-A sensor overlap edges - same replicated nodetrigger stamps + BFS/implicit-owner targeting as onimpact; OnClickNode card reused with sensor copy - velocity node (Scene group): live speed (m/s) of the wired object (or the graph owner) - LOCAL objectSpeeds feed: exact-ish per-step write-back deltas on the initiator, ~10Hz move-delta approximation on peers (documented on the card), 0 when the feed goes quiet - AnimationNode gains a reusable 'toggle' param kind; new ColliderNode / VelocityNode cards; flowSockets INPUT/OUTPUT typed for all four - NODES.md rows for collider / onenter+onexit / velocity - flow-physics-collider suite (9 checks) all green: node overrides Inspector shape, live mid-sim shape swap, object-source hull, sensor pass-through + enter/exit stamps, velocity >0 rolling / 0 quiet; regressions flow-audit/flow-new-nodes/particles-impact/physics-colliders/ collider-live green (flow-nodes-core + flow-physics-nodes = env-gated two-peer suites, no local signaling) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d_value crash
- 'bind:hex={undefined}' on a prop WITH a fallback hard-errors since the
svelte 5.56 safe-bump (props_invalid_value) - the uninitialized fogColor
state CRASHED the entire scene drawer on open (badge, environment,
physics, view sections all gone; inspector.test scene checks failing)
- pre-existing on release/1.1 (reproduced with the base Inspector.svelte);
surfaced here because the scene Physics section hosts the new gravity UI
- applyFog/fog inputs already defaulted fogColor ?? '#ffffff' - the
initializer matches, no behavior change beyond not crashing
- inspector suite back to ALL PASS (17 checks)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nto colliders-v2-editmesh-pro Conflicts: App.svelte debug hook (union: collider libs + trackpadNav); MeshEditPopup.svelte taken from THIS branch (the CL-B floating-toolbar redesign supersedes main's lucide pass on the old pinned strip; the redesign uses no icon classes, so nothing to reconvert).
…tmesh-pro Colliders v2 + edit-mesh pro + physics nodes (CL-A/B/C)
…itmesh pro) into vr-sleeve-palette Conflict: App.svelte debug hook unioned (release/next list + vrSleeve).
VR sleeve palette (K1+K2) as the vrsleeve core module
Union: flowbite-svelte 0.48.6 (main, verified on main: build + modal/settings suites green) + lucide 1.28 + kit 2.70.2 from main; threlte STABLE pins + prettier-plugin-svelte 4.1.1 kept from release/next; lock regenerated. Build green, svelte-check 438/62.
…has + skills + baseline gate 438/62 - CLAUDE.md domain map: faceEdit (Face/Tri/Shell granularity, subdivide/ flip/bridge, inset gizmo-gating fix, shared edit wireframe, scene-root edit proxy, FLOATING toolbar w/ shortcuts), meshEdit (Weld), physics (colliderSpec-driven construction, LIVE mid-sim rebuild, sensors + enter/exit dispatch, material presets, freeze axes, scenePhysics gravity singleton, compound custom colliders, CL-C collider/velocity nodes), colliderHelpers viz, terrainSculpt MESH mode + attr-identity cache guard, floating sculpt/mesh-edit toolbars - new Status (2026-08-01): PR #74 merge summary + new suites + remaining follow-ups (theprototype-docs pages, VR on-device check) - new gotchas: module-level store.subscribe runs at eval (declare let state ABOVE it or SSR TDZ-crashes); svelte 5.56 props_invalid_value on bind:X={undefined} (the fogColor scene-inspector crash) - svelte-check baseline moved DOWN 476/62 -> 438/62: CLAUDE.md notes, e2e-verify skill, release.yml gate all updated - e2e-verify skill: lane-editmesh port 5183 + the [::1] port-shadow trap (strictPort does not catch a v6-only listener; localhost hits the stale server); peer-feature skill: scenePhysics joins the singleton examples Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…kill - CLAUDE.md domain map: vrSleeve entry (forearm ghost strip, held-preview place w/ snap rules as one undo, K2 idb slots) + the GENERIC module-VR hook registries now living in vrControls (nav suppressor / panel-group provider / trigger start-end-swallow / grip-drop interceptor / frame hook) - reuse them for future VR feature modules instead of hardwiring vrControls - src/modules list: vrsleeve = thin shell over $lib/vrSleeve (local-only; disabling the module removes the feature) - new Status (2026-08-01): PR #75 merge summary + the NOT-yet-merged AI assistant v3 flow+physics branch (local ae53c7b on feature/ai-flow-physics-tools, awaiting push/PR; docs-repo page already committed) - e2e-verify skill: vrSleeve in the debugStores hook list, lane-aiphys @5178 port assignment, idb persistence traps (fire-and-forget write aborted by an immediate reload; loaded-once stores on the second module instance) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…inors) into ai-flow-physics-tools - Inspector setPhysics stays on the shared setPhysicsFor path; the CL-A A2 live mid-sim collider rebuild (physicsShapeChanged) + collider-viz poke move INTO setPhysicsFor so the AI set_physics tool and the quick action get them too - flowTools PHYSICS_NODE_TYPES extended to the CL-C physics family (collider override node + onimpact/onenter/onexit triggers + velocity readout) so they gate with the physics checkbox like mass/motor - set_physics gains the CL-A A3 sensor flag (trigger volumes) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ture skill - CLAUDE.md status: replace the not-yet-merged note with the PR #76 summary (flow tools always on, physics tool set + physics node FAMILY behind the per-provider checkbox, setPhysicsFor as the shared physics write path now carrying the CL-A A2 live rebuild, summarizeScene flow/physics/joints, repair additions, sim start/stop outside the undo batch, post-merge verification state) - CLAUDE.md map: flowGraphs.js entry notes the 'flownodes' history kind (serialized copies so replayed re-broadcasts hash identically) - peer-feature skill: history entries that re-broadcast hash-checked content must store serialized copies, never live editor objects ('flownodes' is the reference) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tools AI assistant v3: flow-node behaviors + physics tools
… on runes)
- Nodes.svelte converted to runes: local $state.raw nodes/edges bound via
bind:nodes/bind:edges, two-way BRIDGE to the flowNodes/flowEdges writable
stores (untrack'd subscribe + pushingToStore guard) - the store contract
for nodesHandler/flowRuntime/history/serializers is UNCHANGED, flowGraphs
stays the source of truth
- v1 event surface: on:nodedragstop/panecontextmenu/nodecontextmenu/
edgecontextmenu/paneclick -> callback props with plain-object payloads;
onedgecreate -> onbeforeconnect; dnd handlers stay on SvelteFlow (v1
spreads container attrs); viewport = bind:viewport plain object
- PeerCursors: viewport prop is a plain {x,y,zoom} now
- NO changes needed: Socket.svelte Handle wrapper, all ~30 node cards
(legacy export-let props still type-check under v1), flow.css selectors,
SvelteFlowProvider, style.css path
- flow-adjustable-params e2e: synthetic change event must bubble (svelte 5
delegated onchange); CLAUDE.md gains the bubbles gotcha + the svelte-ignore
comma-separation gotcha (5.5x runes mode honors only the first
space-separated code)
- verification: build green, svelte-check 437/62 (baseline 438/62), 31/35
flow-related suites pass - the 4 failures are the known pre-existing set
(flow-physics-nodes torque check, node-search/script-nodes/sound-node
setup crashes), flow-customnode-io newly PASSES
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Deps migration B: @xyflow/svelte 0.1 -> 1.6 (flow editor on runes)
…-> 1.33 (native dialogs) - tailwind 4 CSS-first via the official codemod: tailwind.config deleted, postcss = @tailwindcss/postcss, app.css = @import tailwindcss + @plugin flowbite/plugin + @custom-variant dark (v3 class semantics) + @theme; ~53 templates take the v4 utility renames; @apply block extracted to ui.utilities.css; autoprefixer removed; 3 codemod-mangled PROSE strings reverted (ring->ring-3 in an AI prompt, a JSDoc, a comment) - flowbite-svelte 1.33 (flowbite 3.1.2 per its dep): Modal/Drawer are native <dialog> TOP-LAYER now - the z-index remap hack in ui.css is DEAD and deleted; tp-modal-* retargeted to dialog.tp-modal-frame; snippet conversions (Accordion headers, Modal footers); Dropdown bind:isOpen + DropdownGroup; BottomNav classes prop; Toast divClass->class + onclose; Inspector/Library drawers replaced with plain divs (1.x Drawer is a focus-stealing dialog - wrong primitive for persistent tool panels) - vite 5.4 SSR chokes on flowbite-svelte 1.x's internal circular import (500 at dev boot, build tolerates it) - ssr.optimizeDeps.include pre-bundles it for SSR - E1 approvals-above-modals now rides the POPOVER API: the critical toasts container is a manual popover shown while approvals are pending, entering the top layer above any open dialog (z-index cannot beat native dialogs) - A6 adapted: a click at the logo closes the modal via outside-close, the next click opens the menu (never-stacked is now platform-enforced); modal-layering asserts :modal + the approval popover; ui-polish asserts the two-step flow - verification: build green, svelte-check 494/62 vs pristine-HEAD 496/62 (-2); settings/sidebar/whats-new/packs/import/controls suites green Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…bite-tw4 Deps migration C: tailwind 4 + flowbite-svelte 1.x (native top-layer dialogs)
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>
…ty, persistence fixes
User feedback on the shipped v2 (H8-H10, H12 of the plan; H11 saved-camera/follow
stays for the user to implement).
- H8 pin render pass rework. Every pin now draws TWICE: a depth-tested SOLID pass
and a depth-test-off GHOST pass at 0.3 opacity, with explicit renderOrders
(ghost UNDER solid, so a visible pin reads as its own saturated colour instead
of the ghost border bleeding through the fill). Occluded = dim silhouette, not
gone and not punching through at full strength. The old single pass was
depthTest:false with NO renderOrder, so "does this pin draw over that object"
depended on scene ADD order - that is why your own new pins floated on top
while the ones received from a peer (objects added after the pins group) were
covered. The number is one pass, never depth-tested, above every pin mesh, so
an overlapping neighbour can no longer hide it.
Two traps worth remembering: the solid pass must WRITE depth (the outline/N8AO
postprocessing passes read the depth buffer, so a pin that writes none gets the
AO and selection edges of whatever is behind it painted across its face), and
the visuals sit 0.06 along the group's local +Z (= toward the viewer after
lookAt) so a pin anchored flat ON a surface no longer half-sinks into it - the
pin GROUP itself stays exactly on the anchor, which annotationWorldPosition and
the anchor suite read.
- H9 pin shapes + a professional look: replicated `shape` (round/star/square,
shared geometry per shape), a border ring in a darker shade of the note colour,
slight fill transparency, and a contrast-aware number (YIQ split, so amber and
green get dark ink, blue and violet get light). Shape selector sits next to the
colour swatches in the edit face. Colour math runs on sRGB bytes, never through
THREE.Color (the linear round-trip darkens hexes).
- H10 author identity. `author` stays the DISPLAY name at save time (never the
literal 'me' any more), and a new `authorKey` - a stable per-device key - says
whether a note is ours. Peer ids are re-issued on every reconnect and nicknames
change, so neither could answer that across a rename or a reload. 'Me' is now
purely a display mapping: a saved .tpscene/autosave shows the owner's nickname
to everyone else, and renaming yourself re-stamps your own notes on the next
save. Old notes with no key fall back to the name/peer-id comparison.
- H12 persistence ("some notes disappear on reload"). Three causes fixed:
annotations were snapshot passengers but never marked the autosave DIRTY (only
objectsGroup/flowGraphs did), so a note added after the last object change was
never written; a note anchored on a SCENE-ROOT object (env rig, module content)
orphaned every boot because those are rebuilt with fresh uuids - notes now
remember the anchor NAME and re-key in the sweep; and the orphan prune fired
instantly, so a restore/regeneration race could eat notes - it now needs the
object to stay missing for 3s, re-arming its own follow-up sweep.
- notes-v2 extended to 51 checks: two-pass structure + renderOrder + always-on-top
number, shape geometry/border/ink and shape replication, the whole author story
(stored vs displayed, rename upgrade, foreign-device view), autosave dirty +
snapshot content, scene-root re-key, prune grace. annotation-anchor and
autosave-object-flows stay green; build green; svelte-check holds 419/62.
roadmap-13-notifications-notes' unread-badge check fails identically on HEAD
(known flake, proven by stashing).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The grid "snapped" while panning in look-at follow mode because the centre was quantised to ONE CELL. A cell-step translation maps the thin lines onto themselves, but the SECTION lines repeat every `sectionEvery` cells - so every thick line hopped one cell on each step while the fine grid stood still. Snapping by the section period (cell x sectionEvery) makes the whole pattern map onto itself: every line lands exactly where a line already was, so following is invisible and the lines stay locked to world coordinates (unsnapped following slides the grid under the objects instead, which is why the snap exists). The fade circle is deliberately left alone: threlte's Grid already defaults its fadeOrigin to the camera position projected onto the grid plane, which glides continuously. Feeding it this snapped anchor is what would re-introduce a jumping fade ring - the exact regression I4 fixed by dropping the eased fade. So the grid never disappears during navigation; nothing pops. grid-snapping gains 3 checks: the centre lands on the section lattice, it still tracks the look-at target, and the fade uniform stays on the unsnapped camera point. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…cclusion, clustering The in-scene quads could be CUT IN HALF by any surface they touched (a billboard rotating into a face) and their occlusion was a per-pixel depth test, so a marker was never reliably "in front" or "behind". Markers are now screen-space DOM badges with a leader line to the exact 3D point (new components/menu/AnnotationMarkers.svelte); the in-scene meshes stay as the VR path only, since DOM is invisible in a headset. The pin GROUP still tracks its anchor in every mode - annotationWorldPosition, the popover and the anchor suite all read it. The triangle/cone under the pin is gone from both paths. - Occlusion is ONE verdict per marker from a raycast (throttled to ~8Hz) with 8cm of slack, so a note resting ON a surface stays solid while one genuinely behind geometry fades its FILL to 50% and dashes its leader. The number keeps full opacity - it is the marker's identity. Nothing can clip a DOM badge, which was the actual request. - Badge style: pill (rounded-rect for the square shape), 26px tall, 13.5px bold tabular number, small type icon, 1.5px light outline + layered drop shadow, contrast-aware ink. The leader is a 1.25px core over a soft 3px glow ending in a dot with a hairline ring exactly on the 3D point. - Clustering: greedy screen-space grouping at 34px (where badges would actually overlap) collapses into one counted badge with a stacked-plates shadow; a click fans the members out in an arc, each keeping its own leader to its own point. - Polish: constant screen size, hover scale + brighter outline + a tooltip with name/preview/author/date, a selected ring on the open note, fade under 0.9m and hidden under 0.35m, off-screen culling that skips the raycast too, and an adaptive outline for light themes / prefers-color-scheme. - GOTCHA worth keeping: an <svg> is a REPLACED element, so `position:fixed; inset:0` still leaves it at its 300x150 intrinsic box and silently clips every line away - it needs explicit width/height 100%. Also, app chrome (Connect bar, z 300) sits above the marker layer (z 28), so a badge under it is not clickable; the suite clicks in page context rather than fighting Playwright's actionability check. notes-v2 is now 63 checks, rewritten around the DOM markers: fill/ink/pill/icon, leader endpoints matching the projected point, occluded fade + dashed leader, the on-surface slack, cluster collapse/expand, hover preview, selected ring, and the pins toggle hiding both paths. annotation-anchor green, build green, svelte-check holds 419/62. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…o jiggle), pill badge everywhere Two follow-ups from the review. - JIGGLE ROOT CAUSE: the marker layer owned a private requestAnimationFrame loop, but threlte's OrbitControls calls controls.update() in a task in the MAIN stage. Those are two separate callback queues: whenever the marker loop ran before threlte's tick it projected LAST frame's camera, so every badge trailed the geometry by one frame while the camera moved. That also explains why entering and leaving VR "fixed" it - an XR session swaps the loop to renderer.setAnimationLoop and threlte re-registers its own rAF afterwards, flipping the callback order so the marker loop happened to read the updated camera. Fix: the projection + occlusion now run in a useTask in the RENDER stage (after the main stage, so the camera is final for the frame) inside AnnotationPins.svelte, which is already in the Canvas, and publish to a new `noteMarkers` store. AnnotationMarkers.svelte became presentation-only: clustering, hover and the badge chrome derive from that store, no loop of its own. Positions therefore always come from the very camera pose that rendered the frame the badge sits on. - Pin SHAPE is now a VR-only distinction: every 2D badge is the same pill, so the overlay reads consistently (the rounded-rect variant and the per-shape icon are gone). The editor's selector stays - it configures the in-scene VR pin - and is labelled "Pin shape - VR only" so it does not look inert. notes-v2 gains a real regression guard for the lag: it spins the camera and compares each badge against the pin projected with the camera at that instant, asserting the residual stays far below the per-frame travel. Verified that the check DISCRIMINATES by temporarily re-introducing a one-frame lag: residual went 0.0px -> 3.5px against a 3.9px/frame step, and the check failed. Plus a check that a 'star' note renders the same pill in 2D. 65 checks green, annotation-anchor green, build green, svelte-check holds 419/62. roadmap-13-notifications-notes' unread-badge check still fails identically on HEAD (known flake). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes the last piece of 15-H. The originally locked fork (follow only while the
popover is open) was dropped on review: nobody keeps a card open to keep
tracking, and closing it to see the scene would kill the thing they wanted.
- FOLLOW IS A SESSION, not a popup side effect. Started explicitly (Follow on the
note's view card) and it OUTLIVES the card: a sticky info toast "Following note
#n - the camera rides it as it moves (Esc to stop)" is both the indicator and
the off-switch (reuses showInfoToast/dismissToastById from 15-L). Local and
per-viewer - never replicated, because the editor camera belongs to whoever is
driving it.
- MOTION: each frame we translate camera.position AND orbitControls.target by the
pin's world delta. Moving both ends preserves the user's own orbit/zoom offset,
so they keep navigating freely while the camera rides the object. The tick runs
in a MAIN-stage useTask - before OrbitControls' own task and the render - so the
pose we write is the pose the frame is composed from, and the render-stage
marker projection already sees it.
- SAVED POSE = AUTHORED FRAMING. "Save camera view" stores {position, target} on
the note (replicated on the unchanged annotation message). Opening flies there
instead of the generic 4m approach; a follow session then keeps that offset
live as the object moves, so the author's framing stays true. Plus a per-note
"Follow the pin when opened" hint that auto-starts the viewer's local session.
- HANDOVER is an EXPLICIT signal, not a deviation guess. New `cameraClaim`
counter in sceneStore (a leaf both modules already import) is bumped by
objectActions.flyTo, so focus/bookmarks/opening another note end the session
cleanly - while the user's own PAN, which moves the orbit target exactly like
our follow step does, no longer looks like someone stealing the camera. First
attempt used a target-deviation epsilon; it would have broken every pan.
Sessions also end on Esc, on VR/spectate/play taking the camera, and when the
followed note or its object goes away.
- Nodes route deliberately NOT taken here: a follow/look-at node belongs to the
roadmap-16 camera OBJECTS (replicable, watchable via PiP) and is in backlog. A
peer's graph must never yank your viewpoint.
notes-v2 +16 checks: pose stored and re-flown (comparing against the ACTUAL
parked pose - OrbitControls.update() re-derives position from its spherical
state, so never assert the numbers you asked for), camera+target translate with
the pin, the chosen offset survives, panning keeps the session, closing the card
keeps it, an external flyTo hands over, Esc stops, the flag replicates and
auto-starts, and old payloads normalize to no pose / follow off. 79 checks green;
annotation-anchor, camera-bookmarks, autosave-object-flows green; build green;
svelte-check holds 419/62.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…c order
- The "Follow the pin when opened" control is a SWITCH now: it arms a mode, which
is why Settings uses Toggle for behaviour flags. GOTCHA worth remembering:
flowbite's plugin emits `[type='checkbox']:checked { background-color:
currentColor !important }`, so no background-color of ours can ever win the ON
state - it rendered flowbite blue no matter the specificity. The cure is to work
WITH that rule and drive the fill through `color`. The suite now asserts the ON
colour so it cannot drift back to blue.
- NEW SETTING "Double-click to open notes" (Settings > Interface, LOCAL, off by
default): a single click on a note marker - and the drawer's group arrows -
then only FLIES the camera to the note, and the card opens on a double click.
Reviewing a scene full of notes becomes pure navigation with nothing in the way.
`focusAnnotation(id)` is the go-there-without-opening path (openAnnotation and
it now share one `flyToAnnotation`, so a saved framing and the follow hint apply
either way).
- With no card open there was nothing to say where you are, so a new
`visitedNote` store records the last note travelled to: it highlights the drawer
row and is what the arrows step from, so every press advances one note and wraps
instead of restarting at the top of the group.
- ESC ORDER, as asked: the first Esc gives the CAMERA back (stops a follow
session), the second closes the card. One window-level handler in
annotationsHandler owns it, replacing the card's own keydown - that only fired
when focus happened to sit inside the card, so Esc did nothing after a marker
click - and the follow-only listener that would have raced it. A modal owns Esc
while it is up.
notes-v2 +11 checks (switch shape + ON colour + stored flag, Esc precedence in
both orders and from outside the card, single-click-flies / double-click-opens
with the setting on and off, arrows travelling without a popup while still
advancing + wrapping, and the row highlight). 90 checks green; annotation-anchor,
autosave-object-flows, camera-bookmarks and all 5 settings suites green; build
green; svelte-check holds 419/62.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e trap, camera ownership
- Architecture map: annotationsHandler's full v2/v3 model (text stays the
description, ONE normalizeAnnotation at every boundary, unchanged wire shape,
authorKey identity, noteMarkers/followingNote/visitedNote/focusAnnotation,
sweepAnnotations' re-key + grace) and the notes UI trio (AnnotationMarkers as the
screen-space layer, AnnotationPins demoted to the VR path, popover + drawer).
- New gotchas, each of which cost real debugging here: a DOM overlay that must agree
with a threlte frame may NOT own a requestAnimationFrame (OrbitControls updates
the camera in a main-stage task; project in useTask({stage: renderStage}) instead
— and note that entering/leaving VR "fixing" a jiggle is the signature of exactly
this) - OrbitControls.update() re-derives the camera position from its spherical
state, so direct writes revert and continuous drivers must move camera AND target,
with cameraClaim as the explicit handover signal because deviation-watching cannot
tell a user pan from a takeover - an <svg> is a replaced element, so fixed+inset:0
still clips it to 300x150 - flowbite forces background-color: currentColor
!important on checked checkboxes, so a custom switch must be coloured through
`color` - depthWrite:false loses the outline/N8AO passes, which then paint the
geometry behind a sprite across its face - pattern FOLLOW must snap by the section
period, not one cell.
- Status entry for the 5-commit 15-H branch.
- e2e-verify: how to read the DOM markers (and why not pinsGroup), chrome above the
marker layer, clustering hiding individual badges, the frame-lag guard that was
proven to fail before being trusted, plus the camera-assertion and
synthetic-click-vs-binding rules.
- peer-feature: how to GROW an existing replicated record without a new message
type, and where the line sits between replicated authorship and a local view of it.
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
[fix] roadmap #15 second drop: properties UX, toasts, stars, PWA, selection/outline (K)
[chore] add npm run certs for the dev https pair
[fix] roadmap #15 batches A+J: collider fixes + clear-scene viewer gate
# Conflicts: # src/extensions/Grid.svelte
15-H: scene notes v2/v3 - model, screen-space markers, saved framing + follow sessions
…t store Found while assembling the release: shift-clicking a second object stopped adding to the selection. ROOT CAUSE: `activeOrbit` (cameraPreview.js, #16) is a DERIVED store, and svelte compiles `$activeOrbit.enabled = x` into `store_mutate()` -> `store.set()` — which a derived store does not have. All seven sites in Scene.svelte therefore threw `TypeError: store.set is not a function`. The one in `onPointerUp` aborted the rest of the handler, so the shift path never reached `raycastSelect` — while a PLAIN click, which never enters the marquee branch, kept working. That asymmetry is why it looked like "shift multi-select is gone" rather than "selection is broken". Fix: `setOrbitEnabled(on)` in cameraPreview.js resolves the store with `get()` and mutates the resolved controls object (mutating the object was always fine — it is the `$store.prop =` syntax that is not). Every call site now goes through it; the one remaining `$activeOrbit` read (editor navigation) is a read and stays. Regression guard in viewport-selection: three boxes, REAL mouse clicks at projected positions — plain click selects one, shift-click adds a second and a third, shift-click on a member toggles it out, a plain click collapses the set, no page errors during the gestures, and the member tints are restored on deselect. A store-level `selectObject` call would never have caught this, since the bug lived in the pointer handler. Verified the guard FAILS without the fix (8 page errors, the set stuck at 1). svelte-check holds 419/62; build green; camera-preview-control, camera-pip, gizmo-orbit-leak, notes-v2, annotation-anchor, grid-snapping green, plus multi-select and camera-objects green with PEER_CONFIG set (both are two-peer suites that need the signaling override on a localhost APP_URL). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…, + Object granularity; AO off by default on mobile Two user reports from the release check. 1) SHELL SELECTION vs THE GIZMO — a real bug, not expected behaviour. The highlight/overlay and the toolbar ops (extrude/inset/move-by-amount) all resolve their target through `opTargetFace()`, which knows about Face/Triangle/Shell and Multi. The GIZMO drag did not: `attachFaceGizmo` and `onFaceGizmoDragChanged` used `faces[faceEditHighlight]` — a single COPLANAR face. So picking Shell on a box lit up the whole island (correct: a shell IS the connected component over welded vertex keys) while dragging moved only the surface under the cursor, and `beginFaceGrab`'s weld-neighbour set stretched the touching corners — the "some vertices are stuck" in the report. Fix: `beginFaceGrab` now takes a synthesized target (like `beginFaceAdjust` already did) or a face index for back-compat, and stores its own `triIndices` instead of re-deriving them from `faces[index]`; `applyFaceGrab` moves that set. `attachFaceGizmo` seats on the op target and STASHES it, because by the time the user presses a gizmo handle the pointer is over the gizmo and the live hover no longer describes the pick. 2) NEW "Object" GRANULARITY — Face / Tri / Shell / Object. Object takes every triangle, so it differs from Shell only when a mesh has several disconnected islands (a merged group, a multi-part import); on a plain box Shell already is the object. Both the toolbar buttons and the VR cycle include it. 3) MOBILE AO. `shaded-ao` was the DEFAULT view mode, and the AO gate (`chromiumMajor >= 151`) was written from desktop ANGLE/D3D11 evidence — so an Android Chromium 151 is treated as fixed and switches a fullscreen N8AO pass on. Reported symptom: the viewport is visible but never updates while you rotate until you leave AO mode, with nothing in the console — the composer stops presenting and the last frame stays up. A fullscreen AO pass is a poor default on a phone GPU regardless of whose driver is at fault, so coarse-pointer devices now start in plain 'shaded' (`defaultViewMode` in sceneStore) and get ONE explanatory toast if AO is turned on deliberately. Desktop is untouched — the view-mode suite still asserts Shaded+AO as the default there. mesh-ops gains 6 checks: every granularity can begin a grab, a SHELL grab moves its whole island rigidly and nothing else (6/15 verts on a two-island mesh), an OBJECT grab moves all 15 across both islands, FACE still grabs just its own face (7/15, unchanged), cancel restores, and the REAL gizmo path drags the whole shell even after the hover clears. Verified they FAIL without the fix (all 0/15). collider- custom + mesh-sculpt green (collider edit runs these same tools on a proxy); build green; svelte-check holds 419/62. view-mode's shadow-catcher check fails identically without these changes (known flake in this env). The mobile default itself is an ON-DEVICE check: `(pointer: coarse)` cannot be emulated in desktop headless. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ings
- CHANGELOG: the 1.2.0 entry ("Make it real"), in the established house style —
emoji-led bullets grouped under h3s like 1.0.0, written for users rather than as
a PR list. Covers colliders v2 + physics nodes, edit-mesh pro + sculpt-anything,
camera objects + PiP, notes v2/v3, the assistant's behaviour tools, the interface
pass (context menus, one number field, toasts, PWA, stars, keyboard menus), the VR
sleeve, and the fixes/deps housekeeping.
- What's New now folds ONE `<details>` per release with the newest open, so the
window shows this release instead of every version ever shipped. Native
`<details>`/`<summary>` means keyboard and screen readers work with no open/closed
state of ours; the chevron is a CSS pseudo-element that rotates. whats-new gains 4
checks (a section per release, newest open + rest folded, bullets still render,
clicking an older heading expands it).
- CLAUDE.md gotchas, both from this release's blockers: never write through a
DERIVED store (`$store.prop =` compiles to store_mutate → store.set, which throws
at RUNTIME while the build stays green — and an asymmetric symptom, one gesture
broken and its sibling fine, is the tell for a throw partway through a shared
handler), and the mesh/face op target is `opTargetFace()` and never
`faces[faceEditHighlight]`. Plus the mobile-AO default and a release status entry
recording the assembled branch, the 419/62 gate, why 1.2.0 is a minor
(SESSION_FORMAT/MODULE_FORMAT still 1) and why `release/next` keeps its name.
- e2e-verify: prove a regression guard by breaking the code (stash, watch it go red,
pop), drive the real input path when the bug lives in a handler (and count
pageerrors while you do), and label what genuinely cannot be tested here as an
on-device check instead of writing a check that can only pass.
svelte-check holds 419/62; build green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…xplain AO when it is CHOSEN User report from the release check: a desktop Chrome **151.0.7922.76** showed "Ambient occlusion is off — this browser version (Chromium 126) has a rendering bug with it." Two bugs, one report. 1) DETECTION. The gate parsed the major out of `navigator.userAgent`, which is exactly what DevTools DEVICE EMULATION overrides — its presets carry a canned, much older Chrome version (the user had been debugging a phone over chrome://inspect). So a 151 desktop read as 126, failed the >=151 gate and lost AO. `chromiumMajor()` now prefers `navigator.userAgentData.brands`, which is the structured source and is not rewritten by a UA override, with the regex kept as the fallback for engines without it. Verified: Playwright's Chromium 151 reports 151 from both, and rewriting the UA string to "Chrome/126" no longer moves the gate. 2) THE TOAST FIRED FOR A MODE NOBODY PICKED. `shaded-ao` is the DEFAULT view mode, and the warning triggered on any `$viewMode === 'shaded-ao'` — so one bad version parse greeted every visitor with a warning about a feature they never chose. It now only speaks when the user actually SWITCHES to ambient occlusion (tracking the previous mode), which is also the right shape for the mobile warning added in the previous commit. The gate moved to viewMode.js as `chromiumMajor()` / `aoSupported()` — it is a view-mode concern, and putting it in a lib makes it testable through `__stores.viewModeCtl` instead of being buried in a component. Outline.svelte keeps only the composer wiring. view-mode gains 3 checks: the gate takes its version from the brand list, aoSupported follows it, and a spoofed UA string cannot drag the version backwards. The driver bug itself is still not testable here (it needs the affected GPU stack) — that stays an on-device observation. svelte-check holds 419/62; build green; whats-new green. view-mode's shadow-catcher check fails identically on the untouched tree (known flake). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… echo
User report: add a box, open Configure Scene — the environment changes and the box
turns light; switching the preset to Studio puts it back.
ROOT CAUSE: svelte-awesome-color-picker v4 calls `onInput` ONCE just from being
MOUNTED. Its `updateColor()` runs out of an `$effect`, and on the first pass the
incoming hex always differs from its own empty snapshot, so it reports a "change"
for a colour nobody touched. Every one of the five pickers in the Inspector then ran
a real mutation on mount:
- background + fog -> `editEnvSky()`, which DETACHES the live preset into a custom
one and re-applies the environment. That is the reported symptom: opening the
panel silently moved Studio -> custom, the scene relit, and picking a preset again
"fixed" it.
- light colour + hemisphere ground -> `sendLightUpdate()`, broadcasting
`{type:'object', override:true}`.
- material colour -> `trackColorGesture()` + `{type:'color'}` — an undo entry and a
replicated edit for merely SELECTING a mesh.
So opening a panel mutated the scene, told every peer about it, and polluted undo.
Fix: one `sameHex()` helper, and every onInput handler returns early when the value
equals the one it already holds (normalised — the picker round-trips through colord,
so case, a leading # and a trailing alpha pair can all differ from THREE's
getHexString). Re-picking the same colour deliberately is also a no-op, which is
correct. The material handler additionally keeps its `color` mirror in sync so a
second gesture is not mistaken for an echo.
color-picker-15c gains 4 checks built from the reported gesture: opening Configure
Scene leaves the preset, the object's shade and the background alone, and selecting a
light replicates nothing but the lock. Verified they FAIL without the fix
(studio -> custom, and ["lock","color"] on the wire).
Note on the test: my first version of the light check watched for `{type:'light'}`,
which this code never sends, and a second watched only for 'object' while the escaping
echo was 'color' — two checks that could not fail. It now asserts on both colour
message types. That is the same trap the e2e skill warns about, hit twice in one sitting.
svelte-check holds 419/62; build green; inspector green; environment-v2 green with
PEER_CONFIG (it is a two-peer suite).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
v4 fires onInput once from mounting, so any handler that mutates per onInput mutates
when a panel merely opens — the Inspector detached the environment preset, broadcast
colour updates and recorded undo entries for a selection. Records the signature
("opening a panel changed my scene") and the sameHex guard, and generalises it to any
third-party input component.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ou did not ask to search
Three reports, two of them one idea.
1) SETTINGS SEARCH WAS DEAD. flowbite-svelte 1.x mounts an AccordionItem's body only
while it is OPEN, and `Accordion` is single-selection by default — so with the
sections collapsed there were literally ZERO `.setting-row` elements in the
document and the filter had nothing to match. (The pre-migration Accordion kept
its content mounted and merely hidden, which is why this silently rotted.)
Fixes: the accordion is `multiple` (its `multiple` prop is read UNTRACKED at init,
so this cannot be toggled per query — and several open sections is the norm for a
settings panel anyway); a query expands every section and restores the previous
expansion when cleared; the filter walks SECTIONS rather than rows, so a body that
has not mounted yet is "unknown" instead of "no matches", and a MutationObserver
re-applies as bodies arrive instead of guessing frames. A row now also matches its
GROUP label and SECTION heading, so "vr" finds the VR section (16 rows) rather
than only rows with "vr" in their own name.
2) + 3) NO AUTOFOCUS ON TOUCH. Long-pressing for the context menu, or opening
Settings, focused a text field and slid the on-screen keyboard over the thing the
user had just opened. Autofocus is a POINTER-device courtesy, so it is now gated
on `(pointer: coarse)` — but the feature is not lost on a touch device that HAS a
keyboard: `typeToFocus` routes the first printable keystroke into the field and
inserts it, so a tablet with a Bluetooth keyboard filters exactly like a PC. It
registers in CAPTURE, so that first key never reaches the app's global shortcuts.
PC behaviour is byte-identical. ("Search nodes…" still focuses on tap — that tap
IS the request.) Settings additionally undoes flowbite Modal's own autofocus two
frames after mount, which is after the modal's focus call and long before any tap.
The rule now lives in ONE place, `lib/inputDevice.js` (coarsePointer / autofocusOk /
isTypingKey / typeToFocus), and sceneStore + Outline use it instead of their own
inline matchMedia copies. It imports nothing, so a store may use it.
Also fixes a suite I broke earlier and had not run: panel-deeplinks still asserted the
grid's per-CELL follow snap, which 15-H13 changed to the section period.
settings-labels gains 8 checks: search mounts every section and finds matches, a
section name matches everything under it, empty sections get out of the way, a
nonsense query shows nothing, clearing restores every header, autofocus is allowed on
this pointer device, only printable keys count as typing, and typeToFocus adopts the
field then stops on teardown. Verified the search checks FAIL without the fix (0 rows
found). svelte-check holds 419/62; build green; settings-layout,
context-menu-redesign, grid-snapping, panel-deeplinks green.
The touch behaviour itself is an ON-DEVICE check: `(pointer: coarse)` cannot be
emulated in desktop headless.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…profile avatar's Three reports. 1) OBJECT ROWS WERE INERT ON TOUCH. The per-row eye / properties / share / delete buttons were `hidden … group-hover/row:flex` — hover-only, and a touch screen never hovers, so on a phone they did not exist at all. They are now permanently visible under `@media (pointer: coarse)` (unlayered component CSS beats Tailwind's layered `hidden`, so no !important). A row also had no context menu on touch, because touch produces no usable `contextmenu` event — the browser's own long-press offers to select/copy text instead. Rows now take the same hold-to-open deal the canvas already makes: 450ms with a 10px slop, so a finger that MOVES is a scroll or a row drag and cancels. A MOUSE press is ignored outright — right-click still owns that path. 2) THE FULL-SCREEN CHANGELOG SAT UNDER THE CHROME. What's New is a floating window (z 40); on narrow it becomes a full-screen sheet, and the peers / notification / notes / profile buttons (997-999) floated on top of it. The narrow block now raises it to 1000 — above the chrome, below the modal/toast/menu tiers. 3) THE PROFILE AVATAR WENT UNDER ITS OWN DROPDOWN. The panel's rounded top-right corner is meant to tuck UNDER the profile circle. The z-index had been written inside the CLASS attribute (`class="… md:order-2; z-index: 999;"`), where it was just a nonsense class name, and the copy in the inner style sat on a STATIC element where z-index does nothing at all — so the Dropdown's real 998 won. It now lives on the positioned Avatar. mobile-ui-fixes gains 7 checks: a row carries its buttons, a long press opens the menu while a tap and a moving press do not, a mouse press never long-presses, the full-screen changelog outranks the chrome, and the avatar is positioned above 998. Verified all four bug-facing checks FAIL without the fixes (no buttons, no menu, 43 > 997, z=0). svelte-check holds 419/62; build green; whats-new, object-list-drag and mobile-hud green. `(pointer: coarse)` cannot be emulated in desktop headless, so the CSS half of (1) and the feel of the hold gesture remain ON-DEVICE checks. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e chrome
- styles/chat.css carried a duplicate `.hidden { display: none }`. It is plain
global CSS, i.e. UNLAYERED, and unlayered beats anything inside Tailwind's
@layer utilities whatever the specificity - so every `group-hover/*:flex`
reveal in the app silently lost: object-list row buttons (eye/properties/
delete) and Library cards never appeared on hover. Tailwind ships `.hidden`
itself, so removing the duplicate costs nothing.
- ContextMenu: the backdrop closes only on a click whose PRESS also landed on
it. A long press opens the menu while the finger is still down, so the
backdrop mounted under that finger and the lift closed what it had just
opened (row context menu flashed open and shut on mobile).
- Object-list window is `position: fixed` like every other floating window
(dragWindow/docking already were) - it was the last `absolute` holdout. An
absolute box parked past the right/bottom edge joins the document scroll
overflow and GROWS the page, dragging the fixed chrome (Connect bar, profile,
corner HUD) sideways with it.
- Users: profile dropdown sits below the top-right chrome, since the chrome is
a fixed+z-index stacking context the avatar inside it can never escape.
- WhatsNew: the narrow full-screen sheet outranks the top-right chrome and, via
a root class, the logo - narrow only; on wide it stays an ordinary window.
mobile-ui-fixes covers all of it: desktop hover reveal (real mouse), the
lift-does-not-close / real-outside-tap pair, and an off-edge drag that must not
grow the page. Each was checked against its own stashed fix. Suite carries a
premise check on the drag because three earlier attempts moved nothing (the top
chrome covers that header at 430px, and synthetic moves with a default clientX
of 0 read as the left dock zone).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- CHANGELOG 1.2.0: the release-verification fixes (row buttons on hover/touch, off-edge window drag, mobile keyboard vs Settings search, AO browser version, Configure Scene repaint). - CLAUDE.md: unlayered global CSS beats every Tailwind utility (the flip side of the modal z-index trick); absolute floating windows grow the document and drag the fixed chrome; long-press menus need press-and-click backdrops; never pop a stash to undo a no-op stash push. - e2e-verify skill: prove guards by editing the fix out, not by stashing, and add a premise check whenever the gesture itself can miss. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…circle The dropdown is a TOP-LAYER popover in flowbite 1.x (popover=manual), so it paints above the whole page whatever the z-index - measured, the panel at 996 covered an avatar at 2000 in the same stacking context, and a screenshot showed the circle gone entirely. Lowering the panel could never work. - The profile circle is now drawn INSIDE the panel, seated in the 1.5rem notch its top-right corner exists for (24px radius = half a 48px avatar, so it is exactly inscribed and the panel overflow does not clip it). Clicking it closes the menu, as clicking the trigger does. - The panel anchors to the CIRCLE (#avatar-trigger) instead of #avatar-menu, an invisible 208x0 flex box whose right edge merely coincided with the circle (both inset 20px). On a phone that coincidence broke and the menu slid to the window edge. margin-top: -50px keeps the overlap now that the trigger has height. Verified aligned at 1280/430/393/360/320. mobile-ui-fixes: what is at the circle position must BE the circle (trusted Playwright click - flowbite ignores el.click() from evaluate), the circle is seated on the panel, clicking it closes the menu, and the trigger is the 48px circle rather than a wide box. The first three fail without the fix. Desktop Chromium at phone viewports does not reproduce the horizontal slide, so that one asserts the mechanism and says so; the phone is the user check. Replaces two earlier checks that compared z-index NUMBERS and passed while the bug shipped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e viewport The menu still slid to the window edge on the phone, clipping the circle. Cause: floating-ui places the panel from the trigger, and under a MOBILE viewport (page scale != 1) that placement drifts right by exactly the trigger inset - 20px, so the panel sat flush at the window edge while the circle stayed 20px in. Reproduced with a Playwright context using isMobile: true. A desktop context does NOT show it, not even with hasTouch and a coarse pointer - both measured clean at 320/360/375/393/412/430/500/600/641/670/720/768/820/1024/1280, which is why the previous attempt looked verified and shipped broken. This edge is fixed chrome geometry (the avatar is right: 20px inside a right: 0 chrome), so it is now pinned in CSS - left: auto / right: 20px with !important, which beats floating-ui's non-important inline left - and floating-ui keeps only the vertical placement, which was always correct. mobile-ui-fixes gains a mobile-emulated page for it; without the pin it reports rightGap 20 / inset 0, the exact numbers from the screenshot. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
1.2.0 — Make it real 🔨
release/next→main. Assembled from PRs #81, #82, #83, #84, #85, #86, #87, plus thefixes found while verifying the release.
What's in it
mid-simulation swaps, sensors, material presets, frozen axes, scene gravity.
bridge, floating toolbars, and the terrain brush generalized to any mesh.
with clustering, saved camera framing, and follow-the-pin sessions (H11).
search in every menu, panel deep links.
Full user-facing list:
CHANGELOG.md, which is also what the in-app What's newwindow renders (now foldable, one section per release, newest open).
Release-verification fixes (found by the user, each with a regression guard)
activeOrbitstoreuserAgentData.brandsonInputonce on mount+/ Settings.top-right-chromeis a stacking context — the panel moves down instead.hiddeninstyles/chat.cssbeat every Tailwindgroup-hover/*:flexposition: absolutewindow, so it grew the documentVerification
npm run buildgreen ·npx svelte-checkholds the 419 / 62 baseline (therelease.yml gate matches).
color-picker-15c, viewport-selection, mesh-ops, grid-snapping, panel-deeplinks,
object-list-drag all green.
dock-sidebar-inset,layout,panels.live vLLM smoke.
After merge
npm version minor+git push origin main --follow-tags(tag triggers release.yml),then the cloud deploy with
CORE_REF=v1.2.0.🤖 Generated with Claude Code