first commit
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useFrame } from '@react-three/fiber';
|
||||
import { usePauseButtonStore, usePlayButtonStore, useResetButtonStore } from '../../../../store/usePlayButtonStore';
|
||||
import { useSceneContext } from '../../../scene/sceneContext';
|
||||
|
||||
type ArmBotCallback = {
|
||||
armBotId: string;
|
||||
callback: () => void;
|
||||
};
|
||||
|
||||
export function useArmBotEventManager() {
|
||||
const {armBotStore} = useSceneContext();
|
||||
const { getArmBotById } = armBotStore();
|
||||
const callbacksRef = useRef<ArmBotCallback[]>([]);
|
||||
const isMonitoringRef = useRef(false);
|
||||
const { isPlaying } = usePlayButtonStore();
|
||||
const { isPaused } = usePauseButtonStore();
|
||||
const { isReset } = useResetButtonStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (isReset) {
|
||||
callbacksRef.current = [];
|
||||
}
|
||||
}, [isReset])
|
||||
|
||||
// Add a new armbot to monitor
|
||||
const addArmBotToMonitor = (armBotId: string, callback: () => void) => {
|
||||
// Avoid duplicates
|
||||
if (!callbacksRef.current.some((entry) => entry.armBotId === armBotId)) {
|
||||
callbacksRef.current.push({ armBotId, callback });
|
||||
}
|
||||
|
||||
// Start monitoring if not already running
|
||||
if (!isMonitoringRef.current) {
|
||||
isMonitoringRef.current = true;
|
||||
}
|
||||
};
|
||||
|
||||
// Remove an armbot from monitoring (e.g., when unmounted)
|
||||
const removeArmBotFromMonitor = (armBotId: string) => {
|
||||
callbacksRef.current = callbacksRef.current.filter(
|
||||
(entry) => entry.armBotId !== armBotId
|
||||
);
|
||||
|
||||
// Stop monitoring if no more armbots to track
|
||||
if (callbacksRef.current.length === 0) {
|
||||
isMonitoringRef.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Check armbot states every frame
|
||||
useFrame(() => {
|
||||
if (!isMonitoringRef.current || callbacksRef.current.length === 0 || !isPlaying || isPaused) return;
|
||||
|
||||
callbacksRef.current.forEach(({ armBotId, callback }) => {
|
||||
const armBot = getArmBotById(armBotId);
|
||||
if (armBot?.isActive === false && armBot?.state === 'idle') {
|
||||
callback();
|
||||
removeArmBotFromMonitor(armBotId); // Remove after triggering
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Cleanup on unmount (optional, since useFrame auto-cleans)
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
callbacksRef.current = [];
|
||||
isMonitoringRef.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
addArmBotToMonitor,
|
||||
removeArmBotFromMonitor,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { useFrame } from '@react-three/fiber';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import * as THREE from 'three';
|
||||
import { MaterialModel } from '../../../materials/instances/material/materialModel';
|
||||
|
||||
type MaterialAnimatorProps = {
|
||||
ikSolver: any;
|
||||
armBot: ArmBotStatus;
|
||||
currentPhase: string;
|
||||
};
|
||||
|
||||
export default function MaterialAnimator({ ikSolver, armBot, currentPhase }: Readonly<MaterialAnimatorProps>) {
|
||||
const materialRef = useRef<any>(null);
|
||||
const [isRendered, setIsRendered] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentPhase === "start-to-end" || currentPhase === "dropping") {
|
||||
setIsRendered(true);
|
||||
} else {
|
||||
setIsRendered(false);
|
||||
}
|
||||
}, [currentPhase]);
|
||||
|
||||
useFrame(() => {
|
||||
if (!ikSolver || !materialRef.current) return;
|
||||
|
||||
const boneTarget = ikSolver.mesh.skeleton.bones.find((b: any) => b.name === "Bone004");
|
||||
const bone = ikSolver.mesh.skeleton.bones.find((b: any) => b.name === "Effector");
|
||||
|
||||
if (!boneTarget || !bone) return;
|
||||
|
||||
if (currentPhase === "start-to-end") {
|
||||
// Get world positions
|
||||
const boneTargetWorldPos = new THREE.Vector3();
|
||||
const boneWorldPos = new THREE.Vector3();
|
||||
boneTarget.getWorldPosition(boneTargetWorldPos);
|
||||
bone.getWorldPosition(boneWorldPos);
|
||||
|
||||
// Calculate direction
|
||||
const direction = new THREE.Vector3();
|
||||
direction.subVectors(boneWorldPos, boneTargetWorldPos).normalize();
|
||||
|
||||
const adjustedPosition = boneWorldPos.clone().addScaledVector(direction, 0.01);
|
||||
|
||||
//set position
|
||||
materialRef.current.position.copy(adjustedPosition);
|
||||
|
||||
// Set rotation
|
||||
const worldQuaternion = new THREE.Quaternion();
|
||||
bone.getWorldQuaternion(worldQuaternion);
|
||||
materialRef.current.quaternion.copy(worldQuaternion);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{isRendered && (
|
||||
<MaterialModel
|
||||
materialId={armBot.currentAction?.materialId ?? ''}
|
||||
matRef={materialRef}
|
||||
materialType={armBot.currentAction?.materialType ?? 'Default material'}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useFrame, useThree } from '@react-three/fiber';
|
||||
import * as THREE from 'three';
|
||||
import { Line, Text } from '@react-three/drei';
|
||||
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) {
|
||||
const { scene } = useThree();
|
||||
const progressRef = useRef(0);
|
||||
const curveRef = useRef<THREE.Vector3[] | null>(null);
|
||||
const totalDistanceRef = useRef(0);
|
||||
const startTimeRef = useRef<number | null>(null);
|
||||
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
|
||||
const { isPlaying } = usePlayButtonStore();
|
||||
const { isPaused } = usePauseButtonStore();
|
||||
const { isReset } = useResetButtonStore();
|
||||
const { speed } = useAnimationPlaySpeed();
|
||||
|
||||
// Update path state whenever `path` prop changes
|
||||
useEffect(() => {
|
||||
setCurrentPath(path);
|
||||
}, [path]);
|
||||
|
||||
// Handle circle points based on armBot position
|
||||
useEffect(() => {
|
||||
const points = generateRingPoints(CIRCLE_RADIUS, 64)
|
||||
setCirclePoints(points);
|
||||
}, [armBot.position]);
|
||||
|
||||
//Handle Reset Animation
|
||||
useEffect(() => {
|
||||
if (isReset || !isPlaying) {
|
||||
progressRef.current = 0;
|
||||
curveRef.current = null;
|
||||
setCurrentPath([]);
|
||||
setCustomCurvePoints(null);
|
||||
totalDistanceRef.current = 0;
|
||||
startTimeRef.current = null;
|
||||
segmentDistancesRef.current = [];
|
||||
if (!ikSolver) return
|
||||
const bone = ikSolver.mesh.skeleton.bones.find((b: any) => b.name === targetBone);
|
||||
if (!bone) return;
|
||||
bone.position.copy(restPosition)
|
||||
ikSolver.update();
|
||||
}
|
||||
}, [isReset, isPlaying])
|
||||
|
||||
//Generate Circle Points
|
||||
function generateRingPoints(radius: any, segments: any) {
|
||||
const points: [number, number, number][] = [];
|
||||
for (let i = 0; i < segments; i++) {
|
||||
// Calculate angle for current segment
|
||||
const angle = (i / segments) * Math.PI * 2;
|
||||
// Calculate x and z coordinates (y remains the same for a flat ring)
|
||||
const x = Math.cos(angle) * radius;
|
||||
const z = Math.sin(angle) * radius;
|
||||
points.push([x, 1.5, z]);
|
||||
}
|
||||
return points;
|
||||
}
|
||||
|
||||
//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;
|
||||
const x = Math.cos(angleRadians) * radius;
|
||||
const z = Math.sin(angleRadians) * radius;
|
||||
const degree = (angleRadians * 180) / Math.PI; // Convert radians to degrees
|
||||
points.push({
|
||||
position: [x, 1.5, z],
|
||||
degree,
|
||||
});
|
||||
}
|
||||
return points;
|
||||
}
|
||||
|
||||
// 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) => {
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
const [x, y, z] = points[i];
|
||||
if (
|
||||
Math.abs(x - nearestPoint[0]) < epsilon &&
|
||||
Math.abs(y - nearestPoint[1]) < epsilon &&
|
||||
Math.abs(z - nearestPoint[2]) < epsilon
|
||||
) {
|
||||
return i; // Found the matching index
|
||||
}
|
||||
}
|
||||
return -1; // Not found
|
||||
};
|
||||
|
||||
//function to find nearest Circlepoints
|
||||
const findNearest = (target: [number, number, number]) => {
|
||||
return circlePoints.reduce((nearest, point) => {
|
||||
const distance = Math.hypot(target[0] - point[0], target[1] - point[1], target[2] - point[2]);
|
||||
const nearestDistance = Math.hypot(target[0] - nearest[0], target[1] - nearest[1], target[2] - nearest[2]);
|
||||
return distance < nearestDistance ? point : nearest;
|
||||
}, 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];
|
||||
|
||||
const raisedStart = [start[0], start[1] + 0.5, start[2]] as [number, number, number];
|
||||
const raisedEnd = [end[0], end[1] + 0.5, end[2]] as [number, number, number];
|
||||
|
||||
const nearestToStart = findNearest(raisedStart);
|
||||
const nearestToEnd = findNearest(raisedEnd);
|
||||
|
||||
const indexOfNearestStart = findNearestIndex(nearestToStart, circlePoints);
|
||||
const indexOfNearestEnd = findNearestIndex(nearestToEnd, circlePoints);
|
||||
|
||||
const totalSegments = 64;
|
||||
const clockwiseDistance = (indexOfNearestEnd - indexOfNearestStart + totalSegments) % totalSegments;
|
||||
const counterClockwiseDistance = (indexOfNearestStart - indexOfNearestEnd + totalSegments) % totalSegments;
|
||||
|
||||
// Try both directions
|
||||
const arcClockwise = collectArcPoints(indexOfNearestStart, indexOfNearestEnd, true);
|
||||
const arcCounterClockwise = collectArcPoints(indexOfNearestStart, indexOfNearestEnd, false);
|
||||
|
||||
const clockwiseForbidden = hasForbiddenDegrees(arcClockwise);
|
||||
const counterClockwiseForbidden = hasForbiddenDegrees(arcCounterClockwise);
|
||||
|
||||
let arcPoints: [number, number, number][] = [];
|
||||
|
||||
if (!clockwiseForbidden && (clockwiseDistance <= counterClockwiseDistance || counterClockwiseForbidden)) {
|
||||
arcPoints = arcClockwise;
|
||||
} else {
|
||||
arcPoints = arcCounterClockwise;
|
||||
}
|
||||
|
||||
const pathVectors = [
|
||||
new THREE.Vector3(start[0], start[1], start[2]),
|
||||
new THREE.Vector3(start[0], curveHeight, start[2]),
|
||||
new THREE.Vector3(nearestToStart[0], curveHeight, nearestToStart[2]),
|
||||
...arcPoints.map(point => new THREE.Vector3(point[0], curveHeight, point[2])),
|
||||
new THREE.Vector3(nearestToEnd[0], curveHeight, nearestToEnd[2]),
|
||||
new THREE.Vector3(end[0], curveHeight, end[2]),
|
||||
new THREE.Vector3(end[0], end[1], end[2])
|
||||
];
|
||||
|
||||
const pathSegments: [THREE.Vector3, THREE.Vector3][] = [];
|
||||
for (let i = 0; i < pathVectors.length - 1; i++) {
|
||||
pathSegments.push([pathVectors[i], pathVectors[i + 1]]);
|
||||
}
|
||||
|
||||
const segmentDistances = pathSegments.map(([p1, p2]) => p1.distanceTo(p2));
|
||||
segmentDistancesRef.current = segmentDistances;
|
||||
const totalDistance = segmentDistances.reduce((sum, d) => sum + d, 0);
|
||||
totalDistanceRef.current = totalDistance;
|
||||
|
||||
setCustomCurvePoints(pathVectors);
|
||||
}
|
||||
}, [circlePoints, currentPath]);
|
||||
|
||||
// Frame update for animation
|
||||
useFrame((state, delta) => {
|
||||
const targetMesh = scene?.getObjectByProperty("uuid", armBot.modelUuid);
|
||||
if (targetMesh) {
|
||||
targetMesh.visible = (!isPlaying)
|
||||
}
|
||||
if (!ikSolver) return;
|
||||
|
||||
const bone = ikSolver.mesh.skeleton.bones.find((b: any) => b.name === targetBone);
|
||||
|
||||
if (!bone) return;
|
||||
if (isPlaying) {
|
||||
if (!isPaused && customCurvePoints && customCurvePoints.length > 0) {
|
||||
const distances = segmentDistancesRef.current;
|
||||
const totalDistance = totalDistanceRef.current;
|
||||
|
||||
progressRef.current += delta * (speed * armBot.speed);
|
||||
const coveredDistance = progressRef.current;
|
||||
|
||||
let index = 0;
|
||||
let accumulatedDistance = 0;
|
||||
|
||||
while (index < distances.length && coveredDistance > accumulatedDistance + distances[index]) {
|
||||
accumulatedDistance += distances[index];
|
||||
index++;
|
||||
}
|
||||
|
||||
if (index < distances.length) {
|
||||
const startPoint = customCurvePoints[index];
|
||||
const endPoint = customCurvePoints[index + 1];
|
||||
const segmentDistance = distances[index];
|
||||
const t = (coveredDistance - accumulatedDistance) / segmentDistance;
|
||||
if (startPoint && endPoint) {
|
||||
const position = startPoint.clone().lerp(endPoint, t);
|
||||
bone.position.copy(position);
|
||||
}
|
||||
}
|
||||
|
||||
if (progressRef.current >= totalDistance) {
|
||||
HandleCallback();
|
||||
setCurrentPath([]);
|
||||
setCustomCurvePoints([]);
|
||||
curveRef.current = null;
|
||||
progressRef.current = 0;
|
||||
startTimeRef.current = null;
|
||||
}
|
||||
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 && (
|
||||
<mesh rotation={armBot.rotation} position={armBot.position} visible={false}>
|
||||
<Line
|
||||
points={customCurvePoints.map((p) => [p.x, p.y, p.z] as [number, number, number])}
|
||||
color="green"
|
||||
lineWidth={5}
|
||||
dashed={false}
|
||||
/>
|
||||
</mesh>
|
||||
)}
|
||||
<group
|
||||
position={[armBot.position[0], armBot.position[1] + 1.5, armBot.position[2]]}
|
||||
rotation={[armBot.rotation[0], armBot.rotation[1], armBot.rotation[2]]}
|
||||
visible={false}
|
||||
>
|
||||
{/* Green ring */}
|
||||
<mesh rotation={[-Math.PI / 2, 0, 0]} visible={false}>
|
||||
<ringGeometry args={[CIRCLE_RADIUS, CIRCLE_RADIUS + 0.02, 64]} />
|
||||
<meshBasicMaterial color="green" side={THREE.DoubleSide} />
|
||||
</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>
|
||||
);
|
||||
})}
|
||||
|
||||
{[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>
|
||||
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default RoboticArmAnimator;
|
||||
@@ -0,0 +1,413 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import * as THREE from "three";
|
||||
import { useThree } from "@react-three/fiber";
|
||||
import IKInstance from '../ikInstance/ikInstance';
|
||||
import RoboticArmAnimator from '../animator/roboticArmAnimator';
|
||||
import MaterialAnimator from '../animator/materialAnimator';
|
||||
import armModel from "../../../../../assets/gltf-glb/rigged/ik_arm_1.glb";
|
||||
import { useAnimationPlaySpeed, usePauseButtonStore, usePlayButtonStore, useResetButtonStore } from '../../../../../store/usePlayButtonStore';
|
||||
import { useProductStore } from '../../../../../store/simulation/useProductStore';
|
||||
import { useTriggerHandler } from '../../../triggers/triggerHandler/useTriggerHandler';
|
||||
import { useSceneContext } from '../../../../scene/sceneContext';
|
||||
import { useProductContext } from '../../../products/productContext';
|
||||
|
||||
function RoboticArmInstance({ armBot }: { readonly armBot: ArmBotStatus }) {
|
||||
|
||||
const [currentPhase, setCurrentPhase] = useState<(string)>("init");
|
||||
const [path, setPath] = useState<[number, number, number][]>([]);
|
||||
const [ikSolver, setIkSolver] = useState<any>(null);
|
||||
const { scene } = useThree();
|
||||
const restPosition = new THREE.Vector3(0, 1.75, -1.6);
|
||||
const targetBone = "Target";
|
||||
const groupRef = useRef<any>(null);
|
||||
const pauseTimeRef = useRef<number | null>(null);
|
||||
const isPausedRef = useRef<boolean>(false);
|
||||
const isSpeedRef = useRef<any>(null);
|
||||
let startTime: number;
|
||||
|
||||
const { selectedProductStore } = useProductContext();
|
||||
const { materialStore, armBotStore, vehicleStore, storageUnitStore } = useSceneContext();
|
||||
const { setArmBotActive, setArmBotState, removeCurrentAction, incrementActiveTime, incrementIdleTime } = armBotStore();
|
||||
const { decrementVehicleLoad, removeLastMaterial } = vehicleStore();
|
||||
const { removeLastMaterial: removeLastStorageMaterial, updateCurrentLoad } = storageUnitStore();
|
||||
const { getMaterialById, setIsVisible, setIsPaused } = materialStore();
|
||||
const { selectedProduct } = selectedProductStore();
|
||||
const { getActionByUuid, getEventByActionUuid, getEventByModelUuid } = useProductStore();
|
||||
const { triggerPointActions } = useTriggerHandler();
|
||||
const { isPlaying } = usePlayButtonStore();
|
||||
const { isReset } = useResetButtonStore();
|
||||
const { isPaused } = usePauseButtonStore();
|
||||
const { speed } = useAnimationPlaySpeed();
|
||||
|
||||
const activeSecondsElapsed = useRef(0);
|
||||
const idleSecondsElapsed = useRef(0);
|
||||
|
||||
const animationFrameIdRef = useRef<number | null>(null);
|
||||
const previousTimeRef = useRef<number | null>(null);
|
||||
|
||||
const lastRemoved = useRef<{ type: string, materialId: string } | null>(null);
|
||||
|
||||
function firstFrame() {
|
||||
startTime = performance.now();
|
||||
step();
|
||||
}
|
||||
|
||||
const action = getActionByUuid(selectedProduct.productUuid, armBot.currentAction?.actionUuid || '');
|
||||
|
||||
const handlePickUpTrigger = () => {
|
||||
if (armBot.currentAction && armBot.currentAction.materialId) {
|
||||
const material = getMaterialById(armBot.currentAction.materialId);
|
||||
if (material && material.previous && material.previous.modelUuid) {
|
||||
const previousModel = getEventByActionUuid(selectedProduct.productUuid, material.previous.actionUuid);
|
||||
if (previousModel) {
|
||||
if (previousModel.type === 'transfer') {
|
||||
setIsVisible(armBot.currentAction.materialId, false);
|
||||
} else if (previousModel.type === 'machine') {
|
||||
// machine specific logic
|
||||
} else if (previousModel.type === 'vehicle') {
|
||||
decrementVehicleLoad(previousModel.modelUuid, 1);
|
||||
removeLastMaterial(previousModel.modelUuid);
|
||||
} else if (previousModel.type === 'storageUnit') {
|
||||
// storage unit logic
|
||||
removeLastStorageMaterial(previousModel.modelUuid);
|
||||
updateCurrentLoad(previousModel.modelUuid, -1)
|
||||
}
|
||||
lastRemoved.current = { type: previousModel.type, materialId: armBot.currentAction.materialId };
|
||||
} else {
|
||||
setIsVisible(armBot.currentAction.materialId, false);
|
||||
}
|
||||
} else {
|
||||
setIsVisible(armBot.currentAction.materialId, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleDropTrigger = () => {
|
||||
|
||||
if (armBot.currentAction) {
|
||||
const action = getActionByUuid(selectedProduct.productUuid, armBot.currentAction.actionUuid);
|
||||
const model = getEventByModelUuid(selectedProduct.productUuid, action?.triggers[0]?.triggeredAsset?.triggeredModel.modelUuid || '');
|
||||
if (action && action.triggers[0]?.triggeredAsset?.triggeredModel.modelUuid) {
|
||||
if (!model) return;
|
||||
if (model.type === 'transfer') {
|
||||
setIsVisible(armBot.currentAction.materialId || '', true);
|
||||
} else if (model.type === 'machine') {
|
||||
//
|
||||
} else if (model.type === 'vehicle') {
|
||||
//
|
||||
} else if (model.type === 'storageUnit') {
|
||||
//
|
||||
}
|
||||
}
|
||||
if (action && armBot.currentAction.materialId) {
|
||||
triggerPointActions(action, armBot.currentAction.materialId)
|
||||
removeCurrentAction(armBot.modelUuid)
|
||||
}
|
||||
|
||||
if (lastRemoved.current) {
|
||||
if (lastRemoved.current.type === 'transfer') {
|
||||
setIsPaused(lastRemoved.current.materialId, true)
|
||||
} else {
|
||||
setIsPaused(lastRemoved.current.materialId, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function step() {
|
||||
if (isPausedRef.current) {
|
||||
if (!pauseTimeRef.current) {
|
||||
pauseTimeRef.current = performance.now();
|
||||
}
|
||||
requestAnimationFrame(() => step());
|
||||
return;
|
||||
}
|
||||
if (pauseTimeRef.current) {
|
||||
const pauseDuration = performance.now() - pauseTimeRef.current;
|
||||
startTime += pauseDuration;
|
||||
pauseTimeRef.current = null;
|
||||
}
|
||||
const elapsedTime = performance.now() - startTime;
|
||||
if (elapsedTime < 1000) {
|
||||
// Wait until 1500ms has passed
|
||||
requestAnimationFrame(step);
|
||||
return;
|
||||
}
|
||||
if (currentPhase === "picking") {
|
||||
setArmBotActive(armBot.modelUuid, true);
|
||||
setArmBotState(armBot.modelUuid, "running");
|
||||
setCurrentPhase("start-to-end");
|
||||
startTime = 0
|
||||
if (!action) return;
|
||||
const startPoint = (action as RoboticArmAction).process.startPoint;
|
||||
const endPoint = (action as RoboticArmAction).process.endPoint;
|
||||
if (startPoint && endPoint) {
|
||||
let curve = createCurveBetweenTwoPoints(
|
||||
new THREE.Vector3(startPoint[0], startPoint[1], startPoint[2]),
|
||||
new THREE.Vector3(endPoint[0], endPoint[1], endPoint[2]));
|
||||
if (curve) {
|
||||
logStatus(armBot.modelUuid, "picking the object");
|
||||
setPath(curve.points.map(point => [point.x, point.y, point.z]))
|
||||
|
||||
handlePickUpTrigger();
|
||||
|
||||
}
|
||||
}
|
||||
logStatus(armBot.modelUuid, "Moving armBot from start point to end position.")
|
||||
} else if (currentPhase === "dropping") {
|
||||
setArmBotActive(armBot.modelUuid, true);
|
||||
setArmBotState(armBot.modelUuid, "running");
|
||||
setCurrentPhase("end-to-rest");
|
||||
startTime = 0;
|
||||
if (!action) return;
|
||||
const endPoint = (action as RoboticArmAction).process.endPoint;
|
||||
if (endPoint) {
|
||||
|
||||
let curve = createCurveBetweenTwoPoints(new THREE.Vector3(endPoint[0], endPoint[1], endPoint[2]), restPosition);
|
||||
if (curve) {
|
||||
logStatus(armBot.modelUuid, "dropping the object");
|
||||
setPath(curve.points.map(point => [point.x, point.y, point.z]));
|
||||
|
||||
handleDropTrigger();
|
||||
|
||||
}
|
||||
}
|
||||
logStatus(armBot.modelUuid, "Moving armBot from end point to rest position.")
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
isPausedRef.current = isPaused;
|
||||
}, [isPaused]);
|
||||
|
||||
useEffect(() => {
|
||||
isSpeedRef.current = speed;
|
||||
}, [speed]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isReset || !isPlaying) {
|
||||
logStatus(armBot.modelUuid, "Simulation Play Reset Successfully")
|
||||
setArmBotActive(armBot.modelUuid, false)
|
||||
setArmBotState(armBot.modelUuid, "idle")
|
||||
setCurrentPhase("init");
|
||||
setPath([])
|
||||
setIkSolver(null);
|
||||
removeCurrentAction(armBot.modelUuid)
|
||||
isPausedRef.current = false
|
||||
pauseTimeRef.current = null
|
||||
startTime = 0
|
||||
activeSecondsElapsed.current = 0;
|
||||
idleSecondsElapsed.current = 0;
|
||||
previousTimeRef.current = null;
|
||||
|
||||
if (animationFrameIdRef.current !== null) {
|
||||
cancelAnimationFrame(animationFrameIdRef.current);
|
||||
animationFrameIdRef.current = null;
|
||||
}
|
||||
const targetBones = ikSolver?.mesh.skeleton.bones.find((b: any) => b.name === targetBone
|
||||
);
|
||||
if (targetBones && isPlaying) {
|
||||
let curve = createCurveBetweenTwoPoints(targetBones.position, restPosition)
|
||||
if (curve) {
|
||||
setPath(curve.points.map(point => [point.x, point.y, point.z]));
|
||||
logStatus(armBot.modelUuid, "Moving armBot from initial point to rest position.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [isReset, isPlaying])
|
||||
|
||||
|
||||
function animate(currentTime: number) {
|
||||
if (previousTimeRef.current === null) {
|
||||
previousTimeRef.current = currentTime;
|
||||
}
|
||||
const deltaTime = (currentTime - previousTimeRef.current) / 1000;
|
||||
previousTimeRef.current = currentTime;
|
||||
if (armBot.isActive) {
|
||||
if (!isPausedRef.current) {
|
||||
activeSecondsElapsed.current += deltaTime * isSpeedRef.current;
|
||||
// console.log(' activeSecondsElapsed.current: ', activeSecondsElapsed.current);
|
||||
}
|
||||
} else {
|
||||
if (!isPausedRef.current) {
|
||||
idleSecondsElapsed.current += deltaTime * isSpeedRef.current;
|
||||
// console.log('idleSecondsElapsed.current: ', idleSecondsElapsed.current);
|
||||
}
|
||||
}
|
||||
animationFrameIdRef.current = requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!isPlaying) return
|
||||
|
||||
if (!armBot.isActive && armBot.state === "idle" && (currentPhase === "rest" || currentPhase === "init")) {
|
||||
cancelAnimationFrame(animationFrameIdRef.current!);
|
||||
animationFrameIdRef.current = null;
|
||||
const roundedActiveTime = Math.round(activeSecondsElapsed.current); // Get the final rounded active time
|
||||
// console.log('🚨Final Active Time:',armBot.modelUuid, roundedActiveTime, 'seconds');
|
||||
incrementActiveTime(armBot.modelUuid, roundedActiveTime);
|
||||
activeSecondsElapsed.current = 0;
|
||||
|
||||
} else if (armBot.isActive && armBot.state !== "idle" && currentPhase !== "rest" && armBot.currentAction) {
|
||||
cancelAnimationFrame(animationFrameIdRef.current!);
|
||||
animationFrameIdRef.current = null;
|
||||
const roundedIdleTime = Math.round(idleSecondsElapsed.current); // Get the final rounded idle time
|
||||
// console.log('🕒 Final Idle Time:', armBot.modelUuid,roundedIdleTime, 'seconds');
|
||||
incrementIdleTime(armBot.modelUuid, roundedIdleTime);
|
||||
idleSecondsElapsed.current = 0;
|
||||
|
||||
}
|
||||
if (animationFrameIdRef.current === null) {
|
||||
animationFrameIdRef.current = requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (animationFrameIdRef.current !== null) {
|
||||
cancelAnimationFrame(animationFrameIdRef.current);
|
||||
animationFrameIdRef.current = null; // Reset the animation frame ID
|
||||
}
|
||||
};
|
||||
|
||||
}, [armBot.isActive, armBot.state, currentPhase])
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const targetMesh = scene?.getObjectByProperty("uuid", armBot.modelUuid);
|
||||
if (targetMesh) {
|
||||
targetMesh.visible = (!isPlaying)
|
||||
}
|
||||
const targetBones = ikSolver?.mesh.skeleton.bones.find((b: any) => b.name === targetBone);
|
||||
if (!isReset && isPlaying) {
|
||||
//Moving armBot from initial point to rest position.
|
||||
if (!armBot?.isActive && armBot?.state == "idle" && currentPhase == "init") {
|
||||
if (targetBones) {
|
||||
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]));
|
||||
}
|
||||
}
|
||||
logStatus(armBot.modelUuid, "Moving armBot from initial point to rest position.")
|
||||
}
|
||||
//Waiting for trigger.
|
||||
else if (armBot && !armBot.isActive && armBot.state === "idle" && currentPhase === "rest" && !armBot.currentAction) {
|
||||
logStatus(armBot.modelUuid, "Waiting to trigger CurrentAction")
|
||||
}
|
||||
//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");
|
||||
if (!action) return;
|
||||
const startPoint = (action as RoboticArmAction).process.startPoint;
|
||||
if (startPoint) {
|
||||
let curve = createCurveBetweenTwoPoints(targetBones.position, new THREE.Vector3(startPoint[0], startPoint[1], startPoint[2]));
|
||||
if (curve) {
|
||||
setPath(curve.points.map(point => [point.x, point.y, point.z]));
|
||||
}
|
||||
}
|
||||
}
|
||||
logStatus(armBot.modelUuid, "Moving armBot from rest point to start position.")
|
||||
}
|
||||
// Moving to Pick to Drop position
|
||||
else if (armBot && !armBot.isActive && armBot.state === "running" && currentPhase === "picking" && armBot.currentAction) {
|
||||
requestAnimationFrame(firstFrame);
|
||||
}
|
||||
//Moving to drop point to restPosition
|
||||
else if (armBot && !armBot.isActive && armBot.state === "running" && currentPhase === "dropping" && armBot.currentAction) {
|
||||
requestAnimationFrame(firstFrame);
|
||||
}
|
||||
} 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
|
||||
isPausedRef.current = false
|
||||
startTime = 0
|
||||
activeSecondsElapsed.current = 0;
|
||||
idleSecondsElapsed.current = 0;
|
||||
previousTimeRef.current = null;
|
||||
if (animationFrameIdRef.current !== null) {
|
||||
cancelAnimationFrame(animationFrameIdRef.current);
|
||||
animationFrameIdRef.current = null;
|
||||
}
|
||||
removeCurrentAction(armBot.modelUuid)
|
||||
}
|
||||
|
||||
}, [currentPhase, armBot, isPlaying, isReset, ikSolver])
|
||||
|
||||
|
||||
function createCurveBetweenTwoPoints(p1: any, p2: any) {
|
||||
const mid = new THREE.Vector3().addVectors(p1, p2).multiplyScalar(0.5);
|
||||
const points = [p1, mid, p2];
|
||||
return new THREE.CatmullRomCurve3(points);
|
||||
}
|
||||
|
||||
const HandleCallback = () => {
|
||||
|
||||
if (armBot.isActive && armBot.state == "running" && currentPhase == "init-to-rest") {
|
||||
logStatus(armBot.modelUuid, "Callback triggered: rest");
|
||||
setArmBotActive(armBot.modelUuid, false)
|
||||
setArmBotState(armBot.modelUuid, "idle")
|
||||
setCurrentPhase("rest");
|
||||
setPath([])
|
||||
}
|
||||
else if (armBot.isActive && armBot.state == "running" && currentPhase == "rest-to-start") {
|
||||
logStatus(armBot.modelUuid, "Callback triggered: pick.");
|
||||
setArmBotActive(armBot.modelUuid, false)
|
||||
setArmBotState(armBot.modelUuid, "running")
|
||||
setCurrentPhase("picking");
|
||||
setPath([])
|
||||
}
|
||||
else if (armBot.isActive && armBot.state == "running" && currentPhase == "start-to-end") {
|
||||
logStatus(armBot.modelUuid, "Callback triggered: drop.");
|
||||
setArmBotActive(armBot.modelUuid, false)
|
||||
setArmBotState(armBot.modelUuid, "running")
|
||||
setCurrentPhase("dropping");
|
||||
setPath([])
|
||||
}
|
||||
else if (armBot.isActive && armBot.state == "running" && currentPhase == "end-to-rest") {
|
||||
logStatus(armBot.modelUuid, "Callback triggered: rest, cycle completed.");
|
||||
setArmBotActive(armBot.modelUuid, false)
|
||||
setArmBotState(armBot.modelUuid, "idle")
|
||||
setCurrentPhase("rest");
|
||||
setPath([])
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
const logStatus = (id: string, status: string) => {
|
||||
// console.log('status: ', status);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isReset && isPlaying && (
|
||||
<>
|
||||
<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} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default RoboticArmInstance;
|
||||
@@ -0,0 +1,94 @@
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import * as THREE from "three";
|
||||
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader";
|
||||
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
|
||||
import { clone } from "three/examples/jsm/utils/SkeletonUtils";
|
||||
import { useLoader, useThree } from "@react-three/fiber";
|
||||
import { CCDIKSolver, CCDIKHelper, } from "three/examples/jsm/animation/CCDIKSolver";
|
||||
import { TransformControls } from '@react-three/drei';
|
||||
|
||||
type IKInstanceProps = {
|
||||
modelUrl: string;
|
||||
ikSolver: any;
|
||||
setIkSolver: any
|
||||
armBot: ArmBotStatus;
|
||||
groupRef: any;
|
||||
};
|
||||
function IKInstance({ modelUrl, setIkSolver, ikSolver, armBot, groupRef }: IKInstanceProps) {
|
||||
const { scene } = useThree()
|
||||
const gltf = useLoader(GLTFLoader, modelUrl, (loader) => {
|
||||
const draco = new DRACOLoader();
|
||||
draco.setDecoderPath("https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/libs/draco/");
|
||||
loader.setDRACOLoader(draco);
|
||||
});
|
||||
const cloned = useMemo(() => clone(gltf?.scene), [gltf]);
|
||||
const targetBoneName = "Target";
|
||||
const skinnedMeshName = "link_0";
|
||||
const [selectedArm, setSelectedArm] = useState<THREE.Group>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!gltf) return;
|
||||
const OOI: any = {};
|
||||
cloned.traverse((n: any) => {
|
||||
if (n.name === targetBoneName) OOI.Target_Bone = n;
|
||||
if (n.name === skinnedMeshName) OOI.Skinned_Mesh = n;
|
||||
});
|
||||
if (!OOI.Target_Bone || !OOI.Skinned_Mesh) return;
|
||||
const iks = [
|
||||
{
|
||||
target: 7,
|
||||
effector: 6,
|
||||
links: [
|
||||
{
|
||||
index: 5,
|
||||
enabled: true,
|
||||
rotationMin: new THREE.Vector3(-Math.PI / 2, 0, 0),
|
||||
rotationMax: new THREE.Vector3(Math.PI / 2, 0, 0),
|
||||
},
|
||||
{
|
||||
index: 4,
|
||||
enabled: true,
|
||||
rotationMin: new THREE.Vector3(-Math.PI / 2, 0, 0),
|
||||
rotationMax: new THREE.Vector3(0, 0, 0),
|
||||
},
|
||||
{
|
||||
index: 3,
|
||||
enabled: true,
|
||||
rotationMin: new THREE.Vector3(0, 0, 0),
|
||||
rotationMax: new THREE.Vector3(2, 0, 0),
|
||||
},
|
||||
{ index: 1, enabled: true, limitation: new THREE.Vector3(0, 1, 0) },
|
||||
{ index: 0, enabled: false, limitation: new THREE.Vector3(0, 0, 0) },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const solver = new CCDIKSolver(OOI.Skinned_Mesh, iks);
|
||||
setIkSolver(solver);
|
||||
|
||||
const helper = new CCDIKHelper(OOI.Skinned_Mesh, iks, 0.05)
|
||||
|
||||
setSelectedArm(OOI.Target_Bone);
|
||||
|
||||
// scene.add(helper);
|
||||
|
||||
}, [cloned, gltf, setIkSolver]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<group ref={groupRef} position={armBot.position} rotation={armBot.rotation} onClick={() => {
|
||||
setSelectedArm(groupRef.current?.getObjectByName(targetBoneName))
|
||||
}}>
|
||||
<primitive
|
||||
uuid={`${armBot.modelUuid}_IK`}
|
||||
object={cloned}
|
||||
scale={[1, 1, 1]}
|
||||
name={armBot.modelName}
|
||||
/>
|
||||
</group>
|
||||
{/* {selectedArm && <TransformControls object={selectedArm} />} */}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default IKInstance;
|
||||
@@ -0,0 +1,22 @@
|
||||
import { useSceneContext } from "../../../scene/sceneContext";
|
||||
import RoboticArmInstance from "./armInstance/roboticArmInstance";
|
||||
// import RoboticArmContentUi from "../../ui3d/RoboticArmContentUi";
|
||||
import React from "react";
|
||||
|
||||
function RoboticArmInstances() {
|
||||
const {armBotStore} = useSceneContext();
|
||||
const { armBots } = armBotStore();
|
||||
|
||||
return (
|
||||
<>
|
||||
{armBots?.map((robot: ArmBotStatus) => (
|
||||
<React.Fragment key={robot.modelUuid}>
|
||||
<RoboticArmInstance armBot={robot} />
|
||||
{/* <RoboticArmContentUi roboticArm={robot} /> */}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default RoboticArmInstances;
|
||||
39
app/src/modules/simulation/roboticArm/roboticArm.tsx
Normal file
39
app/src/modules/simulation/roboticArm/roboticArm.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useSelectedEventSphere } from "../../../store/simulation/useSimulationStore";
|
||||
import { usePlayButtonStore } from "../../../store/usePlayButtonStore";
|
||||
import RoboticArmInstances from "./instances/roboticArmInstances";
|
||||
import ArmBotUI from "../spatialUI/arm/armBotUI";
|
||||
import { useSceneContext } from "../../scene/sceneContext";
|
||||
|
||||
function RoboticArm() {
|
||||
const {armBotStore} = useSceneContext();
|
||||
const { getArmBotById } = armBotStore();
|
||||
const { selectedEventSphere } = useSelectedEventSphere();
|
||||
const { isPlaying } = usePlayButtonStore();
|
||||
const [isArmBotSelected, setIsArmBotSelected] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedEventSphere) {
|
||||
const selectedArmBot = getArmBotById(selectedEventSphere.userData.modelUuid);
|
||||
if (selectedArmBot) {
|
||||
setIsArmBotSelected(true);
|
||||
} else {
|
||||
setIsArmBotSelected(false);
|
||||
}
|
||||
}
|
||||
}, [getArmBotById, selectedEventSphere])
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
<RoboticArmInstances />
|
||||
|
||||
{isArmBotSelected && !isPlaying &&
|
||||
< ArmBotUI />
|
||||
}
|
||||
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default RoboticArm;
|
||||
Reference in New Issue
Block a user