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 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,
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
31
app/src/modules/simulation/vehicle/navMesh/navMesh.tsx
Normal file
31
app/src/modules/simulation/vehicle/navMesh/navMesh.tsx
Normal 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
|
||||
@@ -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;
|
||||
}
|
||||
165
app/src/modules/simulation/vehicle/navMesh/polygonGenerator.tsx
Normal file
165
app/src/modules/simulation/vehicle/navMesh/polygonGenerator.tsx
Normal 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;
|
||||
}
|
||||
39
app/src/modules/simulation/vehicle/vehicles.tsx
Normal file
39
app/src/modules/simulation/vehicle/vehicles.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user