diff --git a/frontend/libs/components/pagescene/src/lib/engine/editor.ts b/frontend/libs/components/pagescene/src/lib/engine/editor.ts index 99fa787757..5b8c1d1b8f 100644 --- a/frontend/libs/components/pagescene/src/lib/engine/editor.ts +++ b/frontend/libs/components/pagescene/src/lib/engine/editor.ts @@ -425,6 +425,25 @@ class Editor { this.cameraController.camera, this.renderer.domElement, ); + // Restore the quaternion captured at mouseup. Chromium synthesizes + // one final `mousemove` while exiting pointer lock so it can + // reposition the cursor; PLC reads that synthesized delta and + // rotates the camera by it because `isLocked` is still true at + // that moment. By the time the `unlock` event fires here, that + // synthetic move has already been applied — overwriting it with + // the pre-unlock snapshot keeps the camera at the orientation the + // user actually released on. + this.cameraController.lockControls.addEventListener("unlock", () => { + const snap = this.mouse_controls?.quatAtUnlockRequest; + const cam = this.cameraController.camera; + if (snap && cam) { + cam.quaternion.copy(snap); + cam.updateMatrixWorld(); + } + if (this.mouse_controls) { + this.mouse_controls.quatAtUnlockRequest = null; + } + }); } // FreeCam math + listeners now live in hooks/useFreeCam.ts; the // editor reads `cameraController.freeCamState` (set by that hook) on diff --git a/frontend/libs/components/pagescene/src/lib/engine/keybinds_controls.ts b/frontend/libs/components/pagescene/src/lib/engine/keybinds_controls.ts index 9fd42d73d3..afc00988ed 100644 --- a/frontend/libs/components/pagescene/src/lib/engine/keybinds_controls.ts +++ b/frontend/libs/components/pagescene/src/lib/engine/keybinds_controls.ts @@ -130,6 +130,14 @@ export class MouseControls { private isBoneDragged: boolean = false; private ignoreNextClick: boolean = false; + // Snapshot of the camera quaternion taken in onMouseUp right before + // lockControls.unlock(). The unlock listener in editor.ts restores + // this value to neutralize the synthetic mousemove Chromium fires + // while pointer-lock is releasing — that synthetic move would + // otherwise be processed by PointerLockControls and lurch the camera + // back by the cursor's "trip home" delta, producing a visible snap. + quatAtUnlockRequest: THREE.Quaternion | null = null; + // Deps held for read callbacks + bus emissions throughout the class. // Constructor still copies primitive fields onto `this.x` for the // existing internals; new code reads through `this.deps` directly. @@ -273,6 +281,13 @@ export class MouseControls { onMouseUp(event: any) { if (event.button === 0 || event.button === 1) { + // Snapshot the quaternion the user actually ended on. The unlock + // listener in editor.ts will restore this once Chromium's + // synthetic-mousemove window has closed, so the camera doesn't + // lurch by the cursor's trip-home delta. + if (this.camera) { + this.quatAtUnlockRequest = this.camera.quaternion.clone(); + } this.lockControls?.unlock(); this.isMouseClicked = false; }