31 vr-core: radial stick nav, a laser you can see, panels on top, worldGrab + bounded teleport (K1/K2) - #253
Conversation
… trigger picks it
The Quest report: "In VR, when I open radial menu, i do not see highlights when i move
joystick, fix navigation." The input was never the problem - vrControls set vrHovered to
the right sector every frame (measured through fakeXR: 8/8 directions). VRMenu.svelte is a
LEGACY-mode component, where `color={sectorColor(s.entry)}` compiles to
untrack(() => sectorColor(...)) depending on `s` alone, so the $vrHovered read INSIDE the
helper registered nothing: no sector mesh ever repainted, under the stick or the ray (the
hub, which reads the store inline, did).
- VRMenu: sectorColor/labelColor take the hovered id as a PARAMETER ($vrHovered passed in
the template), so the colour depends on it.
- vrControls: a stick-lit sector is remembered per frame (radialStickSelection); Scene's
trigger picks it when its own ray misses the ring (before, only a ray hit could be
triggered and a stick-lit sector fell through to an object select). Stick-click and the
hold-mode release are unchanged.
- each stick is read from its OWN inputSource found by handedness (the pointer stick used
sources[<controller slot>], the 194/210 divergence after a hands<->controllers swap);
a greyed (disabled) sector is never lit by the stick.
- one haptic tick per sector change stays behind hapticPulse's C4 gate (Interact ticks,
Edit is silent - the user's earlier rule; recorded in QUESTIONS).
Suite vr-radial-stick (12, fakeXR through the real per-frame path + Scene's select handler):
8 directions light exactly their sector MESH, centred = nothing, 3 ticks for 3 changes,
trigger picks the stick-lit Add, stick-click picks Scene, the other hand's stick, the ray
lights what it hits, a greyed sector stays dark.
Counterfactuals (one batch, all restored): colour helper without the hovered argument ->
mesh-repaint, other-hand, ray and both greyed checks red; trigger without
radialStickSelection -> "the trigger opens the stick-lit sector" red; disabled guard
removed -> "a greyed sector is not lit" red. 6 red in total.
svelte-check 333/47 (base 333/47, identical list modulo line shifts).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…, a filled hover, module menus as beam ends The Quest report: "i want to be able to see from controller ray where i point in menu (now its hard to navigate)". The beam and reticle existed; four things made them hard to use. - vrControls: the beam is NORMAL-blended (the additive glow vanished against a bright sky - the documented additive-burst trap) and a touch wider; the reticle ring gains a solid DOT at the exact hit point (the ring left the point itself empty, which is where a small button is). - vrControls.beamTarget: a module's INTERACTIVE scene-root content ends the beam too (it lives under module-world-root since 30b P5, outside objectsGroup, so the laser passed through a module's own menu/board with no dot). The name list is cached on the registry revision - no sort or allocation per frame. - vrGamePanel: the button under the laser is FILLED (+ a thicker ring), the footer buttons too - a 4 px outline on a board 1.2 m away was too faint to tell which one the ray was on. - vrGameInput.interactiveGroupOf: accept a group under module-world-root. The old `node.parent === scene` test matched nothing after 30b P5 re-homed module groups, so a module's buttons never resolved to a click target for the laser hover or the sweep. Suite vr-laser-panels (23, fakeXR + the REAL Towers, Stars Room and Jam Room templates from scenes preview-1-17 via SCENES_DIR): per game the beam ends on the menu button (length = distance), reticle AND dot on its centre (< 1 cm), the board's hover id is that button and a pixel inside it changes (filled); the beam is normal-blended; an inline module's interactive group is re-homed, ends the beam at 1.49 m and resolves to its group; the reticle sits on a radial sector. Counterfactuals (one batch, restored): no dot -> 3 red; no fill -> 3 red; additive beam -> 1 red; no module roots in beamTarget -> beam 2.90 m red; parent === scene -> group null red. 9 red total. svelte-check 333/47 (identical list modulo line shifts). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…them) + api.vrPanel The Quest report: "menu buttons during game covered by scene objects below untangle (like base/floor objects)". A VR panel was a mesh like any other, so whatever stood between the eyes and it covered its buttons - while the laser, which prefers a panel, still pressed them. - vrPanelOverlay.js (leaf, THREE only): every open panel joins the transparent list at PANEL_ORDER after ONE depth clear (a colourless, never-culled sentinel whose onBeforeRender clears depth), so panels draw over the finished scene and still depth-test against EACH OTHER (a panel's own layers keep their order; a nearer panel covers a farther one - which a blanket depthTest:false gets wrong). Both eyes: without multiview three renders one eye completely, then the other, so the clear touches a finished half; with multiview both views clear together. Raycasts read geometry, so hit tests are untouched. - vrControls: core panels re-marked each frame (idempotent, allocation-free); the sentinel is in the scene only while a panel is open (a frame without one is the old frame); a panel the ray reaches ENDS THE BEAM even with a floor in front (the press already preferred the panel - now the beam agrees); beam + reticle draw at BEAM_ORDER while on a panel. registerOverlayPanel / panelOverlayDebug exported. - moduleSDK: api.vrPanel(object) -> undo (journalled with the module) for a module's own VR menu / level bar - Untangle's bar is the case the user saw. - vrGamePanel surfaces sit at PANEL_ORDER (they already ignored depth); splineEdit's handle group opts out (userData.vrOverlay = false - world handles, not a panel). Suite vr-panels-on-top (12, fakeXR, PIXELS with a red unlit blocker in objectsGroup between the camera and each panel): premise - the blocker hides an ordinary mesh (0.00 not red); the radial ring (1.00), the game board's Start (1.00) and an api.vrPanel module bar (1.00) draw over it; the beam passes the blocker and ends on the board (1.400 m) and on the module bar (0.900 m) at BEAM_ORDER; the trigger presses Start through the blocker; the depth clear ran and stands down with no panel open. Counterfactuals (restored after each): overlay disabled -> ring 0.00, module bar 0.00, clears 0 (3 red); panel hits compared by distance -> beam 0.660 m / 0.475 m and order 0 (3 red). svelte-check 333/47 (identical list). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…world gestures; play.bounds
The Quest reports: "Entangle game does not allow me to scale scene with grips within game,
only in edit mode" and "In Jam Room I would like to be able to fly around and also scale the
entire environment with grips and move around same as in edit mode".
- locomotionPolicy: `play.locomotion.worldGrab: true` -> Interact's worldGestures; the
normaliser keeps exactly the typed booleans teleport / fly / worldGrab (empty -> absent,
so a scene that never used them saves byte-identically).
- vrGrip.gripMovesWorld(mode, worldGrab): Edit always, Interact only with the flag. vrControls'
empty-air grip asks it with the live policy - so two grips scale/rotate the world, the right
grip alone pans it, and a grip on a grabbable body (dynamic, interaction 'grab') still
takes the BODY (pickGripTarget runs first, unchanged).
- teleportRules.js (new pure leaf, P4's rules) - here for normalizeBounds: `play.bounds
{min:[x,y,z], max:[x,y,z]}` is a SIBLING of locomotion; six finite numbers |v| <= 1e5, each
axis ordered, else dropped. scenePhysics keeps it in the play block (omitted when absent);
resolvePlaySettings resolves it field-by-field like spawn, a publisher's in ITS local frame
(`boundsOwner`). Vitest teleportRules (18): bounds, shrink, slab test, raster segment, the
verdict order.
- moduleSDK: `api.locomotion = {boundedTeleport: true, worldGrab: true}` (frozen) - the
feature probe the game lanes publish against (agreed with 31-fb-dungeon and 31-stars-jam).
- wireValidate: no change - scenephysics has no shape there, and the singleton's own
normaliser is its boundary.
Suite vr-locomotion-flags (13, fakeXR through the real grip path): the normaliser (typed flags
only, bounds ordered, bad bounds dropped, untouched scene writes neither key); WITHOUT the
flag Interact's two grips start nothing and the world stays 1:1; WITH it two grips start the
world grab and a spread scales x2.00, a right grip on the floor pans, a grip on the dynamic
cube holds the cube; api.locomotion probe; a module publisher's userData.play turns it on.
Vitest +5 (locomotionPolicy 2, vrGrip 1, + the teleportRules file): 296 green (base 275).
Counterfactuals (one batch, restored): gripMovesWorld(mode) -> 5 red; worldGrab dropped by
the normaliser -> 1 red; bounds not normalised -> 1 red. 7 red.
svelte-check 333/47 (identical list).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…he play area, never through a wall; red when refused
The Quest reports: "Dungeon Realms ... I would like to be able to teleport but not outside the
dungeon walls" and "In Stars room I would like to be able to teleport but not outside the
scene". Edit's teleport lands anywhere the arc does; a game's (Interact/Play with
`play.locomotion.teleport: true`) asks teleportRules for a verdict now.
- computeTeleportArc(origin, dir, group, {bounded, roots}): a bounded arc stops at the FIRST
surface it meets, of any slope (a wall face ends it instead of the arc passing through the
wall to the floor behind), over objectsGroup AND registered module content (a dungeon's
walls live under module-world-root), and reports the landing's world normal.y.
- teleportVerdict(from, to, normalY) (exported; the dungeon lane's e2e entry point), the rules
in order, each in ITS OWN frame so a world grab never skews one:
1 walkable: normal.y >= 0.7 (or the floor plane) else 'steep';
2 inside `play.bounds` (scene frame, or the publisher group's local frame) - else the
scene's content box pulled in 0.3 m on x/z (measured once per aim) - else 'outside';
3 a dungeon raster (dungeonPlay): the target cell is floor ('off-floor') and no wall cell lies
on the straight line ('wall-cell');
4 nothing solid crossed 1.1 m above both ends: a publisher's `play.colliders` AABBs, then a
mesh raycast ('blocked').
- updateTeleport: the arc is GREEN when valid and RED when refused (teleportPreview() reads
it); release teleports only on a valid landing - a red release does nothing.
- executeTeleport(target, bounded): a game's teleport stands your FEET on the landing (a step
onto a platform is a step up); Edit keeps its head-height move exactly as before.
Suite vr-bounded-teleport (19, a walled test room through fakeXR's real right-stick path):
the verdict (floor ok, behind the wall blocked, beyond the bounds outside, a wall face steep;
no bounds -> the content box shrunk 0.3 m); the arc short = green on the floor, the release
moves you there; onto a 0.5 m platform (a sim running) = green, head 0.5 m higher; over the
wall = RED behind it, release does nothing; at the wall = ends on the face, steep; a raster
wall cell on the way refused; Interact without the flag = no arc; Edit = the over-wall
landing green and taken.
Counterfactuals: mesh probe / raster / content bounds / bounded arc removed (one batch) ->
6 red; feet not placed (y: 0) -> "stands you ON it" 1.60 red. Both restored.
svelte-check 333/47 (identical list); vitest 296.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… play.bounds, the bounded teleport, api.vrPanel) - New section "Moving around in a game: locomotion, bounds, VR panels (1.18, roadmap 31)": `locomotion.teleport` (the bounded rules, red arc, feet on the landing), `locomotion.worldGrab`, `locomotion.fly`, `play.bounds` (a sibling of locomotion; a module's in its group's local frame), `play.colliders`, the `api.locomotion` feature probe, the teleportVerdict / teleportPreview debug entry points, and `api.vrPanel(group)` for a module's own VR menu (drawn over the scene, ends the laser). - static/llms-full.txt regenerated (npm run sync-llms). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… Stars Room's floating stars) Found by 31-stars-jam on the union: in the Stars Room, teleportVerdict from the spawn to the floor across the room returned 'blocked'. The wall probe runs 1.1 m above both ends, which is the stars' layer (24 dynamic icosahedra at 0.8-2.6 m), so any floating star in the line refused every landing. meshBetween now skips hits whose top-level object is a DYNAMIC physics body or a spawner's transient copy - a body you can knock aside is not a wall. Static and pick-through solids (the room's glass) still block. vr-bounded-teleport 21/21 (+2: a dynamic box at 1.1 m in the line -> ok; the same box static -> blocked). Counterfactual: the filter removed -> the dynamic check red. svelte-check 333/47 (identical list). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Follow-up 8189daa: a dynamic body (or transient copy) in the line no longer refuses a teleport. Found by 31-stars-jam: the Stars Room's floating stars sit at the 1.1 m probe height. vr-bounded-teleport is now 21/21; with the filter reverted the new check goes red. 31-fb-dungeon verified K1 on the real Dungeon Realms (modules #26). |
…frame (rays carry the camera)
Found by 31-untangle: since P1 the VR rays reach module content (the beam ends on interactive
groups; P4's arc and wall probe raycast registered module groups). three's Sprite.raycast
needs `raycaster.camera` - it warns and THROWS on a null camera ("Cannot read properties of
null (reading 'matrixWorld')"). One Sprite in a module group (Untangle's globe-mode canvas HUD)
threw inside updateVRControls every frame and aborted it before onSqueezeStart: worldGrab
silently dead in the globe, fine on the 2D board.
- vrControls.withRayCamera(ray): every VR ray carries the camera the viewer sees through (the
XR camera in a session, the editor camera otherwise) - controllerRay, pointerHandRay
(api.pointerRay), the teleport arc and wall probe; vrGameInput.controllerRayOf too.
- safeIntersect: a raycast INTO module content is guarded - a module mesh whose raycast
throws logs once per call and yields no hit instead of taking the frame down.
vr-laser-panels 25/25 (+2: a Sprite on the module board -> no page error, the beam ends on
it at 1.200 m). Counterfactual (camera + guard removed): the exact reported pageerror, the
beam passes to 1.490 m, and the radial reticle after it is off (3 red).
Re-run on this commit: vr-bounded-teleport 21/0, vr-panels-on-top 12/0, vr-game-panel 35/0,
vr-sweep 25/0 (with JAM_ROOM_TPSCENE = the preview-1-17 Jam Room - the scenes checkout on
main is older and has no mixer mute buttons, which reads as a 10.4 failure).
svelte-check 333/47 (identical list).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Follow-up c5620ca: a THREE.Sprite in module content threw inside updateVRControls, because Sprite.raycast needs raycaster.camera. That killed worldGrab in Untangle's globe mode (found by 31-untangle). Every VR ray now carries the camera, and raycasts into module content are guarded. vr-laser-panels is 25/25, and the counterfactual reproduces the reported error. vr-bounded-teleport 21, vr-panels-on-top 12, vr-game-panel 35 and vr-sweep 25 (with the preview-1-17 Jam Room) all pass. Build is green. |
Roadmap 31 lane 31-vr-core (contracts K1 + K2). Items R1, G5, U4 (core half), U1/J1 (flags), D2/S1 (bounded teleport) from the user's Quest 3 feedback of 2026-10-01.
The user's items (user-feedback-2026-10-01)
color={sectorColor(s.entry)}compiled to untrack(...), so the $vrHovered read inside the helper never repainted a sector. That broke both the stick and the ray. Fixed by passing the hovered id as a parameter.play.locomotion.worldGrabgives Interact/Play the Edit world gestures (two-grip scale/rotate, right-grip pan) whenever a grip doesn't start on a grabbable.flyis unchanged.play.bounds(a sibling of locomotion) is normalized via teleportRules.normalizeBounds and resolved per publisher frame (boundsOwner).api.locomotion = {boundedTeleport, worldGrab}is the probe.Gates (final, fresh server)
Deviations / decisions
For the integrator
fn(x)is untracked beyond its args — pass the store value as a parameter (VRMenu sectorColor)".8>&- 9>&-)". This blocked every lane for ~2 h on 2026-10-01. Hardening e2e-slot to close the fds for "$@" is worth doing.pointGrabAllowed()skip inside gripTargetOf (Interact ray grabs only). With worldGrab on, such a grip falls through to the world gesture.🤖 Generated with Claude Code