v2 #80

Merged
Vishnu merged 11 commits from v2 into main 2025-05-06 13:55:10 +00:00
8 changed files with 296 additions and 325 deletions
Showing only changes of commit 4ac77e462c - Show all commits

View File

@ -28,22 +28,11 @@ function RoboticArmMechanics() {
selectedEventData.data.modelUuid, selectedEventData.data.modelUuid,
selectedEventData.selectedPoint selectedEventData.selectedPoint
) as RoboticArmPointSchema | undefined; ) as RoboticArmPointSchema | undefined;
const action = getActionByUuid(selectedProduct.productId, selectedAction.actionId) as RoboticArmPointSchema["actions"][0] | undefined; if (point?.actions) {
if (action) { setSelectedPointData(point);
if (point?.actions) { if (point.actions.length > 0) {
setSelectedPointData(point); setActiveOption(point.actions[0].actionType as "default" | "pickAndPlace");
if (point.actions.length > 0 && action) { setSelectedAction(point.actions[0].actionUuid, point.actions[0].actionName);
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);
}
} }
} }
} else { } else {

View File

@ -32,7 +32,7 @@ export function MaterialModel({ materialType, matRef, ...props }: ModelProps) {
<group ref={matRef} {...props} dispose={null}> <group ref={matRef} {...props} dispose={null}>
<primitive <primitive
object={cloned} object={cloned}
scale={[0.25, 0.25, 0.25]} scale={[0.4, 0.4, 0.4]}
/> />
</group> </group>
); );

View File

@ -1,17 +1,16 @@
import { useFrame } from '@react-three/fiber'; import { useFrame } from '@react-three/fiber';
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import * as THREE from 'three'; import * as THREE from 'three';
import { useLogger } from '../../../../../components/ui/log/LoggerContext'; import { MaterialModel } from '../../../materials/instances/material/materialModel';
type MaterialAnimatorProps = { type MaterialAnimatorProps = {
ikSolver: any; ikSolver: any;
armBot: any; armBot: ArmBotStatus;
currentPhase: string; currentPhase: string;
}; };
export default function MaterialAnimator({ ikSolver, armBot, currentPhase }: MaterialAnimatorProps) { export default function MaterialAnimator({ ikSolver, armBot, currentPhase }: MaterialAnimatorProps) {
const sphereRef = useRef<THREE.Mesh>(null); const sphereRef = useRef<any>(null);
const boneWorldPosition = new THREE.Vector3();
const [isRendered, setIsRendered] = useState<boolean>(false); const [isRendered, setIsRendered] = useState<boolean>(false);
useEffect(() => { useEffect(() => {
@ -41,7 +40,8 @@ export default function MaterialAnimator({ ikSolver, armBot, currentPhase }: Mat
const direction = new THREE.Vector3(); const direction = new THREE.Vector3();
direction.subVectors(boneWorldPos, boneTargetWorldPos).normalize(); direction.subVectors(boneWorldPos, boneTargetWorldPos).normalize();
const downwardDirection = direction.clone().negate(); const downwardDirection = direction.clone().negate();
const adjustedPosition = boneWorldPos.clone().addScaledVector(downwardDirection, -0.25);
const adjustedPosition = boneWorldPos.clone().addScaledVector(downwardDirection, -0.01);
//set position //set position
sphereRef.current.position.copy(adjustedPosition); sphereRef.current.position.copy(adjustedPosition);
@ -56,10 +56,10 @@ export default function MaterialAnimator({ ikSolver, armBot, currentPhase }: Mat
return ( return (
<> <>
{isRendered && ( {isRendered && (
<mesh ref={sphereRef}> <MaterialModel
<boxGeometry args={[0.5, 0.5, 0.5]} /> matRef={sphereRef}
<meshStandardMaterial color="yellow" /> materialType={armBot.currentAction?.materialType || 'Default material'}
</mesh> />
)} )}
</> </>
); );

View File

@ -2,38 +2,33 @@ import React, { useEffect, useMemo, useRef, useState } from 'react';
import { useFrame } from '@react-three/fiber'; import { useFrame } from '@react-three/fiber';
import * as THREE from 'three'; import * as THREE from 'three';
import { Line, Text } from '@react-three/drei'; import { Line, Text } from '@react-three/drei';
import { import { useAnimationPlaySpeed, usePauseButtonStore, usePlayButtonStore, useResetButtonStore } from '../../../../../store/usePlayButtonStore';
useAnimationPlaySpeed,
usePauseButtonStore, type PointWithDegree = {
usePlayButtonStore, position: [number, number, number];
useResetButtonStore degree: number;
} from '../../../../../store/usePlayButtonStore'; };
function RoboticArmAnimator({ HandleCallback, restPosition, ikSolver, targetBone, armBot, path }: any) {
function RoboticArmAnimator({
HandleCallback,
restPosition,
ikSolver,
targetBone,
armBot,
path
}: any) {
const progressRef = useRef(0); const progressRef = useRef(0);
const curveRef = useRef<THREE.Vector3[] | null>(null); const curveRef = useRef<THREE.Vector3[] | null>(null);
const [currentPath, setCurrentPath] = useState<[number, number, number][]>([]);
const [circlePoints, setCirclePoints] = useState<[number, number, number][]>([]);
const [customCurvePoints, setCustomCurvePoints] = useState<THREE.Vector3[] | null>(null);
let curveHeight = 1.75
const totalDistanceRef = useRef(0); const totalDistanceRef = useRef(0);
const startTimeRef = useRef<number | null>(null); const startTimeRef = useRef<number | null>(null);
const segmentDistancesRef = useRef<number[]>([]); const segmentDistancesRef = useRef<number[]>([]);
const [currentPath, setCurrentPath] = useState<[number, number, number][]>([]);
const [circlePoints, setCirclePoints] = useState<[number, number, number][]>([]);
const [circlePointsWithDegrees, setCirclePointsWithDegrees] = useState<PointWithDegree[]>([]);
const [customCurvePoints, setCustomCurvePoints] = useState<THREE.Vector3[] | null>(null);
let curveHeight = 1.75
const CIRCLE_RADIUS = 1.6
// Zustand stores // Zustand stores
const { isPlaying } = usePlayButtonStore(); const { isPlaying } = usePlayButtonStore();
const { isPaused } = usePauseButtonStore(); const { isPaused } = usePauseButtonStore();
const { isReset } = useResetButtonStore(); const { isReset } = useResetButtonStore();
const { speed } = useAnimationPlaySpeed(); const { speed } = useAnimationPlaySpeed();
const CIRCLE_RADIUS = 1.6
// Update path state whenever `path` prop changes // Update path state whenever `path` prop changes
useEffect(() => { useEffect(() => {
setCurrentPath(path); setCurrentPath(path);
@ -47,7 +42,7 @@ function RoboticArmAnimator({
//Handle Reset Animation //Handle Reset Animation
useEffect(() => { useEffect(() => {
if (isReset) { if (isReset || !isPlaying) {
progressRef.current = 0; progressRef.current = 0;
curveRef.current = null; curveRef.current = null;
setCurrentPath([]); setCurrentPath([]);
@ -77,8 +72,8 @@ function RoboticArmAnimator({
return points; return points;
} }
//Generate CirclePoints with Angle
function generateRingPointsWithDegrees(radius: number, segments: number) { function generateRingPointsWithDegrees(radius: number, segments: number, initialRotation: [number, number, number]) {
const points: { position: [number, number, number]; degree: number }[] = []; const points: { position: [number, number, number]; degree: number }[] = [];
for (let i = 0; i < segments; i++) { for (let i = 0; i < segments; i++) {
const angleRadians = (i / segments) * Math.PI * 2; const angleRadians = (i / segments) * Math.PI * 2;
@ -92,10 +87,12 @@ function RoboticArmAnimator({
} }
return points; 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 // Function for find nearest Circlepoints Index
const findNearestIndex = (nearestPoint: [number, number, number], points: [number, number, number][], epsilon = 1e-6) => { const findNearestIndex = (nearestPoint: [number, number, number], points: [number, number, number][], epsilon = 1e-6) => {
@ -121,9 +118,34 @@ function RoboticArmAnimator({
}, circlePoints[0]); }, 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) // Handle nearest points and final path (including arc points)
useEffect(() => { useEffect(() => {
if (circlePoints.length > 0 && currentPath.length > 0) { if (circlePoints.length > 0 && currentPath.length > 0) {
const start = currentPath[0]; const start = currentPath[0];
const end = currentPath[currentPath.length - 1]; const end = currentPath[currentPath.length - 1];
@ -140,33 +162,6 @@ function RoboticArmAnimator({
const clockwiseDistance = (indexOfNearestEnd - indexOfNearestStart + totalSegments) % totalSegments; const clockwiseDistance = (indexOfNearestEnd - indexOfNearestStart + totalSegments) % totalSegments;
const counterClockwiseDistance = (indexOfNearestStart - indexOfNearestEnd + 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 // Try both directions
const arcClockwise = collectArcPoints(indexOfNearestStart, indexOfNearestEnd, true); const arcClockwise = collectArcPoints(indexOfNearestStart, indexOfNearestEnd, true);
const arcCounterClockwise = collectArcPoints(indexOfNearestStart, indexOfNearestEnd, false); const arcCounterClockwise = collectArcPoints(indexOfNearestStart, indexOfNearestEnd, false);
@ -206,8 +201,6 @@ function RoboticArmAnimator({
} }
}, [circlePoints, currentPath]); }, [circlePoints, currentPath]);
// Frame update for animation // Frame update for animation
useFrame((state, delta) => { useFrame((state, delta) => {
if (!ikSolver) return; if (!ikSolver) return;
@ -215,7 +208,6 @@ function RoboticArmAnimator({
const bone = ikSolver.mesh.skeleton.bones.find((b: any) => b.name === targetBone); const bone = ikSolver.mesh.skeleton.bones.find((b: any) => b.name === targetBone);
if (!bone) return; if (!bone) return;
if (isPlaying) { if (isPlaying) {
if (isReset) { if (isReset) {
bone.position.copy(restPosition); bone.position.copy(restPosition);
@ -260,12 +252,17 @@ function RoboticArmAnimator({
ikSolver.update(); ikSolver.update();
} }
} else if (!isPlaying && currentPath.length === 0) { } else if (!isPlaying && currentPath.length === 0) {
progressRef.current = 0;
startTimeRef.current = null;
setCurrentPath([]);
setCustomCurvePoints([]);
bone.position.copy(restPosition); bone.position.copy(restPosition);
} }
ikSolver.update(); ikSolver.update();
}); });
//Helper to Visible the Circle and Curve
return ( return (
<> <>
{customCurvePoints && customCurvePoints?.length >= 2 && currentPath && isPlaying && ( {customCurvePoints && customCurvePoints?.length >= 2 && currentPath && isPlaying && (
@ -278,50 +275,16 @@ function RoboticArmAnimator({
/> />
</mesh> </mesh>
)} )}
<group position={[armBot.position[0], armBot.position[1] + 1.5, armBot.position[2]]}> <group
position={[armBot.position[0], armBot.position[1] + 1.5, armBot.position[2]]}
rotation={[armBot.rotation[0], armBot.rotation[1], armBot.rotation[2]]}
>
{/* Green ring */} {/* Green ring */}
<mesh rotation={[-Math.PI / 2, 0, 0]}> <mesh rotation={[-Math.PI / 2, 0, 0]}>
<ringGeometry args={[CIRCLE_RADIUS, CIRCLE_RADIUS + 0.02, 64]} /> <ringGeometry args={[CIRCLE_RADIUS, CIRCLE_RADIUS + 0.02, 64]} />
<meshBasicMaterial color="green" side={THREE.DoubleSide} /> <meshBasicMaterial color="green" side={THREE.DoubleSide} />
</mesh> </mesh>
{/* 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 (
<mesh key={index} position={[x, y, z]}>
<sphereGeometry args={[0.05, 16, 16]} />
<meshStandardMaterial color="red" />
</mesh>
);
})}
{/* 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 (
<Text
key={`text-${index}`}
position={[x, y, z]}
fontSize={0.2}
color="yellow"
anchorX="center"
anchorY="middle"
>
{degree}°
</Text>
);
})}
</group> </group>
</> </>

View File

@ -5,7 +5,6 @@ import { usePauseButtonStore, usePlayButtonStore, useResetButtonStore } from '..
import { useArmBotStore } from '../../../../../store/simulation/useArmBotStore'; import { useArmBotStore } from '../../../../../store/simulation/useArmBotStore';
import armModel from "../../../../../assets/gltf-glb/rigged/ik_arm_1.glb"; import armModel from "../../../../../assets/gltf-glb/rigged/ik_arm_1.glb";
import { useThree } from "@react-three/fiber"; import { useThree } from "@react-three/fiber";
import useModuleStore from '../../../../../store/useModuleStore';
import * as THREE from "three"; import * as THREE from "three";
import MaterialAnimator from '../animator/materialAnimator'; import MaterialAnimator from '../animator/materialAnimator';
@ -24,16 +23,15 @@ function RoboticArmInstance({ armBot }: { armBot: ArmBotStatus }) {
let startTime: number; let startTime: number;
//zustand //zustand
const { addCurrentAction, setArmBotActive, setArmBotState, removeCurrentAction } = useArmBotStore(); const { addCurrentAction, setArmBotActive, setArmBotState, removeCurrentAction } = useArmBotStore();
const { activeModule } = useModuleStore();
const { isPlaying } = usePlayButtonStore(); const { isPlaying } = usePlayButtonStore();
const { isReset, setReset } = useResetButtonStore(); const { isReset } = useResetButtonStore();
const { isPaused } = usePauseButtonStore(); const { isPaused } = usePauseButtonStore();
function firstFrame() { function firstFrame() {
startTime = performance.now(); startTime = performance.now();
step(); step();
} }
function step() { function step() {
if (isPausedRef.current) { if (isPausedRef.current) {
if (!pauseTimeRef.current) { if (!pauseTimeRef.current) {
@ -87,6 +85,7 @@ function RoboticArmInstance({ armBot }: { armBot: ArmBotStatus }) {
logStatus(armBot.modelUuid, "Moving armBot from end point to rest position.") logStatus(armBot.modelUuid, "Moving armBot from end point to rest position.")
} }
} }
useEffect(() => { useEffect(() => {
isPausedRef.current = isPaused; isPausedRef.current = isPaused;
}, [isPaused]); }, [isPaused]);
@ -98,6 +97,7 @@ function RoboticArmInstance({ armBot }: { armBot: ArmBotStatus }) {
setArmBotState(armBot.modelUuid, "idle") setArmBotState(armBot.modelUuid, "idle")
setCurrentPhase("init"); setCurrentPhase("init");
setPath([]) setPath([])
setIkSolver(null);
removeCurrentAction(armBot.modelUuid) removeCurrentAction(armBot.modelUuid)
isPausedRef.current = false isPausedRef.current = false
pauseTimeRef.current = null pauseTimeRef.current = null
@ -117,17 +117,17 @@ function RoboticArmInstance({ armBot }: { armBot: ArmBotStatus }) {
useEffect(() => { useEffect(() => {
const targetMesh = scene?.getObjectByProperty("uuid", armBot.modelUuid); const targetMesh = scene?.getObjectByProperty("uuid", armBot.modelUuid);
if (targetMesh) { if (targetMesh) {
targetMesh.visible = activeModule !== "simulation" targetMesh.visible = (!isPlaying)
} }
const targetBones = ikSolver?.mesh.skeleton.bones.find((b: any) => b.name === targetBone); 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. //Moving armBot from initial point to rest position.
if (!armBot?.isActive && armBot?.state == "idle" && currentPhase == "init") { if (!armBot?.isActive && armBot?.state == "idle" && currentPhase == "init") {
setArmBotActive(armBot.modelUuid, true)
setArmBotState(armBot.modelUuid, "running")
setCurrentPhase("init-to-rest");
if (targetBones) { 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) { if (curve) {
setPath(curve.points.map(point => [point.x, point.y, point.z])); setPath(curve.points.map(point => [point.x, point.y, point.z]));
} }
@ -142,9 +142,9 @@ function RoboticArmInstance({ armBot }: { armBot: ArmBotStatus }) {
}, 3000); }, 3000);
return () => clearTimeout(timeoutId); return () => clearTimeout(timeoutId);
} }
//Moving to pickup point
else if (armBot && !armBot.isActive && armBot.state === "idle" && currentPhase === "rest" && armBot.currentAction) { else if (armBot && !armBot.isActive && armBot.state === "idle" && currentPhase === "rest" && armBot.currentAction) {
if (armBot.currentAction) { if (armBot.currentAction) {
setArmBotActive(armBot.modelUuid, true); setArmBotActive(armBot.modelUuid, true);
setArmBotState(armBot.modelUuid, "running"); setArmBotState(armBot.modelUuid, "running");
setCurrentPhase("rest-to-start"); setCurrentPhase("rest-to-start");
@ -158,17 +158,20 @@ function RoboticArmInstance({ armBot }: { armBot: ArmBotStatus }) {
} }
logStatus(armBot.modelUuid, "Moving armBot from rest point to start position.") 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) { else if (armBot && !armBot.isActive && armBot.state === "idle" && currentPhase === "picking" && armBot.currentAction) {
requestAnimationFrame(firstFrame); requestAnimationFrame(firstFrame);
} }
//Moving to drop point to restPosition
else if (armBot && !armBot.isActive && armBot.state === "idle" && currentPhase === "dropping" && armBot.currentAction) { else if (armBot && !armBot.isActive && armBot.state === "idle" && currentPhase === "dropping" && armBot.currentAction) {
requestAnimationFrame(firstFrame); requestAnimationFrame(firstFrame);
} }
}else{ } else {
logStatus(armBot.modelUuid, "Simulation Play Exited") logStatus(armBot.modelUuid, "Simulation Play Exited")
setArmBotActive(armBot.modelUuid, false) setArmBotActive(armBot.modelUuid, false)
setArmBotState(armBot.modelUuid, "idle") setArmBotState(armBot.modelUuid, "idle")
setCurrentPhase("init"); setCurrentPhase("init");
setIkSolver(null);
setPath([]) setPath([])
isPausedRef.current = false isPausedRef.current = false
pauseTimeRef.current = null pauseTimeRef.current = null
@ -177,7 +180,7 @@ function RoboticArmInstance({ armBot }: { armBot: ArmBotStatus }) {
removeCurrentAction(armBot.modelUuid) removeCurrentAction(armBot.modelUuid)
} }
}, [currentPhase, armBot, isPlaying, ikSolver]) }, [currentPhase, armBot, isPlaying, isReset, ikSolver])
function createCurveBetweenTwoPoints(p1: any, p2: any) { function createCurveBetweenTwoPoints(p1: any, p2: any) {
@ -224,9 +227,13 @@ function RoboticArmInstance({ armBot }: { armBot: ArmBotStatus }) {
return ( return (
<> <>
<IKInstance modelUrl={armModel} setIkSolver={setIkSolver} ikSolver={ikSolver} armBot={armBot} groupRef={groupRef} /> {!isReset && isPlaying && (
<RoboticArmAnimator HandleCallback={HandleCallback} restPosition={restPosition} ikSolver={ikSolver} targetBone={targetBone} armBot={armBot} <>
logStatus={logStatus} path={path} currentPhase={currentPhase} /> <IKInstance modelUrl={armModel} setIkSolver={setIkSolver} ikSolver={ikSolver} armBot={armBot} groupRef={groupRef} />
<RoboticArmAnimator HandleCallback={HandleCallback} restPosition={restPosition} ikSolver={ikSolver} targetBone={targetBone} armBot={armBot}
logStatus={logStatus} path={path} currentPhase={currentPhase} />
</>
)}
<MaterialAnimator ikSolver={ikSolver} armBot={armBot} currentPhase={currentPhase} /> <MaterialAnimator ikSolver={ikSolver} armBot={armBot} currentPhase={currentPhase} />
</> </>
) )

View File

@ -11,7 +11,7 @@ type IKInstanceProps = {
modelUrl: string; modelUrl: string;
ikSolver: any; ikSolver: any;
setIkSolver: any setIkSolver: any
armBot: any; armBot: ArmBotStatus;
groupRef: any; groupRef: any;
}; };
function IKInstance({ modelUrl, setIkSolver, ikSolver, armBot, groupRef }: IKInstanceProps) { 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), rotationMin: new THREE.Vector3(0, 0, 0),
rotationMax: new THREE.Vector3(2, 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: 1, enabled: true, limitation: new THREE.Vector3(0, 1, 0) },
{ index: 0, enabled: false, limitation: new THREE.Vector3(0, 0, 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); setSelectedArm(OOI.Target_Bone);
scene.add(helper); // scene.add(helper);
}, [cloned, gltf, setIkSolver]); }, [cloned, gltf, setIkSolver]);
@ -86,10 +80,10 @@ function IKInstance({ modelUrl, setIkSolver, ikSolver, armBot, groupRef }: IKIns
setSelectedArm(groupRef.current?.getObjectByName(targetBoneName)) setSelectedArm(groupRef.current?.getObjectByName(targetBoneName))
}}> }}>
<primitive <primitive
uuid={"ArmBot-X200"} uuid={armBot.modelUuid}
object={cloned} object={cloned}
scale={[1, 1, 1]} scale={[1, 1, 1]}
name={`arm-bot11`} name={armBot.modelName}
/> />
</group> </group>
{/* {selectedArm && <TransformControls object={selectedArm} />} */} {/* {selectedArm && <TransformControls object={selectedArm} />} */}

View File

@ -1,24 +1,37 @@
import { useEffect } from "react"; import { useEffect, useState } from "react";
import RoboticArmInstances from "./instances/roboticArmInstances";
import { useArmBotStore } from "../../../store/simulation/useArmBotStore"; import { useArmBotStore } from "../../../store/simulation/useArmBotStore";
import { useSelectedEventData, useSelectedEventSphere } from "../../../store/simulation/useSimulationStore"; import { useSelectedEventSphere } from "../../../store/simulation/useSimulationStore";
import ArmBotUI from "../ui/arm/armBotUI";
import { usePlayButtonStore } from "../../../store/usePlayButtonStore"; import { usePlayButtonStore } from "../../../store/usePlayButtonStore";
import ArmBotUI from "../ui/arm/armBotUI";
import RoboticArmInstances from "./instances/roboticArmInstances";
function RoboticArm() { function RoboticArm() {
const { armBots } = useArmBotStore(); const { armBots, getArmBotById } = useArmBotStore();
const { selectedEventSphere } = useSelectedEventSphere(); const { selectedEventSphere } = useSelectedEventSphere();
const { selectedEventData } = useSelectedEventData();
const { isPlaying } = usePlayButtonStore(); const { isPlaying } = usePlayButtonStore();
const [isArmBotSelected, setIsArmBotSelected] = useState(false);
useEffect(() => { useEffect(() => {
// console.log('armBots: ', armBots); // console.log('armBots: ', armBots);
}, [armBots]) }, [armBots])
useEffect(() => {
if (selectedEventSphere) {
const selectedArmBot = getArmBotById(selectedEventSphere.userData.modelUuid);
if (selectedArmBot) {
setIsArmBotSelected(true);
} else {
setIsArmBotSelected(false);
}
}
}, [selectedEventSphere])
return ( return (
<> <>
<RoboticArmInstances /> <RoboticArmInstances />
{selectedEventSphere && selectedEventData?.data.type === "roboticArm" && !isPlaying &&
{isArmBotSelected && !isPlaying &&
< ArmBotUI /> < ArmBotUI />
} }
@ -26,4 +39,4 @@ function RoboticArm() {
); );
} }
export default RoboticArm; export default RoboticArm;

View File

@ -3,195 +3,200 @@ import * as THREE from "three";
import { ThreeEvent, useThree } from "@react-three/fiber"; import { ThreeEvent, useThree } from "@react-three/fiber";
import { useProductStore } from "../../../../store/simulation/useProductStore"; import { useProductStore } from "../../../../store/simulation/useProductStore";
import { import {
useSelectedEventData, useSelectedEventData,
useSelectedProduct, useSelectedProduct,
} from "../../../../store/simulation/useSimulationStore"; } from "../../../../store/simulation/useSimulationStore";
type OnUpdateCallback = (object: THREE.Object3D) => void; type OnUpdateCallback = (object: THREE.Object3D) => void;
export default function useDraggableGLTF(onUpdate: OnUpdateCallback) { export default function useDraggableGLTF(onUpdate: OnUpdateCallback) {
const { getEventByModelUuid, updateAction, getActionByUuid } = const { getEventByModelUuid, updateAction, getActionByUuid } =
useProductStore(); useProductStore();
const { selectedEventData } = useSelectedEventData(); const { selectedEventData } = useSelectedEventData();
const { selectedProduct } = useSelectedProduct(); const { selectedProduct } = useSelectedProduct();
const { camera, gl, controls } = useThree(); const { camera, gl, controls } = useThree();
const activeObjRef = useRef<THREE.Object3D | null>(null); const activeObjRef = useRef<THREE.Object3D | null>(null);
const planeRef = useRef<THREE.Plane>( const planeRef = useRef<THREE.Plane>(
new THREE.Plane(new THREE.Vector3(0, 1, 0), 0) new THREE.Plane(new THREE.Vector3(0, 1, 0), 0)
); );
const offsetRef = useRef<THREE.Vector3>(new THREE.Vector3()); const offsetRef = useRef<THREE.Vector3>(new THREE.Vector3());
const initialPositionRef = useRef<THREE.Vector3>(new THREE.Vector3()); const initialPositionRef = useRef<THREE.Vector3>(new THREE.Vector3());
const raycaster = new THREE.Raycaster(); const raycaster = new THREE.Raycaster();
const pointer = new THREE.Vector2(); const pointer = new THREE.Vector2();
const [objectWorldPos, setObjectWorldPos] = useState(new THREE.Vector3()); const [objectWorldPos, setObjectWorldPos] = useState(new THREE.Vector3());
const handlePointerDown = (e: ThreeEvent<PointerEvent>) => { const handlePointerDown = (e: ThreeEvent<PointerEvent>) => {
e.stopPropagation(); e.stopPropagation();
let obj: THREE.Object3D | null = e.object; let obj: THREE.Object3D | null = e.object;
// Traverse up until we find modelUuid in userData // Traverse up until we find modelUuid in userData
while (obj && !obj.userData?.modelUuid) { while (obj && !obj.userData?.modelUuid) {
obj = obj.parent; 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;
} }
// Update angle in radians if (!obj) return;
angle = (angleDeg * Math.PI) / 180;
}
// Clamp distance // Disable orbit controls while dragging
let distance = Math.sqrt(deltaX * deltaX + deltaZ * deltaZ); if (controls) (controls as any).enabled = false;
const clampedDistance = Math.min(
Math.max(distance, minDistance),
maxDistance
);
// Set final target position activeObjRef.current = obj;
targetPosition.x = centerX + Math.cos(angle) * clampedDistance; initialPositionRef.current.copy(obj.position);
targetPosition.z = centerZ + Math.sin(angle) * clampedDistance;
// Clamp Y axis if needed // Get world position
targetPosition.y = Math.min(Math.max(targetPosition.y, 0.6), 1.9); setObjectWorldPos(obj.getWorldPosition(objectWorldPos));
// Convert to local if parent exists // Set plane at the object's Y level
if (parent) { planeRef.current.set(new THREE.Vector3(0, 1, 0), -objectWorldPos.y);
parent.worldToLocal(targetPosition);
}
// Update the object position // Convert pointer to NDC
activeObjRef.current.position.copy(targetPosition); 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 = () => { // Raycast to intersection
if (controls) (controls as any).enabled = true; raycaster.setFromCamera(pointer, camera);
const intersection = new THREE.Vector3();
raycaster.ray.intersectPlane(planeRef.current, intersection);
if (activeObjRef.current) { // Calculate offset
// Pass the updated position to the onUpdate callback to persist it offsetRef.current.copy(objectWorldPos).sub(intersection);
onUpdate(activeObjRef.current);
}
gl.domElement.removeEventListener("pointermove", handlePointerMove); // Start listening for drag
gl.domElement.removeEventListener("pointerup", handlePointerUp); 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 };
} }