From 75f77c4204f4c2a382ad131bf1f94d60f09154f4 Mon Sep 17 00:00:00 2001 From: Jerald-Golden-B Date: Thu, 14 Aug 2025 13:59:24 +0530 Subject: [PATCH 1/8] added direction movement to asset during move and duplication --- .../selection3D/duplicationControls3D.tsx | 53 ++++++++++++++++++- .../selection3D/moveControls3D.tsx | 41 +++++++++++++- .../utils/shortcutkeys/handleShortcutKeys.ts | 5 +- 3 files changed, 95 insertions(+), 4 deletions(-) diff --git a/app/src/modules/scene/controls/selectionControls/selection3D/duplicationControls3D.tsx b/app/src/modules/scene/controls/selectionControls/selection3D/duplicationControls3D.tsx index 344ae4e..e5ca7ab 100644 --- a/app/src/modules/scene/controls/selectionControls/selection3D/duplicationControls3D.tsx +++ b/app/src/modules/scene/controls/selectionControls/selection3D/duplicationControls3D.tsx @@ -35,6 +35,7 @@ const DuplicationControls3D = ({ const { selectedVersion } = selectedVersionStore(); const { userId, organization } = getUserData(); + const [axisConstraint, setAxisConstraint] = useState<"x" | "z" | null>(null); const [dragOffset, setDragOffset] = useState(null); const [initialPositions, setInitialPositions] = useState>({}); const [isDuplicating, setIsDuplicating] = useState(false); @@ -82,6 +83,19 @@ const DuplicationControls3D = ({ const onKeyDown = (event: KeyboardEvent) => { const keyCombination = detectModifierKeys(event); + if (isDuplicating && duplicatedObjects.length > 0) { + if (event.key.toLowerCase() === 'x') { + setAxisConstraint(prev => prev === 'x' ? null : 'x'); + event.preventDefault(); + return; + } + if (event.key.toLowerCase() === 'z') { + setAxisConstraint(prev => prev === 'z' ? null : 'z'); + event.preventDefault(); + return; + } + } + if (keyCombination === "Ctrl+D" && movedObjects.length === 0 && rotatedObjects.length === 0) { duplicateSelection(); } @@ -127,7 +141,28 @@ const DuplicationControls3D = ({ } if (dragOffset) { - const adjustedHit = new THREE.Vector3().addVectors(intersectionPoint, dragOffset); + let adjustedHit = new THREE.Vector3().addVectors(intersectionPoint, dragOffset); + + if (axisConstraint) { + const model = scene.getObjectByProperty("uuid", duplicatedObjects[0].userData.modelUuid); + if (model) { + + const currentBasePosition = model.position.clone(); + if (axisConstraint === 'x') { + adjustedHit = new THREE.Vector3( + adjustedHit.x, + currentBasePosition.y, + currentBasePosition.z + ); + } else if (axisConstraint === 'z') { + adjustedHit = new THREE.Vector3( + currentBasePosition.x, + currentBasePosition.y, + adjustedHit.z + ); + } + } + } duplicatedObjects.forEach((duplicatedObject: THREE.Object3D) => { if (duplicatedObject.userData.modelUuid) { @@ -154,6 +189,21 @@ const DuplicationControls3D = ({ } }); + useEffect(() => { + if (duplicatedObjects.length > 0) { + const intersectionPoint = new THREE.Vector3(); + raycaster.setFromCamera(pointer, camera); + const hit = raycaster.ray.intersectPlane(plane, intersectionPoint); + if (hit) { + const model = scene.getObjectByProperty("uuid", duplicatedObjects[0].userData.modelUuid); + if (model) { + const newOffset = calculateDragOffset(model, intersectionPoint); + setDragOffset(newOffset); + } + } + } + }, [axisConstraint, camera, duplicatedObjects]) + const duplicateSelection = useCallback(() => { if (selectedAssets.length > 0 && duplicatedObjects.length === 0) { const positions: Record = {}; @@ -585,6 +635,7 @@ const DuplicationControls3D = ({ setSelectedAssets([]); setIsDuplicating(false); setDragOffset(null); + setAxisConstraint(null); }; return null; diff --git a/app/src/modules/scene/controls/selectionControls/selection3D/moveControls3D.tsx b/app/src/modules/scene/controls/selectionControls/selection3D/moveControls3D.tsx index 4916886..865deea 100644 --- a/app/src/modules/scene/controls/selectionControls/selection3D/moveControls3D.tsx +++ b/app/src/modules/scene/controls/selectionControls/selection3D/moveControls3D.tsx @@ -43,6 +43,7 @@ function MoveControls3D({ const { selectedVersionStore } = useVersionContext(); const { selectedVersion } = selectedVersionStore(); + const [axisConstraint, setAxisConstraint] = useState<"x" | "z" | null>(null); const [dragOffset, setDragOffset] = useState(null); const [initialPositions, setInitialPositions] = useState>({}); const [initialStates, setInitialStates] = useState>({}); @@ -114,6 +115,19 @@ function MoveControls3D({ if (pastedObjects.length > 0 || duplicatedObjects.length > 0 || rotatedObjects.length > 0) return; + if (isMoving && movedObjects.length > 0) { + if (event.key.toLowerCase() === 'x') { + setAxisConstraint(prev => prev === 'x' ? null : 'x'); + event.preventDefault(); + return; + } + if (event.key.toLowerCase() === 'z') { + setAxisConstraint(prev => prev === 'z' ? null : 'z'); + event.preventDefault(); + return; + } + } + if (keyCombination !== keyEvent) { if (keyCombination === "Ctrl" || keyCombination === "Ctrl+Shift" || keyCombination === "Shift") { setKeyEvent(keyCombination); @@ -184,9 +198,22 @@ function MoveControls3D({ } } }); - }, 0) + setAxisConstraint(null); + }, 100) }, [movedObjects, initialStates, updateAsset]); + useEffect(() => { + if (movedObjects.length > 0) { + const intersectionPoint = new THREE.Vector3(); + raycaster.setFromCamera(pointer, camera); + const hit = raycaster.ray.intersectPlane(plane, intersectionPoint); + if (hit) { + const newOffset = calculateDragOffset(movedObjects[0], intersectionPoint); + setDragOffset(newOffset); + } + } + }, [axisConstraint, camera, movedObjects]) + useFrame(() => { if (!isMoving || movedObjects.length === 0) return; @@ -204,7 +231,16 @@ function MoveControls3D({ } if (dragOffset) { - const rawBasePosition = new THREE.Vector3().addVectors(intersectionPoint, dragOffset); + let rawBasePosition = new THREE.Vector3().addVectors(intersectionPoint, dragOffset); + + if (axisConstraint) { + const currentBasePosition = movedObjects[0].position.clone(); + if (axisConstraint === 'x') { + rawBasePosition = new THREE.Vector3(rawBasePosition.x, currentBasePosition.y, currentBasePosition.z); + } else if (axisConstraint === 'z') { + rawBasePosition = new THREE.Vector3(currentBasePosition.x, currentBasePosition.y, rawBasePosition.z); + } + } let moveDistance = keyEvent.includes("Shift") ? 0.05 : 1; @@ -422,6 +458,7 @@ function MoveControls3D({ echo.success("Object moved!"); setIsMoving(false); clearSelection(); + setAxisConstraint(null); }; const clearSelection = () => { diff --git a/app/src/utils/shortcutkeys/handleShortcutKeys.ts b/app/src/utils/shortcutkeys/handleShortcutKeys.ts index 7abbe2f..2a0d437 100644 --- a/app/src/utils/shortcutkeys/handleShortcutKeys.ts +++ b/app/src/utils/shortcutkeys/handleShortcutKeys.ts @@ -11,6 +11,7 @@ import useVersionHistoryVisibleStore, { useDfxUpload, useRenameModeStore, useSaveVersion, + useSelectedAssets, useSelectedComment, useSelectedFloorItem, useSelectedWallItem, @@ -40,6 +41,7 @@ const KeyPressListener: React.FC = () => { const { toggleView, setToggleView } = useToggleView(); const { setAddAction } = useAddAction(); const { setSelectedWallItem } = useSelectedWallItem(); + const { selectedAssets } = useSelectedAssets(); const { setActiveTool } = useActiveTool(); const { clearSelectedZone } = useSelectedZoneStore(); const { showShortcuts, setShowShortcuts } = useShortcutStore(); @@ -82,7 +84,7 @@ const KeyPressListener: React.FC = () => { H: "free-hand", }; const tool = toolMap[key]; - if (tool) { + if (tool && selectedAssets.length === 0) { setActiveTool(tool); setActiveSubTool(tool); } @@ -278,6 +280,7 @@ const KeyPressListener: React.FC = () => { hidePlayer, selectedFloorItem, isRenameMode, + selectedAssets ]); return null; From 594445ac2017127a2d1922b514a707bf9627cbae Mon Sep 17 00:00:00 2001 From: Jerald-Golden-B Date: Thu, 14 Aug 2025 14:43:05 +0530 Subject: [PATCH 2/8] move controls snapping and slow movement added --- .../selection3D/moveControls3D.tsx | 46 +++++----- app/src/utils/handleSnap.ts | 87 +++++++++++++++---- 2 files changed, 90 insertions(+), 43 deletions(-) diff --git a/app/src/modules/scene/controls/selectionControls/selection3D/moveControls3D.tsx b/app/src/modules/scene/controls/selectionControls/selection3D/moveControls3D.tsx index 865deea..d578bc1 100644 --- a/app/src/modules/scene/controls/selectionControls/selection3D/moveControls3D.tsx +++ b/app/src/modules/scene/controls/selectionControls/selection3D/moveControls3D.tsx @@ -5,7 +5,7 @@ import { useSelectedAssets, useSocketStore, useToggleView, } from "../../../../. import * as Types from "../../../../../types/world/worldTypes"; import { detectModifierKeys } from "../../../../../utils/shortcutkeys/detectModifierKeys"; import { upsertProductOrEventApi } from "../../../../../services/simulation/products/UpsertProductOrEventApi"; -import { snapControls } from "../../../../../utils/handleSnap"; +import { getSnappedBasePosition } from "../../../../../utils/handleSnap"; import DistanceFindingControls from "./distanceFindingControls"; import { useParams } from "react-router-dom"; import { useProductContext } from "../../../../simulation/products/productContext"; @@ -49,6 +49,10 @@ function MoveControls3D({ const [initialStates, setInitialStates] = useState>({}); const [isMoving, setIsMoving] = useState(false); const mouseButtonsDown = useRef<{ left: boolean; right: boolean }>({ left: false, right: false, }); + // Add a ref to track fine-move base + const fineMoveBaseRef = useRef(null); + const lastPointerPositionRef = useRef(null); + const wasShiftHeldRef = useRef(false); const updateBackend = ( productName: string, @@ -78,10 +82,21 @@ function MoveControls3D({ }; const onKeyUp = (event: KeyboardEvent) => { - const isModifierKey = (!event.shiftKey && !event.ctrlKey); + const keyCombination = detectModifierKeys(event); - if (isModifierKey && keyEvent !== "") { + if (keyCombination === "") { setKeyEvent(""); + } else if (keyCombination === "Ctrl" || keyCombination === "Ctrl+Shift" || keyCombination === "Shift") { + setKeyEvent(keyCombination); + } + if (movedObjects[0]) { + const intersectionPoint = new THREE.Vector3(); + raycaster.setFromCamera(pointer, camera); + const hit = raycaster.ray.intersectPlane(plane, intersectionPoint); + if (hit) { + const newOffset = calculateDragOffset(movedObjects[0], intersectionPoint); + setDragOffset(newOffset); + } } }; @@ -231,30 +246,9 @@ function MoveControls3D({ } if (dragOffset) { - let rawBasePosition = new THREE.Vector3().addVectors(intersectionPoint, dragOffset); + const rawBasePosition = new THREE.Vector3().addVectors(intersectionPoint, dragOffset); - if (axisConstraint) { - const currentBasePosition = movedObjects[0].position.clone(); - if (axisConstraint === 'x') { - rawBasePosition = new THREE.Vector3(rawBasePosition.x, currentBasePosition.y, currentBasePosition.z); - } else if (axisConstraint === 'z') { - rawBasePosition = new THREE.Vector3(currentBasePosition.x, currentBasePosition.y, rawBasePosition.z); - } - } - - let moveDistance = keyEvent.includes("Shift") ? 0.05 : 1; - - const initialBasePosition = initialPositions[movedObjects[0].uuid]; - const positionDifference = new THREE.Vector3().subVectors(rawBasePosition, initialBasePosition); - - let adjustedDifference = positionDifference.multiplyScalar(moveDistance); - - const baseNewPosition = new THREE.Vector3().addVectors(initialBasePosition, adjustedDifference); - - if (keyEvent.includes("Ctrl")) { - baseNewPosition.x = snapControls(baseNewPosition.x, keyEvent); - baseNewPosition.z = snapControls(baseNewPosition.z, keyEvent); - } + const baseNewPosition = getSnappedBasePosition({ rawBasePosition, intersectionPoint, movedObjects, axisConstraint, keyEvent, fineMoveBaseRef, lastPointerPositionRef, wasShiftHeldRef }); movedObjects.forEach((movedAsset: THREE.Object3D) => { if (movedAsset.userData.modelUuid) { diff --git a/app/src/utils/handleSnap.ts b/app/src/utils/handleSnap.ts index bd6a74d..d96bf14 100644 --- a/app/src/utils/handleSnap.ts +++ b/app/src/utils/handleSnap.ts @@ -1,22 +1,75 @@ -export function snapControls(value: number, event: string): number { - const CTRL_DISTANCE = 1; // Snap to whole numbers when Ctrl is pressed - const SHIFT_DISTANCE = 0.01; // Snap to half-step increments when Shift is pressed - const CTRL_SHIFT_DISTANCE = 0.1; // Snap to fine increments when both Ctrl and Shift are pressed +import * as THREE from "three"; - switch (event) { - case "Ctrl": - return Math.round(value / CTRL_DISTANCE) * CTRL_DISTANCE; +export function getSnappedBasePosition({ + rawBasePosition, + intersectionPoint, + movedObjects, + axisConstraint, + keyEvent, + fineMoveBaseRef, + lastPointerPositionRef, + wasShiftHeldRef +}: { + rawBasePosition: THREE.Vector3; + intersectionPoint: THREE.Vector3; + movedObjects: THREE.Object3D[]; + axisConstraint: "x" | "z" | null; + keyEvent: string; + fineMoveBaseRef: React.MutableRefObject; + lastPointerPositionRef: React.MutableRefObject; + wasShiftHeldRef: React.MutableRefObject; +}): THREE.Vector3 { + const CTRL_DISTANCE = 0.5; + const SHIFT_DISTANCE = 0.05; + const CTRL_SHIFT_DISTANCE = 0.05; - case "Shift": - return Math.round(value / SHIFT_DISTANCE) * SHIFT_DISTANCE; + const isShiftHeld = keyEvent.includes("Shift"); - case "Ctrl+Shift": - const base = Math.floor(value / CTRL_DISTANCE) * CTRL_DISTANCE; - const offset = - Math.round((value - base) / CTRL_SHIFT_DISTANCE) * CTRL_SHIFT_DISTANCE; - return base + offset; + // Handle Shift toggle state + if (isShiftHeld !== wasShiftHeldRef.current) { + if (isShiftHeld) { + fineMoveBaseRef.current = movedObjects[0].position.clone(); + lastPointerPositionRef.current = intersectionPoint.clone(); + } else { + fineMoveBaseRef.current = null; + } + wasShiftHeldRef.current = isShiftHeld; + } - default: - return value; // No snapping if no modifier key is pressed - } + // Start from raw + let baseNewPosition = rawBasePosition.clone(); + + // Apply snapping / fine move + if (keyEvent === "Ctrl") { + baseNewPosition.set( + Math.round(baseNewPosition.x / CTRL_DISTANCE) * CTRL_DISTANCE, + baseNewPosition.y, + Math.round(baseNewPosition.z / CTRL_DISTANCE) * CTRL_DISTANCE + ); + } else if (keyEvent === "Ctrl+Shift") { + if (isShiftHeld && fineMoveBaseRef.current && lastPointerPositionRef.current) { + const pointerDelta = new THREE.Vector3().subVectors(intersectionPoint, lastPointerPositionRef.current); + baseNewPosition = fineMoveBaseRef.current.clone().add(pointerDelta.multiplyScalar(SHIFT_DISTANCE)); + } + baseNewPosition.set( + Math.round(baseNewPosition.x / CTRL_SHIFT_DISTANCE) * CTRL_SHIFT_DISTANCE, + baseNewPosition.y, + Math.round(baseNewPosition.z / CTRL_SHIFT_DISTANCE) * CTRL_SHIFT_DISTANCE + ); + } else if (isShiftHeld && fineMoveBaseRef.current && lastPointerPositionRef.current) { + const pointerDelta = new THREE.Vector3().subVectors(intersectionPoint, lastPointerPositionRef.current); + baseNewPosition = fineMoveBaseRef.current.clone().add(pointerDelta.multiplyScalar(SHIFT_DISTANCE)); + } + + // Apply axis constraint last + if (axisConstraint) { + const currentBasePosition = movedObjects[0].position.clone(); + if (axisConstraint === 'x') { + baseNewPosition.set(baseNewPosition.x, currentBasePosition.y, currentBasePosition.z); + } else if (axisConstraint === 'z') { + baseNewPosition.set(currentBasePosition.x, currentBasePosition.y, baseNewPosition.z); + } + } + + return baseNewPosition; } From ab3eb84277f0a662ffbd98a52ba8f3048bf81f30 Mon Sep 17 00:00:00 2001 From: Jerald-Golden-B Date: Thu, 14 Aug 2025 15:42:51 +0530 Subject: [PATCH 3/8] duplication asset snapping movement and slow movement added --- .../selection3D/duplicationControls3D.tsx | 67 ++++++++++++------- .../functions/handleAssetPositionSnap.ts} | 16 ++--- .../selection3D/moveControls3D.tsx | 10 ++- 3 files changed, 55 insertions(+), 38 deletions(-) rename app/src/{utils/handleSnap.ts => modules/scene/controls/selectionControls/selection3D/functions/handleAssetPositionSnap.ts} (89%) diff --git a/app/src/modules/scene/controls/selectionControls/selection3D/duplicationControls3D.tsx b/app/src/modules/scene/controls/selectionControls/selection3D/duplicationControls3D.tsx index e5ca7ab..fcac600 100644 --- a/app/src/modules/scene/controls/selectionControls/selection3D/duplicationControls3D.tsx +++ b/app/src/modules/scene/controls/selectionControls/selection3D/duplicationControls3D.tsx @@ -9,6 +9,7 @@ import { useParams } from "react-router-dom"; import { getUserData } from "../../../../../functions/getUserData"; import { useSceneContext } from "../../../sceneContext"; import { useVersionContext } from "../../../../builder/version/versionContext"; +import { handleAssetPositionSnap } from "./functions/handleAssetPositionSnap"; // import { setAssetsApi } from "../../../../../services/factoryBuilder/asset/floorAsset/setAssetsApi"; @@ -35,11 +36,15 @@ const DuplicationControls3D = ({ const { selectedVersion } = selectedVersionStore(); const { userId, organization } = getUserData(); + const [keyEvent, setKeyEvent] = useState<"Ctrl" | "Shift" | "Ctrl+Shift" | "">(""); const [axisConstraint, setAxisConstraint] = useState<"x" | "z" | null>(null); const [dragOffset, setDragOffset] = useState(null); const [initialPositions, setInitialPositions] = useState>({}); const [isDuplicating, setIsDuplicating] = useState(false); const mouseButtonsDown = useRef<{ left: boolean; right: boolean }>({ left: false, right: false, }); + const fineMoveBaseRef = useRef(null); + const lastPointerPositionRef = useRef(null); + const wasShiftHeldRef = useRef(false); const calculateDragOffset = useCallback((point: THREE.Object3D, hitPoint: THREE.Vector3) => { const pointPosition = new THREE.Vector3().copy(point.position); @@ -78,6 +83,7 @@ const DuplicationControls3D = ({ removeAsset(obj.userData.modelUuid); }); } + setKeyEvent(""); }; const onKeyDown = (event: KeyboardEvent) => { @@ -96,6 +102,14 @@ const DuplicationControls3D = ({ } } + if (keyCombination !== keyEvent) { + if (keyCombination === "Ctrl" || keyCombination === "Ctrl+Shift" || keyCombination === "Shift") { + setKeyEvent(keyCombination); + } else { + setKeyEvent(""); + } + } + if (keyCombination === "Ctrl+D" && movedObjects.length === 0 && rotatedObjects.length === 0) { duplicateSelection(); } @@ -108,11 +122,34 @@ const DuplicationControls3D = ({ } }; + const onKeyUp = (event: KeyboardEvent) => { + const keyCombination = detectModifierKeys(event); + + if (keyCombination === "") { + setKeyEvent(""); + } else if (keyCombination === "Ctrl" || keyCombination === "Ctrl+Shift" || keyCombination === "Shift") { + setKeyEvent(keyCombination); + } + if (duplicatedObjects[0] && keyEvent !== "" && keyCombination === '') { + const intersectionPoint = new THREE.Vector3(); + raycaster.setFromCamera(pointer, camera); + const hit = raycaster.ray.intersectPlane(plane, intersectionPoint); + if (hit) { + const model = scene.getObjectByProperty("uuid", duplicatedObjects[0].userData.modelUuid); + if (model) { + const newOffset = calculateDragOffset(model, intersectionPoint); + setDragOffset(newOffset); + } + } + } + }; + if (!toggleView) { canvasElement.addEventListener("pointerdown", onPointerDown); canvasElement.addEventListener("pointermove", onPointerMove); canvasElement.addEventListener("pointerup", onPointerUp); canvasElement.addEventListener("keydown", onKeyDown); + canvasElement.addEventListener("keyup", onKeyUp); } return () => { @@ -120,8 +157,9 @@ const DuplicationControls3D = ({ canvasElement.removeEventListener("pointermove", onPointerMove); canvasElement.removeEventListener("pointerup", onPointerUp); canvasElement.removeEventListener("keydown", onKeyDown); + canvasElement.addEventListener("keyup", onKeyUp); }; - }, [assets, camera, controls, scene, toggleView, selectedAssets, duplicatedObjects, movedObjects, socket, rotatedObjects]); + }, [assets, camera, controls, scene, toggleView, selectedAssets, duplicatedObjects, movedObjects, socket, rotatedObjects, keyEvent]); useFrame(() => { if (!isDuplicating || duplicatedObjects.length === 0) return; @@ -141,28 +179,9 @@ const DuplicationControls3D = ({ } if (dragOffset) { - let adjustedHit = new THREE.Vector3().addVectors(intersectionPoint, dragOffset); - - if (axisConstraint) { - const model = scene.getObjectByProperty("uuid", duplicatedObjects[0].userData.modelUuid); - if (model) { - - const currentBasePosition = model.position.clone(); - if (axisConstraint === 'x') { - adjustedHit = new THREE.Vector3( - adjustedHit.x, - currentBasePosition.y, - currentBasePosition.z - ); - } else if (axisConstraint === 'z') { - adjustedHit = new THREE.Vector3( - currentBasePosition.x, - currentBasePosition.y, - adjustedHit.z - ); - } - } - } + const rawBasePosition = new THREE.Vector3().addVectors(intersectionPoint, dragOffset); + const model = scene.getObjectByProperty("uuid", duplicatedObjects[0].userData.modelUuid); + const baseNewPosition = handleAssetPositionSnap({ rawBasePosition, intersectionPoint, model, axisConstraint, keyEvent, fineMoveBaseRef, lastPointerPositionRef, wasShiftHeldRef }); duplicatedObjects.forEach((duplicatedObject: THREE.Object3D) => { if (duplicatedObject.userData.modelUuid) { @@ -176,7 +195,7 @@ const DuplicationControls3D = ({ ); const model = scene.getObjectByProperty("uuid", duplicatedObject.userData.modelUuid); - const newPosition = new THREE.Vector3().addVectors(adjustedHit, relativeOffset); + const newPosition = new THREE.Vector3().addVectors(baseNewPosition, relativeOffset); const positionArray: [number, number, number] = [newPosition.x, newPosition.y, newPosition.z]; if (model) { diff --git a/app/src/utils/handleSnap.ts b/app/src/modules/scene/controls/selectionControls/selection3D/functions/handleAssetPositionSnap.ts similarity index 89% rename from app/src/utils/handleSnap.ts rename to app/src/modules/scene/controls/selectionControls/selection3D/functions/handleAssetPositionSnap.ts index d96bf14..00b6924 100644 --- a/app/src/utils/handleSnap.ts +++ b/app/src/modules/scene/controls/selectionControls/selection3D/functions/handleAssetPositionSnap.ts @@ -1,9 +1,9 @@ import * as THREE from "three"; -export function getSnappedBasePosition({ +export function handleAssetPositionSnap({ rawBasePosition, intersectionPoint, - movedObjects, + model, axisConstraint, keyEvent, fineMoveBaseRef, @@ -12,7 +12,7 @@ export function getSnappedBasePosition({ }: { rawBasePosition: THREE.Vector3; intersectionPoint: THREE.Vector3; - movedObjects: THREE.Object3D[]; + model: THREE.Object3D | undefined; axisConstraint: "x" | "z" | null; keyEvent: string; fineMoveBaseRef: React.MutableRefObject; @@ -26,9 +26,9 @@ export function getSnappedBasePosition({ const isShiftHeld = keyEvent.includes("Shift"); // Handle Shift toggle state - if (isShiftHeld !== wasShiftHeldRef.current) { + if (isShiftHeld !== wasShiftHeldRef.current && model) { if (isShiftHeld) { - fineMoveBaseRef.current = movedObjects[0].position.clone(); + fineMoveBaseRef.current = model.position.clone(); lastPointerPositionRef.current = intersectionPoint.clone(); } else { fineMoveBaseRef.current = null; @@ -62,8 +62,8 @@ export function getSnappedBasePosition({ } // Apply axis constraint last - if (axisConstraint) { - const currentBasePosition = movedObjects[0].position.clone(); + if (axisConstraint && model) { + const currentBasePosition = model.position.clone(); if (axisConstraint === 'x') { baseNewPosition.set(baseNewPosition.x, currentBasePosition.y, currentBasePosition.z); } else if (axisConstraint === 'z') { @@ -72,4 +72,4 @@ export function getSnappedBasePosition({ } return baseNewPosition; -} +} \ No newline at end of file diff --git a/app/src/modules/scene/controls/selectionControls/selection3D/moveControls3D.tsx b/app/src/modules/scene/controls/selectionControls/selection3D/moveControls3D.tsx index d578bc1..8fc9c63 100644 --- a/app/src/modules/scene/controls/selectionControls/selection3D/moveControls3D.tsx +++ b/app/src/modules/scene/controls/selectionControls/selection3D/moveControls3D.tsx @@ -5,7 +5,7 @@ import { useSelectedAssets, useSocketStore, useToggleView, } from "../../../../. import * as Types from "../../../../../types/world/worldTypes"; import { detectModifierKeys } from "../../../../../utils/shortcutkeys/detectModifierKeys"; import { upsertProductOrEventApi } from "../../../../../services/simulation/products/UpsertProductOrEventApi"; -import { getSnappedBasePosition } from "../../../../../utils/handleSnap"; +import { handleAssetPositionSnap } from "./functions/handleAssetPositionSnap"; import DistanceFindingControls from "./distanceFindingControls"; import { useParams } from "react-router-dom"; import { useProductContext } from "../../../../simulation/products/productContext"; @@ -34,7 +34,6 @@ function MoveControls3D({ const { selectedProductStore } = useProductContext(); const { selectedProduct } = selectedProductStore(); const { socket } = useSocketStore(); - const [keyEvent, setKeyEvent] = useState<"Ctrl" | "Shift" | "Ctrl+Shift" | "">(""); const { userId, organization } = getUserData(); const { projectId } = useParams(); const { assetStore, eventStore, productStore, undoRedo3DStore } = useSceneContext(); @@ -43,13 +42,13 @@ function MoveControls3D({ const { selectedVersionStore } = useVersionContext(); const { selectedVersion } = selectedVersionStore(); + const [keyEvent, setKeyEvent] = useState<"Ctrl" | "Shift" | "Ctrl+Shift" | "">(""); const [axisConstraint, setAxisConstraint] = useState<"x" | "z" | null>(null); const [dragOffset, setDragOffset] = useState(null); const [initialPositions, setInitialPositions] = useState>({}); const [initialStates, setInitialStates] = useState>({}); const [isMoving, setIsMoving] = useState(false); const mouseButtonsDown = useRef<{ left: boolean; right: boolean }>({ left: false, right: false, }); - // Add a ref to track fine-move base const fineMoveBaseRef = useRef(null); const lastPointerPositionRef = useRef(null); const wasShiftHeldRef = useRef(false); @@ -121,7 +120,6 @@ function MoveControls3D({ clearSelection(); setMovedObjects([]); } - setKeyEvent(""); }; const onKeyDown = (event: KeyboardEvent) => { @@ -247,8 +245,8 @@ function MoveControls3D({ if (dragOffset) { const rawBasePosition = new THREE.Vector3().addVectors(intersectionPoint, dragOffset); - - const baseNewPosition = getSnappedBasePosition({ rawBasePosition, intersectionPoint, movedObjects, axisConstraint, keyEvent, fineMoveBaseRef, lastPointerPositionRef, wasShiftHeldRef }); + const model = movedObjects[0]; + const baseNewPosition = handleAssetPositionSnap({ rawBasePosition, intersectionPoint, model, axisConstraint, keyEvent, fineMoveBaseRef, lastPointerPositionRef, wasShiftHeldRef }); movedObjects.forEach((movedAsset: THREE.Object3D) => { if (movedAsset.userData.modelUuid) { From 922085ec6c31e61761ccda099f4b36b973b5ce25 Mon Sep 17 00:00:00 2001 From: Jerald-Golden-B Date: Thu, 14 Aug 2025 15:55:31 +0530 Subject: [PATCH 4/8] storage to human bug fix --- .../actionHandler/useRetrieveHandler.ts | 21 +++++++++++++------ .../eventManager/useHumanEventManager.ts | 1 + 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/app/src/modules/simulation/actions/storageUnit/actionHandler/useRetrieveHandler.ts b/app/src/modules/simulation/actions/storageUnit/actionHandler/useRetrieveHandler.ts index f48312a..18e8bfd 100644 --- a/app/src/modules/simulation/actions/storageUnit/actionHandler/useRetrieveHandler.ts +++ b/app/src/modules/simulation/actions/storageUnit/actionHandler/useRetrieveHandler.ts @@ -6,7 +6,7 @@ import { useProductContext } from "../../../products/productContext"; import { useHumanEventManager } from "../../../human/eventManager/useHumanEventManager"; export function useRetrieveHandler() { - const { materialStore, armBotStore, machineStore, vehicleStore, storageUnitStore, conveyorStore, craneStore, productStore, humanStore, assetStore } = useSceneContext(); + const { materialStore, armBotStore, machineStore, vehicleStore, storageUnitStore, conveyorStore, craneStore, productStore, humanStore, assetStore, humanEventManagerRef } = useSceneContext(); const { selectedProductStore } = useProductContext(); const { addMaterial } = materialStore(); const { getModelUuidByActionUuid, getPointUuidByActionUuid, getEventByModelUuid, getActionByUuid } = productStore(); @@ -464,11 +464,20 @@ export function useRetrieveHandler() { if (action && action.actionType === 'pickAndDrop' && !hasLock && !crane.isCarrying && !crane.isActive && crane.currentLoad < (action?.maxPickUpCount || 0)) { const material = getLastMaterial(storageUnit.modelUuid); if (material) { - incrementCraneLoad(crane.modelUuid, 1); - addCurrentActionToCrane(crane.modelUuid, action.actionUuid, material.materialType, material.materialId); - addCurrentMaterialToCrane(crane.modelUuid, material.materialType, material.materialId); - - cranePickupLockRef.current.set(crane.modelUuid, true); + if (action.triggers[0].triggeredAsset?.triggeredModel.modelUuid && action.triggers[0].triggeredAsset.triggeredAction?.actionUuid) { + const human = getEventByModelUuid(selectedProduct.productUuid, action.triggers[0].triggeredAsset.triggeredModel.modelUuid); + if (human && human.type === 'human') { + if (!monitoredHumansRef.current.has(human.modelUuid)) { + addHumanToMonitor(human.modelUuid, () => { + incrementCraneLoad(crane.modelUuid, 1); + addCurrentActionToCrane(crane.modelUuid, action.actionUuid, material.materialType, material.materialId); + addCurrentMaterialToCrane(crane.modelUuid, material.materialType, material.materialId); + cranePickupLockRef.current.set(crane.modelUuid, true); + }, action.triggers[0].triggeredAsset.triggeredAction?.actionUuid) + } + monitoredHumansRef.current.add(human.modelUuid); + } + } } } else if (crane.isCarrying && crane.currentPhase === 'pickup-drop' && hasLock) { cranePickupLockRef.current.set(crane.modelUuid, false); diff --git a/app/src/modules/simulation/human/eventManager/useHumanEventManager.ts b/app/src/modules/simulation/human/eventManager/useHumanEventManager.ts index 1dec0d9..8a5b634 100644 --- a/app/src/modules/simulation/human/eventManager/useHumanEventManager.ts +++ b/app/src/modules/simulation/human/eventManager/useHumanEventManager.ts @@ -23,6 +23,7 @@ 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; From 24cc15c2b91f5ac9d03c3cca68ac731018da734e Mon Sep 17 00:00:00 2001 From: Vishnu Date: Thu, 14 Aug 2025 17:07:43 +0530 Subject: [PATCH 5/8] bug fix --- app/src/modules/scene/setup/setup.tsx | 4 ++-- app/src/utils/shortcutkeys/handleShortcutKeys.ts | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/src/modules/scene/setup/setup.tsx b/app/src/modules/scene/setup/setup.tsx index 71dcce7..1887015 100644 --- a/app/src/modules/scene/setup/setup.tsx +++ b/app/src/modules/scene/setup/setup.tsx @@ -5,7 +5,7 @@ import Controls from '../controls/controls'; import { AdaptiveDpr, AdaptiveEvents, Environment } from '@react-three/drei' import background from "../../../assets/textures/hdr/mudroadpuresky2k.hdr"; -// import { Perf } from 'r3f-perf'; +import { Perf } from 'r3f-perf'; function Setup() { return ( @@ -18,7 +18,7 @@ function Setup() { - {/* */} + {/* */} diff --git a/app/src/utils/shortcutkeys/handleShortcutKeys.ts b/app/src/utils/shortcutkeys/handleShortcutKeys.ts index b38292f..507e31b 100644 --- a/app/src/utils/shortcutkeys/handleShortcutKeys.ts +++ b/app/src/utils/shortcutkeys/handleShortcutKeys.ts @@ -41,7 +41,6 @@ const KeyPressListener: React.FC = () => { const { toggleView, setToggleView } = useToggleView(); const { setAddAction } = useAddAction(); const { setSelectedWallItem } = useSelectedWallItem(); - const { selectedAssets } = useSelectedAssets(); const { setActiveTool } = useActiveTool(); const { clearSelectedZone } = useSelectedZoneStore(); const { showShortcuts, setShowShortcuts } = useShortcutStore(); From 592452068ebe80b8f9be5360806612aeb4f0b2fe Mon Sep 17 00:00:00 2001 From: Vishnu Date: Thu, 14 Aug 2025 17:23:05 +0530 Subject: [PATCH 6/8] refactor: comment out unused performance stats component and add styles for stats positioning --- app/src/modules/scene/setup/setup.tsx | 4 ++-- app/src/styles/scene/scene.scss | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/src/modules/scene/setup/setup.tsx b/app/src/modules/scene/setup/setup.tsx index 1887015..71dcce7 100644 --- a/app/src/modules/scene/setup/setup.tsx +++ b/app/src/modules/scene/setup/setup.tsx @@ -5,7 +5,7 @@ import Controls from '../controls/controls'; import { AdaptiveDpr, AdaptiveEvents, Environment } from '@react-three/drei' import background from "../../../assets/textures/hdr/mudroadpuresky2k.hdr"; -import { Perf } from 'r3f-perf'; +// import { Perf } from 'r3f-perf'; function Setup() { return ( @@ -18,7 +18,7 @@ function Setup() { - + {/* */} {/* */} diff --git a/app/src/styles/scene/scene.scss b/app/src/styles/scene/scene.scss index 4db9e8b..6358c81 100644 --- a/app/src/styles/scene/scene.scss +++ b/app/src/styles/scene/scene.scss @@ -134,3 +134,11 @@ align-items: center; } } + +.stats{ + top: auto !important; + bottom: 36px !important; + left: 12px !important; + border-radius: 6px; + overflow: hidden; +} From a8c6f8d80acd9eecbd0476f4b188df47ea77672b Mon Sep 17 00:00:00 2001 From: Jerald-Golden-B Date: Thu, 14 Aug 2025 18:13:28 +0530 Subject: [PATCH 7/8] rotational snap added --- .../functions/handleAssetRotationSnap.ts | 67 ++++++++++++++++ .../selection3D/rotateControls3D.tsx | 76 ++++++++++++------- .../eventManager/useHumanEventManager.ts | 1 - 3 files changed, 117 insertions(+), 27 deletions(-) create mode 100644 app/src/modules/scene/controls/selectionControls/selection3D/functions/handleAssetRotationSnap.ts 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..89fb36b --- /dev/null +++ b/app/src/modules/scene/controls/selectionControls/selection3D/functions/handleAssetRotationSnap.ts @@ -0,0 +1,67 @@ +import * as THREE from "three"; + +function normalizeDegrees(deg: number): number { + // Wrap into [-180, 180] + deg = (deg + 180) % 360; + if (deg < 0) deg += 360; + return deg - 180; +} + +export function handleAssetRotationSnap({ + currentRotation, + rotationDelta, + keyEvent, + snapBaseRef, + prevRotationRef, + wasShiftHeldRef +}: { + currentRotation: THREE.Euler; + rotationDelta: 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 newRotationDeltaDeg = THREE.MathUtils.radToDeg(rotationDelta) * speedFactor; + + const modifierChanged = isShiftHeld !== wasShiftHeldRef.current; + if (modifierChanged) { + wasShiftHeldRef.current = isShiftHeld; + } + + if (isCtrlHeld) { + const snapDeg = isShiftHeld ? CTRL_SHIFT_SNAP_DEG : CTRL_SNAP_DEG; + + // Store base rotation in degrees when Ctrl first pressed + if (snapBaseRef.current === null) { + snapBaseRef.current = normalizeDegrees(THREE.MathUtils.radToDeg(currentRotation.y)); + } else { + snapBaseRef.current = normalizeDegrees(snapBaseRef.current + newRotationDeltaDeg); + } + + // Snap to nearest increment + const snappedDeg = Math.round(snapBaseRef.current / snapDeg) * snapDeg; + + // Normalize so it never goes beyond [-180, 180] + const normalizedSnappedDeg = normalizeDegrees(snappedDeg); + + // Convert back to radians for returning delta + newRotationDeltaDeg = normalizedSnappedDeg - THREE.MathUtils.radToDeg(currentRotation.y); + + } else { + snapBaseRef.current = null; + } + + prevRotationRef.current = currentRotation.y; + + return THREE.MathUtils.degToRad(newRotationDeltaDeg); +} diff --git a/app/src/modules/scene/controls/selectionControls/selection3D/rotateControls3D.tsx b/app/src/modules/scene/controls/selectionControls/selection3D/rotateControls3D.tsx index bd5e254..f82db88 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,15 +39,16 @@ 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); const prevPointerPosition = useRef(null); const rotationCenter = useRef(null); - const mouseButtonsDown = useRef<{ left: boolean; right: boolean }>({ - left: false, - right: false, - }); + const mouseButtonsDown = useRef<{ left: boolean; right: boolean }>({ left: false, right: false, }); + const snapBaseRef = useRef(null); + const prevRotationRef = useRef(null); + const wasShiftHeldRef = useRef(false); const updateBackend = useCallback(( productName: string, @@ -98,6 +100,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") { @@ -105,6 +109,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(); @@ -113,11 +126,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 () => { @@ -125,8 +151,9 @@ 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) => { @@ -154,26 +181,27 @@ function RotateControls3D({ 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 deltaX = currentPointer.x - prevPointerPosition.current.x; - 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 rawAngleDelta = deltaX; + + const angleDelta = handleAssetRotationSnap({ + currentRotation: rotatedObjects[0].rotation, + rotationDelta: rawAngleDelta, + keyEvent, + snapBaseRef, + prevRotationRef, + wasShiftHeldRef + }); const rotationMatrix = new THREE.Matrix4().makeRotationY(angleDelta); @@ -186,7 +214,7 @@ function RotateControls3D({ } }); - prevPointerPosition.current = new THREE.Vector2(point.x, point.z); + prevPointerPosition.current = currentPointer.clone(); } }); @@ -208,17 +236,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; From 64aadbb53da796723752dd22ad27f81be0482956 Mon Sep 17 00:00:00 2001 From: Jerald-Golden-B Date: Wed, 20 Aug 2025 10:39:29 +0530 Subject: [PATCH 8/8] rotationControls snapping bug fixed --- .../functions/handleAssetRotationSnap.ts | 59 ++++++++++-------- .../selection3D/rotateControls3D.tsx | 62 ++++++++++--------- 2 files changed, 65 insertions(+), 56 deletions(-) diff --git a/app/src/modules/scene/controls/selectionControls/selection3D/functions/handleAssetRotationSnap.ts b/app/src/modules/scene/controls/selectionControls/selection3D/functions/handleAssetRotationSnap.ts index 89fb36b..ca16720 100644 --- a/app/src/modules/scene/controls/selectionControls/selection3D/functions/handleAssetRotationSnap.ts +++ b/app/src/modules/scene/controls/selectionControls/selection3D/functions/handleAssetRotationSnap.ts @@ -1,22 +1,15 @@ import * as THREE from "three"; -function normalizeDegrees(deg: number): number { - // Wrap into [-180, 180] - deg = (deg + 180) % 360; - if (deg < 0) deg += 360; - return deg - 180; -} - export function handleAssetRotationSnap({ - currentRotation, - rotationDelta, + object, + pointerDeltaX, keyEvent, snapBaseRef, prevRotationRef, wasShiftHeldRef }: { - currentRotation: THREE.Euler; - rotationDelta: number; + object: THREE.Object3D; + pointerDeltaX: number; keyEvent: string; snapBaseRef: React.MutableRefObject; prevRotationRef: React.MutableRefObject; @@ -31,37 +24,51 @@ export function handleAssetRotationSnap({ const isCtrlHeld = keyEvent.includes("Ctrl"); const speedFactor = isShiftHeld ? SHIFT_SPEED : NORMAL_SPEED; - let newRotationDeltaDeg = THREE.MathUtils.radToDeg(rotationDelta) * speedFactor; + 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); - // Store base rotation in degrees when Ctrl first pressed + // 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 = normalizeDegrees(THREE.MathUtils.radToDeg(currentRotation.y)); - } else { - snapBaseRef.current = normalizeDegrees(snapBaseRef.current + newRotationDeltaDeg); + snapBaseRef.current = currentAngle; + prevRotationRef.current = currentAngle; } + // Accumulate the total rotation from the base + const totalRotation = snapBaseRef.current + deltaAngle; + // Snap to nearest increment - const snappedDeg = Math.round(snapBaseRef.current / snapDeg) * snapDeg; + const snappedAngle = Math.round(totalRotation / snapRad) * snapRad; - // Normalize so it never goes beyond [-180, 180] - const normalizedSnappedDeg = normalizeDegrees(snappedDeg); + // Calculate the delta needed to reach the snapped angle from current rotation + let targetDelta = snappedAngle - currentAngle; - // Convert back to radians for returning delta - newRotationDeltaDeg = normalizedSnappedDeg - THREE.MathUtils.radToDeg(currentRotation.y); + // 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; } - - prevRotationRef.current = currentRotation.y; - - return THREE.MathUtils.degToRad(newRotationDeltaDeg); -} +} \ 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 f82db88..255f8b0 100644 --- a/app/src/modules/scene/controls/selectionControls/selection3D/rotateControls3D.tsx +++ b/app/src/modules/scene/controls/selectionControls/selection3D/rotateControls3D.tsx @@ -156,26 +156,28 @@ function RotateControls3D({ }, [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(() => { @@ -192,25 +194,25 @@ function RotateControls3D({ const center = rotationCenter.current; const deltaX = currentPointer.x - prevPointerPosition.current.x; - const rawAngleDelta = deltaX; - - const angleDelta = handleAssetRotationSnap({ - currentRotation: rotatedObjects[0].rotation, - rotationDelta: rawAngleDelta, - keyEvent, - snapBaseRef, - prevRotationRef, - wasShiftHeldRef - }); - - const rotationMatrix = new THREE.Matrix4().makeRotationY(angleDelta); - 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); } });