30b-vr-modes: Edit vs Interact in VR — grips, walking, spawn, clean views (C1) - #244
Merged
Merged
Conversation
… layers
- Cause: helpers lived on render layer 1, and three's WebXRManager renders each eye
through a sub-camera whose mask it rewrites every frame as
(camera.mask | 0b110) & ~0b100 (left) and & ~0b010 (right) — layer 1 IS the left eye.
So a light helper was drawn by the left eye whatever the editor camera enabled, and
never by the right: the "light source helper with one eye" in Towers, Stars Room and
Football on the Quest.
- helperLayer: HELPER_LAYER 1 -> 30 (3..30 are only ever inherited by the eye cameras;
31 is overloadGuard's REDUCED_LAYER, postprocessing Selection ids count up from 2).
helpersHiddenFor(locked, mode, debug) is the one predicate: Play OR Interact hide
helpers (unless the debug toggle); editorHelpersShown is the same answer as a store.
The editor camera and the camera-marker hop now follow editorMode as well as isLocked.
- helpers-in-play reads the layer from the app instead of assuming 1.
- New suite vr-helper-eyes (13): asserts three's installed WebXRManager still reserves
1/2 for the eyes, then computes both eye masks with that formula — Edit: both eyes
draw the helper and its proxy; VR Interact: neither; desktop Interact and Play: none.
- Counterfactual: HELPER_LAYER back to 1 -> vr-helper-eyes red ("the helper layer is not
an XR eye layer", "Edit: BOTH eyes draw the light helper" L true R false).
- svelte-check 335/47 (unchanged list); vitest 222 green; build green.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- The Quest report: "In interact mode I do not need helper boxes or shapes, outlines around objects." Play already hid the helper layer and the grid; Interact hid nothing. - Every editor helper now answers the ONE predicate from P0 (helperLayer.helpersHidden / editorHelpersShown — Play or Interact, unless "Show helpers in Play (debug)"): the grid (Grid.svelte), collider/trigger wireframes (colliderHelpers), the tiny-object dots (tinyMarkers), a peer's lock box (LockHighlights), the selection + lock outlines (Outline: Interact joins Play), the VR selection shell (VRSelectionShell) and the VR hover box + emissive hover tint (vrControls updateHoverBox / setHovered). Light helpers, frustums and the module-content proxy ride the helper layer (P0). The selection itself survives Interact — only its glare goes. - New suite interact-clean-view (16): a scene holding one of each helper, read in Edit, Interact, back in Edit, VR Interact/Edit, Play, and Play with the debug toggle. - Counterfactuals: colliderHelpers without the helpersHidden gate -> "Interact: no collider wireframes" red (and Play); Outline without the Interact clause -> "Interact: no selection or lock outline (1/1)" red. - Held: editor-modes, play-interact, pointer-capture-play green (= base). - svelte-check 335/47 unchanged; build green. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…teract never does - The Quest report: "When in game and in edit mode I should be able to move around the world and scale it with grips, now it disables this for some reason". Nothing disabled it: the world gestures fire only on a grip that closes on EMPTY AIR, and a game scene has none — Stars Room is a closed room, Football a glass box, Towers a 26 m floor ringed by walls — so every grip grabbed the room. - vrGrip.js (pure, imports nothing): SCENERY (world bounds >= 4 m on any axis, or bounds that contain the head) is never held by a grip; pickGripTarget walks the ray's top-level hits: Edit takes the first non-scenery object, Interact only a grabbable one (a dynamic body under a play block whose interaction is 'grab'; a static object in front blocks); gripMovesWorld: only Edit's empty-air grip moves the world. - vrControls: gripTargetOf (ray hits + the hand-inside test through that rule) feeds onSqueezeStart. Interact: no world grab/pan, no two-hand object scale, a RIGID grab with no snapping and no stick reel/scale, no selection (no editor lock), and no undo entry on release (a player's throw is not an edit). vrGripDebug for suites. - knock.js: in VR only Interact's hands knock (an Edit hand is placing things). - tests/e2e/fakeXR.cjs: a fake XR session whose gamepads a suite presses, so the REAL per-frame path (Scene's useTask -> updateVRControls) runs headless; controller poses are written into the controller matrices. - New: vitest vrGrip (10), e2e vr-grips-by-mode (18): Edit two grips on a wall = world grab that scales x2 and leaves the wall; right grip on the floor = pan; a cube is still held, selected and one undo step. Interact: walls/air hold nothing and never move the world; a static podium is not holdable; the dynamic cube is, unselected, with no undo. - Counterfactuals: scenery not skipped in pickGripTarget -> 8 e2e reds (Edit grabs the wall/floor instead of the world) + 3 vitest reds; gripMovesWorld always true -> "Interact: ... start no world gesture" and "Interact: the world stays put (x2.67)" red. - Held: vr-world-grab 9/2 = base (the 2 are its pre-existing two-peer sync checks, red on pristine base with PEER_CONFIG too); vr-locomotion green. - svelte-check 335/47 unchanged; vitest green; build green. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…; Edit still flies
- The Quest report (the dungeon): "I can go through walls, teleport, I want to be able to
do this only in edit mode and fly only in edit mode. In interactive mode I should be
able to move like in a game."
- locomotionPolicy.js (pure): Edit = fly + teleport + world gestures, no walls (exactly
the old movement); Interact = walk, collide, gravity, snap turn, NO fly or teleport
unless `play.locomotion: {teleport?, fly?}` allows (absent = false; a flier drops
gravity but keeps walls). normalizeLocomotion/normalizeSpawn + yaw helpers + the VR
spawn as two WebXR offsets (used from P4).
- scenePhysics: play.locomotion and play.spawn normalised at the boundary and OMITTED
when absent/empty, so saved scenes stay byte-identical. playSettings.resolvePlaySettings
resolves `locomotion` field by field from the scene and publishers (the dungeon Kit
publishes {teleport:false, fly:false}) and `spawn`.
- charController: tickWalker's body became resolveWalk(feet, height, dt, desired) so the VR
walker resolves through the SAME tiers (rapier capsule with a sim / dungeon raster /
ground plane). Asked by the dungeon lane: the raster now also clamps the rapier tier (Kit
walls are module meshes, never rapier colliders). Desktop parity: tickWalker unchanged
in behaviour (char-controller 55/55, twice).
- vrControls: vrWalkStep (stick -> head-yaw step at 2.2 m/s -> resolveWalk) and
tickVRInteractLocomotion (feet = head world y minus the head's height in the BASE
reference space captured at session start; moves by offsetting the XR reference space;
falls out of the world -> back to the spawn/origin). updateTeleport honours the policy.
VRControls.svelte: Interact runs the walker (a held object no longer stops your feet);
Edit keeps its stick and now FLIES THROUGH walls (the raster clamp moved to Interact).
- Desktop Play keeps its built-in flight (the 21-E6 parity contract) but a 1 m head
capsule now stops it at colliders while a simulation runs (collideRigStep; no world =
byte-for-byte the old step).
- fakeXR.installSpace: a reference space + frame that compose offsets exactly as WebXR
specifies, so the real per-frame walker runs headless.
- New: vitest locomotionPolicy (12, incl. the spawn offsets against WebXR's composition
rule), e2e vr-walk-interact (20): a real rapier world — wall stops at -2.5, gravity lands
the feet, a 0.25 m step is climbed, a 0.6 m block is a wall, a flier climbs; the real
per-frame path walks forward and stops at the wall; teleport off in Interact, on with
play.locomotion.teleport and in Edit; Edit flies through the wall; desktop Play stops.
- Counterfactuals: tickVRInteractLocomotion disabled -> "Interact: the wall stops you (z
-7.55)" red; the teleport gate removed -> "Interact: the right stick does NOT arm a
teleport" red; collideRigStep disabled -> "desktop Play: the wall stops you (z -15.10)" red.
- svelte-check 334/47 (one base error gone with the Edit raster clamp); build green.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… players spawn
- The Quest report: "By default for games there should be interact mode in VR, but I
should be able to switch to edit mode", and "if the game should have a specific place
where the character should stand, it should move."
- vrControls.onVRSessionStart (Scene's XR onsessionstart): remembers the base reference
space, and a GAME (a HUD screen bound to a game state, a spawn, or a userData.play
publisher) lands in Interact. Entering Interact in a session (editorMode subscription,
the LAST statement of the file for the TDZ rule) drops any editor gesture, resets the
world to 1:1 and pays the spawn on the first frame with a viewer pose.
- The mode button: LEFT Y (B on the right when the radial menu lives on the left, so
they never share a button) toggles Edit <-> Interact with a haptic tick and a wrist
label on that controller ('vr-mode-label': INTERACT teal / EDIT amber + the button).
- Spawn = `play.spawn: {position:[x,y,z], yaw}` (y = FEET, three rotation.y) or a
module's `api.setSpawn(position, yaw, {teleport?})` (LOCAL, overrides the scene's,
cleared when the module is disabled; without teleport a new spawn is a checkpoint) +
`api.respawnPlayer()`. VR: vrControls.spawnPlayer turns the rig in place then moves it
so the feet land on the point (locomotionPolicy.vrSpawnOffsets). Desktop Play: the
play camera (playSpawn.spawnDesktopPlayer, ahead of the dungeon's per-peer rooms).
Desktop Interact: the editor view flies there (objectActions.setEditorMode).
- New: e2e vr-mode-spawn (22): a plain scene stays in Edit, a game lands in Interact ON
the spawn facing its yaw with the world back at 1:1; left Y -> Edit with one haptic tick
and the label; walking off and pressing Y again re-spawns; with the menu on the left the
right B switches; api.setSpawn checkpoint vs {teleport:true}, a malformed spawn refused,
the module's spawn dropped on disable; desktop Play and desktop Interact spawn.
- Counterfactuals: no spawn on entering Interact -> "the player stands ON the spawn" and
"facing the spawn's yaw" red; the mode button unwired -> "left Y: Interact -> Edit",
"a haptic tick", "the label follows", "the RIGHT B toggles" red.
- svelte-check 334/47; build green.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…in with it) - The Quest report: "When I spin the world around, the untangled dots do not spin around." The world gestures transform `world-grab-rig`; module viewport content lives at the SCENE ROOT (golden rule 5 keeps it out of objectsGroup), outside the rig. - moduleWorld.js (a leaf): a `module-world-root` group INSIDE the rig (Scene.svelte, a sibling of objectsGroup — never serialised or sent), and every REGISTERED module group (registerInteractiveGroup / registerSystemGroup / registerListedGroup) is re-homed under it as it reaches the scene root (childadded + registry revision), local transform kept — desktop (rig = identity) moves nothing, VR carries it. For every module at once, no module change: `getObjectByName` still finds it, the scene instance's remove() also takes a re-homed group out, and core's scene-root scans (playSettings.playPublishers, moduleContent's rows) read the module root too. Unregistered content stays put. - New e2e module-world-root (11): an inline module's groups land under the root (also a group added before its name was registered), an unregistered one does not, nothing enters objectsGroup, the dot follows a 90-degree spin + x2 scale of the rig, the Module content rows and the play publishers still find them, api.scene().remove works, disable removes them. - Counterfactual: adoptModuleGroups a no-op -> "a registered group lands under the world rig's module root", "...also when added BEFORE its name was registered" and "the dot followed (1.000,1.000,0.000)" red. - Held: object-list-modules 25/25 (= base, UNTANGLE_ZIP/TPSCENE from the sibling checkouts); svelte-check 334/47; vitest 244; build green. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Lane 30b-vr-modes: roadmap 30 round 2 (the Quest round), contract C1. This PR fixes the Quest 3 feedback about modes, grips, walking, spawn and the one-eye helper.
What landed (one commit per phase)
helpersHiddenFor/editorHelpersShown).play.locomotionallows it. That field is omitted when absent, so saved scenes are byte-identical. VR and desktop share one resolver (charController.resolveWalk), and the dungeon raster now also clamps the rapier tier. Edit flies through walls as before. Desktop Play keeps flying, but a head capsule stops it at colliders while a sim runs.play.spawnorapi.setSpawn(pos, yaw, {teleport})/api.respawnPlayer(). In VR your feet land on the spawn, facing its yaw. Desktop Play moves the play camera there, and desktop Interact moves the editor view.module-world-rootinside the world rig. It's still outside objectsGroup, so nothing replicates or saves. Untangle's dots now spin with the world, and this works for every module with no module change.Tests
New suites: vr-helper-eyes (13), interact-clean-view (16), vr-grips-by-mode (18), vr-walk-interact (20), vr-mode-spawn (22), module-world-root (11). New vitest files: vrGrip (10) and locomotionPolicy (12).
tests/e2e/fakeXR.cjsfakes an XR session (gamepads plus a reference space that composes offsets per the WebXR spec), so the real per-frame VR path runs headless.For the counterfactuals I broke every guard in one batch: all six suites and the vitest went red. The per-guard lines are in the commit bodies.
svelte-check 334/47 (base 335/47; one base error was removed and none were added). vitest 244. Build is green.
Deviations
All five are recorded in
QUESTIONS-30b-vr-modes.md, and each was built with the recommended option:For the integrator
true(theirforcearg) to toggleVRMode's hapticPulse.hapticPattern('hit')into onSqueezeStart.api.setSpawn/api.respawnPlayer.play.locomotion/play.spawn.Owed on device (Quest 3)
Screenshots (Edit vs Interact):
/home/deck/.code/lanes-30/after-30b/30b-vr-modes/.🤖 Generated with Claude Code