Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions src/App.svelte

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions src/components/LockHighlights.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import { lockedObjects, objectsGroup } from '../stores/sceneStore';
import { peers } from '../stores/appStore';
import { peerColor } from '$lib/lockControl';
// 30b P1: who-holds-what boxes are editor information - none in Interact/Play
import { editorHelpersShown } from '$lib/helperLayer';

// Wireframe box around every object locked by ANOTHER peer, tinted with
// that peer's color (same hash as pings/cursors). Boxes track the object
Expand All @@ -16,6 +18,7 @@
const helpers = new Map();

useTask(() => {
group.visible = $editorHelpersShown;
/** @type {any} */
const peer = $peers;
const scene = $objectsGroup;
Expand Down
6 changes: 4 additions & 2 deletions src/components/Outline.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import { objectsGroup, selectedObjects, lockedObjects, viewMode, TControls, isLocked } from '../stores/sceneStore.js';
import { objectsGroup, selectedObjects, lockedObjects, viewMode, TControls, isLocked, editorMode } from '../stores/sceneStore.js';
import { showToast } from '../stores/appStore.js';
import { get } from 'svelte/store';
import { chromiumMajor, postSupported } from '$lib/viewMode';
Expand Down Expand Up @@ -504,7 +504,9 @@
// information — who has what selected, what a peer has locked — and in play
// mode they are glare over the thing you are playing with, which is the same
// argument the mesh-edit session already makes one line up.
const playing = $isLocked;
// 30b P1: ...and in INTERACT, which is play's hands without play's lock: a
// player-style view of the scene has no selection glare either
const playing = $isLocked || $editorMode === 'interact';
if ($objectsGroup && !editing && !playing)
for (const uuid of $selectedObjects) addMeshes(outlineEffectSelected.selection, uuid);
outlineEffectLocked.selection.clear();
Expand Down
11 changes: 10 additions & 1 deletion src/components/Scene.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import { sceneHits } from '$lib/scenePick';
import { pickStack, chooseInStack } from '$lib/selectThrough';
import { tickModuleProxy, selectModuleGroup, moduleGroupOf } from '$lib/moduleContent';
import { setModuleWorldRoot } from '$lib/moduleWorld';
import { startPlayInteract, tickPlayInteract, stopPlayInteract, carriedUuid, editorInteractActive, cursorGrabStart, cursorGrabMove, cursorGrabEnd, interactClick } from '$lib/playInteract';
import { registerKeySessionProbe } from '$lib/shortcuts';
import { startKnock, tickKnock, stopKnock } from '$lib/knock';
Expand All @@ -54,7 +55,7 @@
// the annotation is TS syntax — a JSDoc @type cast is ignored here (the documented trap).
let knifeFrom: number[] | null = null;
import { peerScenes } from '$lib/peerScenes';
import { initVRControls, updateVRControls, raycastMenu, raycastPanel, raycastPalette, raycastProps, raycastPrefabs, raycastKeyboard, raycastChat, raycastEdit, raycastSnap, raycastSettings, raycastApprove, placePrefabGhost, vrFaceTrigger, vrVertexTrigger, vrVertexGrabStart, vrVertexGrabEnd, beginStretchSliderDrag, endStretchSliderDrag, executeVRMenuAction, resetWorldRig, onInputSourcesChange, worldToContentPose, boxSelectStart, boxSelectEnd, boxSelectActive, applyVRFrameRate, shouldSendHands, onHandPinchStart, onHandPinchEnd, pinchMenuToggledAt, firePingIfArmed, vrModuleTriggerStart, vrModuleTriggerEnd, vrModuleSelectSwallowed, handSnapshot, vrGrabbedUuid, hapticPulse } from '$lib/vrControls';
import { initVRControls, updateVRControls, raycastMenu, raycastPanel, raycastPalette, raycastProps, raycastPrefabs, raycastKeyboard, raycastChat, raycastEdit, raycastSnap, raycastSettings, raycastApprove, placePrefabGhost, vrFaceTrigger, vrVertexTrigger, vrVertexGrabStart, vrVertexGrabEnd, beginStretchSliderDrag, endStretchSliderDrag, executeVRMenuAction, resetWorldRig, onInputSourcesChange, worldToContentPose, boxSelectStart, boxSelectEnd, boxSelectActive, applyVRFrameRate, shouldSendHands, onHandPinchStart, onHandPinchEnd, pinchMenuToggledAt, firePingIfArmed, vrModuleTriggerStart, vrModuleTriggerEnd, vrModuleSelectSwallowed, handSnapshot, vrGrabbedUuid, hapticPulse, onVRSessionStart } from '$lib/vrControls';
import { vrKeyboardTarget } from '$lib/vrKeyboard';
import { measureMode, measureClick } from '$lib/measure';
import { pinsGroup, openAnnotation, showNotePins } from '$lib/annotationsHandler';
Expand Down Expand Up @@ -1600,6 +1601,11 @@
sceneObjects — they'd leak into GLTF sync). oncreate passes the ref
DIRECTLY (the { ref } destructure trap, N1). -->
<T.Group name="particle-root" oncreate={(ref: any) => setParticleRoot(ref)} />

<!-- 30b P5: module viewport content (registered scene-root groups) is re-homed here so
the VR world gestures carry it too — Untangle's dots spin with the world. Not in
sceneObjects: nothing under it is serialised or sent (golden rule 5). -->
<T.Group name="module-world-root" oncreate={(ref: any) => setModuleWorldRoot(ref)} />
</T.Group>

{#if !$isLocked && !$isVRMode}
Expand Down Expand Up @@ -1642,6 +1648,9 @@ position={[0, 2, 3]}
// B2.1: request the preferred refresh rate (auto = highest supported) —
// without this the Quest stays at its 90Hz default
applyVRFrameRate();
// 30b P4: remember the untouched reference space (the walker's floor), and a
// GAME lands in Interact on its spawn
onVRSessionStart();
}}
onsessionend={() => passthroughActive.set(false)}
>
Expand Down
15 changes: 13 additions & 2 deletions src/components/play/PointerLockControls.svelte
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
<script lang="ts">
import { onDestroy, untrack } from 'svelte'
import { get } from 'svelte/store'
import { Euler, Camera } from 'three'
import { Euler, Camera, Vector3 } from 'three'
import { useThrelte, useParent, useTask } from '@threlte/core'
import { isLocked, playPointerFree, playerCam, editorCam, globalScene } from '../../stores/sceneStore'
import { userdata, peers } from '../../stores/appStore'
import { dungeonData, slideMove, spawnPointFor } from '$lib/dungeonPlay'
import { resolvePlaySettings } from '$lib/playSettings'
import { spawnDesktopPlayer } from '$lib/playSpawn'
// 30 P3: free-cursor games never take the pointer — the real cursor aims
import { playCursorFree } from '$lib/playCursor'
import { inputClaims, getGamepadAxes } from '$lib/inputRuntime'
Expand Down Expand Up @@ -34,7 +35,8 @@
effectiveSpeed,
setJumpRequested,
tickWalker,
walkStep
walkStep,
collideRigStep
} from '$lib/charController'

const { renderer, camera, invalidate } = useThrelte()
Expand Down Expand Up @@ -191,6 +193,9 @@
// in your seed-deterministic room (peers take consecutive rooms).
// untracked: the effect must only depend on $isLocked.
untrack(() => {
// 30b P4: a game's own spawn (the scene's play.spawn, or a module's api.setSpawn)
// wins; the dungeon's per-peer rooms below are what a scene without one gets
if (spawnDesktopPlayer()) return
const data = dungeonData($globalScene)
// resolve the rig ONCE and mutate the object: `$cameraParent.position.x = v`
// compiles to store_mutate -> cameraParent.set(), and useParent() is a
Expand Down Expand Up @@ -282,6 +287,8 @@

const beforeX = $cameraParent?.position.x ?? 0
const beforeZ = $cameraParent?.position.z ?? 0
// 30b P3: the rig's WORLD pose before this frame's built-in step, for the collision pass
const beforeWorld: any = $isLocked === true && $cameraParent ? $cameraParent.getWorldPosition(new Vector3()) : null

if (moveState.forward === 1) {
$cameraParent.translateZ(-speed);
Expand Down Expand Up @@ -370,6 +377,10 @@
}
}

// 30b P3: ...and against the physics world's colliders while a simulation runs, so
// desktop Play stops at a game's walls as the VR walker does (no world, no change)
if ($isLocked === true && rig && beforeWorld) collideRigStep(rig, beforeWorld)

},
{
autoInvalidate: false
Expand Down
27 changes: 10 additions & 17 deletions src/components/play/VRControls.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@
import * as THREE from 'three';
import { useThrelte, useTask } from '@threlte/core';
import { vrFlying, vrMenuOpen, vrObjectsPanelOpen, vrGrabbedHand } from '../../stores/sceneStore';
import { computeMoveOffset, worldScale, twoGripStretchActive, controllerIndexFor, vrNavigationSuppressed } from '$lib/vrControls';
import { computeMoveOffset, worldScale, twoGripStretchActive, controllerIndexFor, vrNavigationSuppressed, tickVRInteractLocomotion } from '$lib/vrControls';
import { inputClaims } from '$lib/inputRuntime';
import { dungeonData, slideMove } from '$lib/dungeonPlay';

const { renderer, camera, scene } = useThrelte();
const { xr } = renderer;
Expand All @@ -23,7 +22,7 @@
const cameraDir = new THREE.Vector3();
const aimDir = new THREE.Vector3();

useTask(() => {
useTask((delta) => {
const controller1 = xr.getController(0);
const controller2 = xr.getController(1);
// controls must live on the dolly or they will not move with it
Expand All @@ -35,12 +34,15 @@
const session = renderer.xr.getSession();
if (!session) return;
if ($vrMenuOpen || $vrObjectsPanelOpen) return; // menu/panel own the sticks (74/101)
if ($inputClaims.includes('locomotion')) return; // K-C: a module drives instead
// 30b P3: INTERACT walks like a game (collision, gravity, step-up; no fly or teleport
// unless the play block allows them) — a held object does not stop your feet
if (tickVRInteractLocomotion(delta, session)) return;
if ($vrGrabbedHand === 'left') return; // a left-hand grab owns its stick (100)
if (twoGripStretchActive()) return; // 186: both grips + sticks stretch, not move
// D9: world pan/grab write reference-space offsets themselves, and the
// mesh-edit gestures read the sticks for reel/scale — never also move
if (vrNavigationSuppressed()) return;
if ($inputClaims.includes('locomotion')) return; // K-C: a module drives instead
const space = xr.getReferenceSpace();
if (!space) return;

Expand All @@ -63,19 +65,10 @@
// grabbed-world scale (71): body-relative speed stays constant
speed: 0.05 * worldScale()
});
if (offset.x || offset.y || offset.z) {
// dungeon collision (58.5): clamp the viewer displacement against
// the raster (offset = -(viewer displacement) per convention)
const data = dungeonData(scene);
if (data) {
const viewer = xr.getCamera(camera.current).position;
const allowed = slideMove(data, viewer.x, viewer.z, -offset.x, -offset.z, 0.3);
offset.x = -(allowed.x - viewer.x);
offset.z = -(allowed.z - viewer.z);
}
if (offset.x || offset.y || offset.z)
xr.setReferenceSpace(space.getOffsetReferenceSpace(new XRRigidTransform(offset)));
}
// 30b P3: EDIT flies through walls (the dungeon raster clamp that stood here is the
// Interact walker's job now — the user's ask: "go through walls ... only in edit")
if (offset.x || offset.y || offset.z)
xr.setReferenceSpace(space.getOffsetReferenceSpace(new XRRigidTransform(offset)));
}
});
</script>
4 changes: 3 additions & 1 deletion src/components/play/VRSelectionShell.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import { selectedObject, isVRMode, objectsGroup, vrWireframeSelection } from '../../stores/sceneStore'
import { editingObject } from '$lib/meshEdit'
import { faceEditObject } from '$lib/faceEdit'
// 30b P1: a selection indicator is editor scaffolding - none in Interact/Play
import { editorHelpersShown } from '$lib/helperLayer'

// VR selection indicator (101/110): the desktop outline is a postprocessing
// composer and does NOT render in WebXR. Default is a two-tone wireframe —
Expand Down Expand Up @@ -106,7 +108,7 @@
// the live structure — a static indicator would just lag behind edits
const editing = !!$editingObject || !!$faceEditObject
const active =
$isVRMode && !editing && !!target?.uuid && !!$objectsGroup?.getObjectByProperty('uuid', target.uuid)
$isVRMode && $editorHelpersShown && !editing && !!target?.uuid && !!$objectsGroup?.getObjectByProperty('uuid', target.uuid)
group.visible = active
if (!active) return
const style = $vrWireframeSelection ? 'wire' : 'shell'
Expand Down
7 changes: 4 additions & 3 deletions src/extensions/Grid.svelte
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
<script lang="ts">
import { Grid } from '@threlte/extras'
import { T, useThrelte, useTask } from '@threlte/core'
import { orbitControls, isLocked } from '../stores/sceneStore'
import { orbitControls } from '../stores/sceneStore'
// 30 P3: the grid is an EDITOR helper — it drew under/through the ground in five of the
// seven Games-tab games. Hidden while playing, on every scene; the existing "Show
// helpers in Play (debug)" toggle is the one way to keep it (no new setting).
import { helpersInPlay } from '../lib/helperLayer'
// 30b P1: ...and in Interact, the same one predicate every editor helper answers to
import { editorHelpersShown } from '../lib/helperLayer'
import { gridSettings, effectiveCell } from '../lib/gridSettings'
import { snapSettings } from '../lib/snapping'
let { showGrid } = $props()
Expand Down Expand Up @@ -75,7 +76,7 @@
const fadeDistance = $derived($gridSettings.fadeMode === 'auto' ? fade : $gridSettings.fadeDistance)
</script>

{#if showGrid && ($isLocked !== true || $helpersInPlay)}
{#if showGrid && $editorHelpersShown}
<Grid
name="editor-grid"
infiniteGrid={$gridSettings.infinite}
Expand Down
3 changes: 2 additions & 1 deletion src/lib/cameraHelpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { wireframeActive } from './viewMode';
// 24-E2: frustums live on the helper layer; MARKERS hop onto it while hidden (Play
// without the debug toggle, or a camera preview) — see helperLayer.js for the rule
import { markHelper, setMarkersHidden, helpersHidden, helpersInPlay } from './helperLayer';
import { isLocked } from '../stores/sceneStore';
import { isLocked, editorMode } from '../stores/sceneStore';
import { safeStorage } from './safeStorage';

// 16-P5: frustum visualization for camera OBJECTS — the colliderHelpers pattern.
Expand Down Expand Up @@ -175,6 +175,7 @@ export function startCameraHelpers() {
// 24-E2: the marker hop follows Play, the debug toggle and the preview
if (!started && typeof window !== 'undefined') {
isLocked.subscribe(applyMarkerLayers);
editorMode.subscribe(applyMarkerLayers); // 30b: Interact hides markers like Play
helpersInPlay.subscribe(applyMarkerLayers);
frustumSuppressed.subscribe(applyMarkerLayers);
}
Expand Down
Loading
Loading