diff --git a/frontend/apps/artcraft/app/src/pages/PageScene/engine/browserChecks.ts b/frontend/apps/artcraft/app/src/pages/PageScene/engine/browserChecks.ts new file mode 100644 index 0000000000..7200bde580 --- /dev/null +++ b/frontend/apps/artcraft/app/src/pages/PageScene/engine/browserChecks.ts @@ -0,0 +1,20 @@ +// Browser feature detection helpers used by the input subsystem. +// Pointer Lock behaves differently on Safari/WebKit, so the input +// hooks fall back to drag-based control there. + +export const isSafariOrWebKit = (): boolean => { + const ua = navigator.userAgent; + const isWebKit = + /AppleWebKit/i.test(ua) && !/Chrome|Chromium|Edg|OPR|CriOS|FxiOS/i.test(ua); + const isSafari = /^((?!chrome|android|crios|fxios).)*safari/i.test(ua); + return isSafari || isWebKit; +}; + +export const hasApplePay = (): boolean => { + // @ts-expect-error Apple Pay is not defined in TypeScript + return !!window.ApplePaySession; +}; + +export const isSafari = (): boolean => isSafariOrWebKit() || hasApplePay(); + +export const isPointerLockSupported = (): boolean => !isSafari(); diff --git a/frontend/apps/artcraft/app/src/pages/PageScene/engine/cameraMath.ts b/frontend/apps/artcraft/app/src/pages/PageScene/engine/cameraMath.ts new file mode 100644 index 0000000000..3105473bda --- /dev/null +++ b/frontend/apps/artcraft/app/src/pages/PageScene/engine/cameraMath.ts @@ -0,0 +1,109 @@ +import { MathUtils, Vector3 } from "three"; +import type { PerspectiveCamera } from "three"; + +// Pure math used by the free-cam viewport controls. +// No DOM access, no event listeners, no signals — every function +// takes inputs and returns values. The input hooks own the listener +// lifecycle and the per-frame loop; this module owns the formulas. + +// Held movement keys (WASD + QE). Stored as 0/1 so update steps can +// just sum them in opposite directions. +export interface HeldMoveKeys { + forward: 0 | 1; + back: 0 | 1; + left: 0 | 1; + right: 0 | 1; + up: 0 | 1; + down: 0 | 1; +} + +// Held rotation keys (arrow keys + roll). Same encoding. +export interface HeldRotateKeys { + pitchUp: 0 | 1; + pitchDown: 0 | 1; + yawLeft: 0 | 1; + yawRight: 0 | 1; + rollLeft: 0 | 1; + rollRight: 0 | 1; +} + +export const emptyMoveKeys = (): HeldMoveKeys => ({ + forward: 0, + back: 0, + left: 0, + right: 0, + up: 0, + down: 0, +}); + +export const emptyRotateKeys = (): HeldRotateKeys => ({ + pitchUp: 0, + pitchDown: 0, + yawLeft: 0, + yawRight: 0, + rollLeft: 0, + rollRight: 0, +}); + +// Per-frame movement direction (in camera-local space). Multiply by +// (delta * movementSpeed) before applying. +export const moveVectorFromKeys = ( + keys: HeldMoveKeys, + autoForward = false, +): Vector3 => { + const forward = keys.forward || (autoForward && !keys.back) ? 1 : 0; + return new Vector3( + -keys.left + keys.right, + -keys.down + keys.up, + -forward + keys.back, + ); +}; + +// Per-frame rotation deltas. Already pre-scaled by rollSpeed. +export const rotationVectorFromKeys = ( + keys: HeldRotateKeys, + rollSpeed: number, +): Vector3 => + new Vector3( + -keys.pitchDown + keys.pitchUp, + -keys.yawRight + keys.yawLeft, + -keys.rollRight + keys.rollLeft, + ).multiplyScalar(rollSpeed); + +// Mouse-drag pan amounts (in camera-local x/y, before applying). +// dx/dy are pointer deltas in pixels; speed scales them. +export const panFromDrag = ( + dx: number, + dy: number, + speed: number, +): { x: number; y: number } => ({ + x: -dx * (speed * 0.01), + y: dy * (speed * 0.01), +}); + +// Wheel-event zoom amount along camera-local z. +export const zoomFromWheel = (deltaY: number): number => deltaY / 120; + +// World-space lookAt point one unit ahead of the camera along its +// current orientation. Used to update camera entries in the store +// after a free-cam move so the camera entry's "lookAt" stays in +// sync with the rendered orientation. +export const lookAtFromCamera = (camera: PerspectiveCamera): Vector3 => { + const lookAt = new Vector3(0, 0, -1); + lookAt.applyQuaternion(camera.quaternion); + lookAt.add(camera.position); + return lookAt; +}; + +// Smooth a velocity vector toward a target with a per-component lerp. +// Returns the same `velocity` instance for chaining. +export const lerpVelocity = ( + velocity: Vector3, + target: Vector3, + smoothing: number, +): Vector3 => { + velocity.x = MathUtils.lerp(velocity.x, target.x, smoothing); + velocity.y = MathUtils.lerp(velocity.y, target.y, smoothing); + velocity.z = MathUtils.lerp(velocity.z, target.z, smoothing); + return velocity; +}; diff --git a/frontend/apps/artcraft/app/src/pages/PageScene/engine/free_cam.ts b/frontend/apps/artcraft/app/src/pages/PageScene/engine/free_cam.ts index f2138b8a41..ebfb830b98 100644 --- a/frontend/apps/artcraft/app/src/pages/PageScene/engine/free_cam.ts +++ b/frontend/apps/artcraft/app/src/pages/PageScene/engine/free_cam.ts @@ -9,28 +9,15 @@ import * as THREE from "three"; import { cameras, selectedCameraId } from "~/pages/PageScene/signals/camera"; import { isPromptBoxFocused } from "~/pages/PageScene/signals/promptBox"; - -export const isSafariOrWebKit = () => { - const ua = navigator.userAgent; - - const isWebKit = /AppleWebKit/i.test(ua) && !/Chrome|Chromium|Edg|OPR|CriOS|FxiOS/i.test(ua); - const isSafari = /^((?!chrome|android|crios|fxios).)*safari/i.test(ua); - - return isSafari || isWebKit; -}; - -export const hasApplePay = () => { - // @ts-expect-error Apple Pay is not defined in TypeScript - return !!window.ApplePaySession; -} - -export const isSafari = () => { - return isSafariOrWebKit() || hasApplePay(); -} - -export const isPointerLockSupported = () => { - return !isSafari(); -} +// Re-exported here for back-compat with keybinds_controls.ts; the +// definitions live in browserChecks.ts and will be imported directly +// once Phase 6 deletes this file. +export { + isSafariOrWebKit, + hasApplePay, + isSafari, + isPointerLockSupported, +} from "./browserChecks"; class FreeCam extends EventDispatcher { object: THREE.PerspectiveCamera;