diff --git a/app/src/modules/scene/controls/selectionControls/selection3D/functions/handleAssetRotationSnap.ts b/app/src/modules/scene/controls/selectionControls/selection3D/functions/handleAssetRotationSnap.ts new file mode 100644 index 0000000..ca16720 --- /dev/null +++ b/app/src/modules/scene/controls/selectionControls/selection3D/functions/handleAssetRotationSnap.ts @@ -0,0 +1,74 @@ +import * as THREE from "three"; + +export function handleAssetRotationSnap({ + object, + pointerDeltaX, + keyEvent, + snapBaseRef, + prevRotationRef, + wasShiftHeldRef +}: { + object: THREE.Object3D; + pointerDeltaX: number; + keyEvent: string; + snapBaseRef: React.MutableRefObject; + prevRotationRef: React.MutableRefObject; + wasShiftHeldRef: React.MutableRefObject; +}): number { + const SHIFT_SPEED = 0.5; // Fine rotation speed + const NORMAL_SPEED = 5; // Normal rotation speed + const CTRL_SNAP_DEG = 15; // 15 degrees + const CTRL_SHIFT_SNAP_DEG = 5; // 5 degrees + + const isShiftHeld = keyEvent.includes("Shift"); + const isCtrlHeld = keyEvent.includes("Ctrl"); + + const speedFactor = isShiftHeld ? SHIFT_SPEED : NORMAL_SPEED; + let deltaAngle = pointerDeltaX * speedFactor; + + // Track if modifier changed + const modifierChanged = isShiftHeld !== wasShiftHeldRef.current; + if (modifierChanged) { + wasShiftHeldRef.current = isShiftHeld; + if (isCtrlHeld) snapBaseRef.current = null; + } + + if (isCtrlHeld) { + const snapDeg = isShiftHeld ? CTRL_SHIFT_SNAP_DEG : CTRL_SNAP_DEG; + const snapRad = THREE.MathUtils.degToRad(snapDeg); + + // Get current Y rotation from object's quaternion + const euler = new THREE.Euler().setFromQuaternion(object.quaternion, 'YXZ'); + let currentAngle = euler.y; + + // Initialize snap base on first frame + if (snapBaseRef.current === null) { + snapBaseRef.current = currentAngle; + prevRotationRef.current = currentAngle; + } + + // Accumulate the total rotation from the base + const totalRotation = snapBaseRef.current + deltaAngle; + + // Snap to nearest increment + const snappedAngle = Math.round(totalRotation / snapRad) * snapRad; + + // Calculate the delta needed to reach the snapped angle from current rotation + let targetDelta = snappedAngle - currentAngle; + + // Handle wrapping around 360 degrees + if (Math.abs(targetDelta) > Math.PI) { + targetDelta = targetDelta - Math.sign(targetDelta) * 2 * Math.PI; + } + + // Update snap base for next frame + snapBaseRef.current = totalRotation; + + return targetDelta; + } else { + // Reset snapping when Ctrl is not held + snapBaseRef.current = null; + prevRotationRef.current = null; + return deltaAngle; + } +} \ No newline at end of file diff --git a/app/src/modules/scene/controls/selectionControls/selection3D/rotateControls3D.tsx b/app/src/modules/scene/controls/selectionControls/selection3D/rotateControls3D.tsx index d3457e2..8ea8e64 100644 --- a/app/src/modules/scene/controls/selectionControls/selection3D/rotateControls3D.tsx +++ b/app/src/modules/scene/controls/selectionControls/selection3D/rotateControls3D.tsx @@ -9,6 +9,8 @@ import { useProductContext } from "../../../../simulation/products/productContex import { getUserData } from "../../../../../functions/getUserData"; import { useSceneContext } from "../../../sceneContext"; import { useVersionContext } from "../../../../builder/version/versionContext"; +import { detectModifierKeys } from "../../../../../utils/shortcutkeys/detectModifierKeys"; +import { handleAssetRotationSnap } from "./functions/handleAssetRotationSnap"; // import { setAssetsApi } from '../../../../../services/factoryBuilder/asset/floorAsset/setAssetsApi'; @@ -23,7 +25,6 @@ function RotateControls3D({ setDuplicatedObjects }: any) { const { camera, gl, scene, pointer, raycaster } = useThree(); - const plane = useMemo(() => new THREE.Plane(new THREE.Vector3(0, 1, 0), 0), []); const { toggleView } = useToggleView(); const { selectedAssets, setSelectedAssets } = useSelectedAssets(); @@ -38,6 +39,7 @@ function RotateControls3D({ const { selectedVersionStore } = useVersionContext(); const { selectedVersion } = selectedVersionStore(); + const [keyEvent, setKeyEvent] = useState<"Ctrl" | "Shift" | "Ctrl+Shift" | "">(""); const [initialRotations, setInitialRotations] = useState>({}); const [initialPositions, setInitialPositions] = useState>({}); const [isRotating, setIsRotating] = useState(false); @@ -45,6 +47,9 @@ function RotateControls3D({ const rotationCenter = useRef(null); const mouseButtonsDown = useRef<{ left: boolean; right: boolean }>({ left: false, right: false, }); const { contextAction, setContextAction } = useContextActionStore() + const snapBaseRef = useRef(null); + const prevRotationRef = useRef(null); + const wasShiftHeldRef = useRef(false); const updateBackend = useCallback(( productName: string, @@ -103,6 +108,8 @@ function RotateControls3D({ }; const onKeyDown = (event: KeyboardEvent) => { + const keyCombination = detectModifierKeys(event); + if (pastedObjects.length > 0 || duplicatedObjects.length > 0 || movedObjects.length > 0) return; if (event.key.toLowerCase() === "r") { @@ -110,6 +117,15 @@ function RotateControls3D({ rotateAssets(); } } + + if (keyCombination !== keyEvent) { + if (keyCombination === "Ctrl" || keyCombination === "Ctrl+Shift" || keyCombination === "Shift") { + setKeyEvent(keyCombination); + } else { + setKeyEvent(""); + } + } + if (event.key.toLowerCase() === "escape") { event.preventDefault(); resetToInitialRotations(); @@ -118,11 +134,24 @@ function RotateControls3D({ } }; + const onKeyUp = (event: KeyboardEvent) => { + const keyCombination = detectModifierKeys(event); + + if (keyCombination === "") { + setKeyEvent(""); + } else if (keyCombination === "Ctrl" || keyCombination === "Ctrl+Shift" || keyCombination === "Shift") { + setKeyEvent(keyCombination); + } + const currentPointer = new THREE.Vector2(pointer.x, pointer.y); + prevPointerPosition.current = currentPointer.clone(); + }; + if (!toggleView) { canvasElement.addEventListener("pointerdown", onPointerDown); canvasElement.addEventListener("pointermove", onPointerMove); canvasElement.addEventListener("pointerup", onPointerUp); canvasElement.addEventListener("keydown", onKeyDown); + canvasElement?.addEventListener("keyup", onKeyUp); } return () => { @@ -130,68 +159,72 @@ function RotateControls3D({ canvasElement.removeEventListener("pointermove", onPointerMove); canvasElement.removeEventListener("pointerup", onPointerUp); canvasElement.removeEventListener("keydown", onKeyDown); + canvasElement?.removeEventListener("keyup", onKeyUp); }; - }, [camera, scene, toggleView, selectedAssets, rotatedObjects, pastedObjects, duplicatedObjects, movedObjects]); + }, [camera, scene, toggleView, selectedAssets, rotatedObjects, pastedObjects, duplicatedObjects, movedObjects, keyEvent]); const resetToInitialRotations = useCallback(() => { - rotatedObjects.forEach((obj: THREE.Object3D) => { - const uuid = obj.uuid; - if (obj.userData.modelUuid) { - const initialRotation = initialRotations[uuid]; - const initialPosition = initialPositions[uuid]; + setTimeout(() => { + rotatedObjects.forEach((obj: THREE.Object3D) => { + const uuid = obj.uuid; + if (obj.userData.modelUuid) { + const initialRotation = initialRotations[uuid]; + const initialPosition = initialPositions[uuid]; - if (initialRotation && initialPosition) { - const rotationArray: [number, number, number] = [initialRotation.x, initialRotation.y, initialRotation.z,]; - const positionArray: [number, number, number] = [initialPosition.x, initialPosition.y, initialPosition.z,]; + if (initialRotation && initialPosition) { + const rotationArray: [number, number, number] = [initialRotation.x, initialRotation.y, initialRotation.z,]; + const positionArray: [number, number, number] = [initialPosition.x, initialPosition.y, initialPosition.z,]; - updateAsset(obj.userData.modelUuid, { - rotation: rotationArray, - position: positionArray, - }); + updateAsset(obj.userData.modelUuid, { + rotation: rotationArray, + position: positionArray, + }); - obj.rotation.copy(initialRotation); - obj.position.copy(initialPosition); + obj.rotation.copy(initialRotation); + obj.position.copy(initialPosition); + } } - } - }); + }); + }, 100) }, [rotatedObjects, initialRotations, initialPositions, updateAsset]); useFrame(() => { if (!isRotating || rotatedObjects.length === 0) return; - const intersectionPoint = new THREE.Vector3(); - raycaster.setFromCamera(pointer, camera); - const point = raycaster.ray.intersectPlane(plane, intersectionPoint); + const currentPointer = new THREE.Vector2(pointer.x, pointer.y); if (mouseButtonsDown.current.left || mouseButtonsDown.current.right) { - if (point) { - prevPointerPosition.current = new THREE.Vector2(point.x, point.z); - } + prevPointerPosition.current = currentPointer.clone(); return; } - if (point && prevPointerPosition.current && rotationCenter.current) { + if (prevPointerPosition.current && rotationCenter.current) { const center = rotationCenter.current; - - const currentAngle = Math.atan2(point.z - center.z, point.x - center.x); - const prevAngle = Math.atan2( - prevPointerPosition.current.y - center.z, - prevPointerPosition.current.x - center.x - ); - const angleDelta = prevAngle - currentAngle; - - const rotationMatrix = new THREE.Matrix4().makeRotationY(angleDelta); + const deltaX = currentPointer.x - prevPointerPosition.current.x; rotatedObjects.forEach((obj: THREE.Object3D) => { if (obj.userData.modelUuid) { + const angleDelta = handleAssetRotationSnap({ + object: obj, + pointerDeltaX: deltaX, + keyEvent, + snapBaseRef, + prevRotationRef, + wasShiftHeldRef + }); + const relativePosition = new THREE.Vector3().subVectors(obj.position, center); + const rotationMatrix = new THREE.Matrix4().makeRotationY(angleDelta); relativePosition.applyMatrix4(rotationMatrix); obj.position.copy(center).add(relativePosition); - obj.rotateOnWorldAxis(new THREE.Vector3(0, 1, 0), angleDelta); + + const rotationQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), angleDelta); + obj.quaternion.multiply(rotationQuat); + obj.rotation.setFromQuaternion(obj.quaternion); } }); - prevPointerPosition.current = new THREE.Vector2(point.x, point.z); + prevPointerPosition.current = currentPointer.clone(); } }); @@ -213,17 +246,13 @@ function RotateControls3D({ setInitialRotations(rotations); setInitialPositions(positions); - const intersectionPoint = new THREE.Vector3(); raycaster.setFromCamera(pointer, camera); - const point = raycaster.ray.intersectPlane(plane, intersectionPoint); - if (point) { - prevPointerPosition.current = new THREE.Vector2(point.x, point.z); - } + prevPointerPosition.current = new THREE.Vector2(pointer.x, pointer.y); setRotatedObjects(selectedAssets); setIsRotating(true); - }, [selectedAssets, camera, pointer, raycaster, plane]); + }, [selectedAssets, camera, pointer, raycaster]); const placeRotatedAssets = useCallback(() => { if (rotatedObjects.length === 0) return; diff --git a/app/src/modules/simulation/human/eventManager/useHumanEventManager.ts b/app/src/modules/simulation/human/eventManager/useHumanEventManager.ts index 8a5b634..1dec0d9 100644 --- a/app/src/modules/simulation/human/eventManager/useHumanEventManager.ts +++ b/app/src/modules/simulation/human/eventManager/useHumanEventManager.ts @@ -23,7 +23,6 @@ export function useHumanEventManager() { }, [isReset, isPlaying]); const addHumanToMonitor = (humanId: string, callback: () => void, actionUuid: string) => { - console.log('humanId: ', humanId); const human = getHumanById(humanId); const action = getActionByUuid(selectedProduct.productUuid, actionUuid); if (!human || !action || (action.actionType !== 'assembly' && action.actionType !== 'worker' && action.actionType !== 'operator') || !humanEventManagerRef.current) return;