editor: unify walkthrough + viewer UI, crouch, screenshot pause (#529)
* feat(walkthrough): unify first-person and baked walkthrough UI into a shared HUD The builder first-person overlay (crosshair, Exit Street View button, hints card) and the baked-GLB walkthrough HUD were two divergent UIs. Extract the GLB-style HUD (reticle, floor/room labels, Esc pill, interact prompt) into a shared WalkthroughHud in packages/editor, feed it from FirstPersonControls via a small useFirstPersonHud store (interact target each frame, floor/zone labels sampled from the camera), and align FOV/projection handling with the baked controller. The now-unused WalkthroughControls glide controller is removed from packages/viewer (WALKTHROUGH_FOV moves to the GLB controller module). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(viewer-ui): shared ViewerControlsBar + ViewerSceneHeader for preview and embedders Preview mode's ViewerOverlay was an older copy of the community viewer UI (separate scan/guide/camera buttons, own render/theme/edges menus, 4-state wall mode). Extract the community design into shared prop-driven components: ViewerControlsBar (visibility, level/wall modes, display menu, walkthrough, orbit/top view) and ViewerSceneHeader (back, project info, optional stats slot, breadcrumb, levels card). ViewerOverlay is now a thin composition of them. The display menu gains the edges submenu everywhere; the vestigial translucent wall mode is dropped (a stale value renders as cutaway). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(walkthrough): hold-Ctrl crouch + P screenshot pause in both controllers Crouch swaps the capsule for a short one (shrinks around the centre, so a mid-jump crouch lowers the head and raises the feet — enough to thread window openings), lowers the eye with a short lerp, and slows movement; standing back up is gated on headroom via an upward raycast against the collider world. Tuning constants live in the GLB controller module and are shared with the editor first-person controller. P releases the pointer lock without leaving the walkthrough so the cursor is free for an OS screenshot (macOS region capture needs a movable pointer); clicking the canvas re-locks and resumes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(walkthrough): crouched profile fits ~1 m openings The float gap counts toward the effective obstacle height — the capsule rides floatHeight (0.5 m) above the ground, so the old crouch spanned 0.5–1.3 m and a 1.14 m opening still blocked it. Crouching now also lowers the float gap (0.25 m) and uses a shorter capsule (0.7 m), for an effective 0.25–0.95 m span; the stand-up headroom check grows to match. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(walkthrough): seamless screenshot pause — auto-release pointer lock on ⌘ Replace the P shortcut: macOS swallows the full ⇧⌘4 but the ⌘-down keystroke still reaches the page, so the moment ⌘ (or PrintScreen) goes down while locked the cursor is released without leaving the walkthrough — the native screenshot flow just works, no user education. The HUD pill flips to "Click to resume" (click-through, so the resuming click lands on the canvas) via a new walkthroughSuspended flag on the viewer store, reset on lock/exit/unmount. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(walkthrough): screenshot pause is P again, advertised in the HUD The ⌘ auto-release fired on every command combo and felt broken. Back to an explicit P toggle, now discoverable: the HUD bottom shows a "P free cursor" pill next to "Esc to exit", and while paused it flips to "Click or P to resume · Esc to exit" (click-through so the resuming click lands on the canvas). P re-locks as well as releasing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(walkthrough): freeze crouch during cursor pause; no editor hints in first person While the P pause is active, Ctrl no longer toggles crouch — ⌃⇧⌘4 (clipboard screenshot) was crouching the player mid-capture; the held state stays frozen until resume. HelperManager now renders nothing in first-person mode, so the Ctrl multi-select hint no longer pops over the walkthrough HUD (Ctrl is crouch there). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: biome formatting + pre-existing useOptionalChain fix in wall panel The wall-panel lint error predates this branch (#526); fixed here to unblock the quality gate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
de13119f86
commit
cb6fadbc28
@@ -923,7 +923,7 @@ export function GlbScene({
|
||||
})
|
||||
|
||||
// E or click activates the openable in view. The click also re-locks the
|
||||
// pointer via WalkthroughControls — harmless overlap; no selection happens.
|
||||
// pointer through the walkthrough controller; no selection happens.
|
||||
const activateWalkDoor = useCallback(() => {
|
||||
if (walkDoorRef.current) toggleOpenable(walkDoorRef.current)
|
||||
}, [toggleOpenable])
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
type Object3D,
|
||||
type PerspectiveCamera,
|
||||
Quaternion,
|
||||
Raycaster,
|
||||
Vector3,
|
||||
} from 'three'
|
||||
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js'
|
||||
@@ -25,7 +26,12 @@ import { useGLTFKTX2 } from '../../hooks/use-gltf-ktx2'
|
||||
import { SCENE_LAYER } from '../../lib/layers'
|
||||
import useViewer from '../../store/use-viewer'
|
||||
import BVHEcctrl, { type BVHEcctrlApi, type MovementInput } from './bvh-ecctrl'
|
||||
import { WALKTHROUGH_FOV } from './walkthrough-controls'
|
||||
|
||||
// First-person FOV. The orbit camera is 50° (set on the Canvas), which feels
|
||||
// cramped on foot; ~60° vertical (~90° horizontal at 16:9) restores peripheral
|
||||
// awareness without wide-angle distortion. Applied only while walking — both
|
||||
// walkthrough controllers read this and restore the orbit FOV on exit.
|
||||
export const WALKTHROUGH_FOV = 60
|
||||
|
||||
// Eye/capsule geometry mirrors the editor's first-person controller so the
|
||||
// baked walkthrough feels identical. The capsule centre sits below the eye; the
|
||||
@@ -37,6 +43,28 @@ const SPAWN_EYE_HEIGHT = 1.65
|
||||
const LOOK_SENSITIVITY = 0.002
|
||||
const VOID_FALL_RESPAWN_DEPTH = 12
|
||||
|
||||
// Crouch (hold Ctrl): swap to a short capsule — it shrinks around the centre,
|
||||
// so a crouch mid-jump also lowers the head AND raises the feet, letting the
|
||||
// player thread window openings. Standing back up is gated on headroom.
|
||||
// The float gap counts toward the effective obstacle height (the capsule rides
|
||||
// floatHeight above the ground), so crouching also lowers it: crouched span is
|
||||
// CROUCH_FLOAT_HEIGHT + capsule = 0.25 + 0.7 = 0.95 m — fits a 1 m opening.
|
||||
export const STAND_CAPSULE: [number, number, number, number] = [0.25, 0.8, 4, 8]
|
||||
export const CROUCH_CAPSULE: [number, number, number, number] = [0.25, 0.2, 4, 8]
|
||||
export const STAND_FLOAT_HEIGHT = 0.5
|
||||
export const CROUCH_FLOAT_HEIGHT = 0.25
|
||||
export const CROUCH_EYE_OFFSET = 0.1
|
||||
export const CROUCH_WALK_SPEED = 1
|
||||
export const CROUCH_RUN_SPEED = 1.4
|
||||
// Headroom (from the capsule centre, upward) required before uncrouching:
|
||||
// standing raises the centre by half the length delta plus the float delta,
|
||||
// and the standing capsule top sits standLength/2 + radius above the centre.
|
||||
export const STAND_CLEARANCE = 1.25
|
||||
export const EYE_LERP_SPEED = 12
|
||||
|
||||
const standClearanceRaycaster = new Raycaster()
|
||||
const UP = new Vector3(0, 1, 0)
|
||||
|
||||
// Kinds that must not block the player: room helpers, the spawn marker, the
|
||||
// ceiling/roof shell (you walk under them), and door/window leaves — excluding
|
||||
// the latter lets you pass any doorway whether the leaf is open or shut (the
|
||||
@@ -54,7 +82,7 @@ const keyboardMap: Array<{ name: Exclude<keyof MovementInput, 'joystick'>; keys:
|
||||
{ name: 'run', keys: ['ShiftLeft', 'ShiftRight'] },
|
||||
]
|
||||
|
||||
const cameraOffset = new Vector3(0, CAMERA_EYE_OFFSET, 0)
|
||||
const cameraOffset = new Vector3()
|
||||
const cameraEuler = new Euler(0, 0, 0, 'YXZ')
|
||||
const spawnQuat = new Quaternion()
|
||||
const spawnEuler = new Euler(0, 0, 0, 'YXZ')
|
||||
@@ -238,6 +266,10 @@ export function GlbWalkthroughController({ url }: { url: string }) {
|
||||
const controllerRef = useRef<BVHEcctrlApi | null>(null)
|
||||
const yawRef = useRef(0)
|
||||
const pitchRef = useRef(0)
|
||||
const crouchKeyRef = useRef(false)
|
||||
const suspendRef = useRef(false)
|
||||
const eyeOffsetRef = useRef(CAMERA_EYE_OFFSET)
|
||||
const [crouched, setCrouched] = useState(false)
|
||||
const [start, setStart] = useState<{ position: [number, number, number] } | null>(null)
|
||||
const [world, setWorld] = useState<GlbColliderWorld | null>(null)
|
||||
|
||||
@@ -333,20 +365,58 @@ export function GlbWalkthroughController({ url }: { url: string }) {
|
||||
if (event.code === 'Escape' && document.pointerLockElement !== canvas) {
|
||||
useViewer.getState().setWalkthroughMode(false)
|
||||
}
|
||||
// P toggles a cursor pause (advertised in the HUD): frees the pointer
|
||||
// without leaving the walkthrough — e.g. for an OS screenshot, which
|
||||
// needs a movable cursor — and click or P resumes.
|
||||
if (event.code === 'KeyP') {
|
||||
if (document.pointerLockElement === canvas) {
|
||||
suspendRef.current = true
|
||||
useViewer.getState().setWalkthroughSuspended(true)
|
||||
document.exitPointerLock()
|
||||
} else if (suspendRef.current) {
|
||||
const result = canvas.requestPointerLock?.() as Promise<void> | undefined
|
||||
if (result && typeof result.catch === 'function') result.catch(() => {})
|
||||
}
|
||||
}
|
||||
// While paused (P), crouch is frozen as-is — ⌃⇧⌘4 (clipboard screenshot)
|
||||
// must not toggle it under the user.
|
||||
if ((event.code === 'ControlLeft' || event.code === 'ControlRight') && !suspendRef.current) {
|
||||
crouchKeyRef.current = true
|
||||
}
|
||||
}
|
||||
const onKeyUp = (event: KeyboardEvent) => {
|
||||
if ((event.code === 'ControlLeft' || event.code === 'ControlRight') && !suspendRef.current) {
|
||||
crouchKeyRef.current = false
|
||||
}
|
||||
}
|
||||
const onBlur = () => {
|
||||
if (!suspendRef.current) crouchKeyRef.current = false
|
||||
}
|
||||
const onPointerLockChange = () => {
|
||||
if (document.pointerLockElement === canvas) wasLocked = true
|
||||
else if (wasLocked) useViewer.getState().setWalkthroughMode(false)
|
||||
if (document.pointerLockElement === canvas) {
|
||||
wasLocked = true
|
||||
suspendRef.current = false
|
||||
useViewer.getState().setWalkthroughSuspended(false)
|
||||
} else if (suspendRef.current) {
|
||||
// Deliberately released (screenshot pause) — stay in walkthrough.
|
||||
} else if (wasLocked) {
|
||||
useViewer.getState().setWalkthroughMode(false)
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousemove', onMouseMove)
|
||||
canvas.addEventListener('click', onClick)
|
||||
document.addEventListener('keydown', onKeyDown)
|
||||
document.addEventListener('keyup', onKeyUp)
|
||||
window.addEventListener('blur', onBlur)
|
||||
document.addEventListener('pointerlockchange', onPointerLockChange)
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', onMouseMove)
|
||||
canvas.removeEventListener('click', onClick)
|
||||
document.removeEventListener('keydown', onKeyDown)
|
||||
document.removeEventListener('keyup', onKeyUp)
|
||||
window.removeEventListener('blur', onBlur)
|
||||
document.removeEventListener('pointerlockchange', onPointerLockChange)
|
||||
useViewer.getState().setWalkthroughSuspended(false)
|
||||
if (document.pointerLockElement === canvas) document.exitPointerLock()
|
||||
}
|
||||
}, [gl])
|
||||
@@ -367,8 +437,16 @@ export function GlbWalkthroughController({ url }: { url: string }) {
|
||||
controllerRef.current = api
|
||||
}, [])
|
||||
|
||||
const hasStandingClearance = useCallback((position: Vector3) => {
|
||||
const mesh = worldRef.current?.mesh
|
||||
if (!mesh) return true
|
||||
standClearanceRaycaster.set(position, UP)
|
||||
standClearanceRaycaster.far = STAND_CLEARANCE
|
||||
return standClearanceRaycaster.intersectObject(mesh, false).length === 0
|
||||
}, [])
|
||||
|
||||
// Drive the camera from the capsule each frame + respawn if it falls into void.
|
||||
useFrame(() => {
|
||||
useFrame((_, delta) => {
|
||||
const group = controllerRef.current?.group
|
||||
if (!group) return
|
||||
|
||||
@@ -377,8 +455,18 @@ export function GlbWalkthroughController({ url }: { url: string }) {
|
||||
controllerRef.current?.resetLinVel()
|
||||
}
|
||||
|
||||
// Crouch follows the held key; standing back up waits for headroom.
|
||||
// Frozen while the cursor pause is active.
|
||||
if (!suspendRef.current && crouchKeyRef.current !== crouched) {
|
||||
if (crouchKeyRef.current) setCrouched(true)
|
||||
else if (hasStandingClearance(group.position)) setCrouched(false)
|
||||
}
|
||||
const targetEyeOffset = crouched ? CROUCH_EYE_OFFSET : CAMERA_EYE_OFFSET
|
||||
eyeOffsetRef.current +=
|
||||
(targetEyeOffset - eyeOffsetRef.current) * Math.min(1, delta * EYE_LERP_SPEED)
|
||||
|
||||
group.rotation.y = 0
|
||||
camera.position.copy(group.position).add(cameraOffset)
|
||||
camera.position.copy(group.position).add(cameraOffset.set(0, eyeOffsetRef.current, 0))
|
||||
cameraEuler.set(pitchRef.current, yawRef.current, 0, 'YXZ')
|
||||
camera.quaternion.setFromEuler(cameraEuler)
|
||||
camera.updateMatrixWorld(true)
|
||||
@@ -391,7 +479,7 @@ export function GlbWalkthroughController({ url }: { url: string }) {
|
||||
<BVHEcctrl
|
||||
acceleration={26}
|
||||
airDragFactor={0.3}
|
||||
colliderCapsuleArgs={[0.25, 0.8, 4, 8]}
|
||||
colliderCapsuleArgs={crouched ? CROUCH_CAPSULE : STAND_CAPSULE}
|
||||
colliderMeshes={[world.mesh]}
|
||||
collisionCheckIteration={3}
|
||||
collisionPushBackDamping={0.1}
|
||||
@@ -401,15 +489,15 @@ export function GlbWalkthroughController({ url }: { url: string }) {
|
||||
fallGravityFactor={4}
|
||||
floatCheckType="BOTH"
|
||||
floatDampingC={36}
|
||||
floatHeight={0.5}
|
||||
floatHeight={crouched ? CROUCH_FLOAT_HEIGHT : STAND_FLOAT_HEIGHT}
|
||||
floatPullBackHeight={0.35}
|
||||
floatSensorRadius={0.15}
|
||||
floatSpringK={1200}
|
||||
gravity={9.81}
|
||||
jumpVel={5}
|
||||
maxRunSpeed={5}
|
||||
maxRunSpeed={crouched ? CROUCH_RUN_SPEED : 5}
|
||||
maxSlope={1.2}
|
||||
maxWalkSpeed={2}
|
||||
maxWalkSpeed={crouched ? CROUCH_WALK_SPEED : 2}
|
||||
position={start.position}
|
||||
ref={setControllerApi}
|
||||
/>
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { PointerLockControls } from '@react-three/drei'
|
||||
import { useFrame, useThree } from '@react-three/fiber'
|
||||
import { useCallback, useEffect, useRef } from 'react'
|
||||
import { type PerspectiveCamera, Vector3 } from 'three'
|
||||
import useViewer from '../../store/use-viewer'
|
||||
|
||||
const MOVE_SPEED = 5
|
||||
const EYE_HEIGHT = 1.6
|
||||
|
||||
// First-person FOV. The orbit camera is 50° (set on the Canvas), which feels
|
||||
// cramped on foot; ~60° vertical (~90° horizontal at 16:9) restores peripheral
|
||||
// awareness without wide-angle distortion. Applied only while walking — both
|
||||
// walkthrough controllers read this and restore the orbit FOV on exit.
|
||||
export const WALKTHROUGH_FOV = 60
|
||||
|
||||
const _direction = new Vector3()
|
||||
const _forward = new Vector3()
|
||||
const _right = new Vector3()
|
||||
|
||||
export const WalkthroughControls = () => {
|
||||
const controlsRef = useRef<any>(null!)
|
||||
const walkthroughMode = useViewer((s: any) => s.walkthroughMode)
|
||||
const keys = useRef({ w: false, a: false, s: false, d: false })
|
||||
const camera = useThree((s) => s.camera)
|
||||
|
||||
// Set initial eye height
|
||||
useEffect(() => {
|
||||
if (walkthroughMode) {
|
||||
camera.position.y = EYE_HEIGHT
|
||||
}
|
||||
}, [walkthroughMode, camera])
|
||||
|
||||
// Widen FOV while walking; restore the orbit FOV on exit.
|
||||
useEffect(() => {
|
||||
if (!walkthroughMode) return
|
||||
const cam = camera as PerspectiveCamera
|
||||
if (!cam.isPerspectiveCamera) return
|
||||
const prevFov = cam.fov
|
||||
cam.fov = WALKTHROUGH_FOV
|
||||
cam.updateProjectionMatrix()
|
||||
return () => {
|
||||
cam.fov = prevFov
|
||||
cam.updateProjectionMatrix()
|
||||
}
|
||||
}, [walkthroughMode, camera])
|
||||
|
||||
// Keyboard handlers
|
||||
useEffect(() => {
|
||||
if (!walkthroughMode) return
|
||||
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return
|
||||
const key = e.key.toLowerCase()
|
||||
|
||||
// ESC exits walkthrough mode completely
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
useViewer.getState().setWalkthroughMode(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (key === 'w' || key === 'arrowup') keys.current.w = true
|
||||
if (key === 'a' || key === 'arrowleft') keys.current.a = true
|
||||
if (key === 's' || key === 'arrowdown') keys.current.s = true
|
||||
if (key === 'd' || key === 'arrowright') keys.current.d = true
|
||||
}
|
||||
|
||||
const onKeyUp = (e: KeyboardEvent) => {
|
||||
const key = e.key.toLowerCase()
|
||||
if (key === 'w' || key === 'arrowup') keys.current.w = false
|
||||
if (key === 'a' || key === 'arrowleft') keys.current.a = false
|
||||
if (key === 's' || key === 'arrowdown') keys.current.s = false
|
||||
if (key === 'd' || key === 'arrowright') keys.current.d = false
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', onKeyDown)
|
||||
window.addEventListener('keyup', onKeyUp)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', onKeyDown)
|
||||
window.removeEventListener('keyup', onKeyUp)
|
||||
// Reset keys on cleanup
|
||||
keys.current = { w: false, a: false, s: false, d: false }
|
||||
}
|
||||
}, [walkthroughMode])
|
||||
|
||||
// Release pointer lock when walkthrough mode is turned off
|
||||
useEffect(() => {
|
||||
if (!walkthroughMode && document.pointerLockElement) {
|
||||
document.exitPointerLock()
|
||||
}
|
||||
}, [walkthroughMode])
|
||||
|
||||
// Movement loop
|
||||
useFrame((_, delta) => {
|
||||
if (!(walkthroughMode && controlsRef.current)) return
|
||||
|
||||
_direction.set(0, 0, 0)
|
||||
|
||||
// Get camera forward and right vectors (XZ plane only)
|
||||
camera.getWorldDirection(_forward)
|
||||
_forward.y = 0
|
||||
_forward.normalize()
|
||||
|
||||
_right.crossVectors(_forward, camera.up).normalize()
|
||||
|
||||
if (keys.current.w) _direction.add(_forward)
|
||||
if (keys.current.s) _direction.sub(_forward)
|
||||
if (keys.current.d) _direction.add(_right)
|
||||
if (keys.current.a) _direction.sub(_right)
|
||||
|
||||
if (_direction.lengthSq() > 0) {
|
||||
_direction.normalize().multiplyScalar(MOVE_SPEED * delta)
|
||||
camera.position.add(_direction)
|
||||
// Keep eye height constant
|
||||
camera.position.y = EYE_HEIGHT
|
||||
}
|
||||
})
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (walkthroughMode && controlsRef.current) {
|
||||
// Feature detection: some browsers (Facebook/Instagram in-app, older Safari)
|
||||
// don't support pointer lock on the canvas element
|
||||
if (typeof controlsRef.current.lock === 'function') {
|
||||
try {
|
||||
controlsRef.current.lock()
|
||||
} catch {
|
||||
// Silently ignore — pointer lock unavailable in this browser context
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [walkthroughMode])
|
||||
|
||||
// Click to lock
|
||||
useEffect(() => {
|
||||
if (!walkthroughMode) return
|
||||
const canvas = document.querySelector('canvas')
|
||||
if (!canvas) return
|
||||
|
||||
canvas.addEventListener('click', handleClick)
|
||||
return () => canvas.removeEventListener('click', handleClick)
|
||||
}, [walkthroughMode, handleClick])
|
||||
|
||||
if (!walkthroughMode) return null
|
||||
|
||||
// Skip PointerLockControls on browsers that don't support pointer lock
|
||||
// (Facebook/Instagram in-app browsers, some iOS WebViews)
|
||||
if (typeof document !== 'undefined' && !('requestPointerLock' in HTMLElement.prototype)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <PointerLockControls ref={controlsRef} />
|
||||
}
|
||||
@@ -34,14 +34,25 @@ export {
|
||||
GlbScene,
|
||||
type GlbWalkthrough,
|
||||
} from './components/viewer/glb-scene'
|
||||
export { GlbWalkthroughController } from './components/viewer/glb-walkthrough-controller'
|
||||
export {
|
||||
CROUCH_CAPSULE,
|
||||
CROUCH_EYE_OFFSET,
|
||||
CROUCH_FLOAT_HEIGHT,
|
||||
CROUCH_RUN_SPEED,
|
||||
CROUCH_WALK_SPEED,
|
||||
EYE_LERP_SPEED,
|
||||
GlbWalkthroughController,
|
||||
STAND_CAPSULE,
|
||||
STAND_CLEARANCE,
|
||||
STAND_FLOAT_HEIGHT,
|
||||
WALKTHROUGH_FOV,
|
||||
} from './components/viewer/glb-walkthrough-controller'
|
||||
export type { HoverStyle, HoverStyles } from './components/viewer/post-processing'
|
||||
export {
|
||||
DEFAULT_HOVER_STYLES,
|
||||
SSGI_PARAMS,
|
||||
} from './components/viewer/post-processing'
|
||||
export { SceneEnvironment } from './components/viewer/scene-environment'
|
||||
export { WalkthroughControls } from './components/viewer/walkthrough-controls'
|
||||
export { useAssetUrl } from './hooks/use-asset-url'
|
||||
export { useGLTFKTX2 } from './hooks/use-gltf-ktx2'
|
||||
export { useNodeEvents } from './hooks/use-node-events'
|
||||
|
||||
@@ -153,6 +153,11 @@ type ViewerState = {
|
||||
walkthroughMode: boolean
|
||||
setWalkthroughMode: (mode: boolean) => void
|
||||
|
||||
/** Pointer lock temporarily released mid-walkthrough (⌘/PrintScreen — OS
|
||||
* screenshot needs a movable cursor); clicking the canvas re-locks. */
|
||||
walkthroughSuspended: boolean
|
||||
setWalkthroughSuspended: (suspended: boolean) => void
|
||||
|
||||
cameraDragging: boolean
|
||||
setCameraDragging: (dragging: boolean) => void
|
||||
|
||||
@@ -515,7 +520,10 @@ const useViewer = create<ViewerState>()(
|
||||
setDebugColors: (enabled) => set({ debugColors: enabled }),
|
||||
|
||||
walkthroughMode: false,
|
||||
setWalkthroughMode: (mode) => set({ walkthroughMode: mode }),
|
||||
setWalkthroughMode: (mode) => set({ walkthroughMode: mode, walkthroughSuspended: false }),
|
||||
|
||||
walkthroughSuspended: false,
|
||||
setWalkthroughSuspended: (suspended) => set({ walkthroughSuspended: suspended }),
|
||||
|
||||
cameraDragging: false,
|
||||
setCameraDragging: (dragging) => set({ cameraDragging: dragging }),
|
||||
|
||||
Reference in New Issue
Block a user