From 4ac77e462c2689e1b659236c128364803c684daa Mon Sep 17 00:00:00 2001 From: Gomathi9520 Date: Tue, 6 May 2025 18:38:27 +0530 Subject: [PATCH] arm points path upated and points restricted between 270 and 360 --- .../mechanics/roboticArmMechanics.tsx | 21 +- .../instances/material/materialModel.tsx | 2 +- .../instances/animator/materialAnimator.tsx | 18 +- .../instances/animator/roboticArmAnimator.tsx | 153 +++----- .../armInstance/roboticArmInstance.tsx | 39 +- .../instances/ikInstance/ikInstance.tsx | 14 +- .../simulation/roboticArm/roboticArm.tsx | 29 +- .../simulation/ui/arm/useDraggableGLTF.ts | 345 +++++++++--------- 8 files changed, 296 insertions(+), 325 deletions(-) diff --git a/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/roboticArmMechanics.tsx b/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/roboticArmMechanics.tsx index c7bd8bc..997d078 100644 --- a/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/roboticArmMechanics.tsx +++ b/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/roboticArmMechanics.tsx @@ -28,22 +28,11 @@ function RoboticArmMechanics() { selectedEventData.data.modelUuid, selectedEventData.selectedPoint ) as RoboticArmPointSchema | undefined; - const action = getActionByUuid(selectedProduct.productId, selectedAction.actionId) as RoboticArmPointSchema["actions"][0] | undefined; - if (action) { - if (point?.actions) { - setSelectedPointData(point); - if (point.actions.length > 0 && action) { - setActiveOption(action.actionType as "default" | "pickAndPlace"); - setSelectedAction(selectedAction.actionId, selectedAction.actionName); - } - } - } else { - if (point?.actions) { - setSelectedPointData(point); - if (point.actions.length > 0) { - setActiveOption(point.actions[0].actionType as "default" | "pickAndPlace"); - setSelectedAction(point.actions[0].actionUuid, point.actions[0].actionName); - } + if (point?.actions) { + setSelectedPointData(point); + if (point.actions.length > 0) { + setActiveOption(point.actions[0].actionType as "default" | "pickAndPlace"); + setSelectedAction(point.actions[0].actionUuid, point.actions[0].actionName); } } } else { diff --git a/app/src/modules/simulation/materials/instances/material/materialModel.tsx b/app/src/modules/simulation/materials/instances/material/materialModel.tsx index f27541b..2093584 100644 --- a/app/src/modules/simulation/materials/instances/material/materialModel.tsx +++ b/app/src/modules/simulation/materials/instances/material/materialModel.tsx @@ -32,7 +32,7 @@ export function MaterialModel({ materialType, matRef, ...props }: ModelProps) { ); diff --git a/app/src/modules/simulation/roboticArm/instances/animator/materialAnimator.tsx b/app/src/modules/simulation/roboticArm/instances/animator/materialAnimator.tsx index 893f759..b458d32 100644 --- a/app/src/modules/simulation/roboticArm/instances/animator/materialAnimator.tsx +++ b/app/src/modules/simulation/roboticArm/instances/animator/materialAnimator.tsx @@ -1,17 +1,16 @@ import { useFrame } from '@react-three/fiber'; import React, { useEffect, useRef, useState } from 'react'; import * as THREE from 'three'; -import { useLogger } from '../../../../../components/ui/log/LoggerContext'; +import { MaterialModel } from '../../../materials/instances/material/materialModel'; type MaterialAnimatorProps = { ikSolver: any; - armBot: any; + armBot: ArmBotStatus; currentPhase: string; }; export default function MaterialAnimator({ ikSolver, armBot, currentPhase }: MaterialAnimatorProps) { - const sphereRef = useRef(null); - const boneWorldPosition = new THREE.Vector3(); + const sphereRef = useRef(null); const [isRendered, setIsRendered] = useState(false); useEffect(() => { @@ -41,7 +40,8 @@ export default function MaterialAnimator({ ikSolver, armBot, currentPhase }: Mat const direction = new THREE.Vector3(); direction.subVectors(boneWorldPos, boneTargetWorldPos).normalize(); const downwardDirection = direction.clone().negate(); - const adjustedPosition = boneWorldPos.clone().addScaledVector(downwardDirection, -0.25); + + const adjustedPosition = boneWorldPos.clone().addScaledVector(downwardDirection, -0.01); //set position sphereRef.current.position.copy(adjustedPosition); @@ -56,10 +56,10 @@ export default function MaterialAnimator({ ikSolver, armBot, currentPhase }: Mat return ( <> {isRendered && ( - - - - + )} ); diff --git a/app/src/modules/simulation/roboticArm/instances/animator/roboticArmAnimator.tsx b/app/src/modules/simulation/roboticArm/instances/animator/roboticArmAnimator.tsx index a1e88cc..3ca7665 100644 --- a/app/src/modules/simulation/roboticArm/instances/animator/roboticArmAnimator.tsx +++ b/app/src/modules/simulation/roboticArm/instances/animator/roboticArmAnimator.tsx @@ -2,38 +2,33 @@ import React, { useEffect, useMemo, useRef, useState } from 'react'; import { useFrame } from '@react-three/fiber'; import * as THREE from 'three'; import { Line, Text } from '@react-three/drei'; -import { - useAnimationPlaySpeed, - usePauseButtonStore, - usePlayButtonStore, - useResetButtonStore -} from '../../../../../store/usePlayButtonStore'; +import { useAnimationPlaySpeed, usePauseButtonStore, usePlayButtonStore, useResetButtonStore } from '../../../../../store/usePlayButtonStore'; + +type PointWithDegree = { + position: [number, number, number]; + degree: number; +}; + +function RoboticArmAnimator({ HandleCallback, restPosition, ikSolver, targetBone, armBot, path }: any) { -function RoboticArmAnimator({ - HandleCallback, - restPosition, - ikSolver, - targetBone, - armBot, - path -}: any) { const progressRef = useRef(0); const curveRef = useRef(null); - const [currentPath, setCurrentPath] = useState<[number, number, number][]>([]); - const [circlePoints, setCirclePoints] = useState<[number, number, number][]>([]); - const [customCurvePoints, setCustomCurvePoints] = useState(null); - let curveHeight = 1.75 const totalDistanceRef = useRef(0); const startTimeRef = useRef(null); const segmentDistancesRef = useRef([]); + const [currentPath, setCurrentPath] = useState<[number, number, number][]>([]); + const [circlePoints, setCirclePoints] = useState<[number, number, number][]>([]); + const [circlePointsWithDegrees, setCirclePointsWithDegrees] = useState([]); + const [customCurvePoints, setCustomCurvePoints] = useState(null); + let curveHeight = 1.75 + const CIRCLE_RADIUS = 1.6 + // Zustand stores const { isPlaying } = usePlayButtonStore(); const { isPaused } = usePauseButtonStore(); const { isReset } = useResetButtonStore(); const { speed } = useAnimationPlaySpeed(); - const CIRCLE_RADIUS = 1.6 - // Update path state whenever `path` prop changes useEffect(() => { setCurrentPath(path); @@ -47,7 +42,7 @@ function RoboticArmAnimator({ //Handle Reset Animation useEffect(() => { - if (isReset) { + if (isReset || !isPlaying) { progressRef.current = 0; curveRef.current = null; setCurrentPath([]); @@ -77,8 +72,8 @@ function RoboticArmAnimator({ return points; } - - function generateRingPointsWithDegrees(radius: number, segments: number) { + //Generate CirclePoints with Angle + function generateRingPointsWithDegrees(radius: number, segments: number, initialRotation: [number, number, number]) { const points: { position: [number, number, number]; degree: number }[] = []; for (let i = 0; i < segments; i++) { const angleRadians = (i / segments) * Math.PI * 2; @@ -92,10 +87,12 @@ function RoboticArmAnimator({ } return points; } - useEffect(() => { - const points = generateRingPointsWithDegrees(CIRCLE_RADIUS, 64); - }, [armBot.position]); + // Handle circle points based on armBot position + useEffect(() => { + const points = generateRingPointsWithDegrees(CIRCLE_RADIUS, 64, armBot.rotation); + setCirclePointsWithDegrees(points) + }, [armBot.rotation]); // Function for find nearest Circlepoints Index const findNearestIndex = (nearestPoint: [number, number, number], points: [number, number, number][], epsilon = 1e-6) => { @@ -121,9 +118,34 @@ function RoboticArmAnimator({ }, circlePoints[0]); }; + // Helper function to collect points and check forbidden degrees + const collectArcPoints = (startIdx: number, endIdx: number, clockwise: boolean) => { + const totalSegments = 64; + const arcPoints: [number, number, number][] = []; + let i = startIdx; + + while (i !== (endIdx + (clockwise ? 1 : -1) + totalSegments) % totalSegments) { + const { degree, position } = circlePointsWithDegrees[i]; + // Skip over + arcPoints.push(position); + i = (i + (clockwise ? 1 : -1) + totalSegments) % totalSegments; + } + return arcPoints; + }; + + //Range to restrict angle + const hasForbiddenDegrees = (arc: [number, number, number][]) => { + return arc.some(p => { + const idx = findNearestIndex(p, circlePoints); + const degree = circlePointsWithDegrees[idx]?.degree || 0; + return degree >= 271 && degree <= 300; // Forbidden range: 271° to 300° + }); + }; + // Handle nearest points and final path (including arc points) useEffect(() => { if (circlePoints.length > 0 && currentPath.length > 0) { + const start = currentPath[0]; const end = currentPath[currentPath.length - 1]; @@ -140,33 +162,6 @@ function RoboticArmAnimator({ const clockwiseDistance = (indexOfNearestEnd - indexOfNearestStart + totalSegments) % totalSegments; const counterClockwiseDistance = (indexOfNearestStart - indexOfNearestEnd + totalSegments) % totalSegments; - // Prepare degrees - const degreesList = generateRingPointsWithDegrees(CIRCLE_RADIUS, totalSegments); - - // Helper function to collect points and check forbidden degrees - const collectArcPoints = (startIdx: number, endIdx: number, clockwise: boolean) => { - const arcPoints: [number, number, number][] = []; - let i = startIdx; - - while (i !== (endIdx + (clockwise ? 1 : -1) + totalSegments) % totalSegments) { - const { degree, position } = degreesList[i]; - - // Skip over - arcPoints.push(position); - i = (i + (clockwise ? 1 : -1) + totalSegments) % totalSegments; - } - return arcPoints; - }; - - const hasForbiddenDegrees = (arc: [number, number, number][]) => { - return arc.some(p => { - const idx = findNearestIndex(p, circlePoints); - const degree = degreesList[idx]?.degree || 0; - return degree >= 271 && degree <= 300; // Forbidden range: 271° to 300° - }); - }; - - // Try both directions const arcClockwise = collectArcPoints(indexOfNearestStart, indexOfNearestEnd, true); const arcCounterClockwise = collectArcPoints(indexOfNearestStart, indexOfNearestEnd, false); @@ -206,8 +201,6 @@ function RoboticArmAnimator({ } }, [circlePoints, currentPath]); - - // Frame update for animation useFrame((state, delta) => { if (!ikSolver) return; @@ -215,7 +208,6 @@ function RoboticArmAnimator({ const bone = ikSolver.mesh.skeleton.bones.find((b: any) => b.name === targetBone); if (!bone) return; - if (isPlaying) { if (isReset) { bone.position.copy(restPosition); @@ -260,12 +252,17 @@ function RoboticArmAnimator({ ikSolver.update(); } } else if (!isPlaying && currentPath.length === 0) { + progressRef.current = 0; + startTimeRef.current = null; + setCurrentPath([]); + setCustomCurvePoints([]); bone.position.copy(restPosition); + } ikSolver.update(); }); - + //Helper to Visible the Circle and Curve return ( <> {customCurvePoints && customCurvePoints?.length >= 2 && currentPath && isPlaying && ( @@ -278,50 +275,16 @@ function RoboticArmAnimator({ /> )} - + {/* Green ring */} - {/* Markers at 90°, 180°, 270°, 360° */} - {[90, 180, 270, 360].map((degree, index) => { - const rad = (degree * Math.PI) / 180; - const x = CIRCLE_RADIUS * Math.cos(rad); - const z = CIRCLE_RADIUS * Math.sin(rad); - const y = 0; // same plane as the ring (Y axis) - - return ( - - - - - ); - })} - - {/* Optional: Text Labels */} - - {[90, 180, 270, 360].map((degree, index) => { - const rad = (degree * Math.PI) / 180; - const x = CIRCLE_RADIUS * Math.cos(rad); - const z = CIRCLE_RADIUS * Math.sin(rad); - const y = 0.15; // lift the text slightly above the ring - - return ( - - {degree}° - - ); - })} - diff --git a/app/src/modules/simulation/roboticArm/instances/armInstance/roboticArmInstance.tsx b/app/src/modules/simulation/roboticArm/instances/armInstance/roboticArmInstance.tsx index ea6521b..035f25e 100644 --- a/app/src/modules/simulation/roboticArm/instances/armInstance/roboticArmInstance.tsx +++ b/app/src/modules/simulation/roboticArm/instances/armInstance/roboticArmInstance.tsx @@ -5,7 +5,6 @@ import { usePauseButtonStore, usePlayButtonStore, useResetButtonStore } from '.. import { useArmBotStore } from '../../../../../store/simulation/useArmBotStore'; import armModel from "../../../../../assets/gltf-glb/rigged/ik_arm_1.glb"; import { useThree } from "@react-three/fiber"; -import useModuleStore from '../../../../../store/useModuleStore'; import * as THREE from "three"; import MaterialAnimator from '../animator/materialAnimator'; @@ -24,16 +23,15 @@ function RoboticArmInstance({ armBot }: { armBot: ArmBotStatus }) { let startTime: number; //zustand const { addCurrentAction, setArmBotActive, setArmBotState, removeCurrentAction } = useArmBotStore(); - const { activeModule } = useModuleStore(); const { isPlaying } = usePlayButtonStore(); - const { isReset, setReset } = useResetButtonStore(); + const { isReset } = useResetButtonStore(); const { isPaused } = usePauseButtonStore(); - function firstFrame() { startTime = performance.now(); step(); } + function step() { if (isPausedRef.current) { if (!pauseTimeRef.current) { @@ -87,6 +85,7 @@ function RoboticArmInstance({ armBot }: { armBot: ArmBotStatus }) { logStatus(armBot.modelUuid, "Moving armBot from end point to rest position.") } } + useEffect(() => { isPausedRef.current = isPaused; }, [isPaused]); @@ -98,6 +97,7 @@ function RoboticArmInstance({ armBot }: { armBot: ArmBotStatus }) { setArmBotState(armBot.modelUuid, "idle") setCurrentPhase("init"); setPath([]) + setIkSolver(null); removeCurrentAction(armBot.modelUuid) isPausedRef.current = false pauseTimeRef.current = null @@ -117,17 +117,17 @@ function RoboticArmInstance({ armBot }: { armBot: ArmBotStatus }) { useEffect(() => { const targetMesh = scene?.getObjectByProperty("uuid", armBot.modelUuid); if (targetMesh) { - targetMesh.visible = activeModule !== "simulation" + targetMesh.visible = (!isPlaying) } const targetBones = ikSolver?.mesh.skeleton.bones.find((b: any) => b.name === targetBone); - if (isPlaying) { + if (!isReset && isPlaying) { //Moving armBot from initial point to rest position. if (!armBot?.isActive && armBot?.state == "idle" && currentPhase == "init") { - setArmBotActive(armBot.modelUuid, true) - setArmBotState(armBot.modelUuid, "running") - setCurrentPhase("init-to-rest"); if (targetBones) { - let curve = createCurveBetweenTwoPoints(targetBones.position, targetBones.position) + setArmBotActive(armBot.modelUuid, true) + setArmBotState(armBot.modelUuid, "running") + setCurrentPhase("init-to-rest"); + let curve = createCurveBetweenTwoPoints(targetBones.position, restPosition) if (curve) { setPath(curve.points.map(point => [point.x, point.y, point.z])); } @@ -142,9 +142,9 @@ function RoboticArmInstance({ armBot }: { armBot: ArmBotStatus }) { }, 3000); return () => clearTimeout(timeoutId); } + //Moving to pickup point else if (armBot && !armBot.isActive && armBot.state === "idle" && currentPhase === "rest" && armBot.currentAction) { if (armBot.currentAction) { - setArmBotActive(armBot.modelUuid, true); setArmBotState(armBot.modelUuid, "running"); setCurrentPhase("rest-to-start"); @@ -158,17 +158,20 @@ function RoboticArmInstance({ armBot }: { armBot: ArmBotStatus }) { } logStatus(armBot.modelUuid, "Moving armBot from rest point to start position.") } + // Moving to Pick to Drop position else if (armBot && !armBot.isActive && armBot.state === "idle" && currentPhase === "picking" && armBot.currentAction) { requestAnimationFrame(firstFrame); } + //Moving to drop point to restPosition else if (armBot && !armBot.isActive && armBot.state === "idle" && currentPhase === "dropping" && armBot.currentAction) { requestAnimationFrame(firstFrame); } - }else{ + } else { logStatus(armBot.modelUuid, "Simulation Play Exited") setArmBotActive(armBot.modelUuid, false) setArmBotState(armBot.modelUuid, "idle") setCurrentPhase("init"); + setIkSolver(null); setPath([]) isPausedRef.current = false pauseTimeRef.current = null @@ -177,7 +180,7 @@ function RoboticArmInstance({ armBot }: { armBot: ArmBotStatus }) { removeCurrentAction(armBot.modelUuid) } - }, [currentPhase, armBot, isPlaying, ikSolver]) + }, [currentPhase, armBot, isPlaying, isReset, ikSolver]) function createCurveBetweenTwoPoints(p1: any, p2: any) { @@ -224,9 +227,13 @@ function RoboticArmInstance({ armBot }: { armBot: ArmBotStatus }) { return ( <> - - + {!isReset && isPlaying && ( + <> + + + + )} ) diff --git a/app/src/modules/simulation/roboticArm/instances/ikInstance/ikInstance.tsx b/app/src/modules/simulation/roboticArm/instances/ikInstance/ikInstance.tsx index be41a95..4bd05a6 100644 --- a/app/src/modules/simulation/roboticArm/instances/ikInstance/ikInstance.tsx +++ b/app/src/modules/simulation/roboticArm/instances/ikInstance/ikInstance.tsx @@ -11,7 +11,7 @@ type IKInstanceProps = { modelUrl: string; ikSolver: any; setIkSolver: any - armBot: any; + armBot: ArmBotStatus; groupRef: any; }; function IKInstance({ modelUrl, setIkSolver, ikSolver, armBot, groupRef }: IKInstanceProps) { @@ -57,12 +57,6 @@ function IKInstance({ modelUrl, setIkSolver, ikSolver, armBot, groupRef }: IKIns rotationMin: new THREE.Vector3(0, 0, 0), rotationMax: new THREE.Vector3(2, 0, 0), }, - // { - // index: 1, - // enabled: true, - // rotationMin: new THREE.Vector3(0, -Math.PI, 0), - // rotationMax: new THREE.Vector3(0, Math.PI, 0), - // }, { index: 1, enabled: true, limitation: new THREE.Vector3(0, 1, 0) }, { index: 0, enabled: false, limitation: new THREE.Vector3(0, 0, 0) }, ], @@ -76,7 +70,7 @@ function IKInstance({ modelUrl, setIkSolver, ikSolver, armBot, groupRef }: IKIns setSelectedArm(OOI.Target_Bone); - scene.add(helper); + // scene.add(helper); }, [cloned, gltf, setIkSolver]); @@ -86,10 +80,10 @@ function IKInstance({ modelUrl, setIkSolver, ikSolver, armBot, groupRef }: IKIns setSelectedArm(groupRef.current?.getObjectByName(targetBoneName)) }}> {/* {selectedArm && } */} diff --git a/app/src/modules/simulation/roboticArm/roboticArm.tsx b/app/src/modules/simulation/roboticArm/roboticArm.tsx index e112884..91ccb55 100644 --- a/app/src/modules/simulation/roboticArm/roboticArm.tsx +++ b/app/src/modules/simulation/roboticArm/roboticArm.tsx @@ -1,24 +1,37 @@ -import { useEffect } from "react"; -import RoboticArmInstances from "./instances/roboticArmInstances"; +import { useEffect, useState } from "react"; import { useArmBotStore } from "../../../store/simulation/useArmBotStore"; -import { useSelectedEventData, useSelectedEventSphere } from "../../../store/simulation/useSimulationStore"; -import ArmBotUI from "../ui/arm/armBotUI"; +import { useSelectedEventSphere } from "../../../store/simulation/useSimulationStore"; import { usePlayButtonStore } from "../../../store/usePlayButtonStore"; +import ArmBotUI from "../ui/arm/armBotUI"; +import RoboticArmInstances from "./instances/roboticArmInstances"; function RoboticArm() { - const { armBots } = useArmBotStore(); + const { armBots, getArmBotById } = useArmBotStore(); const { selectedEventSphere } = useSelectedEventSphere(); - const { selectedEventData } = useSelectedEventData(); const { isPlaying } = usePlayButtonStore(); + const [isArmBotSelected, setIsArmBotSelected] = useState(false); useEffect(() => { // console.log('armBots: ', armBots); }, [armBots]) + useEffect(() => { + if (selectedEventSphere) { + const selectedArmBot = getArmBotById(selectedEventSphere.userData.modelUuid); + if (selectedArmBot) { + setIsArmBotSelected(true); + } else { + setIsArmBotSelected(false); + } + } + }, [selectedEventSphere]) + return ( <> + - {selectedEventSphere && selectedEventData?.data.type === "roboticArm" && !isPlaying && + + {isArmBotSelected && !isPlaying && < ArmBotUI /> } @@ -26,4 +39,4 @@ function RoboticArm() { ); } -export default RoboticArm; +export default RoboticArm; \ No newline at end of file diff --git a/app/src/modules/simulation/ui/arm/useDraggableGLTF.ts b/app/src/modules/simulation/ui/arm/useDraggableGLTF.ts index fdb271c..e1f7fbf 100644 --- a/app/src/modules/simulation/ui/arm/useDraggableGLTF.ts +++ b/app/src/modules/simulation/ui/arm/useDraggableGLTF.ts @@ -3,195 +3,200 @@ import * as THREE from "three"; import { ThreeEvent, useThree } from "@react-three/fiber"; import { useProductStore } from "../../../../store/simulation/useProductStore"; import { - useSelectedEventData, - useSelectedProduct, + useSelectedEventData, + useSelectedProduct, } from "../../../../store/simulation/useSimulationStore"; type OnUpdateCallback = (object: THREE.Object3D) => void; export default function useDraggableGLTF(onUpdate: OnUpdateCallback) { - const { getEventByModelUuid, updateAction, getActionByUuid } = - useProductStore(); - const { selectedEventData } = useSelectedEventData(); - const { selectedProduct } = useSelectedProduct(); - const { camera, gl, controls } = useThree(); - const activeObjRef = useRef(null); - const planeRef = useRef( - new THREE.Plane(new THREE.Vector3(0, 1, 0), 0) - ); - const offsetRef = useRef(new THREE.Vector3()); - const initialPositionRef = useRef(new THREE.Vector3()); + const { getEventByModelUuid, updateAction, getActionByUuid } = + useProductStore(); + const { selectedEventData } = useSelectedEventData(); + const { selectedProduct } = useSelectedProduct(); + const { camera, gl, controls } = useThree(); + const activeObjRef = useRef(null); + const planeRef = useRef( + new THREE.Plane(new THREE.Vector3(0, 1, 0), 0) + ); + const offsetRef = useRef(new THREE.Vector3()); + const initialPositionRef = useRef(new THREE.Vector3()); - const raycaster = new THREE.Raycaster(); - const pointer = new THREE.Vector2(); - const [objectWorldPos, setObjectWorldPos] = useState(new THREE.Vector3()); + const raycaster = new THREE.Raycaster(); + const pointer = new THREE.Vector2(); + const [objectWorldPos, setObjectWorldPos] = useState(new THREE.Vector3()); - const handlePointerDown = (e: ThreeEvent) => { - e.stopPropagation(); + const handlePointerDown = (e: ThreeEvent) => { + e.stopPropagation(); - let obj: THREE.Object3D | null = e.object; + let obj: THREE.Object3D | null = e.object; - // Traverse up until we find modelUuid in userData - while (obj && !obj.userData?.modelUuid) { - obj = obj.parent; - } - - if (!obj) return; - - // Disable orbit controls while dragging - if (controls) (controls as any).enabled = false; - - activeObjRef.current = obj; - initialPositionRef.current.copy(obj.position); - - // Get world position - setObjectWorldPos(obj.getWorldPosition(objectWorldPos)); - - // Set plane at the object's Y level - planeRef.current.set(new THREE.Vector3(0, 1, 0), -objectWorldPos.y); - - // Convert pointer to NDC - const rect = gl.domElement.getBoundingClientRect(); - pointer.x = ((e.clientX - rect.left) / rect.width) * 2 - 1; - pointer.y = -((e.clientY - rect.top) / rect.height) * 2 + 1; - - // Raycast to intersection - raycaster.setFromCamera(pointer, camera); - const intersection = new THREE.Vector3(); - raycaster.ray.intersectPlane(planeRef.current, intersection); - - // Calculate offset - offsetRef.current.copy(objectWorldPos).sub(intersection); - - // Start listening for drag - gl.domElement.addEventListener("pointermove", handlePointerMove); - gl.domElement.addEventListener("pointerup", handlePointerUp); - }; - - const handlePointerMove = (e: PointerEvent) => { - if (!activeObjRef.current) return; - if (selectedEventData?.data.type === "roboticArm") { - const selectedArmBot = getEventByModelUuid( - selectedProduct.productId, - selectedEventData.data.modelUuid - ); - if (!selectedArmBot) return; - // Check if Shift key is pressed - const isShiftKeyPressed = e.shiftKey; - - // Get the mouse position relative to the canvas - const rect = gl.domElement.getBoundingClientRect(); - pointer.x = ((e.clientX - rect.left) / rect.width) * 2 - 1; - pointer.y = -((e.clientY - rect.top) / rect.height) * 2 + 1; - - // Update raycaster to point to the mouse position - raycaster.setFromCamera(pointer, camera); - - // Create a vector to store intersection point - const intersection = new THREE.Vector3(); - const intersects = raycaster.ray.intersectPlane( - planeRef.current, - intersection - ); - if (!intersects) return; - - // Add offset for dragging - intersection.add(offsetRef.current); - - // Get the parent's world matrix if exists - const parent = activeObjRef.current.parent; - const targetPosition = new THREE.Vector3(); - - // OnPointerDown - initialPositionRef.current.copy(objectWorldPos); - - // OnPointerMove - if (isShiftKeyPressed) { - const { x: initialX, y: initialY } = initialPositionRef.current; - const { x: objectX, z: objectZ } = objectWorldPos; - - const deltaX = intersection.x - initialX; - - targetPosition.set(objectX, initialY + deltaX, objectZ); - } else { - // For free movement - targetPosition.copy(intersection); - } - - // CONSTRAIN MOVEMENT HERE: - const centerX = selectedArmBot.position[0]; - const centerZ = selectedArmBot.position[2]; - const minDistance = 1.2; - const maxDistance = 2; - - let deltaX = targetPosition.x - centerX; - let deltaZ = targetPosition.z - centerZ; - - // Calculate angle in radians - let angle = Math.atan2(deltaZ, deltaX); // [-PI, PI] - - // Convert angle to degrees - let angleDeg = (angle * 180) / Math.PI; // [-180, 180] - - // Normalize to [0, 360] - if (angleDeg < 0) { - angleDeg += 360; - } - - // Allow only angles between 90° and 270° - if (angleDeg < 0 || angleDeg > 270) { - // Clamp to nearest boundary (90° or 270°) - const distanceTo90 = Math.abs(angleDeg - 0); - const distanceTo270 = Math.abs(angleDeg - 270); - - if (distanceTo90 < distanceTo270) { - angleDeg = 0; - } else { - return - // angleDeg = 270; + // Traverse up until we find modelUuid in userData + while (obj && !obj.userData?.modelUuid) { + obj = obj.parent; } - // Update angle in radians - angle = (angleDeg * Math.PI) / 180; - } + if (!obj) return; - // Clamp distance - let distance = Math.sqrt(deltaX * deltaX + deltaZ * deltaZ); - const clampedDistance = Math.min( - Math.max(distance, minDistance), - maxDistance - ); + // Disable orbit controls while dragging + if (controls) (controls as any).enabled = false; - // Set final target position - targetPosition.x = centerX + Math.cos(angle) * clampedDistance; - targetPosition.z = centerZ + Math.sin(angle) * clampedDistance; + activeObjRef.current = obj; + initialPositionRef.current.copy(obj.position); - // Clamp Y axis if needed - targetPosition.y = Math.min(Math.max(targetPosition.y, 0.6), 1.9); + // Get world position + setObjectWorldPos(obj.getWorldPosition(objectWorldPos)); - // Convert to local if parent exists - if (parent) { - parent.worldToLocal(targetPosition); - } + // Set plane at the object's Y level + planeRef.current.set(new THREE.Vector3(0, 1, 0), -objectWorldPos.y); - // Update the object position - activeObjRef.current.position.copy(targetPosition); - } - }; + // Convert pointer to NDC + const rect = gl.domElement.getBoundingClientRect(); + pointer.x = ((e.clientX - rect.left) / rect.width) * 2 - 1; + pointer.y = -((e.clientY - rect.top) / rect.height) * 2 + 1; - const handlePointerUp = () => { - if (controls) (controls as any).enabled = true; + // Raycast to intersection + raycaster.setFromCamera(pointer, camera); + const intersection = new THREE.Vector3(); + raycaster.ray.intersectPlane(planeRef.current, intersection); - if (activeObjRef.current) { - // Pass the updated position to the onUpdate callback to persist it - onUpdate(activeObjRef.current); - } + // Calculate offset + offsetRef.current.copy(objectWorldPos).sub(intersection); - gl.domElement.removeEventListener("pointermove", handlePointerMove); - gl.domElement.removeEventListener("pointerup", handlePointerUp); + // Start listening for drag + gl.domElement.addEventListener("pointermove", handlePointerMove); + gl.domElement.addEventListener("pointerup", handlePointerUp); + }; - activeObjRef.current = null; - }; + const handlePointerMove = (e: PointerEvent) => { + if (!activeObjRef.current) return; + if (selectedEventData?.data.type === "roboticArm") { + const selectedArmBot = getEventByModelUuid( + selectedProduct.productId, + selectedEventData.data.modelUuid + ); + if (!selectedArmBot) return; + // Check if Shift key is pressed + const isShiftKeyPressed = e.shiftKey; - return { handlePointerDown }; + // Get the mouse position relative to the canvas + const rect = gl.domElement.getBoundingClientRect(); + pointer.x = ((e.clientX - rect.left) / rect.width) * 2 - 1; + pointer.y = -((e.clientY - rect.top) / rect.height) * 2 + 1; + + // Update raycaster to point to the mouse position + raycaster.setFromCamera(pointer, camera); + + // Create a vector to store intersection point + const intersection = new THREE.Vector3(); + const intersects = raycaster.ray.intersectPlane( + planeRef.current, + intersection + ); + if (!intersects) return; + + // Add offset for dragging + intersection.add(offsetRef.current); + + // Get the parent's world matrix if exists + const parent = activeObjRef.current.parent; + const targetPosition = new THREE.Vector3(); + + // OnPointerDown + initialPositionRef.current.copy(objectWorldPos); + + // OnPointerMove + if (isShiftKeyPressed) { + const { x: initialX, y: initialY } = initialPositionRef.current; + const { x: objectX, z: objectZ } = objectWorldPos; + + const deltaX = intersection.x - initialX; + + targetPosition.set(objectX, initialY + deltaX, objectZ); + } else { + // For free movement + targetPosition.copy(intersection); + } + + // CONSTRAIN MOVEMENT HERE: + const centerX = selectedArmBot.position[0]; + const centerZ = selectedArmBot.position[2]; + const minDistance = 1.2; + const maxDistance = 2; + + const delta = new THREE.Vector3(targetPosition.x - centerX, 0, targetPosition.z - centerZ); + + // Create quaternion from rotation + const robotEuler = new THREE.Euler(selectedArmBot.rotation[0], selectedArmBot.rotation[1], selectedArmBot.rotation[2]); + const robotQuaternion = new THREE.Quaternion().setFromEuler(robotEuler); + + // Inverse rotate + const inverseQuaternion = robotQuaternion.clone().invert(); + delta.applyQuaternion(inverseQuaternion); + + // Angle in robot local space + let relativeAngle = Math.atan2(delta.z, delta.x); + let angleDeg = (relativeAngle * 180) / Math.PI; + if (angleDeg < 0) { + angleDeg += 360; + } + + // Clamp angle + if (angleDeg < 0 || angleDeg > 270) { + const distanceTo90 = Math.abs(angleDeg - 0); + const distanceTo270 = Math.abs(angleDeg - 270); + if (distanceTo90 < distanceTo270) { + angleDeg = 0; + } else { + return; + } + relativeAngle = (angleDeg * Math.PI) / 180; + } + + // Distance clamp + const distance = delta.length(); + const clampedDistance = Math.min(Math.max(distance, minDistance), maxDistance); + + // Calculate local target + const finalLocal = new THREE.Vector3( + Math.cos(relativeAngle) * clampedDistance, + 0, + Math.sin(relativeAngle) * clampedDistance + ); + + // Rotate back to world space + finalLocal.applyQuaternion(robotQuaternion); + + targetPosition.x = centerX + finalLocal.x; + targetPosition.z = centerZ + finalLocal.z; + + + // Clamp Y axis if needed + targetPosition.y = Math.min(Math.max(targetPosition.y, 0.6), 1.9); + + // Convert to local if parent exists + if (parent) { + parent.worldToLocal(targetPosition); + } + + // Update the object position + activeObjRef.current.position.copy(targetPosition); + } + }; + + const handlePointerUp = () => { + if (controls) (controls as any).enabled = true; + + if (activeObjRef.current) { + // Pass the updated position to the onUpdate callback to persist it + onUpdate(activeObjRef.current); + } + + gl.domElement.removeEventListener("pointermove", handlePointerMove); + gl.domElement.removeEventListener("pointerup", handlePointerUp); + + activeObjRef.current = null; + }; + + return { handlePointerDown }; }