Skip to content

30-core-modes: Edit vs Interact, select-through, every module's content in the object list, and the pointer-capture fix - #239

Open
AlexZ005 wants to merge 5 commits into
release/nextfrom
feat/30-editor-modes
Open

AlexZ005 wants to merge 5 commits into
release/nextfrom
feat/30-editor-modes

Conversation

@AlexZ005

Copy link
Copy Markdown
Collaborator

Roadmap 30, lane 30-core-modes. Base release/next = 1.16.0 (adb356d). Five commits, one per phase.

What landed

  • P0 c162c20 — the pointer-capture throw (~100 errors on a Quest = one per press). three 0.185's OrbitControls.onPointerDown calls setPointerCapture unguarded and the editor controls stayed mounted in play; Chromium throws InvalidStateError under a pointer lock, once per press, later surfacing as "Something went wrong". The active OrbitControls stands down per frame while isLocked === true (per frame because TransformControls' unmount cleanup re-enables it a tick later) and the capture is guarded on the element while a lock exists.
  • P1 4b1e7ed — Edit and Interact. LOCAL editorMode store; a toggle cell on the Controls bar (#editor-mode-toggle, aria-pressed, appended at the right end — loadLayout's own rule for a default id added later) and key I through the shortcut registry (stands down in text fields, mesh sessions, and via a new registerKeySessionProbe seam for sculpt/spline/draw). EDIT: a click selects; module click handlers run only if registered for Edit; On Click nodes do not fire. INTERACT: play's tap aimed by the cursor (module groups, handlers, On Click, the miss), no selection, no gizmo, and a press-drag on a dynamic body while a sim runs carries it with playInteract's own hold. SDK: registerClickHandler(fn, {modes}), default ['interact','play'].
  • P2 665e68c — select-through + click-cycle. Pure leaf selectThrough.js: the pick prefers the first opaque target (opacity < 0.25, userData.pick === 'through', or hidden = see-through); a repeat on the same spot walks down the stack. Inspector ▸ Object "Click-through in the viewport" (replicates as a props write like physics, one undo).
  • P3 5b09954 — every module's content in the object list. A read-only "Module content" section (one row per registered scene-root group, module badge, named children), a click selects a scene-root PROXY (never replicated, never saved, never in the selection set) and frames it; an Inspector card says whose it is. An Edit viewport click on module content selects its proxy. SDK: api.registerListedGroup(name, {label, icon}).
  • P4 86d4ee0api.pointerRay() under a pointer lock is the crosshair ray (it was the stale mouse ray); a free-cursor scene (30-core-flow's play.cursor: 'free', read defensively) keeps the mouse.

Suites (final battery on a fresh server)

suite result base
pointer-capture-play (new, P0) 12/12
editor-modes (new, P1+P4, two peers, real music-lab) 45/45
select-through (new, P2, two peers) 17/17
object-list-modules (new, P3, two peers, real untangle) 24/24
vitest selectThrough (new) 18/18
controls-roster (updated for the new cell) 108/108
selection-extras (held) 19/19 19/19
play-interact (held) 46/46 not measured (lock)
sdk-game-seams (held) 78/78 not measured (lock)
module-toolbox (held) 46/46 not measured (lock)
game-untangle (held) 46/46 not measured (lock)
toolbar-position (the bar grew a cell) 66/66
reset-windows 5/5

Every guard has a counterfactual in its commit body (P0: both halves removed → 20 errors for 20 presses + the toast; P1: every handler hears every click → 5 red incl. the piano; P2: nearest hit only → 5 red; P3: section removed → red, viewport branch off → red; P4: mouse ray always → red).

svelte-check 335/47 (base 336/47; one pre-existing implicit-any removed, nothing new). vitest 214/214 (+18 selectThrough). Build green.

Selection probe (real Templates modal + feed), objects that select themselves

game before after
Stars Room 1/15 (every click → "Wall south") 10/15 (rest: the see-through walls themselves, reachable by the repeat click; a star behind a planet)
Football 3/16 8/16 (remaining = HUD menu buttons over the viewport — cause 4, 30-core-flow)
Jam Room 10/13 (Piano, Drum machine, Sampler never select) 13/13

(Untangle reads 0/4 before AND after: the probe's first click, at the floor's centre, lands on the play FAB and enters play — a probe artifact.)

Deviations

  • Click-cycle timing: a repeat cycles only after the 400 ms double-click window (≤ 1500 ms), not "< 600 ms" — inside 400 ms is the configurable double-click action (fork recorded in the lane's questions file).
  • The mode toggle sits at the right end of the default bar (the codebase's rule for a default id added later); controls-roster updated in-commit.
  • VR's trigger has no editor mode yet: it keeps offering every click handler first.
  • Base held-suite runs were abandoned under a saturated e2e lock (selection-extras measured 19/19 at base); every held suite is green on the branch.

Outside my lane (minimal)

commandsHandler.svelte.js (one objectParameters receive branch for pick), Inspector.svelte (the checkbox + the Module content card), controls-roster.test.cjs.

For the integrator

  • CHANGELOG (P0): "~100 setPointerCapture InvalidStateError on Quest (and 'Something went wrong' after leaving play) = one per press: three 0.185 OrbitControls captures the pointer unguarded and the editor controls stayed mounted in play."
  • MODULES.md / AUTHORING: registerClickHandler(fn, {modes}) ('edit' | 'interact' | 'play'; absent = interact + play — an editor tool passes {modes: ['edit']}), registerListedGroup(name, {label, icon?}), pointerRay() = crosshair under a lock. The manager's module card could say the same in one line.
  • CLAUDE.md: selectThrough.js / moduleContent.js leaves, editorMode + setEditorMode, playInteract's cursor carry, registerKeySessionProbe, runClickHandlers/CLICK_MODES, the timing fork.
  • 30-mod-audit builds on this branch (handlers that are editor tools want {modes: ['edit']}; labels via registerListedGroup).

Owed on device

  • The Edit/Interact toggle's feel and whether I is the right key.
  • A Quest controller in 2D browser mode pressing in play with no setPointerCapture error.

🤖 Generated with Claude Code

AlexZ005 and others added 5 commits September 23, 2026 00:33
…t = one per press)

- three 0.185's OrbitControls.onPointerDown calls domElement.setPointerCapture with no
  guard, and the editor <OrbitControls> stays MOUNTED in play. Chromium refuses a capture
  with InvalidStateError while the page holds a pointer lock, and the throw lands before
  _addPointer, so EVERY press in play threw once and later surfaced as "Something went
  wrong ... Copy diagnostics" (the Quest diagnostics log, nodes/2.*.js:372:15736).
- Half 1: Scene's frame task stands the active OrbitControls down while isLocked ===
  true and restores exactly what it stood down when play ends. Per FRAME, not on the
  isLocked edge: TransformControls unmounts in play and its auto-pause cleanup writes
  enabled = true unconditionally a tick later, which would stomp an edge write.
- Half 2: setPointerCapture is guarded on the element the controls listen on (skipped
  while document.pointerLockElement is set). Smaller than subclassing, which would mean
  replacing threlte's <OrbitControls> with a hand-built <T is>; it also covers the camera
  preview's OrbitControls, which listens on the same element. It calls the prototype
  method at call time, and is removed on destroy.
- New suite pointer-capture-play (12): Chromium's rule modelled in-page (a
  pointerLockElement getter + a prototype setPointerCapture that throws under a lock,
  checked as a premise), 20 real CDP presses in play, a hand re-enable to prove the
  guard alone, no diagnostics toast after leaving play, and the editor still orbits.
- Counterfactuals: both halves removed -> 20 errors for 20 presses, the toast, 6 red
  (the reported bug, reproduced); capture guard alone removed -> the re-enabled-controls
  check red (2 errors) + the toast.
- svelte-check 336/47 = base (line shifts only); vitest 196/196; build green;
  selection-extras 19/19 at base.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ers routed by mode

- New LOCAL store `editorMode` ('edit' | 'interact', sceneStore; never sent, never
  saved). EDIT (default): a viewport click SELECTS first; a module click handler runs
  only if it registered for Edit; On Click nodes do NOT fire from the editor pick.
  INTERACT: a click is play's tap aimed by the cursor (module groups, handlers that run
  in 'interact', On Click nodes, the miss), nothing is selected and the gizmo is put
  away; a press on a dynamic body while a sim runs CARRIES it along the cursor ray with
  playInteract's own hold (one spring, one throw, one cancel discipline — only the aim
  differs) and the camera controls stand down for that gesture. Play is unchanged.
- SDK: `api.registerClickHandler(fn, {modes})`, modes any of 'edit'|'interact'|'play';
  ABSENT = ['interact', 'play'], so a module that never passes it stops eating editor
  clicks (the reported Jam Room case: Piano / Drum machine / Sampler never selectable).
  One dispatch `runClickHandlers(object, mode)` for the editor pick, Interact and the
  play tap. A core handler pushed straight into the array (vrPatch's plug click —
  patching is authoring) runs in every mode. VR's trigger passes no mode and keeps
  offering every handler first (VR has no mode toggle yet — recorded).
- UI: an Edit/Interact toggle cell ('Interact mode (I)', a real <button> with
  aria-pressed, `#editor-mode-toggle`) joins the default Controls bar at the right end
  (the place loadLayout already appends a default id an older record lacks, so the well
  keeps its slot); key I through the shortcut registry, standing down in text fields,
  mesh sessions (MESH_EDIT_KEYS already) and — through a new `registerKeySessionProbe`
  seam Scene feeds — sculpt, spline edit and draw. A transform tool (1/2/3, the bar's
  Move/Rotate/Scale) picked in Interact goes back to Edit in that tool.
- objectActions: setEditorMode / toggleEditorMode (Interact detaches the gizmo, Edit
  re-seats it on the selection), and applySelectionSet attaches no gizmo in Interact.
- New suite editor-modes (41, two peers): toggle + key + registry row + text field;
  an edit-only handler runs in Edit and the default one does not (and vice versa in
  Interact); an On Click node fires only in Interact and its pulse reaches the peer;
  the gizmo leaves and comes back; the REAL music-lab piano (installed zip): an Edit
  click on a key selects the piano and sends no devicenote, an Interact click sends one
  and selects nothing; an Interact press-drag carries a dynamic crate with the cursor
  (up the screen = up in the world), without orbiting, and lets go on release.
- controls-roster updated in-commit for the new default cell (108/108): the default
  row, the counts and the rightmost cell; its cell locator addresses the CELL rather
  than a <p>.
- Counterfactual: every handler hears every click again (the pre-P1 routing) ->
  5 red: the no-{modes} handler runs in Edit (2.3), both counts
  double (2.5/2.6), and the Edit click on a piano key plays a note instead of selecting
  the piano (5.3/5.4) — the reported bug, reproduced.
- svelte-check 335/47 (base 336/47: one pre-existing implicit-any in Scene removed,
  nothing new); vitest 196/196; build green. Held: play-interact 46/46, selection-extras 19/19,
  sdk-game-seams 78/78, module-toolbox 46/46, game-untangle 46/46.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- The pick returned the NEAREST hit whatever its opacity, so a near-invisible shell won
  every click (measured: Stars Room's 0.12-opacity walls took 16 of 16 clicks on stars,
  pads and planets; Football's 0.06 ceiling ate the lamps and bars). New pure leaf
  `selectThrough.js` (imports nothing): `pickStack` collapses raw hits into the stack of
  top-level targets (a target is opaque if ANY of its hits is), `isSeeThrough` = the
  top-level `userData.pick === 'through'` flag, a surface under 0.25 effective opacity
  (the hit face's own material on a multi-material mesh), or a hidden node on the way
  up; `primaryIndex` = the first opaque target, else the nearest.
- CLICK-CYCLE: a plain repeat within 4 px walks DOWN the stack and wraps (so the shell in
  front is reachable too), with a one-time "Click again to select behind" hint. TIMING
  DEVIATES FROM THE PLAN (< 600 ms): a repeat cycles only after the 400 ms double-click
  window and within 1500 ms — inside 400 ms is the configurable double-click action,
  and a cycle there would kill double-click-to-open-properties wherever anything sits
  behind the cursor. Recorded as a fork in the lane's questions file.
- Wired into Scene's raycastSelect (Edit and VR; the cycle needs click coordinates, so
  desktop only) and into Interact's click, so a glass wall does not take a star's click
  there either. An Edit-mode module handler is offered the CHOSEN hit.
- Inspector ▸ Object: "Click-through in the viewport" (#inspector-pick-through), fanned
  over the selection. objectActions.setPickThrough writes userData.pick through ONE
  `props` undo entry and the existing `objectParameters` message (parameter 'pick',
  null = cleared) — the userData.physics shape; commandsHandler gains the receive branch.
  Absent is the default, so a never-flagged object serialises byte-identically.
- New suites: select-through (17, two peers: a box inside a 0.1 shell selects the box,
  the repeat reaches the shell and comes back, a double-click does not cycle, the real
  checkbox makes an opaque wall stand aside, the flag reaches the peer, one undo clears
  it on both, no key on an unflagged object); vitest selectThrough (18).
- Counterfactual: nearest hit only -> 5 red (the box inside the shell selects the shell,
  the cycle checks, and the flagged wall still takes the click).
- Probe (SELECT_ONLY, stars-room + football, real Templates modal + feed): Stars Room objects that select THEMSELVES
  1/15 -> 10/15 (the rest: the see-through walls themselves, reachable by the repeat,
  and a star genuinely behind a planet); Football 3/16 -> 8/16 (the remaining misses
  are the HUD menu buttons over the viewport — roadmap cause 4, 30-core-flow).
- svelte-check 335/47 (= after P1, nothing new); vitest 214/214; build green.
  Held on this commit: editor-modes 41/41, selection-extras 19/19.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…content section

- Scene-root module content (the untangle board, the dungeon, the sabers, ...) lives
  outside objectsGroup by golden rule 5, so it was never picked and never LISTED (only
  the advanced System filter polled it, naming no module). New LEAF `moduleContent.js`
  (THREE + stores + helperLayer): the registry moduleSDK writes on every
  registerInteractiveGroup / registerSystemGroup (and the new registerListedGroup), the
  rows (named children to depth 2, capped at 200 with a "+N more" line), and the
  selection PROXY — a scene-root Box3Helper on the helper layer sized to the group's
  bounds, following it at 4 Hz. Never in objectsGroup: it never replicates, never
  saves and never enters the selection SET, so the outline, the gizmo and every
  act-on-selection command are untouched. Any object selection or deselect clears it.
- `ModuleContent.svelte` under the scene tree: one READ-ONLY row per group present in
  the scene, the module's name as a badge, expandable, a local hide-in-viewport eye; a
  click selects the proxy, frames it and opens the Inspector; its context menu is
  Frame it / Open <the module's toolbox> / Hide in viewport (this device) — no rename,
  delete or drag. Re-derives on pokeScene AND every second (module content changes
  without a poke — the System view's precedent, which stays).
- Inspector: a card instead of transform rows ("Made by the <module> module — edit it
  with its toolbox or nodes", plus the toolbox's button when it has one).
- Viewport: an EDIT click on module content that no edit handler took selects its
  proxy when it is nearer than the object hit; in INTERACT the click stays the module's.
- SDK: `api.registerListedGroup(name, {label, icon})` (journalled) names a group for a
  person; without it the row reads the group name humanised ('untangle-module' ->
  'Untangle module'). moduleSDK re-exports `moduleContentDebug` for suites (no new
  debug-hook tail).
- New suite object-list-modules (24, two peers, the REAL untangle zip + template from
  the feed / sibling checkout, skip-never-fail): the row, its label and badge, the
  dots listed, not in the scene tree, a read-only menu; a click selects a proxy at the
  scene root around the board, frames it, the Inspector card, an empty object set;
  object counts unchanged on both peers, no proxy on the peer or in a saved payload;
  deselect removes it; an Edit viewport click selects the proxy, an Interact one does not.
- Counterfactual: the section filtered out of the list -> 1.1 red (the row is
  gone and the suite stops there); the Edit viewport branch disabled -> 5.2 red (an Edit
  click on the board selects nothing).
- svelte-check 335/47 (nothing new); vitest 214/214; build green. Held: editor-modes 41/41, selection-extras 19/19,
  module-toolbox 46/46, game-untangle 46/46.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Under a pointer lock the cursor is pinned, so pointerRay() kept answering with the
  mouse position from the moment the lock began — a STALE ray that never moved again.
  Untangle's carried dot followed it, which is part of why a drag in play went nowhere.
  While isLocked === true, the pointer is locked and play is not in its menu substate
  (playPointerFree), pointerRay() now returns the CROSSHAIR ray: the view's centre,
  NDC (0,0) through the play camera — the ray playInteract already aims with.
- Hook for 30-core-flow: a scene whose play block says `cursor: 'free'` plays with the
  real cursor and no lock, so its ray stays the mouse's. Read defensively through
  resolvePlaySettings (absent = 'locked', today's play) — nothing to merge when that
  field lands.
- editor-modes gains section 7 (pointer lock stubbed): the mouse ray is off-centre, the
  locked ray matches the crosshair's direction (dot > 0.99999) and origin, and the mouse
  ray is back once unlocked. 45/45.
- Counterfactual: the mouse ray always -> 7.2 red (dot 0.853839: the locked ray is the stale
  mouse ray).
- svelte-check 335/47 (nothing new); vitest 214/214; build green. Held: sdk-game-seams 78/78, play-interact 46/46.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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