mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-14 22:02:36 +00:00
3D: Fix broken freecam regression after refactor (#1468)
This commit is contained in:
1 parent
ab4b33a966
commit
2725281775
5 files changed
+81
-36
No files matched your search
+7
-1
@@ -184,7 +184,13 @@ export const ControlPanelSceneObject = () => {
|
||||
// - Ctrl+Z / Ctrl+Y / Ctrl+Shift+Z (capture phase): flush before the
|
||||
// engine's keymap handler runs the undo, so the pending entry is
|
||||
// the one being reverted.
|
||||
//
|
||||
// Scoped to `isShowing` so the listeners aren't attached when the
|
||||
// panel is hidden (notably during CAMERA_VIEW, where stray
|
||||
// document mouseup/keydown listeners can interact poorly with the
|
||||
// freecam pointer-capture and canvas keydown handlers).
|
||||
useEffect(() => {
|
||||
if (!isShowing) return;
|
||||
const flush = () => {
|
||||
transformSessionRef.current?.commit();
|
||||
transformSessionRef.current = null;
|
||||
@@ -204,7 +210,7 @@ export const ControlPanelSceneObject = () => {
|
||||
document.removeEventListener("mouseup", flush);
|
||||
document.removeEventListener("keydown", onKeyDown, true);
|
||||
};
|
||||
}, []);
|
||||
}, [isShowing]);
|
||||
|
||||
if (!currentSceneObject) {
|
||||
return null;
|
||||
|
||||
@@ -103,7 +103,13 @@ class Editor {
|
||||
|
||||
sceneManager: SceneManager | undefined;
|
||||
|
||||
focused: boolean = false;
|
||||
// True when the user is allowed to drive the camera with the mouse
|
||||
// (left/middle-click look-around). Flipped to false WHILE a gizmo
|
||||
// handle is being dragged, then back to true on dragend so the
|
||||
// gizmo drag doesn't accidentally pan the camera. Defaults to true
|
||||
// so camera controls are available before the user has touched the
|
||||
// gizmo for the first time.
|
||||
focused: boolean = true;
|
||||
|
||||
renderIndex: number;
|
||||
stats: Stats;
|
||||
|
||||
@@ -224,6 +224,18 @@ export class MouseControls {
|
||||
!this.isBoneDragged
|
||||
) {
|
||||
this.isMouseClicked = true;
|
||||
// Request pointer lock here (a real primary-gesture event)
|
||||
// rather than from onMouseMove. Browsers reliably honor lock
|
||||
// requests from mousedown but may reject ones from mousemove
|
||||
// — the latter started failing intermittently after the
|
||||
// useFreeCam listeners became always-attached.
|
||||
if (
|
||||
isPointerLockSupported() &&
|
||||
this.lockControls &&
|
||||
!this.lockControls.isLocked
|
||||
) {
|
||||
this.lockControls.lock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -72,6 +72,10 @@ const openAssetModal = () => {
|
||||
store.setAssetModalVisibleDuringDrag(true);
|
||||
};
|
||||
|
||||
const toggleCameraView = (editor: Editor) => {
|
||||
editor.cameraController.switchCameraView();
|
||||
};
|
||||
|
||||
const undo = async (editor: Editor) => {
|
||||
await editor.history.undo();
|
||||
};
|
||||
@@ -109,6 +113,8 @@ export const buildKeymap = (): KeyBinding[] => [
|
||||
run: focusSelected },
|
||||
{ code: "KeyB", label: "Open asset menu", group: "View",
|
||||
run: openAssetModal },
|
||||
{ code: "Space", label: "Toggle camera view", group: "View",
|
||||
run: toggleCameraView, preventDefault: true },
|
||||
{ code: "Escape", label: "Clear selection / exit pose", group: "Selection",
|
||||
run: onEscape },
|
||||
|
||||
|
||||
@@ -11,19 +11,41 @@ import {
|
||||
type FreeCamControlState,
|
||||
} from "~/pages/PageScene/engine/cameraMath";
|
||||
import { usePageSceneStore } from "~/pages/PageScene/PageSceneStore";
|
||||
import { EditorStates } from "~/pages/PageScene/enums";
|
||||
import type Editor from "~/pages/PageScene/engine/editor";
|
||||
|
||||
// Drives the 3D viewport's camera while the editor is in CAMERA_VIEW.
|
||||
// Owns:
|
||||
// - Listener attachment to the canvas (no window/document handlers).
|
||||
// - Drag state + held-key state in refs.
|
||||
// - The shared FreeCamControlState handed to the editor's render
|
||||
// loop via setFreeCamState.
|
||||
// Drives the 3D viewport's "fly" camera controls (right-click pan,
|
||||
// wheel zoom, WASD/QE forward/back/strafe/up/down, arrow-key roll/yaw)
|
||||
// for navigating the scene in EDIT mode. CAMERA_VIEW (the virtual-
|
||||
// camera POV preview) re-uses the same input pipeline.
|
||||
//
|
||||
// The editor reads this state on every frame (see editor.ts render
|
||||
// loop) and integrates it via freeCamFrameTick. There is no FreeCam
|
||||
// class instance — the math lives in cameraMath.ts.
|
||||
// Pointer + wheel listeners go on the canvas (events naturally route
|
||||
// to canvas regardless of focus). WASD/QE/arrow keys go on document
|
||||
// with an editable-element guard, so the user can fly without
|
||||
// clicking the canvas first to give it focus.
|
||||
|
||||
const EDITABLE_INPUT_TYPES = new Set([
|
||||
"text",
|
||||
"search",
|
||||
"email",
|
||||
"password",
|
||||
"number",
|
||||
"url",
|
||||
"tel",
|
||||
]);
|
||||
|
||||
const isEventFromEditableElement = (event: KeyboardEvent): boolean => {
|
||||
const target = event.target;
|
||||
if (!(target instanceof HTMLElement)) return false;
|
||||
if (target instanceof HTMLInputElement) {
|
||||
if (target.disabled || target.readOnly) return false;
|
||||
const type = target.type?.toLowerCase() ?? "";
|
||||
return type === "" || EDITABLE_INPUT_TYPES.has(type);
|
||||
}
|
||||
if (target instanceof HTMLTextAreaElement) {
|
||||
return !(target.disabled || target.readOnly);
|
||||
}
|
||||
return target.isContentEditable;
|
||||
};
|
||||
|
||||
export const useFreeCam = (
|
||||
canvas: HTMLCanvasElement | null,
|
||||
@@ -33,9 +55,6 @@ export const useFreeCam = (
|
||||
const dragRef = useRef<{ x: number; y: number; pointerId: number } | null>(
|
||||
null,
|
||||
);
|
||||
const enabled = usePageSceneStore(
|
||||
(s) => s.editorState === EditorStates.CAMERA_VIEW,
|
||||
);
|
||||
|
||||
// Hand the state to the editor so its render loop can integrate it.
|
||||
useEffect(() => {
|
||||
@@ -44,27 +63,17 @@ export const useFreeCam = (
|
||||
return () => editor.cameraController.setFreeCamState(null);
|
||||
}, [editor]);
|
||||
|
||||
// Toggle enabled and clear motion when leaving CAMERA_VIEW so the
|
||||
// camera doesn't drift on a stale held key.
|
||||
// Attach listeners. Pointer/wheel on canvas (so they only fire
|
||||
// inside the viewport); keys on document so the user doesn't need
|
||||
// to click canvas first for WASD to work.
|
||||
useEffect(() => {
|
||||
if (!canvas || !editor) return;
|
||||
const state = stateRef.current;
|
||||
state.enabled = enabled;
|
||||
if (!enabled) {
|
||||
state.moveKeys = emptyMoveKeys();
|
||||
state.rotateKeys = emptyRotateKeys();
|
||||
state.velocity.set(0, 0, 0);
|
||||
dragRef.current = null;
|
||||
}
|
||||
}, [enabled]);
|
||||
|
||||
// Attach listeners to the canvas. Canvas-scoped means no listener
|
||||
// leaks beyond the viewport, and no need for mouseover gating.
|
||||
useEffect(() => {
|
||||
if (!canvas || !enabled || !editor) return;
|
||||
const state = stateRef.current;
|
||||
state.enabled = true;
|
||||
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (usePageSceneStore.getState().isPromptBoxFocused) return;
|
||||
if (isEventFromEditableElement(e)) return;
|
||||
const moveSlot = moveSlotForKeyCode(e.code);
|
||||
if (moveSlot) state.moveKeys[moveSlot] = 1;
|
||||
const rotateSlot = rotateSlotForKeyCode(e.code);
|
||||
@@ -73,6 +82,7 @@ export const useFreeCam = (
|
||||
|
||||
const onKeyUp = (e: KeyboardEvent) => {
|
||||
if (usePageSceneStore.getState().isPromptBoxFocused) return;
|
||||
if (isEventFromEditableElement(e)) return;
|
||||
const moveSlot = moveSlotForKeyCode(e.code);
|
||||
if (moveSlot) state.moveKeys[moveSlot] = 0;
|
||||
const rotateSlot = rotateSlotForKeyCode(e.code);
|
||||
@@ -128,22 +138,27 @@ export const useFreeCam = (
|
||||
|
||||
const onContextMenu = (e: Event) => e.preventDefault();
|
||||
|
||||
canvas.addEventListener("keydown", onKeyDown);
|
||||
canvas.addEventListener("keyup", onKeyUp);
|
||||
canvas.addEventListener("pointerdown", onPointerDown);
|
||||
canvas.addEventListener("pointerup", onPointerUp);
|
||||
canvas.addEventListener("pointermove", onPointerMove);
|
||||
canvas.addEventListener("wheel", onWheel, { passive: true });
|
||||
canvas.addEventListener("contextmenu", onContextMenu);
|
||||
document.addEventListener("keydown", onKeyDown);
|
||||
document.addEventListener("keyup", onKeyUp);
|
||||
|
||||
return () => {
|
||||
canvas.removeEventListener("keydown", onKeyDown);
|
||||
canvas.removeEventListener("keyup", onKeyUp);
|
||||
state.enabled = false;
|
||||
state.moveKeys = emptyMoveKeys();
|
||||
state.rotateKeys = emptyRotateKeys();
|
||||
state.velocity.set(0, 0, 0);
|
||||
dragRef.current = null;
|
||||
canvas.removeEventListener("pointerdown", onPointerDown);
|
||||
canvas.removeEventListener("pointerup", onPointerUp);
|
||||
canvas.removeEventListener("pointermove", onPointerMove);
|
||||
canvas.removeEventListener("wheel", onWheel);
|
||||
canvas.removeEventListener("contextmenu", onContextMenu);
|
||||
document.removeEventListener("keydown", onKeyDown);
|
||||
document.removeEventListener("keyup", onKeyUp);
|
||||
};
|
||||
}, [canvas, editor, enabled]);
|
||||
}, [canvas, editor]);
|
||||
};
|
||||
Reference in new issue
Block a user