Skip to content

Release 1.2.0 — Make it real - #88

Merged
AlexZ005 merged 98 commits into
mainfrom
release/next
Aug 5, 2026
Merged

Release 1.2.0 — Make it real#88
AlexZ005 merged 98 commits into
mainfrom
release/next

Conversation

@AlexZ005

@AlexZ005 AlexZ005 commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

1.2.0 — Make it real 🔨

release/nextmain. Assembled from PRs #81, #82, #83, #84, #85, #86, #87, plus the
fixes found while verifying the release.

What's in it

  • Physics & colliders v2 — per-object collision shapes with a wireframe view, live
    mid-simulation swaps, sensors, material presets, frozen axes, scene gravity.
  • Edit Mesh Pro + sculpt — Face/Triangle/Shell granularity, subdivide/flip/weld/
    bridge, floating toolbars, and the terrain brush generalized to any mesh.
  • Scene notes v2/v3 — professional pins with occlusion dimming, screen-space badges
    with clustering, saved camera framing, and follow-the-pin sessions (H11).
  • Cameras & menus (Feature/object-groups #16) — camera objects with true-swap preview and PiP, keyboard +
    search in every menu, panel deep links.
  • AI assistant v3 — flow and physics tools, so it builds behaviour, not just objects.

Full user-facing list: CHANGELOG.md, which is also what the in-app What's new
window renders (now foldable, one section per release, newest open).

Release-verification fixes (found by the user, each with a regression guard)

Report Cause
Shift multi-select dead wrote through the DERIVED activeOrbit store
Gizmo dragged the wrong thing after inset/extrude face gizmo seated on a stale target
"Chromium 126" on Chrome 151 UA string parse; now reads userAgentData.brands
Configure Scene repainted a new box colour picker fires onInput once on mount
Mobile keyboard popped for + / Settings autofocus is fine-pointer only; a real keyboard still type-to-filters
Settings search stopped filtering accordion sections walked wrong after the flowbite 1.x change
What's New under the top-right chrome / logo window tier vs the ad-hoc chrome z-values (narrow only)
Avatar under its own menu .top-right-chrome is a stacking context — the panel moves down instead
Object rows had no eye/cog/x, no long-press an unlayered .hidden in styles/chat.css beat every Tailwind group-hover/*:flex
Long-press menu closed instantly the opening finger's lift landed on the backdrop
Dragging the object list moved Connect/profile/HUD it was the last position: absolute window, so it grew the document

Verification

  • npm run build green · npx svelte-check holds the 419 / 62 baseline (the
    release.yml gate matches).
  • Suites: notes-v2, mobile-ui-fixes, whats-new, settings-labels, view-mode,
    color-picker-15c, viewport-selection, mesh-ops, grid-snapping, panel-deeplinks,
    object-list-drag all green.
  • Pre-existing localhost failures, identical on a clean tree: dock-sidebar-inset,
    layout, panels.
  • Owed on-device (user): mobile row buttons + hold feel, VR pins, PWA install,
    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

AlexZ005 and others added 30 commits July 28, 2026 22:54
…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)
AlexZ005 and others added 29 commits August 5, 2026 05:26
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>
@AlexZ005
AlexZ005 merged commit ca0c33d into main Aug 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant