first commit

This commit is contained in:
2025-06-10 15:28:23 +05:30
commit e22a2dc275
699 changed files with 100382 additions and 0 deletions

View File

@@ -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 VehicleCallback = {
vehicleId: string;
callback: () => void;
};
export function useVehicleEventManager() {
const { vehicleStore } = useSceneContext();
const { getVehicleById } = vehicleStore();
const callbacksRef = useRef<VehicleCallback[]>([]);
const isMonitoringRef = useRef(false);
const { isPlaying } = usePlayButtonStore();
const { isPaused } = usePauseButtonStore();
const { isReset } = useResetButtonStore();
useEffect(() => {
if (isReset) {
callbacksRef.current = [];
}
}, [isReset])
// Add a new vehicle to monitor
const addVehicleToMonitor = (vehicleId: string, callback: () => void) => {
// Avoid duplicates
if (!callbacksRef.current.some((entry) => entry.vehicleId === vehicleId)) {
callbacksRef.current.push({ vehicleId, callback });
}
// Start monitoring if not already running
if (!isMonitoringRef.current) {
isMonitoringRef.current = true;
}
};
// Remove a vehicle from monitoring
const removeVehicleFromMonitor = (vehicleId: string) => {
callbacksRef.current = callbacksRef.current.filter(
(entry) => entry.vehicleId !== vehicleId
);
// Stop monitoring if no more vehicles to track
if (callbacksRef.current.length === 0) {
isMonitoringRef.current = false;
}
};
// Check vehicle states every frame
useFrame(() => {
if (!isMonitoringRef.current || callbacksRef.current.length === 0 || !isPlaying || isPaused) return;
callbacksRef.current.forEach(({ vehicleId, callback }) => {
const vehicle = getVehicleById(vehicleId);
if (vehicle && vehicle.isActive === false && vehicle.state === 'idle' && vehicle.isPicking && vehicle.currentLoad < vehicle.point.action.loadCapacity) {
callback();
removeVehicleFromMonitor(vehicleId); // Remove after triggering
}
});
});
// Cleanup on unmount
useEffect(() => {
return () => {
callbacksRef.current = [];
isMonitoringRef.current = false;
};
}, []);
return {
addVehicleToMonitor,
removeVehicleFromMonitor,
};
}

View File

@@ -0,0 +1,50 @@
import { useEffect, useRef, useState } from 'react';
import { useThree, useFrame } from '@react-three/fiber';
import * as THREE from 'three';
import { MaterialModel } from '../../../materials/instances/material/materialModel';
type MaterialAnimatorProps = {
agvDetail: VehicleStatus;
};
const MaterialAnimator = ({ agvDetail }: MaterialAnimatorProps) => {
const meshRef = useRef<any>(null!);
const [hasLoad, setHasLoad] = useState(false);
const { scene } = useThree();
const offset = new THREE.Vector3(0, 0.85, 0);
useEffect(() => {
setHasLoad(agvDetail.currentLoad > 0);
}, [agvDetail.currentLoad]);
useFrame(() => {
if (!hasLoad || !meshRef.current) return;
const agvModel = scene.getObjectByProperty("uuid", agvDetail.modelUuid) as THREE.Object3D;
if (agvModel) {
const worldPosition = offset.clone().applyMatrix4(agvModel.matrixWorld);
meshRef.current.position.copy(worldPosition);
meshRef.current.rotation.copy(agvModel.rotation);
}
});
return (
<>
{hasLoad && (
<>
{agvDetail.currentMaterials.length > 0 &&
<MaterialModel
matRef={meshRef}
materialId={agvDetail.currentMaterials[0].materialId || ''}
materialType={agvDetail.currentMaterials[0].materialType || 'Default material'}
/>
}
</>
)}
</>
);
};
export default MaterialAnimator;

View File

@@ -0,0 +1,250 @@
import { useEffect, useRef, useState } from 'react'
import { useFrame, useThree } from '@react-three/fiber';
import * as THREE from 'three';
import { Line } from '@react-three/drei';
import { useAnimationPlaySpeed, usePauseButtonStore, usePlayButtonStore, useResetButtonStore } from '../../../../../store/usePlayButtonStore';
import { useSceneContext } from '../../../../scene/sceneContext';
interface VehicleAnimatorProps {
path: [number, number, number][];
handleCallBack: () => void;
reset: () => void;
startUnloadingProcess: () => void;
currentPhase: string;
agvUuid: string;
agvDetail: VehicleStatus;
}
function VehicleAnimator({ path, handleCallBack, currentPhase, agvUuid, agvDetail, reset, startUnloadingProcess }: Readonly<VehicleAnimatorProps>) {
const { vehicleStore } = useSceneContext();
const { getVehicleById } = vehicleStore();
const { isPaused } = usePauseButtonStore();
const { isPlaying } = usePlayButtonStore();
const { speed } = useAnimationPlaySpeed();
const { isReset, setReset } = useResetButtonStore();
const progressRef = useRef<number>(0);
const movingForward = useRef<boolean>(true);
const completedRef = useRef<boolean>(false);
const [objectRotation, setObjectRotation] = useState<{ x: number; y: number; z: number } | undefined>(agvDetail.point?.action?.pickUpPoint?.rotation || { x: 0, y: 0, z: 0 })
const [restRotation, setRestingRotation] = useState<boolean>(true);
const [currentPath, setCurrentPath] = useState<[number, number, number][]>([]);
const { scene } = useThree();
useEffect(() => {
if (currentPhase === 'stationed-pickup' && path.length > 0) {
// console.log('path: ', path);
setCurrentPath(path);
setObjectRotation(agvDetail.point.action?.pickUpPoint?.rotation)
} else if (currentPhase === 'pickup-drop' && path.length > 0) {
setObjectRotation(agvDetail.point.action?.unLoadPoint?.rotation)
setCurrentPath(path);
} else if (currentPhase === 'drop-pickup' && path.length > 0) {
setObjectRotation(agvDetail.point.action?.pickUpPoint?.rotation)
setCurrentPath(path);
}
}, [currentPhase, path, objectRotation]);
useEffect(() => {
completedRef.current = false;
}, [currentPath]);
useEffect(() => {
if (isReset || !isPlaying) {
reset();
setCurrentPath([]);
completedRef.current = false;
movingForward.current = true;
progressRef.current = 0;
setReset(false);
setRestingRotation(true);
const object = scene.getObjectByProperty('uuid', agvUuid);
const vehicle = getVehicleById(agvDetail.modelUuid);
if (object && vehicle) {
object.position.set(vehicle.position[0], vehicle.position[1], vehicle.position[2]);
object.rotation.set(vehicle.rotation[0], vehicle.rotation[1], vehicle.rotation[2]);
}
}
}, [isReset, isPlaying])
// useFrame((_, delta) => {
// const object = scene.getObjectByProperty('uuid', agvUuid);
// if (!object || currentPath.length < 2) return;
// if (isPaused) return;
// let totalDistance = 0;
// const distances = [];
// let accumulatedDistance = 0;
// let index = 0;
// const rotationSpeed = 1;
// for (let i = 0; i < currentPath.length - 1; i++) {
// const start = new THREE.Vector3(...currentPath[i]);
// const end = new THREE.Vector3(...currentPath[i + 1]);
// const segmentDistance = start.distanceTo(end);
// distances.push(segmentDistance);
// totalDistance += segmentDistance;
// }
// while (index < distances.length && progressRef.current > accumulatedDistance + distances[index]) {
// accumulatedDistance += distances[index];
// index++;
// }
// if (index < distances.length) {
// const start = new THREE.Vector3(...currentPath[index]);
// const end = new THREE.Vector3(...currentPath[index + 1]);
// const segmentDistance = distances[index];
// const currentDirection = new THREE.Vector3().subVectors(end, start).normalize();
// const targetAngle = Math.atan2(currentDirection.x, currentDirection.z);
// const currentAngle = object.rotation.y;
// let angleDifference = targetAngle - currentAngle;
// if (angleDifference > Math.PI) angleDifference -= 2 * Math.PI;
// if (angleDifference < -Math.PI) angleDifference += 2 * Math.PI;
// const maxRotationStep = (rotationSpeed * speed * agvDetail.speed) * delta;
// object.rotation.y += Math.sign(angleDifference) * Math.min(Math.abs(angleDifference), maxRotationStep);
// const isAligned = Math.abs(angleDifference) < 0.01;
// if (isAligned) {
// progressRef.current += delta * (speed * agvDetail.speed);
// const t = (progressRef.current - accumulatedDistance) / segmentDistance;
// const position = start.clone().lerp(end, t);
// object.position.copy(position);
// }
// }
// if (progressRef.current >= totalDistance) {
// if (restRotation && objectRotation) {
// const targetEuler = new THREE.Euler(
// objectRotation.x,
// objectRotation.y - (agvDetail.point.action.steeringAngle),
// objectRotation.z
// );
// const targetQuaternion = new THREE.Quaternion().setFromEuler(targetEuler);
// object.quaternion.slerp(targetQuaternion, delta * (rotationSpeed * speed * agvDetail.speed));
// if (object.quaternion.angleTo(targetQuaternion) < 0.01) {
// object.quaternion.copy(targetQuaternion);
// object.rotation.copy(targetEuler);
// setRestingRotation(false);
// }
// return;
// }
// }
// if (progressRef.current >= totalDistance) {
// setRestingRotation(true);
// progressRef.current = 0;
// movingForward.current = !movingForward.current;
// setCurrentPath([]);
// handleCallBack();
// if (currentPhase === 'pickup-drop') {
// requestAnimationFrame(startUnloadingProcess);
// }
// }
// });
const lastTimeRef = useRef(performance.now());
useFrame(() => {
const now = performance.now();
const delta = (now - lastTimeRef.current) / 1000;
lastTimeRef.current = now;
const object = scene.getObjectByProperty('uuid', agvUuid);
if (!object || currentPath.length < 2) return;
if (isPaused) return;
let totalDistance = 0;
const distances = [];
let accumulatedDistance = 0;
let index = 0;
const rotationSpeed = 1;
for (let i = 0; i < currentPath.length - 1; i++) {
const start = new THREE.Vector3(...currentPath[i]);
const end = new THREE.Vector3(...currentPath[i + 1]);
const segmentDistance = start.distanceTo(end);
distances.push(segmentDistance);
totalDistance += segmentDistance;
}
while (index < distances.length && progressRef.current > accumulatedDistance + distances[index]) {
accumulatedDistance += distances[index];
index++;
}
if (index < distances.length) {
const start = new THREE.Vector3(...currentPath[index]);
const end = new THREE.Vector3(...currentPath[index + 1]);
const segmentDistance = distances[index];
const currentDirection = new THREE.Vector3().subVectors(end, start).normalize();
const targetAngle = Math.atan2(currentDirection.x, currentDirection.z);
const currentAngle = object.rotation.y;
let angleDifference = targetAngle - currentAngle;
if (angleDifference > Math.PI) angleDifference -= 2 * Math.PI;
if (angleDifference < -Math.PI) angleDifference += 2 * Math.PI;
const maxRotationStep = (rotationSpeed * speed * agvDetail.speed) * delta;
object.rotation.y += Math.sign(angleDifference) * Math.min(Math.abs(angleDifference), maxRotationStep);
const isAligned = Math.abs(angleDifference) < 0.01;
if (isAligned) {
progressRef.current += delta * (speed * agvDetail.speed);
const t = (progressRef.current - accumulatedDistance) / segmentDistance;
const position = start.clone().lerp(end, t);
object.position.copy(position);
}
}
if (progressRef.current >= totalDistance) {
if (restRotation && objectRotation) {
const targetEuler = new THREE.Euler(
objectRotation.x,
objectRotation.y - agvDetail.point.action.steeringAngle,
objectRotation.z
);
const targetQuaternion = new THREE.Quaternion().setFromEuler(targetEuler);
object.quaternion.slerp(targetQuaternion, delta * (rotationSpeed * speed * agvDetail.speed));
if (object.quaternion.angleTo(targetQuaternion) < 0.01) {
object.quaternion.copy(targetQuaternion);
object.rotation.copy(targetEuler);
setRestingRotation(false);
}
return;
}
}
if (progressRef.current >= totalDistance) {
setRestingRotation(true);
progressRef.current = 0;
movingForward.current = !movingForward.current;
setCurrentPath([]);
handleCallBack();
if (currentPhase === 'pickup-drop') {
requestAnimationFrame(startUnloadingProcess);
}
}
});
return (
<>
{currentPath.length > 0 && (
// helper
<group visible={false}>
<Line points={currentPath} color="blue" lineWidth={3} />
{currentPath.map((point, index) => (
<mesh key={index} position={point}>
<sphereGeometry args={[0.1, 16, 16]} />
<meshStandardMaterial color="red" />
</mesh>
))}
</group>
)}
</>
);
}
export default VehicleAnimator;

View File

@@ -0,0 +1,525 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import VehicleAnimator from '../animator/vehicleAnimator';
import * as THREE from 'three';
import { NavMeshQuery } from '@recast-navigation/core';
import { useNavMesh } from '../../../../../store/builder/store';
import { useAnimationPlaySpeed, usePauseButtonStore, usePlayButtonStore } from '../../../../../store/usePlayButtonStore';
import { useProductStore } from '../../../../../store/simulation/useProductStore';
import { useTriggerHandler } from '../../../triggers/triggerHandler/useTriggerHandler';
import MaterialAnimator from '../animator/materialAnimator';
import { useSceneContext } from '../../../../scene/sceneContext';
import { useProductContext } from '../../../products/productContext';
function VehicleInstance({ agvDetail }: Readonly<{ agvDetail: VehicleStatus }>) {
const { navMesh } = useNavMesh();
const { isPlaying } = usePlayButtonStore();
const { materialStore, armBotStore, conveyorStore, vehicleStore, storageUnitStore } = useSceneContext();
const { removeMaterial, setEndTime } = materialStore();
const { getStorageUnitById } = storageUnitStore();
const { getArmBotById } = armBotStore();
const { getConveyorById } = conveyorStore();
const { triggerPointActions } = useTriggerHandler();
const { getActionByUuid, getEventByModelUuid, getTriggerByUuid } = useProductStore();
const { selectedProductStore } = useProductContext();
const { selectedProduct } = selectedProductStore();
const { vehicles, setVehicleActive, setVehicleState, setVehiclePicking, clearCurrentMaterials, setVehicleLoad, decrementVehicleLoad, removeLastMaterial, getLastMaterial, incrementIdleTime, incrementActiveTime, resetTime } = vehicleStore();
const [currentPhase, setCurrentPhase] = useState<string>('stationed');
const [path, setPath] = useState<[number, number, number][]>([]);
const pauseTimeRef = useRef<number | null>(null);
const idleTimeRef = useRef<number>(0);
const activeTimeRef = useRef<number>(0);
const isPausedRef = useRef<boolean>(false);
const isSpeedRef = useRef<number>(0);
let startTime: number;
let fixedInterval: number;
const { speed } = useAnimationPlaySpeed();
const { isPaused } = usePauseButtonStore();
const previousTimeRef = useRef<number | null>(null);
const animationFrameIdRef = useRef<number | null>(null);
useEffect(() => {
isPausedRef.current = isPaused;
}, [isPaused]);
useEffect(() => {
isSpeedRef.current = speed;
}, [speed]);
const computePath = useCallback(
(start: any, end: any) => {
try {
const navMeshQuery = new NavMeshQuery(navMesh);
const { path: segmentPath } = navMeshQuery.computePath(start, end);
return (
segmentPath?.map(({ x, y, z }) => [x, 0, z] as [number, number, number]) || []
);
} catch {
echo.error("Failed to compute path");
return [];
}
},
[navMesh]
);
function vehicleStatus(modelId: string, status: string) {
// console.log(`${modelId} , ${status}`);
}
// Function to reset everything
function reset() {
setCurrentPhase('stationed');
setVehicleActive(agvDetail.modelUuid, false);
setVehiclePicking(agvDetail.modelUuid, false);
setVehicleState(agvDetail.modelUuid, 'idle');
setVehicleLoad(agvDetail.modelUuid, 0);
setPath([]);
startTime = 0;
isPausedRef.current = false;
pauseTimeRef.current = 0;
resetTime(agvDetail.modelUuid)
activeTimeRef.current = 0
idleTimeRef.current = 0
previousTimeRef.current = null
if (animationFrameIdRef.current !== null) {
cancelAnimationFrame(animationFrameIdRef.current)
animationFrameIdRef.current = null
}
}
useEffect(() => {
if (isPlaying) {
if (!agvDetail.point.action.unLoadPoint || !agvDetail.point.action.pickUpPoint) return;
if (!agvDetail.isActive && agvDetail.state === 'idle' && currentPhase === 'stationed') {
const toPickupPath = computePath(
new THREE.Vector3(agvDetail?.position[0], agvDetail?.position[1], agvDetail?.position[2]),
agvDetail?.point?.action?.pickUpPoint?.position
);
setPath(toPickupPath);
setCurrentPhase('stationed-pickup');
setVehicleState(agvDetail.modelUuid, 'running');
setVehiclePicking(agvDetail.modelUuid, false);
setVehicleActive(agvDetail.modelUuid, true);
vehicleStatus(agvDetail.modelUuid, 'Started from station, heading to pickup');
return;
} else if (!agvDetail.isActive && agvDetail.state === 'idle' && currentPhase === 'picking') {
if (agvDetail.currentLoad === agvDetail.point.action.loadCapacity && agvDetail.currentMaterials.length > 0) {
if (agvDetail.point.action.pickUpPoint && agvDetail.point.action.unLoadPoint) {
const toDrop = computePath(
agvDetail.point.action.pickUpPoint.position,
agvDetail.point.action.unLoadPoint.position
);
setPath(toDrop);
setCurrentPhase('pickup-drop');
setVehicleState(agvDetail.modelUuid, 'running');
setVehiclePicking(agvDetail.modelUuid, false);
setVehicleActive(agvDetail.modelUuid, true);
vehicleStatus(agvDetail.modelUuid, 'Started from pickup point, heading to drop point');
}
}
} else if (!agvDetail.isActive && agvDetail.state === 'idle' && currentPhase === 'dropping' && agvDetail.currentLoad === 0) {
if (agvDetail.point.action.pickUpPoint && agvDetail.point.action.unLoadPoint) {
const dropToPickup = computePath(
agvDetail.point.action.unLoadPoint.position,
agvDetail.point.action.pickUpPoint.position
);
setPath(dropToPickup);
setCurrentPhase('drop-pickup');
setVehicleState(agvDetail.modelUuid, 'running');
setVehiclePicking(agvDetail.modelUuid, false);
setVehicleActive(agvDetail.modelUuid, true);
vehicleStatus(agvDetail.modelUuid, 'Started from dropping point, heading to pickup point');
}
}
}
else {
reset()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [vehicles, currentPhase, path, isPlaying]);
function animate(currentTime: number) {
if (previousTimeRef.current === null) {
previousTimeRef.current = currentTime;
}
const deltaTime = (currentTime - previousTimeRef.current) / 1000;
previousTimeRef.current = currentTime;
if (agvDetail.isActive) {
if (!isPausedRef.current) {
activeTimeRef.current += deltaTime * isSpeedRef.current;
}
} else {
if (!isPausedRef.current) {
idleTimeRef.current += deltaTime * isSpeedRef.current; // Scale idle time by speed
}
}
animationFrameIdRef.current = requestAnimationFrame(animate);
}
useEffect(() => {
if (!isPlaying) return
if (!agvDetail.isActive) {
const roundedActiveTime = Math.round(activeTimeRef.current);
// console.log('Final Active Time:', roundedActiveTime, 'seconds');
incrementActiveTime(agvDetail.modelUuid, roundedActiveTime);
activeTimeRef.current = 0;
} else {
const roundedIdleTime = Math.round(idleTimeRef.current);
// console.log('Final Idle Time:', roundedIdleTime, 'seconds');
incrementIdleTime(agvDetail.modelUuid, roundedIdleTime);
idleTimeRef.current = 0;
}
if (animationFrameIdRef.current === null) {
animationFrameIdRef.current = requestAnimationFrame(animate);
}
return () => {
if (animationFrameIdRef.current !== null) {
cancelAnimationFrame(animationFrameIdRef.current);
animationFrameIdRef.current = null;
}
};
}, [agvDetail, isPlaying]);
function handleCallBack() {
if (currentPhase === 'stationed-pickup') {
setCurrentPhase('picking');
setVehicleState(agvDetail.modelUuid, 'idle');
setVehiclePicking(agvDetail.modelUuid, true);
setVehicleActive(agvDetail.modelUuid, false);
vehicleStatus(agvDetail.modelUuid, 'Reached pickup point, waiting for material');
setPath([]);
} else if (currentPhase === 'pickup-drop') {
setCurrentPhase('dropping');
setVehicleState(agvDetail.modelUuid, 'idle');
setVehiclePicking(agvDetail.modelUuid, false);
setVehicleActive(agvDetail.modelUuid, false);
vehicleStatus(agvDetail.modelUuid, 'Reached drop point');
setPath([]);
} else if (currentPhase === 'drop-pickup') {
setCurrentPhase('picking');
setVehicleState(agvDetail.modelUuid, 'idle');
setVehiclePicking(agvDetail.modelUuid, true);
setVehicleActive(agvDetail.modelUuid, false);
setPath([]);
clearCurrentMaterials(agvDetail.modelUuid)
vehicleStatus(agvDetail.modelUuid, 'Reached pickup point again, cycle complete');
}
}
function startUnloadingProcess() {
if (agvDetail.point.action.triggers.length > 0) {
const trigger = getTriggerByUuid(selectedProduct.productUuid, agvDetail.point.action.triggers[0]?.triggerUuid);
const model = getEventByModelUuid(selectedProduct.productUuid, trigger?.triggeredAsset?.triggeredModel?.modelUuid || '');
if (trigger && model) {
if (model.type === 'transfer') {
const action = getActionByUuid(selectedProduct.productUuid, agvDetail.point.action.actionUuid);
if (action) {
handleMaterialDropToConveyor(model);
}
} else if (model.type === 'machine') {
//
} else if (model.type === 'roboticArm') {
const action = getActionByUuid(selectedProduct.productUuid, agvDetail.point.action.actionUuid);
if (action) {
handleMaterialDropToArmBot(model);
}
} else if (model.type === 'storageUnit') {
const action = getActionByUuid(selectedProduct.productUuid, agvDetail.point.action.actionUuid);
if (action) {
handleMaterialDropToStorageUnit(model);
}
}
} else {
const droppedMaterial = agvDetail.currentLoad;
startTime = performance.now();
handleMaterialDropByDefault(droppedMaterial);
}
} else {
const droppedMaterial = agvDetail.currentLoad;
startTime = performance.now();
handleMaterialDropByDefault(droppedMaterial);
}
}
function handleMaterialDropToStorageUnit(model: StorageEventSchema) {
if (model) {
if (model.point.action.actionType === 'store') {
loopMaterialDropToStorage(
agvDetail.modelUuid,
agvDetail.currentLoad,
agvDetail.point.action.unLoadDuration,
model.modelUuid,
model.point.action.storageCapacity,
agvDetail.point.action
);
}
}
}
function loopMaterialDropToStorage(
vehicleId: string,
vehicleCurrentLoad: number,
unLoadDuration: number,
storageUnitId: string,
storageMaxCapacity: number,
action: VehicleAction
) {
startTime = performance.now();
const fixedInterval = ((unLoadDuration / vehicleCurrentLoad) * (1000 / isSpeedRef.current));
const unloadLoop = () => {
if (isPausedRef.current) {
pauseTimeRef.current ??= performance.now();
requestAnimationFrame(unloadLoop);
return;
}
if (pauseTimeRef.current) {
const pauseDuration = performance.now() - pauseTimeRef.current;
startTime += pauseDuration;
pauseTimeRef.current = null;
}
const elapsedTime = performance.now() - startTime;
const storageUnit = getStorageUnitById(storageUnitId);
if (elapsedTime >= fixedInterval) {
if (storageUnit && agvDetail &&
storageUnit.currentLoad < storageMaxCapacity &&
vehicleCurrentLoad > 0) {
decrementVehicleLoad(vehicleId, 1);
vehicleCurrentLoad -= 1;
const material = removeLastMaterial(vehicleId);
if (material) {
triggerPointActions(action, material.materialId);
}
if (vehicleCurrentLoad > 0 && storageUnit.currentLoad < storageMaxCapacity) {
startTime = performance.now();
requestAnimationFrame(unloadLoop);
}
}
} else {
requestAnimationFrame(unloadLoop);
}
};
const storageUnit = getStorageUnitById(storageUnitId);
if (storageUnit && vehicleCurrentLoad > 0 && storageUnit?.currentLoad < storageMaxCapacity) {
unloadLoop();
}
}
function handleMaterialDropToConveyor(model: ConveyorEventSchema) {
const conveyor = getConveyorById(model.modelUuid);
if (conveyor) {
loopMaterialDropToConveyor(
agvDetail.modelUuid,
agvDetail.currentLoad,
conveyor.modelUuid,
agvDetail.point.action.unLoadDuration,
agvDetail.point.action
);
}
}
function loopMaterialDropToConveyor(
vehicleId: string,
vehicleCurrentLoad: number,
conveyorId: string,
unLoadDuration: number,
action: VehicleAction
) {
let lastIncrementTime = performance.now();
let pauseStartTime: number | null = null;
let totalPausedDuration = 0;
const fixedInterval = (unLoadDuration * 1000) / speed;
const dropLoop = (currentTime: number) => {
const conveyor = getConveyorById(conveyorId);
if (isPausedRef.current || (conveyor && conveyor.isPaused)) {
if (pauseStartTime === null) {
pauseStartTime = currentTime;
}
requestAnimationFrame(dropLoop);
return;
}
// If we were paused but now resumed
if (pauseStartTime !== null) {
totalPausedDuration += currentTime - pauseStartTime;
pauseStartTime = null;
}
// Adjust for paused time
const adjustedCurrentTime = currentTime - totalPausedDuration;
const elapsedSinceLastIncrement = adjustedCurrentTime - lastIncrementTime;
if (elapsedSinceLastIncrement >= fixedInterval) {
if (conveyor && vehicleCurrentLoad > 0) {
decrementVehicleLoad(vehicleId, 1);
vehicleCurrentLoad -= 1;
const material = removeLastMaterial(vehicleId);
if (material) {
triggerPointActions(action, material.materialId);
}
// Update the last increment time (using adjusted time)
lastIncrementTime = adjustedCurrentTime;
}
}
// Continue the loop if there's more load to drop
if (vehicleCurrentLoad > 0) {
requestAnimationFrame(dropLoop);
}
};
requestAnimationFrame(dropLoop);
}
function handleMaterialDropToArmBot(model: RoboticArmEventSchema) {
const armBot = getArmBotById(model.modelUuid);
if (armBot && armBot.state === 'idle' && !armBot.isActive) {
loopMaterialDropToArmBot(
agvDetail.modelUuid,
agvDetail.currentLoad,
agvDetail.point.action.unLoadDuration,
model.modelUuid,
agvDetail.point.action
);
}
}
function loopMaterialDropToArmBot(
vehicleId: string,
vehicleCurrentLoad: number,
unLoadDuration: number,
armBotId: string,
action: VehicleAction
) {
startTime = performance.now();
const armBot = getArmBotById(armBotId);
if (!armBot || armBot.state !== 'idle' || armBot.isActive || vehicleCurrentLoad <= 0) {
return;
}
const checkIdleDuration = () => {
if (isPausedRef.current) {
pauseTimeRef.current ??= performance.now();
requestAnimationFrame(checkIdleDuration);
return;
}
if (pauseTimeRef.current) {
const pauseDuration = performance.now() - pauseTimeRef.current;
startTime += pauseDuration;
pauseTimeRef.current = null;
}
const elapsedTime = performance.now() - startTime;
if (elapsedTime >= unLoadDuration * (1000 / speed)) {
const material = getLastMaterial(vehicleId);
if (material) {
vehicleCurrentLoad -= 1;
triggerPointActions(action, material.materialId);
if (vehicleCurrentLoad > 0) {
setTimeout(() => {
const waitForNextTransfer = () => {
const currentArmBot = getArmBotById(armBotId);
if (currentArmBot && currentArmBot.state === 'idle' && !currentArmBot.isActive) {
startTime = performance.now();
loopMaterialDropToArmBot(vehicleId, vehicleCurrentLoad, unLoadDuration, armBotId, action);
} else {
requestAnimationFrame(waitForNextTransfer);
}
};
waitForNextTransfer();
}, 0)
}
}
} else {
requestAnimationFrame(checkIdleDuration);
}
};
checkIdleDuration();
}
function handleMaterialDropByDefault(droppedMaterial: number) {
if (isPausedRef.current) {
pauseTimeRef.current ??= performance.now();
requestAnimationFrame(() => handleMaterialDropByDefault(droppedMaterial));
return;
}
if (pauseTimeRef.current) {
const pauseDuration = performance.now() - pauseTimeRef.current;
startTime += pauseDuration;
pauseTimeRef.current = null;
}
const elapsedTime = performance.now() - startTime;
const unLoadDuration = agvDetail.point.action.unLoadDuration;
fixedInterval = ((unLoadDuration / agvDetail.currentLoad) * (1000 / isSpeedRef.current));
if (elapsedTime >= fixedInterval) {
let droppedMat = droppedMaterial - 1;
decrementVehicleLoad(agvDetail.modelUuid, 1);
const material = removeLastMaterial(agvDetail.modelUuid);
if (material) {
setEndTime(material.materialId, performance.now());
removeMaterial(material.materialId);
}
if (droppedMat > 0) {
startTime = performance.now();
requestAnimationFrame(() => handleMaterialDropByDefault(droppedMat));
} else {
return;
}
} else {
requestAnimationFrame(() => handleMaterialDropByDefault(droppedMaterial));
}
}
return (
<>
<VehicleAnimator
path={path}
handleCallBack={handleCallBack}
currentPhase={currentPhase}
agvUuid={agvDetail?.modelUuid}
agvDetail={agvDetail}
reset={reset}
startUnloadingProcess={startUnloadingProcess}
/>
<MaterialAnimator agvDetail={agvDetail} />
</>
);
}
export default VehicleInstance;

View File

@@ -0,0 +1,24 @@
import React from "react";
import VehicleInstance from "./instance/vehicleInstance";
import VehicleContentUi from "../../ui3d/VehicleContentUi";
import { useSceneContext } from "../../../scene/sceneContext";
import { useViewSceneStore } from "../../../../store/builder/store";
function VehicleInstances() {
const { vehicleStore } = useSceneContext();
const { vehicles } = vehicleStore();
const { viewSceneLabels } = useViewSceneStore();
return (
<>
{vehicles.map((vehicle: VehicleStatus) => (
<React.Fragment key={vehicle.modelUuid}>
<VehicleInstance agvDetail={vehicle} />
{viewSceneLabels && <VehicleContentUi vehicle={vehicle} />}
</React.Fragment>
))}
</>
);
}
export default VehicleInstances;

View File

@@ -0,0 +1,31 @@
import { useRef } from "react";
import { useNavMesh } from "../../../../store/builder/store";
import PolygonGenerator from "./polygonGenerator";
import NavMeshDetails from "./navMeshDetails";
import * as CONSTANTS from "../../../../types/world/worldConstants";
import * as Types from "../../../../types/world/worldTypes";
type NavMeshProps = {
lines: Types.RefLines
};
function NavMesh({ lines }: NavMeshProps) {
let groupRef = useRef() as Types.RefGroup;
const { setNavMesh } = useNavMesh();
return (
<>
<PolygonGenerator groupRef={groupRef} lines={lines} />
<NavMeshDetails lines={lines} setNavMesh={setNavMesh} groupRef={groupRef} />
<group ref={groupRef} visible={false} name="Meshes">
<mesh rotation-x={CONSTANTS.planeConfig.rotation} position={CONSTANTS.planeConfig.position3D} receiveShadow>
<planeGeometry args={[300, 300]} />
<meshBasicMaterial color={CONSTANTS.planeConfig.color} />
</mesh>
</group>
</>
)
}
export default NavMesh

View File

@@ -0,0 +1,69 @@
import React, { useEffect } from "react";
import { init as initRecastNavigation } from "@recast-navigation/core";
import { generateSoloNavMesh } from "@recast-navigation/generators";
import { DebugDrawer, getPositionsAndIndices } from "@recast-navigation/three";
import { useThree } from "@react-three/fiber";
import * as THREE from "three";
import * as Types from "../../../../types/world/worldTypes";
interface NavMeshDetailsProps {
setNavMesh: (navMesh: any) => void;
groupRef: React.MutableRefObject<THREE.Group | null>;
lines: Types.RefLines;
}
export default function NavMeshDetails({
lines,
setNavMesh,
groupRef,
}: NavMeshDetailsProps) {
const { scene } = useThree();
useEffect(() => {
const initializeNavigation = async () => {
try {
await initRecastNavigation();
if (!groupRef.current || groupRef.current.children.length === 0) {
return;
}
const meshes = groupRef?.current?.children as THREE.Mesh[];
const [positions, indices] = getPositionsAndIndices(meshes);
// const cellSize = 0.2;
// const cellHeight = 0.7;
// const walkableRadius = 0.5;
const cellSize = 0.3;
const cellHeight = 0.7;
const walkableRadius = 0.7;
const { success, navMesh } = generateSoloNavMesh(positions, indices, {
cs: cellSize,
ch: cellHeight,
walkableRadius: Math.round(walkableRadius / cellHeight),
});
if (!success || !navMesh) {
return;
}
setNavMesh(navMesh);
scene.children
.filter((child) => child instanceof DebugDrawer)
.forEach((child) => scene.remove(child));
const debugDrawer = new DebugDrawer();
debugDrawer.drawNavMesh(navMesh);
// scene.add(debugDrawer);
} catch (error) {
echo.error("Failed to initialize navigation")
}
};
initializeNavigation();
}, [scene, groupRef, lines.current]);
return null;
}

View File

@@ -0,0 +1,165 @@
import * as THREE from "three";
import { useEffect } from "react";
import * as turf from "@turf/turf";
import * as Types from "../../../../types/world/worldTypes";
import arrayLinesToObject from "../../../builder/geomentries/lines/lineConvertions/arrayLinesToObject";
import { useAisleStore } from "../../../../store/builder/useAisleStore";
import { useThree } from "@react-three/fiber";
import { clone } from "chart.js/dist/helpers/helpers.core";
interface PolygonGeneratorProps {
groupRef: React.MutableRefObject<THREE.Group | null>;
lines: Types.RefLines;
}
export default function PolygonGenerator({
groupRef,
lines,
}: PolygonGeneratorProps) {
const { aisles } = useAisleStore();
const { scene } = useThree();
useEffect(() => {
let allLines = arrayLinesToObject(lines.current);
const wallLines = allLines?.filter((line) => line?.type === "WallLine");
const result = aisles
.filter(
(aisle) =>
aisle.type.aisleType === "dotted-aisle" ||
aisle.type.aisleType === "solid-aisle" ||
aisle.type.aisleType === "dashed-aisle" ||
aisle.type.aisleType === "arc-aisle"
)
.map((aisle) =>
aisle.points.map((point) => ({
position: [point.position[0], point.position[2]],
uuid: point.pointUuid,
}))
);
const arcAndCircleResult = aisles
.filter(
(aisle) =>
aisle.type.aisleType === "circle-aisle" ||
aisle.type.aisleType === "arc-aisle"
)
arcAndCircleResult.forEach((arc) => {
const arcGroup = scene.getObjectByProperty("uuid", arc.aisleUuid);
if (!arcGroup) return;
const cloned = arcGroup?.clone();
cloned.position.set(cloned.position.x, cloned.position.y + 5, cloned.position.z);
cloned?.traverse((child) => {
if (child instanceof THREE.Mesh && child.geometry instanceof THREE.ExtrudeGeometry) {
const extrudeGeometry = child.geometry as THREE.ExtrudeGeometry;
extrudeGeometry.scale(1, 1, 500);
child.geometry = extrudeGeometry;
child.position.set(cloned.position.x, cloned.position.y + 0.1, cloned.position.z)
child.rotateX(Math.PI / 2);
child.name = "agv-arc-collider"
groupRef.current?.add(child)
}
});
});
const wallPoints = wallLines
.map((pair) => pair?.line.map((vals) => vals.position))
.filter((wall): wall is THREE.Vector3[] => !!wall);
if (!result || result.some((line) => !line)) {
return;
}
const lineFeatures = result?.map((line: any) =>
turf.lineString(line.map((p: any) => p?.position))
);
const validLineFeatures = lineFeatures.filter((line) => {
const coords = line.geometry.coordinates;
return coords.length >= 2;
});
const polygons = turf.polygonize(turf.featureCollection(validLineFeatures));
renderWallGeometry(wallPoints);
if (polygons.features.length > 0) {
polygons.features.forEach((feature) => {
if (feature.geometry.type === "Polygon") {
const shape = new THREE.Shape();
const coords = feature.geometry.coordinates[0];
shape.moveTo(coords[0][0], coords[0][1]);
for (let i = 1; i < coords.length; i++) {
shape.lineTo(coords[i][0], coords[i][1]);
}
shape.lineTo(coords[0][0], coords[0][1]);
const extrudeSettings = {
depth: 5,
bevelEnabled: false,
};
const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
const material = new THREE.MeshBasicMaterial({ color: "blue", transparent: true, opacity: 0.5 });
const mesh = new THREE.Mesh(geometry, material);
mesh.rotateX(Math.PI / 2);
mesh.name = "agv-collider";
mesh.position.y = 5;
mesh.receiveShadow = true;
groupRef.current?.add(mesh);
}
});
}
}, [lines.current, aisles, scene]);
const renderWallGeometry = (walls: THREE.Vector3[][]) => {
walls.forEach((wall) => {
if (wall.length < 2) return;
for (let i = 0; i < wall.length - 1; i++) {
const start = new THREE.Vector3(wall[i].x, wall[i].y, wall[i].z);
const end = new THREE.Vector3(
wall[i + 1].x,
wall[i + 1].y,
wall[i + 1].z
);
const wallHeight = 10;
const direction = new THREE.Vector3().subVectors(end, start);
const length = direction.length();
direction.normalize();
const wallGeometry = new THREE.BoxGeometry(length, wallHeight);
const wallMaterial = new THREE.MeshBasicMaterial({
color: "#aaa",
transparent: true,
opacity: 0.5,
});
const wallMesh = new THREE.Mesh(wallGeometry, wallMaterial);
const midPoint = new THREE.Vector3()
.addVectors(start, end)
.multiplyScalar(0.5);
wallMesh.position.set(midPoint.x, wallHeight / 2, midPoint.z);
const quaternion = new THREE.Quaternion();
quaternion.setFromUnitVectors(new THREE.Vector3(1, 0, 0), direction);
wallMesh.quaternion.copy(quaternion);
wallMesh.name = "agv-collider";
groupRef.current?.add(wallMesh);
}
});
};
return null;
}

View File

@@ -0,0 +1,39 @@
import { useEffect, useState } from "react";
import { useSelectedEventSphere } from "../../../store/simulation/useSimulationStore";
import { usePlayButtonStore } from "../../../store/usePlayButtonStore";
import VehicleInstances from "./instances/vehicleInstances";
import VehicleUI from "../spatialUI/vehicle/vehicleUI";
import { useSceneContext } from "../../scene/sceneContext";
function Vehicles() {
const { vehicleStore } = useSceneContext();
const { getVehicleById } = vehicleStore();
const { selectedEventSphere } = useSelectedEventSphere();
const { isPlaying } = usePlayButtonStore();
const [isVehicleSelected, setIsVehicleSelected] = useState(false);
useEffect(() => {
if (selectedEventSphere) {
const selectedVehicle = getVehicleById(selectedEventSphere.userData.modelUuid);
if (selectedVehicle) {
setIsVehicleSelected(true);
} else {
setIsVehicleSelected(false);
}
}
}, [getVehicleById, selectedEventSphere])
return (
<>
<VehicleInstances />
{isVehicleSelected && selectedEventSphere && !isPlaying &&
<VehicleUI />
}
</>
);
}
export default Vehicles;