feat: Refactor camera controls and view switching; enhance event handling for improved user experience and state management

This commit is contained in:
2025-07-31 13:38:50 +05:30
parent 3cfde617cc
commit 5b62f54769
5 changed files with 135 additions and 175 deletions

View File

@@ -9,124 +9,89 @@ import { detectModifierKeys } from "../../../utils/shortcutkeys/detectModifierKe
import { firstPersonCamera } from "./firstPersonCamera"; import { firstPersonCamera } from "./firstPersonCamera";
const CamMode: React.FC = () => { const CamMode: React.FC = () => {
const { camMode, setCamMode } = useCamMode(); const { camMode, setCamMode } = useCamMode();
const [, get] = useKeyboardControls(); const [_, get] = useKeyboardControls();
const [isTransitioning, setIsTransitioning] = useState(false); const [isTransitioning, setIsTransitioning] = useState(false);
const state: any = useThree(); const state: any = useThree();
const { toggleView } = useToggleView(); const { toggleView } = useToggleView();
const [isShiftActive, setIsShiftActive] = useState(false); const [isShiftActive, setIsShiftActive] = useState(false);
useEffect(() => { useEffect(() => {
const handlePointerLockChange = async () => { const handlePointerLockChange = async () => {
if (document.pointerLockElement && !toggleView) { if (document.pointerLockElement && !toggleView) {
// Pointer is locked } else if (camMode === "FirstPerson" && !toggleView) {
} else if (camMode === "FirstPerson" && !toggleView) { setCamMode("ThirdPerson");
// Pointer is unlocked await switchToThirdPerson(state.controls, state.camera);
setCamMode("ThirdPerson"); }
await switchToThirdPerson(state.controls, state.camera); };
}
};
document.addEventListener("pointerlockchange", handlePointerLockChange); document.addEventListener("pointerlockchange", handlePointerLockChange);
return () => { return () => {
document.removeEventListener( document.removeEventListener("pointerlockchange", handlePointerLockChange);
"pointerlockchange", };
handlePointerLockChange }, [camMode, toggleView, setCamMode, state.controls, state.camera]);
);
};
}, [camMode, toggleView, setCamMode, state.controls, state.camera]);
useEffect(() => { useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => { const handleKeyPress = async (event: KeyboardEvent) => {
if (event.key === "Shift") { if (!state.controls) return;
setIsShiftActive(true);
}
};
const handleKeyUp = (event: KeyboardEvent) => { const keyCombination = detectModifierKeys(event);
if (event.key === "Shift") {
setIsShiftActive(false);
}
};
window.addEventListener("keydown", handleKeyDown); if (keyCombination === "/" && !isTransitioning && !toggleView) {
window.addEventListener("keyup", handleKeyUp); firstPersonCamera({
setIsTransitioning,
state,
camMode,
setCamMode,
switchToFirstPerson,
switchToThirdPerson,
});
}
return () => { if (keyCombination === 'Shift') {
window.removeEventListener("keydown", handleKeyDown); setIsShiftActive(true);
window.removeEventListener("keyup", handleKeyUp); }
}; };
}, []);
useEffect(() => { const handleKeyUp = (event: KeyboardEvent) => {
const handleKeyPress = async (event: KeyboardEvent) => { if (event.key === "Shift") {
if (!state.controls) return; setIsShiftActive(false);
}
};
const keyCombination = detectModifierKeys(event); window.addEventListener("keydown", handleKeyPress);
window.addEventListener("keyup", handleKeyUp);
if (keyCombination === "/" && !isTransitioning && !toggleView) { return () => {
firstPersonCamera({ window.removeEventListener("keydown", handleKeyPress);
setIsTransitioning, window.removeEventListener("keyup", handleKeyUp);
state, };
camMode, // eslint-disable-next-line react-hooks/exhaustive-deps
setCamMode, }, [camMode, isTransitioning, toggleView, state.controls, state.camera, setCamMode]);
switchToFirstPerson,
switchToThirdPerson,
});
}
};
window.addEventListener("keydown", handleKeyPress); useFrame(() => {
return () => { const { forward, backward, left, right } = get();
window.removeEventListener("keydown", handleKeyPress); if (!state.controls) return;
}; if (camMode === "ThirdPerson" || !document.pointerLockElement) return;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
camMode,
isTransitioning,
toggleView,
state.controls,
state.camera,
setCamMode,
]);
useFrame(() => { const speedMultiplier = isShiftActive ? 4 : 1;
const { forward, backward, left, right } = get();
if (!state.controls) return;
if (camMode === "ThirdPerson" || !document.pointerLockElement) return;
const speedMultiplier = isShiftActive ? 4 : 1; if (forward) {
state.controls.forward(CONSTANTS.firstPersonControls.forwardSpeed * speedMultiplier, true);
}
if (backward) {
state.controls.forward(CONSTANTS.firstPersonControls.backwardSpeed * speedMultiplier, true);
}
if (left) {
state.controls.truck(CONSTANTS.firstPersonControls.leftSpeed * speedMultiplier, 0, true);
}
if (right) {
state.controls.truck(CONSTANTS.firstPersonControls.rightSpeed * speedMultiplier, 0, true);
}
});
if (forward) { return null;
state.controls.forward(
CONSTANTS.firstPersonControls.forwardSpeed * speedMultiplier,
true
);
}
if (backward) {
state.controls.forward(
CONSTANTS.firstPersonControls.backwardSpeed * speedMultiplier,
true
);
}
if (left) {
state.controls.truck(
CONSTANTS.firstPersonControls.leftSpeed * speedMultiplier,
0,
true
);
}
if (right) {
state.controls.truck(
CONSTANTS.firstPersonControls.rightSpeed * speedMultiplier,
0,
true
);
}
});
return null; // This component does not render any UI
}; };
export default CamMode; export default CamMode;

View File

@@ -1,73 +1,68 @@
import * as THREE from "three"; import { useEffect } from "react";
import { useEffect, useRef } from "react";
import { useToggleView } from "../../../store/builder/store";
import { useThree } from "@react-three/fiber"; import { useThree } from "@react-three/fiber";
import { getCamera } from "../../../services/factoryBuilder/camera/getCameraApi"; import * as THREE from 'three';
import * as CONSTANTS from '../../../types/world/worldConstants'; import { PerspectiveCamera, OrthographicCamera, CameraControls } from '@react-three/drei';
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import * as CONSTANTS from '../../../types/world/worldConstants';
import { getCamera } from "../../../services/factoryBuilder/camera/getCameraApi";
import { getUserData } from "../../../functions/getUserData"; import { getUserData } from "../../../functions/getUserData";
import { CameraControls } from "@react-three/drei"; import { useToggleView } from "../../../store/builder/store";
export default function SwitchView() { export default function SwitchView() {
const { toggleView } = useToggleView(); const { toggleView } = useToggleView();
const state: any = useThree(); const { controls } = useThree();
const { set } = useThree(); const { projectId } = useParams();
const perspectiveCamera = useRef<THREE.PerspectiveCamera | null>(null); const { organization } = getUserData();
const orthoCamera = useRef<THREE.OrthographicCamera | null>(null);
orthoCamera.current = new THREE.OrthographicCamera(-window.innerWidth / 2, window.innerWidth / 2, window.innerHeight / 2, -window.innerHeight / 2, 0.01, 1000);
perspectiveCamera.current = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.01, 1000);
const { projectId } = useParams();
const { organization } = getUserData();
useEffect(() => { useEffect(() => {
if (!perspectiveCamera.current || !orthoCamera.current) return; if (toggleView && controls) {
if (toggleView) { (controls as any).mouseButtons.left = CONSTANTS.twoDimension.leftMouse;
orthoCamera.current.zoom = 10; (controls as any).mouseButtons.right = CONSTANTS.twoDimension.rightMouse;
orthoCamera.current.position.set(...CONSTANTS.twoDimension.defaultPosition); } else {
orthoCamera.current.lookAt(new THREE.Vector3(...CONSTANTS.twoDimension.defaultTarget)); try {
orthoCamera.current.updateProjectionMatrix(); getCamera(organization, localStorage.getItem('userId')!, projectId).then((data) => {
set({ camera: orthoCamera.current }); if (data && data.position && data.target) {
orthoCamera.current.updateProjectionMatrix(); (controls as CameraControls)?.setPosition(data.position.x, data.position.y, data.position.z);
} else if (!toggleView) { (controls as CameraControls)?.setTarget(data.target.x, data.target.y, data.target.z);
perspectiveCamera.current.position.set(...CONSTANTS.threeDimension.defaultPosition); } else {
perspectiveCamera.current.lookAt(new THREE.Vector3(...CONSTANTS.threeDimension.defaultTarget)); (controls as CameraControls)?.setPosition(...CONSTANTS.threeDimension.defaultPosition);
set({ camera: perspectiveCamera.current }); (controls as CameraControls)?.setTarget(...CONSTANTS.threeDimension.defaultTarget);
} }
}, [toggleView, set]); });
} catch (error) {
echo.error("Failed to retrieve camera position or target");
(controls as CameraControls)?.setPosition(...CONSTANTS.threeDimension.defaultPosition);
(controls as CameraControls)?.setTarget(...CONSTANTS.threeDimension.defaultTarget);
}
useEffect(() => { if (controls) {
if (toggleView && state.controls) { (controls as any).mouseButtons.left = CONSTANTS.threeDimension.leftMouse;
state.controls.mouseButtons.left = CONSTANTS.twoDimension.leftMouse; (controls as any).mouseButtons.right = CONSTANTS.threeDimension.rightMouse;
state.controls.mouseButtons.right = CONSTANTS.twoDimension.rightMouse; }
} else { }
try { }, [toggleView, controls]);
getCamera(organization, localStorage.getItem('userId')!, projectId).then((data) => {
if (data && data.position && data.target) {
// state.controls?.setLookAt(data.position.x, data.position.y, data.position.z, data.target.x, data.target.y, data.target.z, true)
state.controls?.setPosition(data.position.x, data.position.y, data.position.z);
state.controls?.setTarget(data.target.x, data.target.y, data.target.z);
} else {
// state.controls?.setLookAt(...CONSTANTS.threeDimension.defaultPosition, ...CONSTANTS.threeDimension.defaultTarget, true);
state.controls?.setPosition(...CONSTANTS.threeDimension.defaultPosition);
state.controls?.setTarget(...CONSTANTS.threeDimension.defaultTarget);
}
});
} catch (error) {
echo.error("Failed to retrieve camera position or target");
console.error("Failed to retrieve camera position or target:", error);
// state.controls?.setLookAt(...CONSTANTS.threeDimension.defaultPosition, ...CONSTANTS.threeDimension.defaultTarget, true);
state.controls?.setPosition(...CONSTANTS.threeDimension.defaultPosition);
state.controls?.setTarget(...CONSTANTS.threeDimension.defaultTarget);
}
if (state.controls) { return (
state.controls.mouseButtons.left = CONSTANTS.threeDimension.leftMouse; <>
state.controls.mouseButtons.right = CONSTANTS.threeDimension.rightMouse; {toggleView ? (
} <OrthographicCamera
} makeDefault
}, [toggleView, state.controls]); position={CONSTANTS.twoDimension.defaultPosition}
zoom={10}
return ( near={0.01}
<></> far={1000}
); onUpdate={(self) => self.lookAt(new THREE.Vector3(...CONSTANTS.twoDimension.defaultTarget))}
/>
) : (
<PerspectiveCamera
makeDefault
fov={75}
position={CONSTANTS.threeDimension.defaultPosition}
near={0.01}
far={1000}
onUpdate={(self) => self.lookAt(new THREE.Vector3(...CONSTANTS.threeDimension.defaultTarget))}
/>
)}
</>
);
} }

View File

@@ -13,16 +13,17 @@ import SelectionControls3D from "./selectionControls/selection3D/selectionContro
import TransformControl from "./transformControls/transformControls"; import TransformControl from "./transformControls/transformControls";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { getUserData } from "../../../functions/getUserData"; import { getUserData } from "../../../functions/getUserData";
import SelectionControls2D from "./selectionControls/selection2D/selectionControls2D"; import SelectionControls2D from "./selectionControls/selection2D/selectionControls2D";
import UndoRedo2DControls from "./undoRedoControls/undoRedo2D/undoRedo2DControls"; import UndoRedo2DControls from "./undoRedoControls/undoRedo2D/undoRedo2DControls";
export default function Controls() { export default function Controls() {
const controlsRef = useRef<CameraControls>(null); const controlsRef = useRef<CameraControls>(null);
const state = useThree();
const { toggleView } = useToggleView(); const { toggleView } = useToggleView();
const { resetCamera, setResetCamera } = useResetCamera(); const { resetCamera, setResetCamera } = useResetCamera();
const { socket } = useSocketStore(); const { socket } = useSocketStore();
const state = useThree();
const { projectId } = useParams(); const { projectId } = useParams();
const { userId, organization } = getUserData(); const { userId, organization } = getUserData();
@@ -33,7 +34,6 @@ export default function Controls() {
} }
getCamera(organization, userId, projectId).then((data) => { getCamera(organization, userId, projectId).then((data) => {
// console.log('data: ', data);
if (data && data.position && data.target) { if (data && data.position && data.target) {
controlsRef.current?.setPosition(data.position.x, data.position.y, data.position.z); controlsRef.current?.setPosition(data.position.x, data.position.y, data.position.z);
controlsRef.current?.setTarget(data.target.x, data.target.y, data.target.z); controlsRef.current?.setTarget(data.target.x, data.target.y, data.target.z);
@@ -41,8 +41,7 @@ export default function Controls() {
controlsRef.current?.setPosition(...CONSTANTS.threeDimension.defaultPosition); controlsRef.current?.setPosition(...CONSTANTS.threeDimension.defaultPosition);
controlsRef.current?.setTarget(...CONSTANTS.threeDimension.defaultTarget); controlsRef.current?.setTarget(...CONSTANTS.threeDimension.defaultTarget);
} }
}) }).catch((error) => console.error("Failed to fetch camera data:", error));
.catch((error) => console.error("Failed to fetch camera data:", error));
}, []); }, []);
useEffect(() => { useEffect(() => {
@@ -60,7 +59,6 @@ export default function Controls() {
socketId: socket.id, socketId: socket.id,
projectId projectId
}; };
// console.log('CameracamData: ', camData);
socket.emit('v1:Camera:set', camData) socket.emit('v1:Camera:set', camData)
setResetCamera(false); setResetCamera(false);
@@ -130,7 +128,7 @@ export default function Controls() {
camera={state.camera} camera={state.camera}
verticalDragToForward={true} verticalDragToForward={true}
boundaryEnclosesCamera={true} boundaryEnclosesCamera={true}
dollyToCursor={toggleView} dollyDragInverted
> >
<SwitchView /> <SwitchView />

View File

@@ -333,6 +333,7 @@ const SelectionControls3D: React.FC = () => {
return ( return (
<> <>
<MoveControls3D movedObjects={movedObjects} setMovedObjects={setMovedObjects} pastedObjects={pastedObjects} setpastedObjects={setpastedObjects} duplicatedObjects={duplicatedObjects} setDuplicatedObjects={setDuplicatedObjects} rotatedObjects={rotatedObjects} setRotatedObjects={setRotatedObjects} boundingBoxRef={boundingBoxRef} /> <MoveControls3D movedObjects={movedObjects} setMovedObjects={setMovedObjects} pastedObjects={pastedObjects} setpastedObjects={setpastedObjects} duplicatedObjects={duplicatedObjects} setDuplicatedObjects={setDuplicatedObjects} rotatedObjects={rotatedObjects} setRotatedObjects={setRotatedObjects} boundingBoxRef={boundingBoxRef} />
<RotateControls3D rotatedObjects={rotatedObjects} setRotatedObjects={setRotatedObjects} movedObjects={movedObjects} setMovedObjects={setMovedObjects} pastedObjects={pastedObjects} setpastedObjects={setpastedObjects} duplicatedObjects={duplicatedObjects} setDuplicatedObjects={setDuplicatedObjects} /> <RotateControls3D rotatedObjects={rotatedObjects} setRotatedObjects={setRotatedObjects} movedObjects={movedObjects} setMovedObjects={setMovedObjects} pastedObjects={pastedObjects} setpastedObjects={setpastedObjects} duplicatedObjects={duplicatedObjects} setDuplicatedObjects={setDuplicatedObjects} />
@@ -340,6 +341,7 @@ const SelectionControls3D: React.FC = () => {
<DuplicationControls3D duplicatedObjects={duplicatedObjects} setDuplicatedObjects={setDuplicatedObjects} setpastedObjects={setpastedObjects} movedObjects={movedObjects} setMovedObjects={setMovedObjects} rotatedObjects={rotatedObjects} setRotatedObjects={setRotatedObjects} boundingBoxRef={boundingBoxRef} /> <DuplicationControls3D duplicatedObjects={duplicatedObjects} setDuplicatedObjects={setDuplicatedObjects} setpastedObjects={setpastedObjects} movedObjects={movedObjects} setMovedObjects={setMovedObjects} rotatedObjects={rotatedObjects} setRotatedObjects={setRotatedObjects} boundingBoxRef={boundingBoxRef} />
<CopyPasteControls3D copiedObjects={copiedObjects} setCopiedObjects={setCopiedObjects} pastedObjects={pastedObjects} setpastedObjects={setpastedObjects} setDuplicatedObjects={setDuplicatedObjects} movedObjects={movedObjects} setMovedObjects={setMovedObjects} rotatedObjects={rotatedObjects} setRotatedObjects={setRotatedObjects} boundingBoxRef={boundingBoxRef} /> <CopyPasteControls3D copiedObjects={copiedObjects} setCopiedObjects={setCopiedObjects} pastedObjects={pastedObjects} setpastedObjects={setpastedObjects} setDuplicatedObjects={setDuplicatedObjects} movedObjects={movedObjects} setMovedObjects={setMovedObjects} rotatedObjects={rotatedObjects} setRotatedObjects={setRotatedObjects} boundingBoxRef={boundingBoxRef} />
</> </>
); );
}; };

View File

@@ -206,7 +206,7 @@ export const thirdPersonControls: ThirdPersonControls = {
polarRotateSpeed: 1, // Speed of rotation around the polar axis polarRotateSpeed: 1, // Speed of rotation around the polar axis
truckSpeed: 2, // Speed of truck movement truckSpeed: 2, // Speed of truck movement
maxDistance: 100, // Maximum distance from the target maxDistance: 100, // Maximum distance from the target
maxPolarAngle: Math.PI / 2 - 0.05, // Maximum polar angle maxPolarAngle: Math.PI / 2, // Maximum polar angle
minZoom: 6, // Minimum zoom level minZoom: 6, // Minimum zoom level
maxZoom: 100, // Maximum zoom level maxZoom: 100, // Maximum zoom level
targetOffset: 20, // Offset of the target from the camera targetOffset: 20, // Offset of the target from the camera