v2 #80
|
@ -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 {
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -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} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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} />} */}
|
||||||
|
|
|
@ -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;
|
|
@ -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 };
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue