first commit
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
import { useCallback } from "react";
|
||||
import { useSceneContext } from "../../../../scene/sceneContext";
|
||||
|
||||
export function useDefaultHandler() {
|
||||
const { materialStore } = useSceneContext();
|
||||
const { getMaterialById } = materialStore();
|
||||
|
||||
const defaultLogStatus = (materialUuid: string, status: string) => {
|
||||
echo.info(`${materialUuid}, ${status}`);
|
||||
}
|
||||
|
||||
const handleDefault = useCallback((action: ConveyorAction, materialId?: string) => {
|
||||
if (!action || action.actionType !== 'default' || !materialId) return;
|
||||
|
||||
const material = getMaterialById(materialId);
|
||||
if (!material) return;
|
||||
|
||||
defaultLogStatus(material.materialName, `performed Default action`);
|
||||
|
||||
}, [getMaterialById]);
|
||||
|
||||
return {
|
||||
handleDefault,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { useFrame } from "@react-three/fiber";
|
||||
import { usePlayButtonStore, usePauseButtonStore, useResetButtonStore, useAnimationPlaySpeed } from "../../../../../store/usePlayButtonStore";
|
||||
import { useSceneContext } from "../../../../scene/sceneContext";
|
||||
|
||||
interface DelayInstance {
|
||||
initialDelay: number;
|
||||
delayEndTime: number;
|
||||
materialId?: string;
|
||||
action: ConveyorAction;
|
||||
isPaused: boolean;
|
||||
remainingTime: number;
|
||||
lastUpdateTime: number;
|
||||
elapsedTime: number;
|
||||
}
|
||||
|
||||
export function useDelayHandler() {
|
||||
const { isPlaying } = usePlayButtonStore();
|
||||
const { isPaused } = usePauseButtonStore();
|
||||
const { isReset } = useResetButtonStore();
|
||||
const { speed } = useAnimationPlaySpeed();
|
||||
const { materialStore } = useSceneContext();
|
||||
const { setIsPaused } = materialStore();
|
||||
const activeDelays = useRef<Map<string, DelayInstance>>(new Map());
|
||||
const lastUpdateTimeRef = useRef<number>(performance.now());
|
||||
|
||||
const cleanupDelay = useCallback(() => {
|
||||
activeDelays.current.clear();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isReset) {
|
||||
cleanupDelay();
|
||||
}
|
||||
}, [isReset, cleanupDelay]);
|
||||
|
||||
const delayLogStatus = (materialUuid: string, status: string) => {
|
||||
echo.info(`${materialUuid}, ${status}`);
|
||||
};
|
||||
|
||||
useFrame(() => {
|
||||
const currentTime = performance.now();
|
||||
const deltaTime = currentTime - lastUpdateTimeRef.current;
|
||||
lastUpdateTimeRef.current = currentTime;
|
||||
const completedDelays: string[] = [];
|
||||
|
||||
activeDelays.current.forEach((delay, key) => {
|
||||
if (isPaused && !delay.isPaused) {
|
||||
delay.remainingTime = Math.max(0, delay.delayEndTime - currentTime);
|
||||
delay.isPaused = true;
|
||||
} else if (!isPaused && delay.isPaused) {
|
||||
delay.delayEndTime = currentTime + delay.remainingTime;
|
||||
delay.isPaused = false;
|
||||
delay.lastUpdateTime = currentTime;
|
||||
delay.elapsedTime = 0;
|
||||
} else if (!delay.isPaused && isPlaying) {
|
||||
delay.elapsedTime += deltaTime * speed;
|
||||
|
||||
if (delay.elapsedTime >= delay.initialDelay) {
|
||||
if (delay.materialId) {
|
||||
delayLogStatus(delay.materialId, `Delay completed`);
|
||||
setIsPaused(delay.materialId, false);
|
||||
}
|
||||
completedDelays.push(key);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
completedDelays.forEach(key => {
|
||||
activeDelays.current.delete(key);
|
||||
});
|
||||
});
|
||||
|
||||
const handleDelay = useCallback((action: ConveyorAction, materialId?: string) => {
|
||||
if (!action || action.actionType !== 'delay' || !materialId) return;
|
||||
|
||||
const delayMs = (action.delay || 0) * 1000;
|
||||
if (delayMs <= 0) return;
|
||||
|
||||
const key = materialId ? `${materialId}-${action.actionUuid}` : action.actionUuid;
|
||||
|
||||
if (activeDelays.current.has(key)) {
|
||||
activeDelays.current.delete(key);
|
||||
}
|
||||
|
||||
const now = performance.now();
|
||||
activeDelays.current.set(key, {
|
||||
initialDelay: delayMs,
|
||||
delayEndTime: now + delayMs,
|
||||
materialId,
|
||||
action,
|
||||
isPaused: false,
|
||||
remainingTime: 0,
|
||||
lastUpdateTime: now,
|
||||
elapsedTime: 0
|
||||
});
|
||||
|
||||
delayLogStatus(materialId, `Started ${delayMs / 1000}s delay`);
|
||||
setIsPaused(materialId, true);
|
||||
}, [isPlaying]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
cleanupDelay();
|
||||
};
|
||||
}, [cleanupDelay]);
|
||||
|
||||
return {
|
||||
handleDelay,
|
||||
cleanupDelay
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useCallback } from "react";
|
||||
import { useSceneContext } from "../../../../scene/sceneContext";
|
||||
|
||||
export function useDespawnHandler() {
|
||||
const { materialStore } = useSceneContext();
|
||||
const { getMaterialById, removeMaterial, setEndTime } = materialStore();
|
||||
|
||||
const deSpawnLogStatus = (materialUuid: string, status: string) => {
|
||||
echo.info(`${materialUuid}, ${status}`);
|
||||
}
|
||||
|
||||
const handleDespawn = useCallback((action: ConveyorAction, materialId?: string) => {
|
||||
if (!action || action.actionType !== 'despawn' || !materialId) return;
|
||||
|
||||
const material = getMaterialById(materialId);
|
||||
if (!material) return;
|
||||
|
||||
setEndTime(material.materialId, performance.now());
|
||||
removeMaterial(material.materialId);
|
||||
|
||||
deSpawnLogStatus(material.materialName, `Despawned`);
|
||||
|
||||
}, [getMaterialById, removeMaterial]);
|
||||
|
||||
return {
|
||||
handleDespawn,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import * as THREE from 'three';
|
||||
import { useFrame } from "@react-three/fiber";
|
||||
import { useProductStore } from "../../../../../store/simulation/useProductStore";
|
||||
import { usePlayButtonStore, useAnimationPlaySpeed, usePauseButtonStore, useResetButtonStore } from "../../../../../store/usePlayButtonStore";
|
||||
import { useSceneContext } from "../../../../scene/sceneContext";
|
||||
import { useProductContext } from "../../../products/productContext";
|
||||
|
||||
interface SpawnInstance {
|
||||
lastSpawnTime: number | null;
|
||||
startTime: number;
|
||||
spawnCount: number;
|
||||
params: {
|
||||
material: string;
|
||||
intervalMs: number;
|
||||
totalCount: number;
|
||||
action: ConveyorAction;
|
||||
};
|
||||
pauseStartTime: number;
|
||||
remainingTime: number;
|
||||
isPaused: boolean;
|
||||
}
|
||||
|
||||
export function useSpawnHandler() {
|
||||
const { materialStore, conveyorStore } = useSceneContext();
|
||||
const { addMaterial } = materialStore();
|
||||
const { getConveyorById } = conveyorStore();
|
||||
const { getModelUuidByActionUuid, getPointUuidByActionUuid } = useProductStore();
|
||||
const { isPlaying } = usePlayButtonStore();
|
||||
const { isPaused } = usePauseButtonStore();
|
||||
const { speed } = useAnimationPlaySpeed();
|
||||
const { isReset } = useResetButtonStore();
|
||||
const { selectedProductStore } = useProductContext();
|
||||
const { selectedProduct } = selectedProductStore();
|
||||
|
||||
const [activeSpawns, setActiveSpawns] = useState<Map<string, SpawnInstance>>(new Map());
|
||||
|
||||
const getConveyorPausedState = useCallback((action: ConveyorAction) => {
|
||||
const modelUuid = getModelUuidByActionUuid(selectedProduct.productUuid, action.actionUuid);
|
||||
if (!modelUuid) return false;
|
||||
|
||||
const conveyor = getConveyorById(modelUuid);
|
||||
if (!conveyor) return false;
|
||||
return conveyor.isPaused;
|
||||
}, [getConveyorById, getModelUuidByActionUuid, selectedProduct.productUuid]);
|
||||
|
||||
const shouldPauseSpawn = useCallback((action: ConveyorAction) => {
|
||||
return isPaused || getConveyorPausedState(action);
|
||||
}, [isPaused, getConveyorPausedState]);
|
||||
|
||||
const clearAllSpawns = useCallback(() => {
|
||||
setActiveSpawns(new Map());
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isReset) {
|
||||
clearAllSpawns();
|
||||
}
|
||||
}, [isReset, clearAllSpawns]);
|
||||
|
||||
const spawnLogStatus = (materialUuid: string, status: string) => {
|
||||
echo.info(`${materialUuid}, ${status}`);
|
||||
}
|
||||
|
||||
const createNewMaterial = useCallback((materialType: string, action: ConveyorAction) => {
|
||||
const modelUuid = getModelUuidByActionUuid(selectedProduct.productUuid, action.actionUuid);
|
||||
const pointUuid = getPointUuidByActionUuid(selectedProduct.productUuid, action.actionUuid);
|
||||
if (!modelUuid || !pointUuid) return;
|
||||
const currentTime = performance.now();
|
||||
|
||||
const newMaterial: MaterialSchema = {
|
||||
materialId: THREE.MathUtils.generateUUID(),
|
||||
materialName: `${materialType}-${Date.now()}`,
|
||||
materialType: materialType,
|
||||
isActive: false,
|
||||
isVisible: true,
|
||||
isPaused: false,
|
||||
isRendered: true,
|
||||
startTime: currentTime,
|
||||
current: {
|
||||
modelUuid: modelUuid,
|
||||
pointUuid: pointUuid,
|
||||
actionUuid: action.actionUuid
|
||||
},
|
||||
};
|
||||
|
||||
if (action.triggers[0]?.triggeredAsset?.triggeredModel.modelUuid &&
|
||||
action.triggers[0]?.triggeredAsset?.triggeredPoint?.pointUuid &&
|
||||
action.triggers[0]?.triggeredAsset?.triggeredAction?.actionUuid
|
||||
) {
|
||||
newMaterial.next = {
|
||||
modelUuid: action.triggers[0]?.triggeredAsset?.triggeredModel.modelUuid,
|
||||
pointUuid: action.triggers[0]?.triggeredAsset?.triggeredPoint?.pointUuid,
|
||||
}
|
||||
}
|
||||
|
||||
addMaterial(newMaterial);
|
||||
return newMaterial;
|
||||
}, [addMaterial, getModelUuidByActionUuid, getPointUuidByActionUuid, selectedProduct.productUuid]);
|
||||
|
||||
useFrame(() => {
|
||||
const currentTime = performance.now();
|
||||
const completedActions: string[] = [];
|
||||
let hasChanges = false;
|
||||
|
||||
activeSpawns.forEach(spawn => {
|
||||
const isPausedNow = shouldPauseSpawn(spawn.params.action);
|
||||
|
||||
if (isPausedNow && !spawn.isPaused) {
|
||||
if (spawn.lastSpawnTime === null) {
|
||||
spawn.remainingTime = Math.max(0, (spawn.params.intervalMs / speed) - (currentTime - spawn.startTime));
|
||||
} else {
|
||||
spawn.remainingTime = Math.max(0, (spawn.params.intervalMs / speed) - (currentTime - spawn.lastSpawnTime));
|
||||
}
|
||||
spawn.pauseStartTime = currentTime;
|
||||
spawn.isPaused = true;
|
||||
hasChanges = true;
|
||||
} else if (!isPausedNow && spawn.isPaused) {
|
||||
const pauseDuration = currentTime - spawn.pauseStartTime;
|
||||
if (spawn.lastSpawnTime === null) {
|
||||
spawn.startTime += pauseDuration;
|
||||
} else {
|
||||
spawn.lastSpawnTime += pauseDuration;
|
||||
}
|
||||
spawn.isPaused = false;
|
||||
spawn.pauseStartTime = 0;
|
||||
spawn.remainingTime = 0;
|
||||
hasChanges = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (isPlaying && !isReset && !isPaused) {
|
||||
activeSpawns.forEach((spawn, actionUuid) => {
|
||||
if (spawn.isPaused) return;
|
||||
|
||||
const { material, intervalMs, totalCount, action } = spawn.params;
|
||||
const adjustedInterval = intervalMs / speed;
|
||||
const isFirstSpawn = spawn.lastSpawnTime === null;
|
||||
|
||||
// First spawn
|
||||
if (isFirstSpawn) {
|
||||
const elapsed = currentTime - spawn.startTime;
|
||||
if (elapsed >= adjustedInterval) {
|
||||
const createdMaterial = createNewMaterial(material, action);
|
||||
if (createdMaterial) {
|
||||
spawnLogStatus(createdMaterial.materialName, `[${elapsed.toFixed(2)}ms] Spawned ${material} (1/${totalCount})`);
|
||||
}
|
||||
spawn.lastSpawnTime = currentTime;
|
||||
spawn.spawnCount = 1;
|
||||
hasChanges = true;
|
||||
|
||||
if (totalCount <= 1) {
|
||||
completedActions.push(actionUuid);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Subsequent spawns
|
||||
if (spawn.lastSpawnTime !== null) {
|
||||
const timeSinceLast = currentTime - spawn.lastSpawnTime;
|
||||
if (timeSinceLast >= adjustedInterval) {
|
||||
const count = spawn.spawnCount + 1;
|
||||
const createdMaterial = createNewMaterial(material, action);
|
||||
if (createdMaterial) {
|
||||
spawnLogStatus(createdMaterial.materialName, `[${timeSinceLast.toFixed(2)}ms] Spawned ${material} (${count}/${totalCount})`);
|
||||
}
|
||||
spawn.lastSpawnTime = currentTime;
|
||||
spawn.spawnCount = count;
|
||||
hasChanges = true;
|
||||
|
||||
if (count >= totalCount) {
|
||||
completedActions.push(actionUuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (hasChanges || completedActions.length > 0) {
|
||||
setActiveSpawns(prevSpawns => {
|
||||
const newSpawns = new Map(prevSpawns);
|
||||
|
||||
completedActions.forEach(actionUuid => {
|
||||
newSpawns.delete(actionUuid);
|
||||
});
|
||||
|
||||
return newSpawns;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const handleSpawn = useCallback((action: ConveyorAction) => {
|
||||
if (!action || action.actionType !== 'spawn') return;
|
||||
|
||||
const { material, spawnInterval = 0, spawnCount = 1, actionUuid } = action;
|
||||
const intervalMs = spawnInterval * 1000;
|
||||
|
||||
setActiveSpawns(prevSpawns => {
|
||||
const newSpawns = new Map(prevSpawns);
|
||||
|
||||
if (newSpawns.has(actionUuid)) {
|
||||
newSpawns.delete(actionUuid);
|
||||
}
|
||||
|
||||
newSpawns.set(actionUuid, {
|
||||
lastSpawnTime: null,
|
||||
startTime: performance.now(),
|
||||
spawnCount: 0,
|
||||
params: {
|
||||
material,
|
||||
intervalMs,
|
||||
totalCount: spawnCount,
|
||||
action: action
|
||||
},
|
||||
pauseStartTime: 0,
|
||||
remainingTime: 0,
|
||||
isPaused: false
|
||||
});
|
||||
|
||||
return newSpawns;
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearAllSpawns();
|
||||
};
|
||||
}, [clearAllSpawns]);
|
||||
|
||||
return {
|
||||
handleSpawn,
|
||||
clearAllSpawns
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useCallback } from "react";
|
||||
import { useSceneContext } from "../../../../scene/sceneContext";
|
||||
|
||||
export function useSwapHandler() {
|
||||
const { materialStore } = useSceneContext();
|
||||
const { getMaterialById, setMaterial } = materialStore();
|
||||
|
||||
const swapLogStatus = (materialUuid: string, status: string) => {
|
||||
echo.info(`${materialUuid}, ${status}`);
|
||||
}
|
||||
|
||||
const handleSwap = useCallback((action: ConveyorAction, materialId?: string) => {
|
||||
if (!action || action.actionType !== 'swap' || !materialId) return;
|
||||
|
||||
const { material: newMaterialType } = action;
|
||||
const material = getMaterialById(materialId);
|
||||
if (!material) return;
|
||||
|
||||
setMaterial(material.materialId, newMaterialType);
|
||||
swapLogStatus(material.materialName, `Swapped to ${newMaterialType}`);
|
||||
|
||||
}, [getMaterialById, setMaterial]);
|
||||
|
||||
return {
|
||||
handleSwap,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { useEffect, useCallback } from "react";
|
||||
import { useDefaultHandler } from "./actionHandler/useDefaultHandler";
|
||||
import { useSpawnHandler } from "./actionHandler/useSpawnHandler";
|
||||
import { useSwapHandler } from "./actionHandler/useSwapHandler";
|
||||
import { useDelayHandler } from "./actionHandler/useDelayHandler";
|
||||
import { useDespawnHandler } from "./actionHandler/useDespawnHandler";
|
||||
|
||||
export function useConveyorActions() {
|
||||
const { handleDefault } = useDefaultHandler();
|
||||
const { handleSpawn, clearAllSpawns } = useSpawnHandler();
|
||||
const { handleSwap } = useSwapHandler();
|
||||
const { handleDespawn } = useDespawnHandler();
|
||||
const { handleDelay, cleanupDelay } = useDelayHandler();
|
||||
|
||||
const handleDefaultAction = useCallback((action: ConveyorAction, materialId?: string) => {
|
||||
handleDefault(action, materialId);
|
||||
}, [handleDefault]);
|
||||
|
||||
const handleSpawnAction = useCallback((action: ConveyorAction) => {
|
||||
handleSpawn(action);
|
||||
}, [handleSpawn]);
|
||||
|
||||
const handleSwapAction = useCallback((action: ConveyorAction, materialId?: string) => {
|
||||
handleSwap(action, materialId);
|
||||
}, [handleSwap]);
|
||||
|
||||
const handleDelayAction = useCallback((action: ConveyorAction, materialId?: string) => {
|
||||
handleDelay(action, materialId);
|
||||
}, [handleDelay]);
|
||||
|
||||
const handleDespawnAction = useCallback((action: ConveyorAction, materialId?: string) => {
|
||||
handleDespawn(action, materialId)
|
||||
}, [handleDespawn]);
|
||||
|
||||
const handleConveyorAction = useCallback((action: ConveyorAction, materialId?: string) => {
|
||||
if (!action) return;
|
||||
|
||||
switch (action.actionType) {
|
||||
case 'default':
|
||||
handleDefaultAction(action);
|
||||
break;
|
||||
case 'spawn':
|
||||
handleSpawnAction(action);
|
||||
break;
|
||||
case 'swap':
|
||||
handleSwapAction(action, materialId);
|
||||
break;
|
||||
case 'delay':
|
||||
handleDelayAction(action, materialId);
|
||||
break;
|
||||
case 'despawn':
|
||||
handleDespawnAction(action, materialId);
|
||||
break;
|
||||
default:
|
||||
console.warn(`Unknown conveyor action type: ${action.actionType}`);
|
||||
}
|
||||
}, [handleDefaultAction, handleSpawnAction, handleSwapAction, handleDelayAction, handleDespawnAction]);
|
||||
|
||||
const cleanup = useCallback(() => {
|
||||
clearAllSpawns();
|
||||
cleanupDelay();
|
||||
}, [clearAllSpawns, cleanupDelay]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
cleanup();
|
||||
};
|
||||
}, [cleanup]);
|
||||
|
||||
return {
|
||||
handleConveyorAction,
|
||||
cleanup
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useCallback } from "react";
|
||||
import { useProductStore } from "../../../../../store/simulation/useProductStore";
|
||||
import { useSceneContext } from "../../../../scene/sceneContext";
|
||||
import { useProductContext } from "../../../products/productContext";
|
||||
|
||||
export function useProcessHandler() {
|
||||
const { materialStore, machineStore } = useSceneContext();
|
||||
const { selectedProductStore } = useProductContext();
|
||||
const { getMaterialById, setMaterial } = materialStore();
|
||||
const { addCurrentAction } = machineStore();
|
||||
const { getModelUuidByActionUuid } = useProductStore();
|
||||
const { selectedProduct } = selectedProductStore();
|
||||
|
||||
const processLogStatus = (materialUuid: string, status: string) => {
|
||||
echo.log(`${materialUuid}, ${status}`);
|
||||
}
|
||||
|
||||
const handleProcess = useCallback((action: MachineAction, materialId?: string) => {
|
||||
if (!action || action.actionType !== 'process' || !materialId) return;
|
||||
|
||||
const { swapMaterial: newMaterialType } = action;
|
||||
|
||||
const material = getMaterialById(materialId);
|
||||
if (!material) return;
|
||||
|
||||
const modelUuid = getModelUuidByActionUuid(selectedProduct.productUuid, action.actionUuid);
|
||||
|
||||
if (!modelUuid) return;
|
||||
setMaterial(material.materialId, newMaterialType);
|
||||
|
||||
addCurrentAction(modelUuid, action.actionUuid, newMaterialType, material.materialId);
|
||||
|
||||
processLogStatus(material.materialName, `Swapped to ${newMaterialType}`);
|
||||
processLogStatus(material.materialName, `starts Process action`);
|
||||
|
||||
}, [getMaterialById]);
|
||||
|
||||
return {
|
||||
handleProcess
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { useEffect, useCallback } from 'react';
|
||||
import { useProcessHandler } from './actionHandler/useProcessHandler';
|
||||
|
||||
export function useMachineActions() {
|
||||
const { handleProcess } = useProcessHandler();
|
||||
|
||||
const handleProcessAction = useCallback((action: MachineAction, materialId: string) => {
|
||||
handleProcess(action, materialId);
|
||||
}, [handleProcess]);
|
||||
|
||||
const handleMachineAction = useCallback((action: MachineAction, materialId: string) => {
|
||||
if (!action) return;
|
||||
|
||||
switch (action.actionType) {
|
||||
case 'process':
|
||||
handleProcessAction(action, materialId);
|
||||
break;
|
||||
default:
|
||||
console.warn(`Unknown machine action type: ${action.actionType}`);
|
||||
}
|
||||
}, [handleProcessAction]);
|
||||
|
||||
const cleanup = useCallback(() => {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
cleanup();
|
||||
};
|
||||
}, [cleanup]);
|
||||
|
||||
return {
|
||||
handleMachineAction,
|
||||
cleanup,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { useCallback } from "react";
|
||||
import { useProductStore } from "../../../../../store/simulation/useProductStore";
|
||||
import { useSceneContext } from "../../../../scene/sceneContext";
|
||||
import { useProductContext } from "../../../products/productContext";
|
||||
|
||||
export function usePickAndPlaceHandler() {
|
||||
const { materialStore, armBotStore } = useSceneContext();
|
||||
const { selectedProductStore } = useProductContext();
|
||||
const { getMaterialById } = materialStore();
|
||||
const { addCurrentAction } = armBotStore();
|
||||
const { getModelUuidByActionUuid } = useProductStore();
|
||||
const { selectedProduct } = selectedProductStore();
|
||||
|
||||
const pickAndPlaceLogStatus = (materialUuid: string, status: string) => {
|
||||
echo.warn(`${materialUuid}, ${status}`);
|
||||
}
|
||||
|
||||
const handlePickAndPlace = useCallback((action: RoboticArmAction, materialId?: string) => {
|
||||
if (!action || action.actionType !== 'pickAndPlace' || !materialId) return;
|
||||
|
||||
const material = getMaterialById(materialId);
|
||||
if (!material) return;
|
||||
|
||||
const modelUuid = getModelUuidByActionUuid(selectedProduct.productUuid, action.actionUuid);
|
||||
|
||||
if (!modelUuid) return;
|
||||
|
||||
addCurrentAction(
|
||||
modelUuid,
|
||||
action.actionUuid,
|
||||
material.materialType,
|
||||
material.materialId
|
||||
);
|
||||
|
||||
pickAndPlaceLogStatus(material.materialName, `is going to be picked by armBot ${modelUuid}`);
|
||||
|
||||
}, [getMaterialById, getModelUuidByActionUuid, selectedProduct.productUuid, addCurrentAction]);
|
||||
|
||||
return {
|
||||
handlePickAndPlace,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { useEffect, useCallback } from 'react';
|
||||
import { usePickAndPlaceHandler } from './actionHandler/usePickAndPlaceHandler';
|
||||
|
||||
export function useRoboticArmActions() {
|
||||
const { handlePickAndPlace } = usePickAndPlaceHandler();
|
||||
|
||||
const handlePickAndPlaceAction = useCallback((action: RoboticArmAction, materialId: string) => {
|
||||
handlePickAndPlace(action, materialId);
|
||||
}, [handlePickAndPlace]);
|
||||
|
||||
const handleRoboticArmAction = useCallback((action: RoboticArmAction, materialId: string) => {
|
||||
if (!action) return;
|
||||
|
||||
switch (action.actionType) {
|
||||
case 'pickAndPlace':
|
||||
handlePickAndPlaceAction(action, materialId);
|
||||
break;
|
||||
default:
|
||||
console.warn(`Unknown robotic arm action type: ${action.actionType}`);
|
||||
}
|
||||
}, [handlePickAndPlaceAction]);
|
||||
|
||||
const cleanup = useCallback(() => {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
cleanup();
|
||||
};
|
||||
}, [cleanup]);
|
||||
|
||||
return {
|
||||
handleRoboticArmAction,
|
||||
cleanup
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
import { useCallback, useState, useEffect, useRef } from "react";
|
||||
import { useFrame } from "@react-three/fiber";
|
||||
import { useProductStore } from "../../../../../store/simulation/useProductStore";
|
||||
import { usePlayButtonStore, usePauseButtonStore, useResetButtonStore, useAnimationPlaySpeed } from "../../../../../store/usePlayButtonStore";
|
||||
import { useSceneContext } from "../../../../scene/sceneContext";
|
||||
import { useProductContext } from "../../../products/productContext";
|
||||
|
||||
export function useRetrieveHandler() {
|
||||
const { materialStore, armBotStore, vehicleStore, storageUnitStore } = useSceneContext();
|
||||
const { selectedProductStore } = useProductContext();
|
||||
const { addMaterial } = materialStore();
|
||||
const { getModelUuidByActionUuid, getPointUuidByActionUuid, getEventByModelUuid, getActionByUuid } = useProductStore();
|
||||
const { getStorageUnitById, getLastMaterial, updateCurrentLoad, removeLastMaterial } = storageUnitStore();
|
||||
const { getVehicleById, incrementVehicleLoad, addCurrentMaterial } = vehicleStore();
|
||||
const { selectedProduct } = selectedProductStore();
|
||||
const { getArmBotById, addCurrentAction } = armBotStore();
|
||||
const { isPlaying } = usePlayButtonStore();
|
||||
const { speed } = useAnimationPlaySpeed();
|
||||
const { isPaused } = usePauseButtonStore();
|
||||
const { isReset } = useResetButtonStore();
|
||||
|
||||
const [activeRetrievals, setActiveRetrievals] = useState<Map<string, { action: StorageAction, isProcessing: boolean, lastCheckTime: number }>>(new Map());
|
||||
const retrievalTimeRef = useRef<Map<string, number>>(new Map());
|
||||
|
||||
const [initialDelayComplete, setInitialDelayComplete] = useState(false);
|
||||
const delayTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const retrieveLogStatus = (materialUuid: string, status: string) => {
|
||||
echo.info(`${materialUuid}, ${status}`);
|
||||
}
|
||||
|
||||
const createNewMaterial = useCallback((materialId: string, materialType: string, action: StorageAction) => {
|
||||
const modelUuid = getModelUuidByActionUuid(selectedProduct.productUuid, action.actionUuid);
|
||||
const pointUuid = getPointUuidByActionUuid(selectedProduct.productUuid, action.actionUuid);
|
||||
if (!modelUuid || !pointUuid) return null;
|
||||
const currentTime = performance.now();
|
||||
|
||||
if (action.triggers[0]?.triggeredAsset?.triggeredModel.modelUuid &&
|
||||
action.triggers[0]?.triggeredAsset?.triggeredPoint?.pointUuid &&
|
||||
action.triggers[0]?.triggeredAsset?.triggeredAction?.actionUuid
|
||||
) {
|
||||
const newMaterial: MaterialSchema = {
|
||||
materialId: materialId,
|
||||
materialName: `${materialType}-${Date.now()}`,
|
||||
materialType: materialType,
|
||||
isActive: false,
|
||||
isVisible: false,
|
||||
isPaused: false,
|
||||
isRendered: true,
|
||||
startTime: currentTime,
|
||||
previous: {
|
||||
modelUuid: modelUuid,
|
||||
pointUuid: pointUuid,
|
||||
actionUuid: action.actionUuid
|
||||
},
|
||||
current: {
|
||||
modelUuid: action.triggers[0]?.triggeredAsset.triggeredModel.modelUuid,
|
||||
pointUuid: action.triggers[0]?.triggeredAsset.triggeredPoint.pointUuid,
|
||||
actionUuid: action.triggers[0]?.triggeredAsset.triggeredAction.actionUuid
|
||||
},
|
||||
};
|
||||
|
||||
addMaterial(newMaterial);
|
||||
return newMaterial;
|
||||
}
|
||||
return null;
|
||||
}, [addMaterial, getModelUuidByActionUuid, getPointUuidByActionUuid, selectedProduct.productUuid]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isPlaying && !initialDelayComplete) {
|
||||
delayTimerRef.current = setTimeout(() => {
|
||||
setInitialDelayComplete(true);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (delayTimerRef.current) {
|
||||
clearTimeout(delayTimerRef.current);
|
||||
}
|
||||
};
|
||||
}, [isPlaying, initialDelayComplete]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isReset || isPaused) {
|
||||
setInitialDelayComplete(false);
|
||||
if (delayTimerRef.current) {
|
||||
clearTimeout(delayTimerRef.current);
|
||||
}
|
||||
}
|
||||
}, [isReset, isPaused]);
|
||||
|
||||
useFrame(() => {
|
||||
if (!isPlaying || isPaused || isReset || !initialDelayComplete) return;
|
||||
|
||||
const currentTime = performance.now();
|
||||
const completedActions: string[] = [];
|
||||
let hasChanges = false;
|
||||
|
||||
activeRetrievals.forEach((retrieval, actionUuid) => {
|
||||
if (retrieval.isProcessing) return;
|
||||
|
||||
const storageUnit = getStorageUnitById(
|
||||
getModelUuidByActionUuid(selectedProduct.productUuid, retrieval.action.actionUuid) ?? ''
|
||||
);
|
||||
|
||||
if (!storageUnit || storageUnit.currentLoad <= 0) {
|
||||
completedActions.push(actionUuid);
|
||||
hasChanges = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (retrieval.action.triggers.length === 0 || !retrieval.action.triggers[0]?.triggeredAsset) {
|
||||
return;
|
||||
}
|
||||
|
||||
const triggeredModel = getEventByModelUuid(
|
||||
selectedProduct.productUuid,
|
||||
retrieval.action.triggers[0]?.triggeredAsset.triggeredModel.modelUuid
|
||||
);
|
||||
|
||||
if (!triggeredModel) return;
|
||||
|
||||
let isIdle = false;
|
||||
|
||||
if (triggeredModel.type === 'roboticArm') {
|
||||
const armBot = getArmBotById(triggeredModel.modelUuid);
|
||||
isIdle = (armBot && !armBot.isActive && armBot.state === 'idle' && !armBot.currentAction) || false;
|
||||
|
||||
if (!armBot) return;
|
||||
if (!retrievalTimeRef.current.has(actionUuid) && isIdle) {
|
||||
retrievalTimeRef.current.set(actionUuid, currentTime);
|
||||
return;
|
||||
}
|
||||
|
||||
const idleStartTime = retrievalTimeRef.current.get(actionUuid);
|
||||
const minIdleTimeBeforeFirstRetrieval = 5000 / speed;
|
||||
const minDelayBetweenRetrievals = 5000 / speed;
|
||||
|
||||
const canProceedFirstRetrieval = idleStartTime !== undefined &&
|
||||
(currentTime - idleStartTime) >= minIdleTimeBeforeFirstRetrieval;
|
||||
|
||||
const lastRetrievalTime = retrievalTimeRef.current.get(`${actionUuid}_last`) ?? null;
|
||||
const canProceedSubsequent = lastRetrievalTime === null ||
|
||||
(currentTime - lastRetrievalTime) >= minDelayBetweenRetrievals;
|
||||
|
||||
const canProceed = lastRetrievalTime === null ? canProceedFirstRetrieval : canProceedSubsequent;
|
||||
|
||||
if (isIdle && canProceed) {
|
||||
setActiveRetrievals(prev => {
|
||||
const newRetrievals = new Map(prev);
|
||||
newRetrievals.set(actionUuid, {
|
||||
...retrieval,
|
||||
isProcessing: true,
|
||||
lastCheckTime: currentTime
|
||||
});
|
||||
return newRetrievals;
|
||||
});
|
||||
|
||||
const lastMaterial = getLastMaterial(storageUnit.modelUuid);
|
||||
if (lastMaterial) {
|
||||
|
||||
if (retrieval.action.triggers[0]?.triggeredAsset.triggeredAction?.actionUuid) {
|
||||
const action = getActionByUuid(selectedProduct.productUuid, retrieval.action.triggers[0]?.triggeredAsset.triggeredAction.actionUuid);
|
||||
if (action && action.triggers.length > 0 && action.triggers[0]?.triggeredAsset?.triggeredModel.modelUuid) {
|
||||
const model = getEventByModelUuid(selectedProduct.productUuid, action.triggers[0]?.triggeredAsset.triggeredModel.modelUuid);
|
||||
if (model) {
|
||||
if (model.type === 'vehicle') {
|
||||
const vehicle = getVehicleById(model.modelUuid);
|
||||
if (vehicle && !vehicle.isActive && vehicle.state === 'idle' && vehicle.isPicking && vehicle.currentLoad < vehicle.point.action.loadCapacity) {
|
||||
|
||||
const material = createNewMaterial(
|
||||
lastMaterial.materialId,
|
||||
lastMaterial.materialType,
|
||||
storageUnit.point.action
|
||||
);
|
||||
if (material) {
|
||||
|
||||
addCurrentAction(
|
||||
triggeredModel.modelUuid,
|
||||
retrieval.action.triggers[0]?.triggeredAsset.triggeredAction?.actionUuid ?? '',
|
||||
material.materialType,
|
||||
material.materialId
|
||||
);
|
||||
retrieveLogStatus(material.materialName, `is being picked by ${armBot?.modelName}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const material = createNewMaterial(
|
||||
lastMaterial.materialId,
|
||||
lastMaterial.materialType,
|
||||
storageUnit.point.action
|
||||
);
|
||||
if (material) {
|
||||
|
||||
addCurrentAction(
|
||||
triggeredModel.modelUuid,
|
||||
retrieval.action.triggers[0]?.triggeredAsset.triggeredAction?.actionUuid ?? '',
|
||||
material.materialType,
|
||||
material.materialId
|
||||
);
|
||||
retrieveLogStatus(material.materialName, `is being picked by ${armBot?.modelName}`);
|
||||
}
|
||||
}
|
||||
setActiveRetrievals(prev => {
|
||||
const newRetrievals = new Map(prev);
|
||||
newRetrievals.set(actionUuid, {
|
||||
...retrieval,
|
||||
isProcessing: false,
|
||||
lastCheckTime: currentTime,
|
||||
});
|
||||
return newRetrievals;
|
||||
});
|
||||
|
||||
retrievalTimeRef.current.set(actionUuid, currentTime);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
} else if (!isIdle) {
|
||||
retrievalTimeRef.current.delete(actionUuid);
|
||||
}
|
||||
|
||||
} else if (triggeredModel.type === 'vehicle') {
|
||||
const vehicle = getVehicleById(triggeredModel.modelUuid);
|
||||
isIdle = (vehicle && !vehicle.isActive && vehicle.state === 'idle' && vehicle.isPicking && vehicle.currentLoad < vehicle.point.action.loadCapacity) || false;
|
||||
|
||||
if (!vehicle) return;
|
||||
|
||||
const loadDuration = vehicle.point.action.unLoadDuration;
|
||||
let minDelayBetweenRetrievals = (loadDuration * 1000) / speed;
|
||||
const minIdleTimeBeforeFirstRetrieval = 3000 / speed;
|
||||
|
||||
if (!retrievalTimeRef.current.has(actionUuid) && isIdle) {
|
||||
retrievalTimeRef.current.set(actionUuid, currentTime);
|
||||
return;
|
||||
}
|
||||
|
||||
const idleStartTime = retrievalTimeRef.current.get(actionUuid);
|
||||
const lastRetrievalTime = retrievalTimeRef.current.get(`${actionUuid}_last`) ?? null;
|
||||
|
||||
const canProceedFirstRetrieval = idleStartTime !== undefined &&
|
||||
(currentTime - idleStartTime) >= minIdleTimeBeforeFirstRetrieval;
|
||||
|
||||
const canProceedSubsequent = lastRetrievalTime === null ||
|
||||
(currentTime - lastRetrievalTime) >= minDelayBetweenRetrievals;
|
||||
|
||||
const canProceed = lastRetrievalTime === null ? canProceedFirstRetrieval : canProceedSubsequent;
|
||||
|
||||
if (isIdle && canProceed) {
|
||||
setActiveRetrievals(prev => {
|
||||
const newRetrievals = new Map(prev);
|
||||
newRetrievals.set(actionUuid, {
|
||||
...retrieval,
|
||||
isProcessing: true,
|
||||
lastCheckTime: currentTime
|
||||
});
|
||||
return newRetrievals;
|
||||
});
|
||||
|
||||
const lastMaterial = getLastMaterial(storageUnit.modelUuid);
|
||||
if (lastMaterial) {
|
||||
if (vehicle?.currentLoad < vehicle.point.action.loadCapacity) {
|
||||
const material = createNewMaterial(
|
||||
lastMaterial.materialId,
|
||||
lastMaterial.materialType,
|
||||
storageUnit.point.action
|
||||
);
|
||||
|
||||
if (material) {
|
||||
removeLastMaterial(storageUnit.modelUuid);
|
||||
updateCurrentLoad(storageUnit.modelUuid, -1)
|
||||
incrementVehicleLoad(vehicle.modelUuid, 1);
|
||||
addCurrentMaterial(vehicle.modelUuid, material.materialType, material.materialId);
|
||||
retrieveLogStatus(material.materialName, `is picked by ${vehicle.modelName}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setActiveRetrievals(prev => {
|
||||
const newRetrievals = new Map(prev);
|
||||
newRetrievals.set(actionUuid, {
|
||||
...retrieval,
|
||||
isProcessing: false,
|
||||
lastCheckTime: currentTime,
|
||||
});
|
||||
return newRetrievals;
|
||||
});
|
||||
|
||||
retrievalTimeRef.current.set(actionUuid, currentTime);
|
||||
retrievalTimeRef.current.set(`${actionUuid}_last`, currentTime);
|
||||
} else if (!isIdle) {
|
||||
retrievalTimeRef.current.delete(actionUuid);
|
||||
retrievalTimeRef.current.delete(`${actionUuid}_last`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (hasChanges || completedActions.length > 0) {
|
||||
setActiveRetrievals(prev => {
|
||||
const newRetrievals = new Map(prev);
|
||||
completedActions.forEach(actionUuid => newRetrievals.delete(actionUuid));
|
||||
return newRetrievals;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const handleRetrieve = useCallback((action: StorageAction) => {
|
||||
if (!action || action.actionType !== 'retrieve') return;
|
||||
|
||||
setActiveRetrievals(prev => {
|
||||
const newRetrievals = new Map(prev);
|
||||
newRetrievals.set(action.actionUuid, {
|
||||
action,
|
||||
isProcessing: false,
|
||||
lastCheckTime: performance.now(),
|
||||
});
|
||||
return newRetrievals;
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isReset) {
|
||||
setActiveRetrievals(new Map());
|
||||
setInitialDelayComplete(false);
|
||||
}
|
||||
}, [isReset]);
|
||||
|
||||
return {
|
||||
handleRetrieve,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { useCallback } from "react";
|
||||
import { useProductStore } from "../../../../../store/simulation/useProductStore";
|
||||
import { useSceneContext } from "../../../../scene/sceneContext";
|
||||
import { useProductContext } from "../../../products/productContext";
|
||||
|
||||
export function useStoreHandler() {
|
||||
const { materialStore, storageUnitStore } = useSceneContext();
|
||||
const { getMaterialById, removeMaterial, setEndTime } = materialStore();
|
||||
const { addCurrentMaterial, updateCurrentLoad } = storageUnitStore();
|
||||
const { getModelUuidByActionUuid } = useProductStore();
|
||||
const { selectedProductStore } = useProductContext();
|
||||
const { selectedProduct } = selectedProductStore();
|
||||
|
||||
const storeLogStatus = (materialUuid: string, status: string) => {
|
||||
echo.info(`${materialUuid}, ${status}`);
|
||||
}
|
||||
|
||||
const handleStore = useCallback((action: StorageAction, materialId?: string) => {
|
||||
if (!action || action.actionType !== 'store' || !materialId) return;
|
||||
|
||||
const material = getMaterialById(materialId);
|
||||
if (!material) return;
|
||||
|
||||
const modelUuid = getModelUuidByActionUuid(selectedProduct.productUuid, action.actionUuid);
|
||||
if (!modelUuid) return;
|
||||
|
||||
setEndTime(material.materialId, performance.now());
|
||||
removeMaterial(material.materialId);
|
||||
addCurrentMaterial(modelUuid, material.materialType, material.materialId);
|
||||
updateCurrentLoad(modelUuid, 1);
|
||||
|
||||
storeLogStatus(material.materialName, `performed Store action`);
|
||||
|
||||
}, [getMaterialById]);
|
||||
|
||||
return {
|
||||
handleStore,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useEffect, useCallback } from 'react';
|
||||
import { useStoreHandler } from './actionHandler/useStoreHandler';
|
||||
import { useRetrieveHandler } from './actionHandler/useRetrieveHandler';
|
||||
|
||||
export function useStorageActions() {
|
||||
const { handleStore } = useStoreHandler();
|
||||
const { handleRetrieve } = useRetrieveHandler();
|
||||
|
||||
const handleStoreAction = useCallback((action: StorageAction, materialId: string) => {
|
||||
handleStore(action, materialId);
|
||||
}, [handleStore]);
|
||||
|
||||
const handleRetrieveAction = useCallback((action: StorageAction) => {
|
||||
handleRetrieve(action);
|
||||
}, [handleRetrieve]);
|
||||
|
||||
const handleStorageAction = useCallback((action: StorageAction, materialId: string) => {
|
||||
if (!action) return;
|
||||
|
||||
switch (action.actionType) {
|
||||
case 'store':
|
||||
handleStoreAction(action, materialId);
|
||||
break;
|
||||
case 'retrieve':
|
||||
handleRetrieveAction(action);
|
||||
break;
|
||||
default:
|
||||
console.warn(`Unknown storage action type: ${action.actionType}`);
|
||||
}
|
||||
}, [handleStoreAction]);
|
||||
|
||||
const cleanup = useCallback(() => {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
cleanup();
|
||||
};
|
||||
}, [cleanup]);
|
||||
|
||||
return {
|
||||
handleStorageAction,
|
||||
cleanup
|
||||
};
|
||||
}
|
||||
67
app/src/modules/simulation/actions/useActionHandler.ts
Normal file
67
app/src/modules/simulation/actions/useActionHandler.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import {
|
||||
usePlayButtonStore,
|
||||
useResetButtonStore,
|
||||
} from "../../../store/usePlayButtonStore";
|
||||
import { useConveyorActions } from "./conveyor/useConveyorActions";
|
||||
import { useMachineActions } from "./machine/useMachineActions";
|
||||
import { useRoboticArmActions } from "./roboticArm/useRoboticArmActions";
|
||||
import { useStorageActions } from "./storageUnit/useStorageUnitActions";
|
||||
import { useVehicleActions } from "./vehicle/useVehicleActions";
|
||||
import { useCallback, useEffect } from "react";
|
||||
|
||||
export function useActionHandler() {
|
||||
const { isReset } = useResetButtonStore();
|
||||
const { isPlaying } = usePlayButtonStore();
|
||||
const { handleConveyorAction, cleanup: cleanupConveyor } = useConveyorActions();
|
||||
const { handleVehicleAction, cleanup: cleanupVehicle } = useVehicleActions();
|
||||
const { handleRoboticArmAction, cleanup: cleanupRoboticArm } = useRoboticArmActions();
|
||||
const { handleMachineAction, cleanup: cleanupMachine } = useMachineActions();
|
||||
const { handleStorageAction, cleanup: cleanupStorage } = useStorageActions();
|
||||
|
||||
const handleAction = useCallback((action: Action, materialId?: string) => {
|
||||
if (!action) return;
|
||||
try {
|
||||
switch (action.actionType) {
|
||||
case 'default': case 'spawn': case 'swap': case 'delay': case 'despawn':
|
||||
handleConveyorAction(action as ConveyorAction, materialId as string);
|
||||
break;
|
||||
case 'travel':
|
||||
handleVehicleAction(action as VehicleAction, materialId as string);
|
||||
break;
|
||||
case 'pickAndPlace':
|
||||
handleRoboticArmAction(action as RoboticArmAction, materialId as string);
|
||||
break;
|
||||
case 'process':
|
||||
handleMachineAction(action as MachineAction, materialId as string);
|
||||
break;
|
||||
case 'store': case 'retrieve':
|
||||
handleStorageAction(action as StorageAction, materialId as string);
|
||||
break;
|
||||
default:
|
||||
console.warn(`Unknown action type: ${(action as Action).actionType}`);
|
||||
}
|
||||
} catch (error) {
|
||||
echo.error("Failed to handle action");
|
||||
console.error("Error handling action:", error);
|
||||
}
|
||||
}, [handleConveyorAction, handleVehicleAction, handleRoboticArmAction, handleMachineAction, handleStorageAction,]);
|
||||
|
||||
const cleanup = useCallback(() => {
|
||||
cleanupConveyor();
|
||||
cleanupVehicle();
|
||||
cleanupRoboticArm();
|
||||
cleanupMachine();
|
||||
cleanupStorage();
|
||||
}, [cleanupConveyor, cleanupVehicle, cleanupRoboticArm, cleanupMachine, cleanupStorage,]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
cleanup();
|
||||
};
|
||||
}, [cleanup, isReset, isPlaying]);
|
||||
|
||||
return {
|
||||
handleAction,
|
||||
cleanup,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { useCallback } from "react";
|
||||
import { useProductStore } from "../../../../../store/simulation/useProductStore";
|
||||
import { useSceneContext } from "../../../../scene/sceneContext";
|
||||
import { useProductContext } from "../../../products/productContext";
|
||||
|
||||
export function useTravelHandler() {
|
||||
const { materialStore, vehicleStore } = useSceneContext();
|
||||
const { selectedProductStore } = useProductContext();
|
||||
const { getMaterialById } = materialStore();
|
||||
const { getModelUuidByActionUuid } = useProductStore();
|
||||
const { selectedProduct } = selectedProductStore();
|
||||
const { incrementVehicleLoad, addCurrentMaterial } = vehicleStore();
|
||||
|
||||
const travelLogStatus = (materialUuid: string, status: string) => {
|
||||
echo.info(`${materialUuid}, ${status}`);
|
||||
}
|
||||
|
||||
const handleTravel = useCallback((action: VehicleAction, materialId?: string) => {
|
||||
if (!action || action.actionType !== 'travel' || !materialId) return;
|
||||
|
||||
const material = getMaterialById(materialId);
|
||||
if (!material) return;
|
||||
|
||||
const modelUuid = getModelUuidByActionUuid(selectedProduct.productUuid, action.actionUuid);
|
||||
|
||||
if (!modelUuid) return;
|
||||
|
||||
incrementVehicleLoad(modelUuid, 1);
|
||||
addCurrentMaterial(modelUuid, material.materialType, material.materialId);
|
||||
|
||||
travelLogStatus(material.materialName, `is triggering travel from ${modelUuid}`);
|
||||
|
||||
}, [addCurrentMaterial, getMaterialById, getModelUuidByActionUuid, incrementVehicleLoad, selectedProduct.productUuid]);
|
||||
|
||||
return {
|
||||
handleTravel,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { useEffect, useCallback } from 'react';
|
||||
import { useTravelHandler } from './actionHandler/useTravelHandler';
|
||||
|
||||
export function useVehicleActions() {
|
||||
const { handleTravel } = useTravelHandler();
|
||||
|
||||
const handleTravelAction = useCallback((action: VehicleAction, materialId: string) => {
|
||||
handleTravel(action, materialId);
|
||||
}, [handleTravel]);
|
||||
|
||||
const handleVehicleAction = useCallback((action: VehicleAction, materialId: string) => {
|
||||
if (!action) return;
|
||||
|
||||
switch (action.actionType) {
|
||||
case 'travel':
|
||||
handleTravelAction(action, materialId);
|
||||
break;
|
||||
default:
|
||||
console.warn(`Unknown vehicle action type: ${action.actionType}`);
|
||||
}
|
||||
}, [handleTravelAction]);
|
||||
|
||||
const cleanup = useCallback(() => {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
cleanup();
|
||||
};
|
||||
}, [cleanup]);
|
||||
|
||||
return {
|
||||
handleVehicleAction,
|
||||
cleanup
|
||||
};
|
||||
}
|
||||
126
app/src/modules/simulation/analysis/ROI/roiData.tsx
Normal file
126
app/src/modules/simulation/analysis/ROI/roiData.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useInputValues, useProductionCapacityData, useROISummaryData } from '../../../../store/builder/store';
|
||||
import { usePlayButtonStore } from '../../../../store/usePlayButtonStore';
|
||||
import { useProductContext } from '../../products/productContext';
|
||||
|
||||
export default function ROIData() {
|
||||
const { selectedProductStore } = useProductContext();
|
||||
const { inputValues } = useInputValues();
|
||||
const { productionCapacityData } = useProductionCapacityData()
|
||||
const { selectedProduct } = selectedProductStore();
|
||||
const { isPlaying } = usePlayButtonStore();
|
||||
const { setRoiSummaryData } = useROISummaryData();
|
||||
useEffect(() => {
|
||||
if (!isPlaying) {
|
||||
setRoiSummaryData({
|
||||
productName: "",
|
||||
roiPercentage: 0,
|
||||
paybackPeriod: 0,
|
||||
totalCost: 0,
|
||||
revenueGenerated: 0,
|
||||
netProfit: 0,
|
||||
netLoss: 0,
|
||||
})
|
||||
return;
|
||||
}
|
||||
|
||||
if (inputValues === undefined) return;
|
||||
|
||||
const electricityCost = parseFloat(inputValues["Electricity cost"]);
|
||||
const fixedCost = parseFloat(inputValues["Fixed costs"]);
|
||||
const laborCost = parseFloat(inputValues["Labor Cost"]);
|
||||
const maintenanceCost = parseFloat(inputValues["Maintenance cost"]); // Remove space typ
|
||||
const materialCost = parseFloat(inputValues["Material cost"]);
|
||||
const productionPeriod = parseFloat(inputValues["Production period"]);
|
||||
const salvageValue = parseFloat(inputValues["Salvage value"]);
|
||||
const sellingPrice = parseFloat(inputValues["Selling price"]);
|
||||
const initialInvestment = parseFloat(inputValues["Initial Investment"]);
|
||||
const shiftLength = parseFloat(inputValues["Shift length"]);
|
||||
const shiftsPerDay = parseFloat(inputValues["Shifts / day"]);
|
||||
const workingDaysPerYear = parseFloat(inputValues["Working days / year"]);
|
||||
|
||||
if (!isNaN(electricityCost) && !isNaN(fixedCost) && !isNaN(laborCost) && !isNaN(maintenanceCost) &&
|
||||
!isNaN(materialCost) && !isNaN(productionPeriod) && !isNaN(salvageValue) && !isNaN(sellingPrice) &&
|
||||
!isNaN(shiftLength) && !isNaN(shiftsPerDay) && !isNaN(workingDaysPerYear) && productionCapacityData > 0) {
|
||||
|
||||
|
||||
|
||||
|
||||
const totalHoursPerYear = shiftLength * shiftsPerDay * workingDaysPerYear;
|
||||
|
||||
// Total good units produced per year
|
||||
const annualProductionUnits = productionCapacityData * totalHoursPerYear;
|
||||
|
||||
// Revenue for a year
|
||||
const annualRevenue = annualProductionUnits * sellingPrice;
|
||||
|
||||
// Costs
|
||||
const totalMaterialCost = annualProductionUnits * materialCost;
|
||||
const totalLaborCost = laborCost * totalHoursPerYear;
|
||||
const totalEnergyCost = electricityCost * totalHoursPerYear;
|
||||
const totalMaintenanceCost = maintenanceCost + fixedCost;
|
||||
|
||||
const totalAnnualCost = totalMaterialCost + totalLaborCost + totalEnergyCost + totalMaintenanceCost;
|
||||
|
||||
// Annual Profit
|
||||
const annualProfit = annualRevenue - totalAnnualCost;
|
||||
console.log('annualProfit: ', annualProfit);
|
||||
|
||||
// Net Profit over production period
|
||||
const netProfit = annualProfit * productionPeriod;
|
||||
|
||||
// ROI
|
||||
const roiPercentage = ((netProfit + salvageValue - initialInvestment) / initialInvestment) * 100;
|
||||
|
||||
// Payback Period
|
||||
const paybackPeriod = initialInvestment / (annualProfit || 1); // Avoid division by 0
|
||||
console.log('paybackPeriod: ', paybackPeriod);
|
||||
|
||||
// console.log("--- ROI Breakdown ---");
|
||||
// console.log("Annual Production Units:", annualProductionUnits.toFixed(2));
|
||||
// console.log("Annual Revenue:", annualRevenue.toFixed(2));
|
||||
// console.log("Total Annual Cost:", totalAnnualCost.toFixed(2));
|
||||
// console.log("Annual Profit:", annualProfit.toFixed(2));
|
||||
// console.log("Net Profit:", netProfit.toFixed(2));
|
||||
// console.log("ROI %:", roiPercentage.toFixed(2));
|
||||
// console.log("Payback Period (years):", paybackPeriod.toFixed(2));
|
||||
|
||||
setRoiSummaryData({
|
||||
productName: selectedProduct.productName,
|
||||
roiPercentage: parseFloat((roiPercentage / 100).toFixed(2)), // normalized to 0.x format
|
||||
paybackPeriod: parseFloat(paybackPeriod.toFixed(2)),
|
||||
totalCost: parseFloat(totalAnnualCost.toFixed(2)),
|
||||
revenueGenerated: parseFloat(annualRevenue.toFixed(2)),
|
||||
netProfit: netProfit > 0 ? parseFloat(netProfit.toFixed(2)) : 0,
|
||||
netLoss: netProfit < 0 ? -netProfit : 0
|
||||
});
|
||||
|
||||
const productCount = 1000;
|
||||
|
||||
// Cost per unit (based on full annual cost)
|
||||
const costPerUnit = totalAnnualCost / annualProductionUnits;
|
||||
|
||||
const costForTargetUnits = productCount * costPerUnit;
|
||||
const revenueForTargetUnits = productCount * sellingPrice;
|
||||
const profitForTargetUnits = revenueForTargetUnits - costForTargetUnits;
|
||||
|
||||
const netProfitForTarget = profitForTargetUnits > 0 ? profitForTargetUnits : 0;
|
||||
const netLossForTarget = profitForTargetUnits < 0 ? -profitForTargetUnits : 0;
|
||||
|
||||
// console.log("--- Fixed Product Count (" + productCount + ") ---");
|
||||
// console.log("Cost per Unit:", costPerUnit.toFixed(2));
|
||||
// console.log("Total Cost for " + productCount + " Units:", costForTargetUnits.toFixed(2));
|
||||
// console.log("Revenue for " + productCount + " Units:", revenueForTargetUnits.toFixed(2));
|
||||
// console.log("Profit:", netProfitForTarget.toFixed(2));
|
||||
// console.log("Loss:", netLossForTarget.toFixed(2));
|
||||
|
||||
}
|
||||
|
||||
}, [inputValues, productionCapacityData]);
|
||||
|
||||
return (
|
||||
<></>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import { useInputValues, useProductionCapacityData, useThroughPutData } from '../../../../store/builder/store'
|
||||
import { usePlayButtonStore } from '../../../../store/usePlayButtonStore';
|
||||
|
||||
export default function ProductionCapacityData() {
|
||||
const { throughputData } = useThroughPutData()
|
||||
const { productionCapacityData, setProductionCapacityData } = useProductionCapacityData()
|
||||
const { inputValues } = useInputValues();
|
||||
const { isPlaying } = usePlayButtonStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isPlaying) {
|
||||
setProductionCapacityData(0);
|
||||
return;
|
||||
}
|
||||
if (!inputValues || throughputData === undefined) return;
|
||||
|
||||
const shiftLength = parseFloat(inputValues["Shift length"]);
|
||||
const shiftsPerDay = parseFloat(inputValues["Shifts / day"]);
|
||||
const workingDaysPerYear = parseFloat(inputValues["Working days / year"]);
|
||||
const yieldRate = parseFloat(inputValues["Yield rate"]);
|
||||
|
||||
if (!isNaN(shiftLength) && !isNaN(shiftsPerDay) && !isNaN(workingDaysPerYear) &&
|
||||
!isNaN(yieldRate) && throughputData >= 0) {
|
||||
// Total units produced per day before yield
|
||||
const dailyProduction = throughputData * shiftLength * shiftsPerDay;
|
||||
|
||||
|
||||
// Units after applying yield rate
|
||||
const goodUnitsPerDay = dailyProduction * (yieldRate / 100);
|
||||
|
||||
|
||||
// Annual output
|
||||
const annualProduction = goodUnitsPerDay * workingDaysPerYear;
|
||||
|
||||
|
||||
// Final production capacity per hour (after yield)
|
||||
const productionPerHour = throughputData * (yieldRate / 100);
|
||||
|
||||
|
||||
// Set the final capacity (units/hour)
|
||||
setProductionCapacityData(Number(productionPerHour.toFixed(2)));
|
||||
}
|
||||
}, [throughputData, inputValues]);
|
||||
|
||||
return (
|
||||
<></>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
25
app/src/modules/simulation/analysis/simulationAnalysis.tsx
Normal file
25
app/src/modules/simulation/analysis/simulationAnalysis.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import { usePlayButtonStore } from '../../../store/usePlayButtonStore'
|
||||
import ProductionCapacityData from './productionCapacity/productionCapacityData'
|
||||
import ThroughPutData from './throughPut/throughPutData'
|
||||
import ROIData from './ROI/roiData'
|
||||
|
||||
function SimulationAnalysis() {
|
||||
const { isPlaying } = usePlayButtonStore()
|
||||
// useEffect(()=>{
|
||||
// if (isPlaying) {
|
||||
//
|
||||
// } else {
|
||||
//
|
||||
// }
|
||||
// },[isPlaying])
|
||||
return (
|
||||
<>
|
||||
<ThroughPutData />
|
||||
<ProductionCapacityData />
|
||||
<ROIData />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SimulationAnalysis
|
||||
@@ -0,0 +1,208 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useProductStore } from '../../../../store/simulation/useProductStore';
|
||||
import { determineExecutionMachineSequences } from '../../simulator/functions/determineExecutionMachineSequences';
|
||||
import { useMachineCount, useMachineUptime, useMaterialCycle, useProcessBar, useThroughPutData } from '../../../../store/builder/store';
|
||||
import { usePlayButtonStore } from '../../../../store/usePlayButtonStore';
|
||||
import { useSceneContext } from '../../../scene/sceneContext';
|
||||
import { useProductContext } from '../../products/productContext';
|
||||
|
||||
export default function ThroughPutData() {
|
||||
const { materialStore, armBotStore, machineStore, conveyorStore, vehicleStore, storageUnitStore } = useSceneContext();
|
||||
const { selectedProductStore } = useProductContext();
|
||||
const { selectedProduct } = selectedProductStore();
|
||||
const { products, getProductById } = useProductStore();
|
||||
const { armBots } = armBotStore();
|
||||
const { vehicles } = vehicleStore();
|
||||
const { machines } = machineStore();
|
||||
const { conveyors } = conveyorStore();
|
||||
const { storageUnits } = storageUnitStore();
|
||||
const { materialHistory, materials } = materialStore();
|
||||
const { machineCount, setMachineCount } = useMachineCount();
|
||||
const { machineActiveTime, setMachineActiveTime } = useMachineUptime();
|
||||
const { materialCycleTime, setMaterialCycleTime } = useMaterialCycle();
|
||||
const { setProcessBar } = useProcessBar();
|
||||
const { setThroughputData } = useThroughPutData()
|
||||
const { isPlaying } = usePlayButtonStore();
|
||||
|
||||
// Setting machine count
|
||||
let totalItems = 0;
|
||||
let totalActiveTime = 0;
|
||||
useEffect(() => {
|
||||
if (!isPlaying) {
|
||||
totalActiveTime = 0;
|
||||
totalItems = 0;
|
||||
setMachineCount(0);
|
||||
setMachineActiveTime(0);
|
||||
setMaterialCycleTime(0);
|
||||
setProcessBar([]);
|
||||
setThroughputData(0);
|
||||
return;
|
||||
} else {
|
||||
let process: any = [];
|
||||
const fetchProductSequenceData = async () => {
|
||||
const productData = getProductById(selectedProduct.productUuid);
|
||||
if (productData) {
|
||||
const productSequenceData = await determineExecutionMachineSequences([productData])
|
||||
if (productSequenceData?.length > 0) {
|
||||
productSequenceData.forEach((sequence) => {
|
||||
sequence.forEach((item) => {
|
||||
if (item.type === "roboticArm") {
|
||||
armBots.filter(arm => arm.modelUuid === item.modelUuid)
|
||||
.forEach(arm => {
|
||||
if (arm.activeTime > 0) {
|
||||
process.push({ modelid: arm.modelUuid, modelName: arm.modelName, activeTime: arm?.activeTime })
|
||||
totalActiveTime += arm.activeTime;
|
||||
}
|
||||
});
|
||||
} else if (item.type === "vehicle") {
|
||||
vehicles.filter(vehicle => vehicle.modelUuid === item.modelUuid)
|
||||
.forEach(vehicle => {
|
||||
if (vehicle.activeTime > 0) {
|
||||
process.push({ modelid: vehicle.modelUuid, modelName: vehicle.modelName, activeTime: vehicle?.activeTime })
|
||||
|
||||
totalActiveTime += vehicle.activeTime;
|
||||
}
|
||||
});
|
||||
} else if (item.type === "machine") {
|
||||
machines.filter(machine => machine.modelUuid === item.modelUuid)
|
||||
.forEach(machine => {
|
||||
if (machine.activeTime > 0) {
|
||||
process.push({ modelid: machine.modelUuid, modelName: machine.modelName, activeTime: machine?.activeTime })
|
||||
totalActiveTime += machine.activeTime;
|
||||
}
|
||||
});
|
||||
} else if (item.type === "transfer") {
|
||||
conveyors.filter(conveyor => conveyor.modelUuid === item.modelUuid)
|
||||
.forEach(conveyor => {
|
||||
if (conveyor.activeTime > 0) {
|
||||
// totalActiveTime += conveyor.activeTime;
|
||||
}
|
||||
});
|
||||
} else if (item.type === "storageUnit") {
|
||||
storageUnits.filter(storage => storage.modelUuid === item.modelUuid)
|
||||
.forEach(storage => {
|
||||
if (storage.activeTime > 0) {
|
||||
// totalActiveTime += storage.activeTime;
|
||||
//
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
totalItems += sequence.length;
|
||||
});
|
||||
|
||||
|
||||
setMachineCount(totalItems);
|
||||
setMachineActiveTime(totalActiveTime);
|
||||
let arr = process.map((item: any) => ({
|
||||
name: item.modelName,
|
||||
completed: Math.round((item.activeTime / totalActiveTime) * 100)
|
||||
}));
|
||||
setProcessBar(arr);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fetchProductSequenceData();
|
||||
}
|
||||
// if (materialCycleTime <= 0) return
|
||||
}, [products, selectedProduct, getProductById, setMachineCount, materialCycleTime, armBots, vehicles, machines]);
|
||||
|
||||
useEffect(() => {
|
||||
async function getMachineActive() {
|
||||
const productData = getProductById(selectedProduct.productUuid);
|
||||
let anyArmActive;
|
||||
let anyVehicleActive;
|
||||
let anyMachineActive;
|
||||
|
||||
if (productData) {
|
||||
const productSequenceData = await determineExecutionMachineSequences([productData]);
|
||||
if (productSequenceData?.length > 0) {
|
||||
productSequenceData.forEach(sequence => {
|
||||
sequence.forEach(item => {
|
||||
if (item.type === "roboticArm") {
|
||||
armBots
|
||||
.filter(arm => arm.modelUuid === item.modelUuid)
|
||||
.forEach(arm => {
|
||||
if (arm.isActive) {
|
||||
anyArmActive = true;
|
||||
} else {
|
||||
anyArmActive = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (item.type === "vehicle") {
|
||||
vehicles
|
||||
.filter(vehicle => vehicle.modelUuid === item.modelUuid)
|
||||
.forEach(vehicle => {
|
||||
if (vehicle.isActive) {
|
||||
anyVehicleActive = true;
|
||||
} else {
|
||||
anyVehicleActive = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (item.type === "machine") {
|
||||
machines
|
||||
.filter(machine => machine.modelUuid === item.modelUuid)
|
||||
.forEach(machine => {
|
||||
if (machine.isActive) {
|
||||
anyMachineActive = true;
|
||||
} else {
|
||||
anyMachineActive = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const allInactive = !anyArmActive && !anyVehicleActive && !anyMachineActive;
|
||||
if (allInactive && materials.length === 0 && materialHistory.length > 0) {
|
||||
|
||||
let totalCycleTimeSum = 0;
|
||||
let cycleCount = 0;
|
||||
|
||||
materialHistory.forEach((material) => {
|
||||
const start = material.material.startTime ?? 0;
|
||||
const end = material.material.endTime ?? 0;
|
||||
if (start === 0 || end === 0) return;
|
||||
|
||||
const totalCycleTime = (end - start) / 1000; // Convert milliseconds to seconds
|
||||
totalCycleTimeSum += totalCycleTime;
|
||||
cycleCount++;
|
||||
});
|
||||
|
||||
if (cycleCount > 0) {
|
||||
const averageCycleTime = totalCycleTimeSum / cycleCount;
|
||||
setMaterialCycleTime(Number(averageCycleTime.toFixed(2)));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isPlaying) {
|
||||
setTimeout(() => {
|
||||
getMachineActive();
|
||||
}, 500)
|
||||
}
|
||||
}, [armBots, materials, materialHistory, machines, vehicles, selectedProduct])
|
||||
|
||||
useEffect(() => {
|
||||
if (machineActiveTime > 0 && materialCycleTime > 0 && machineCount > 0) {
|
||||
|
||||
|
||||
|
||||
const utilization = machineActiveTime / 3600; // Active time per hour
|
||||
const unitsPerMachinePerHour = 3600 / materialCycleTime;
|
||||
const throughput = unitsPerMachinePerHour * machineCount * utilization;
|
||||
setThroughputData(Number(throughput.toFixed(2))); // Keep as number
|
||||
//
|
||||
}
|
||||
}, [machineActiveTime, materialCycleTime, machineCount]);
|
||||
|
||||
return (
|
||||
<>
|
||||
</>
|
||||
);
|
||||
}
|
||||
14
app/src/modules/simulation/conveyor/conveyor.tsx
Normal file
14
app/src/modules/simulation/conveyor/conveyor.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import React from 'react'
|
||||
import ConveyorInstances from './instances/conveyorInstances'
|
||||
|
||||
function Conveyor() {
|
||||
return (
|
||||
<>
|
||||
|
||||
<ConveyorInstances />
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Conveyor
|
||||
@@ -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 ConveyorCallback = {
|
||||
conveyorId: string;
|
||||
callback: () => void;
|
||||
};
|
||||
|
||||
export function useConveyorEventManager() {
|
||||
const { conveyorStore } = useSceneContext();
|
||||
const { getConveyorById } = conveyorStore();
|
||||
const callbacksRef = useRef<ConveyorCallback[]>([]);
|
||||
const isMonitoringRef = useRef(false);
|
||||
const { isPlaying } = usePlayButtonStore();
|
||||
const { isPaused } = usePauseButtonStore();
|
||||
const { isReset } = useResetButtonStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (isReset) {
|
||||
callbacksRef.current = [];
|
||||
}
|
||||
}, [isReset])
|
||||
|
||||
// Add a new conveyor to monitor
|
||||
const addConveyorToMonitor = (conveyorId: string, callback: () => void) => {
|
||||
// Avoid duplicates
|
||||
if (!callbacksRef.current.some((entry) => entry.conveyorId === conveyorId)) {
|
||||
callbacksRef.current.push({ conveyorId, callback });
|
||||
}
|
||||
|
||||
// Start monitoring if not already running
|
||||
if (!isMonitoringRef.current) {
|
||||
isMonitoringRef.current = true;
|
||||
}
|
||||
};
|
||||
|
||||
// Remove a conveyor from monitoring
|
||||
const removeConveyorFromMonitor = (conveyorId: string) => {
|
||||
callbacksRef.current = callbacksRef.current.filter(
|
||||
(entry) => entry.conveyorId !== conveyorId
|
||||
);
|
||||
|
||||
// Stop monitoring if no more conveyors to track
|
||||
if (callbacksRef.current.length === 0) {
|
||||
isMonitoringRef.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Check conveyor states every frame
|
||||
useFrame(() => {
|
||||
if (!isMonitoringRef.current || callbacksRef.current.length === 0 || !isPlaying || isPaused) return;
|
||||
|
||||
callbacksRef.current.forEach(({ conveyorId, callback }) => {
|
||||
const conveyor = getConveyorById(conveyorId);
|
||||
if (conveyor?.isPaused === false) {
|
||||
callback();
|
||||
removeConveyorFromMonitor(conveyorId); // Remove after triggering
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
callbacksRef.current = [];
|
||||
isMonitoringRef.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
addConveyorToMonitor,
|
||||
removeConveyorFromMonitor,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useResetButtonStore } from '../../../../../store/usePlayButtonStore';
|
||||
import { useProductStore } from '../../../../../store/simulation/useProductStore';
|
||||
import { useSceneContext } from '../../../../scene/sceneContext';
|
||||
import { useProductContext } from '../../../products/productContext';
|
||||
// import { findConveyorSubsequence } from '../../../simulator/functions/getConveyorSequencesInProduct';
|
||||
|
||||
function ConveyorInstance({ conveyor }: { readonly conveyor: ConveyorStatus }) {
|
||||
const { getProductById } = useProductStore();
|
||||
const { selectedProductStore } = useProductContext();
|
||||
const { selectedProduct } = selectedProductStore();
|
||||
const { materialStore, conveyorStore } = useSceneContext();
|
||||
const { getMaterialsByCurrentModelUuid, materials } = materialStore();
|
||||
const { isReset } = useResetButtonStore();
|
||||
const { setConveyorPaused } = conveyorStore();
|
||||
|
||||
useEffect(() => {
|
||||
const product = getProductById(selectedProduct.productUuid);
|
||||
if (!product) return;
|
||||
|
||||
const conveyorMaterials = getMaterialsByCurrentModelUuid(conveyor.modelUuid);
|
||||
if (conveyorMaterials && conveyorMaterials?.length > 0) {
|
||||
|
||||
const hasPausedMaterials = conveyorMaterials.some(material => material.isPaused);
|
||||
|
||||
if (hasPausedMaterials) {
|
||||
setConveyorPaused(conveyor.modelUuid, true);
|
||||
} else {
|
||||
setConveyorPaused(conveyor.modelUuid, false);
|
||||
}
|
||||
} else {
|
||||
setConveyorPaused(conveyor.modelUuid, false);
|
||||
}
|
||||
|
||||
// const conveyorSubsequence = findConveyorSubsequence(product, conveyor.modelUuid);
|
||||
|
||||
// if (!conveyorSubsequence || !conveyorSubsequence.currentSubSequence) {
|
||||
// setConveyorPaused(conveyor.modelUuid, false);
|
||||
// return;
|
||||
// }
|
||||
|
||||
// const { currentSubSequence } = conveyorSubsequence;
|
||||
|
||||
// const allMaterials = currentSubSequence.flatMap(event =>
|
||||
// getMaterialsByCurrentModelUuid(event.modelUuid)
|
||||
// );
|
||||
|
||||
// const hasPausedMaterials = allMaterials.some(mat => mat?.isPaused);
|
||||
|
||||
// currentSubSequence.forEach(event => {
|
||||
// if (event.type === 'transfer') {
|
||||
// setConveyorPaused(event.modelUuid, hasPausedMaterials);
|
||||
// }
|
||||
// });
|
||||
|
||||
}, [materials, conveyor.modelUuid, getMaterialsByCurrentModelUuid, setConveyorPaused, isReset, selectedProduct.productUuid, getProductById]);
|
||||
|
||||
useEffect(() => {
|
||||
// console.log('conveyor: ', conveyor);
|
||||
}, [conveyor])
|
||||
|
||||
return (
|
||||
<>
|
||||
</>
|
||||
)
|
||||
};
|
||||
|
||||
export default ConveyorInstance
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react'
|
||||
import ConveyorInstance from './conveyorInstance/conveyorInstance'
|
||||
import { useSceneContext } from '../../../scene/sceneContext';
|
||||
|
||||
function ConveyorInstances() {
|
||||
|
||||
const { conveyorStore } = useSceneContext();
|
||||
const { conveyors } = conveyorStore();
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
{conveyors.map((conveyor: ConveyorStatus) =>
|
||||
<ConveyorInstance conveyor={conveyor} key={conveyor.modelUuid} />
|
||||
)}
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ConveyorInstances
|
||||
221
app/src/modules/simulation/events/arrows/arrows.tsx
Normal file
221
app/src/modules/simulation/events/arrows/arrows.tsx
Normal file
@@ -0,0 +1,221 @@
|
||||
import * as THREE from "three";
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import { useThree } from "@react-three/fiber";
|
||||
import { useDeleteTool } from "../../../../store/builder/store";
|
||||
|
||||
interface ConnectionLine {
|
||||
id: string;
|
||||
startPointUuid: string;
|
||||
endPointUuid: string;
|
||||
trigger: TriggerSchema;
|
||||
}
|
||||
|
||||
export function Arrows({ connections }: { readonly connections: ConnectionLine[] }) {
|
||||
const [hoveredLineKey, setHoveredLineKey] = useState<string | null>(null);
|
||||
const groupRef = useRef<THREE.Group>(null);
|
||||
const { scene } = useThree();
|
||||
const { deleteTool } = useDeleteTool();
|
||||
|
||||
const getWorldPositionFromScene = (uuid: string): THREE.Vector3 | null => {
|
||||
const obj = scene.getObjectByProperty("uuid", uuid);
|
||||
if (!obj) return null;
|
||||
const pos = new THREE.Vector3();
|
||||
obj.getWorldPosition(pos);
|
||||
return pos;
|
||||
};
|
||||
|
||||
const createArrow = (
|
||||
key: string,
|
||||
fullCurve: THREE.QuadraticBezierCurve3,
|
||||
centerT: number,
|
||||
segmentSize: number,
|
||||
scale: number,
|
||||
reverse = false
|
||||
) => {
|
||||
const t1 = Math.max(0, centerT - segmentSize / 2);
|
||||
const t2 = Math.min(1, centerT + segmentSize / 2);
|
||||
const subCurve = getSubCurve(fullCurve, t1, t2, reverse);
|
||||
|
||||
const shaftGeometry = new THREE.TubeGeometry(subCurve, 8, 0.01 * scale, 8, false);
|
||||
|
||||
const end = subCurve.getPoint(1);
|
||||
const tangent = subCurve.getTangent(1).normalize();
|
||||
|
||||
const arrowHeadLength = 0.15 * scale;
|
||||
const arrowRadius = 0.01 * scale;
|
||||
const arrowHeadRadius = arrowRadius * 2.5;
|
||||
|
||||
const headGeometry = new THREE.ConeGeometry(arrowHeadRadius, arrowHeadLength, 8);
|
||||
headGeometry.translate(0, arrowHeadLength / 2, 0);
|
||||
|
||||
const rotation = new THREE.Quaternion().setFromUnitVectors(
|
||||
new THREE.Vector3(0, 1, 0),
|
||||
tangent
|
||||
);
|
||||
|
||||
return (
|
||||
<group key={key}>
|
||||
<mesh
|
||||
geometry={shaftGeometry}
|
||||
onPointerOver={() => setHoveredLineKey(key)}
|
||||
onPointerOut={() => setHoveredLineKey(null)}
|
||||
>
|
||||
<meshStandardMaterial
|
||||
color={deleteTool && hoveredLineKey === key ? "red" : "#42a5f5"}
|
||||
/>
|
||||
</mesh>
|
||||
<mesh
|
||||
position={end}
|
||||
quaternion={rotation}
|
||||
geometry={headGeometry}
|
||||
onPointerOver={() => setHoveredLineKey(key)}
|
||||
onPointerOut={() => setHoveredLineKey(null)}
|
||||
>
|
||||
<meshStandardMaterial
|
||||
color={deleteTool && hoveredLineKey === key ? "red" : "#42a5f5"}
|
||||
/>
|
||||
</mesh>
|
||||
</group>
|
||||
);
|
||||
};
|
||||
|
||||
const getSubCurve = (
|
||||
curve: THREE.Curve<THREE.Vector3>,
|
||||
t1: number,
|
||||
t2: number,
|
||||
reverse = false
|
||||
) => {
|
||||
const divisions = 10;
|
||||
const subPoints = Array.from({ length: divisions + 1 }, (_, i) => {
|
||||
const t = THREE.MathUtils.lerp(t1, t2, i / divisions);
|
||||
return curve.getPoint(t);
|
||||
});
|
||||
|
||||
if (reverse) subPoints.reverse();
|
||||
|
||||
return new THREE.CatmullRomCurve3(subPoints);
|
||||
};
|
||||
|
||||
const arrowGroups = connections.flatMap((connection) => {
|
||||
const start = getWorldPositionFromScene(connection.startPointUuid);
|
||||
const end = getWorldPositionFromScene(connection.endPointUuid);
|
||||
if (!start || !end) return [];
|
||||
|
||||
const isBidirectional = connections.some(
|
||||
(other) =>
|
||||
other.startPointUuid === connection.endPointUuid &&
|
||||
other.endPointUuid === connection.startPointUuid
|
||||
);
|
||||
|
||||
if (isBidirectional && connection.startPointUuid < connection.endPointUuid) {
|
||||
const distance = start.distanceTo(end);
|
||||
const heightFactor = Math.max(0.5, distance * 0.2);
|
||||
const control = new THREE.Vector3(
|
||||
(start.x + end.x) / 2,
|
||||
Math.max(start.y, end.y) + heightFactor,
|
||||
(start.z + end.z) / 2
|
||||
);
|
||||
const curve = new THREE.QuadraticBezierCurve3(start, control, end);
|
||||
const scale = THREE.MathUtils.clamp(distance * 0.75, 0.5, 3);
|
||||
|
||||
return [
|
||||
createArrow(connection.id + "-fwd", curve, 0.33, 0.25, scale, true),
|
||||
createArrow(connection.id + "-bwd", curve, 0.66, 0.25, scale, false),
|
||||
];
|
||||
}
|
||||
|
||||
if (!isBidirectional) {
|
||||
const distance = start.distanceTo(end);
|
||||
const heightFactor = Math.max(0.5, distance * 0.2);
|
||||
const control = new THREE.Vector3(
|
||||
(start.x + end.x) / 2,
|
||||
Math.max(start.y, end.y) + heightFactor,
|
||||
(start.z + end.z) / 2
|
||||
);
|
||||
const curve = new THREE.QuadraticBezierCurve3(start, control, end);
|
||||
const scale = THREE.MathUtils.clamp(distance * 0.75, 0.5, 5);
|
||||
|
||||
return [
|
||||
createArrow(connection.id, curve, 0.5, 0.3, scale)
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
});
|
||||
|
||||
return <group ref={groupRef} name="connectionArrows">{arrowGroups}</group>;
|
||||
}
|
||||
|
||||
export function ArrowOnQuadraticBezier({
|
||||
start,
|
||||
mid,
|
||||
end,
|
||||
color = "#42a5f5",
|
||||
}: {
|
||||
start: number[];
|
||||
mid: number[];
|
||||
end: number[];
|
||||
color?: string;
|
||||
}) {
|
||||
const minScale = 0.5;
|
||||
const maxScale = 5;
|
||||
const segmentSize = 0.3;
|
||||
|
||||
const startVec = useMemo(() => new THREE.Vector3(...start), [start]);
|
||||
const midVec = useMemo(() => new THREE.Vector3(...mid), [mid]);
|
||||
const endVec = useMemo(() => new THREE.Vector3(...end), [end]);
|
||||
|
||||
const fullCurve = useMemo(
|
||||
() => new THREE.QuadraticBezierCurve3(startVec, midVec, endVec),
|
||||
[startVec, midVec, endVec]
|
||||
);
|
||||
|
||||
const distance = useMemo(() => startVec.distanceTo(endVec), [startVec, endVec]);
|
||||
const scale = useMemo(() => THREE.MathUtils.clamp(distance * 0.75, minScale, maxScale), [distance]);
|
||||
|
||||
const arrowHeadLength = 0.15 * scale;
|
||||
const arrowRadius = 0.01 * scale;
|
||||
const arrowHeadRadius = arrowRadius * 2.5;
|
||||
|
||||
const subCurve = useMemo(() => {
|
||||
const centerT = 0.5;
|
||||
const t1 = Math.max(0, centerT - segmentSize / 2);
|
||||
const t2 = Math.min(1, centerT + segmentSize / 2);
|
||||
|
||||
const divisions = 10;
|
||||
const subPoints = Array.from({ length: divisions + 1 }, (_, i) => {
|
||||
const t = THREE.MathUtils.lerp(t1, t2, i / divisions);
|
||||
return fullCurve.getPoint(t);
|
||||
});
|
||||
return new THREE.CatmullRomCurve3(subPoints);
|
||||
}, [fullCurve, segmentSize]);
|
||||
|
||||
const tubeGeometry = useMemo(
|
||||
() => new THREE.TubeGeometry(subCurve, 20, arrowRadius, 8, false),
|
||||
[subCurve, arrowRadius]
|
||||
);
|
||||
|
||||
const arrowPosition = useMemo(() => subCurve.getPoint(1), [subCurve]);
|
||||
const arrowTangent = useMemo(() => subCurve.getTangent(1).normalize(), [subCurve]);
|
||||
|
||||
const arrowRotation = useMemo(() => {
|
||||
return new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 1, 0), arrowTangent);
|
||||
}, [arrowTangent]);
|
||||
|
||||
const coneGeometry = useMemo(() => {
|
||||
const geom = new THREE.ConeGeometry(arrowHeadRadius, arrowHeadLength, 8);
|
||||
geom.translate(0, arrowHeadLength / 2, 0);
|
||||
return geom;
|
||||
}, [arrowHeadRadius, arrowHeadLength]);
|
||||
|
||||
return (
|
||||
<group name="ArrowWithTube">
|
||||
<mesh name="ArrowWithTube" geometry={tubeGeometry}>
|
||||
<meshStandardMaterial color={color} />
|
||||
</mesh>
|
||||
<mesh name="ArrowWithTube" position={arrowPosition} quaternion={arrowRotation} geometry={coneGeometry}>
|
||||
<meshStandardMaterial color={color} />
|
||||
</mesh>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,362 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import * as THREE from "three";
|
||||
import { useEventsStore } from "../../../../../store/simulation/useEventsStore";
|
||||
import { useProductStore } from "../../../../../store/simulation/useProductStore";
|
||||
import useModuleStore, { useSubModuleStore } from "../../../../../store/useModuleStore";
|
||||
import { TransformControls } from "@react-three/drei";
|
||||
import { detectModifierKeys } from "../../../../../utils/shortcutkeys/detectModifierKeys";
|
||||
import { useSelectedEventSphere, useSelectedEventData, } from "../../../../../store/simulation/useSimulationStore";
|
||||
import { useThree } from "@react-three/fiber";
|
||||
import { usePlayButtonStore } from "../../../../../store/usePlayButtonStore";
|
||||
import { upsertProductOrEventApi } from "../../../../../services/simulation/products/UpsertProductOrEventApi";
|
||||
import { useProductContext } from "../../../products/productContext";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
function PointsCreator() {
|
||||
const { gl, raycaster, scene, pointer, camera } = useThree();
|
||||
const { subModule } = useSubModuleStore();
|
||||
const { selectedProductStore } = useProductContext();
|
||||
const { events, updatePoint, getPointByUuid, getEventByModelUuid } = useEventsStore();
|
||||
const { getEventByModelUuid: getEventByModelUuidFromProduct, updatePoint: updatePointFromProduct, getEventByModelUuid: getEventByModelUuidFromProduct2, getPointByUuid: getPointByUuidFromProduct } = useProductStore();
|
||||
const { selectedProduct } = selectedProductStore();
|
||||
const { activeModule } = useModuleStore();
|
||||
const transformRef = useRef<any>(null);
|
||||
const [transformMode, setTransformMode] = useState<"translate" | "rotate" | null>(null);
|
||||
const sphereRefs = useRef<{ [key: string]: THREE.Mesh }>({});
|
||||
const { selectedEventSphere, setSelectedEventSphere, clearSelectedEventSphere, } = useSelectedEventSphere();
|
||||
const { setSelectedEventData, clearSelectedEventData } = useSelectedEventData();
|
||||
const { isPlaying } = usePlayButtonStore();
|
||||
|
||||
const { projectId } = useParams();
|
||||
|
||||
const updateBackend = (
|
||||
productName: string,
|
||||
productUuid: string,
|
||||
projectId: string,
|
||||
eventData: EventsSchema
|
||||
) => {
|
||||
upsertProductOrEventApi({
|
||||
productName: productName,
|
||||
productUuid: productUuid,
|
||||
projectId: projectId,
|
||||
eventDatas: eventData
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedEventSphere) {
|
||||
const eventData = getEventByModelUuid(
|
||||
selectedEventSphere.userData.modelUuid
|
||||
);
|
||||
|
||||
if (eventData) {
|
||||
setSelectedEventData(eventData, selectedEventSphere.userData.pointUuid);
|
||||
} else {
|
||||
clearSelectedEventData();
|
||||
}
|
||||
} else {
|
||||
clearSelectedEventData();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedEventSphere]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
const keyCombination = detectModifierKeys(e);
|
||||
if (!selectedEventSphere) return;
|
||||
if (keyCombination === "G") {
|
||||
setTransformMode((prev) => (prev === "translate" ? null : "translate"));
|
||||
}
|
||||
if (keyCombination === "R") {
|
||||
setTransformMode((prev) => (prev === "rotate" ? null : "rotate"));
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [selectedEventSphere]);
|
||||
|
||||
const updatePointToState = (selectedEventSphere: THREE.Mesh) => {
|
||||
let point: PointsScheme = JSON.parse(
|
||||
JSON.stringify(
|
||||
getPointByUuid(
|
||||
selectedEventSphere.userData.modelUuid,
|
||||
selectedEventSphere.userData.pointUuid
|
||||
)
|
||||
)
|
||||
);
|
||||
if (point) {
|
||||
point.position = [
|
||||
selectedEventSphere.position.x,
|
||||
selectedEventSphere.position.y,
|
||||
selectedEventSphere.position.z,
|
||||
];
|
||||
point.rotation = [
|
||||
selectedEventSphere.rotation.x,
|
||||
selectedEventSphere.rotation.y,
|
||||
selectedEventSphere.rotation.z,
|
||||
];
|
||||
|
||||
const event = getEventByModelUuidFromProduct(selectedProduct.productUuid, selectedEventSphere.userData.modelUuid);
|
||||
|
||||
if (event && selectedProduct.productUuid !== '') {
|
||||
|
||||
const updatedPoint = JSON.parse(
|
||||
JSON.stringify(
|
||||
getPointByUuidFromProduct(selectedProduct.productUuid, selectedEventSphere.userData.modelUuid, selectedEventSphere.userData.pointUuid)
|
||||
)
|
||||
);
|
||||
if (updatedPoint) {
|
||||
updatedPoint.position = point.position;
|
||||
updatedPoint.rotation = point.rotation;
|
||||
|
||||
const updatedEvent = updatePointFromProduct(
|
||||
selectedProduct.productUuid,
|
||||
selectedEventSphere.userData.modelUuid,
|
||||
selectedEventSphere.userData.pointUuid,
|
||||
updatedPoint
|
||||
)
|
||||
if (updatedEvent) {
|
||||
updatePoint(
|
||||
selectedEventSphere.userData.modelUuid,
|
||||
selectedEventSphere.userData.pointUuid,
|
||||
point
|
||||
)
|
||||
updateBackend(
|
||||
selectedProduct.productName,
|
||||
selectedProduct.productUuid,
|
||||
projectId || '',
|
||||
updatedEvent
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const canvasElement = gl.domElement;
|
||||
|
||||
let drag = false;
|
||||
let isMouseDown = false;
|
||||
|
||||
const onMouseDown = () => {
|
||||
isMouseDown = true;
|
||||
drag = false;
|
||||
};
|
||||
|
||||
const onMouseUp = () => {
|
||||
if (selectedEventSphere && !drag) {
|
||||
raycaster.setFromCamera(pointer, camera);
|
||||
const intersects = raycaster
|
||||
.intersectObjects(scene.children, true)
|
||||
.filter((intersect) => intersect.object.name === "Event-Sphere");
|
||||
if (intersects.length === 0) {
|
||||
clearSelectedEventSphere();
|
||||
setTransformMode(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseMove = () => {
|
||||
if (isMouseDown) {
|
||||
drag = true;
|
||||
}
|
||||
};
|
||||
|
||||
if (subModule === "mechanics") {
|
||||
canvasElement.addEventListener("mousedown", onMouseDown);
|
||||
canvasElement.addEventListener("mouseup", onMouseUp);
|
||||
canvasElement.addEventListener("mousemove", onMouseMove);
|
||||
}
|
||||
|
||||
return () => {
|
||||
canvasElement.removeEventListener("mousedown", onMouseDown);
|
||||
canvasElement.removeEventListener("mouseup", onMouseUp);
|
||||
canvasElement.removeEventListener("mousemove", onMouseMove);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [gl, subModule, selectedEventSphere]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{activeModule === "simulation" && (
|
||||
<>
|
||||
<group name="EventPointsGroup" visible={!isPlaying}>
|
||||
{events.map((event, index) => {
|
||||
const updatedEvent = selectedProduct.productUuid !== ''
|
||||
? getEventByModelUuidFromProduct2(selectedProduct.productUuid, event.modelUuid)
|
||||
: null;
|
||||
|
||||
const usedEvent = updatedEvent || event;
|
||||
|
||||
if (usedEvent.type === "transfer") {
|
||||
return (
|
||||
<group
|
||||
key={`${index}-${usedEvent.modelUuid}`}
|
||||
position={usedEvent.position}
|
||||
rotation={usedEvent.rotation}
|
||||
>
|
||||
{usedEvent.points.map((point, j) => (
|
||||
<mesh
|
||||
name="Event-Sphere"
|
||||
uuid={point.uuid}
|
||||
ref={(el) => (sphereRefs.current[point.uuid] = el!)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSelectedEventSphere(
|
||||
sphereRefs.current[point.uuid]
|
||||
);
|
||||
}}
|
||||
key={`${index}-${point.uuid}`}
|
||||
position={new THREE.Vector3(...point.position)}
|
||||
userData={{
|
||||
modelUuid: usedEvent.modelUuid,
|
||||
pointUuid: point.uuid,
|
||||
}}
|
||||
>
|
||||
<sphereGeometry args={[0.1, 16, 16]} />
|
||||
<meshStandardMaterial color="orange" />
|
||||
</mesh>
|
||||
))}
|
||||
</group>
|
||||
);
|
||||
} else if (usedEvent.type === "vehicle") {
|
||||
const point = usedEvent.point;
|
||||
return (
|
||||
<group
|
||||
key={`${index}-${usedEvent.modelUuid}`}
|
||||
position={usedEvent.position}
|
||||
rotation={usedEvent.rotation}
|
||||
>
|
||||
<mesh
|
||||
name="Event-Sphere"
|
||||
uuid={point.uuid}
|
||||
ref={(el) => (sphereRefs.current[point.uuid] = el!)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSelectedEventSphere(
|
||||
sphereRefs.current[point.uuid]
|
||||
);
|
||||
}}
|
||||
position={new THREE.Vector3(...point.position)}
|
||||
userData={{
|
||||
modelUuid: usedEvent.modelUuid,
|
||||
pointUuid: point.uuid,
|
||||
}}
|
||||
>
|
||||
<sphereGeometry args={[0.1, 16, 16]} />
|
||||
<meshStandardMaterial color="blue" />
|
||||
</mesh>
|
||||
</group>
|
||||
);
|
||||
} else if (usedEvent.type === "roboticArm") {
|
||||
const point = usedEvent.point;
|
||||
return (
|
||||
<group
|
||||
key={`${index}-${usedEvent.modelUuid}`}
|
||||
position={usedEvent.position}
|
||||
rotation={usedEvent.rotation}
|
||||
>
|
||||
<mesh
|
||||
name="Event-Sphere"
|
||||
uuid={point.uuid}
|
||||
ref={(el) => (sphereRefs.current[point.uuid] = el!)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSelectedEventSphere(
|
||||
sphereRefs.current[point.uuid]
|
||||
);
|
||||
}}
|
||||
position={new THREE.Vector3(...point.position)}
|
||||
userData={{
|
||||
modelUuid: usedEvent.modelUuid,
|
||||
pointUuid: point.uuid,
|
||||
}}
|
||||
>
|
||||
<sphereGeometry args={[0.1, 16, 16]} />
|
||||
<meshStandardMaterial color="green" />
|
||||
</mesh>
|
||||
</group>
|
||||
);
|
||||
} else if (usedEvent.type === "machine") {
|
||||
const point = usedEvent.point;
|
||||
return (
|
||||
<group
|
||||
key={`${index}-${usedEvent.modelUuid}`}
|
||||
position={usedEvent.position}
|
||||
rotation={usedEvent.rotation}
|
||||
>
|
||||
<mesh
|
||||
name="Event-Sphere"
|
||||
uuid={point.uuid}
|
||||
ref={(el) => (sphereRefs.current[point.uuid] = el!)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSelectedEventSphere(
|
||||
sphereRefs.current[point.uuid]
|
||||
);
|
||||
}}
|
||||
position={new THREE.Vector3(...point.position)}
|
||||
userData={{
|
||||
modelUuid: usedEvent.modelUuid,
|
||||
pointUuid: point.uuid,
|
||||
}}
|
||||
>
|
||||
<sphereGeometry args={[0.1, 16, 16]} />
|
||||
<meshStandardMaterial color="purple" />
|
||||
</mesh>
|
||||
</group>
|
||||
);
|
||||
} else if (usedEvent.type === "storageUnit") {
|
||||
const point = usedEvent.point;
|
||||
return (
|
||||
<group
|
||||
key={`${index}-${usedEvent.modelUuid}`}
|
||||
position={usedEvent.position}
|
||||
rotation={usedEvent.rotation}
|
||||
>
|
||||
<mesh
|
||||
name="Event-Sphere"
|
||||
uuid={point.uuid}
|
||||
ref={(el) => (sphereRefs.current[point.uuid] = el!)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSelectedEventSphere(
|
||||
sphereRefs.current[point.uuid]
|
||||
);
|
||||
}}
|
||||
position={new THREE.Vector3(...point.position)}
|
||||
userData={{
|
||||
modelUuid: usedEvent.modelUuid,
|
||||
pointUuid: point.uuid,
|
||||
}}
|
||||
>
|
||||
<sphereGeometry args={[0.1, 16, 16]} />
|
||||
<meshStandardMaterial color="red" />
|
||||
</mesh>
|
||||
</group>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
})}
|
||||
</group>
|
||||
|
||||
{selectedEventSphere && transformMode && (
|
||||
<TransformControls
|
||||
ref={transformRef}
|
||||
object={selectedEventSphere}
|
||||
mode={transformMode}
|
||||
onMouseUp={(e) => {
|
||||
updatePointToState(selectedEventSphere);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default PointsCreator;
|
||||
@@ -0,0 +1,37 @@
|
||||
import { upsertProductOrEventApi } from "../../../../../services/simulation/products/UpsertProductOrEventApi";
|
||||
|
||||
interface HandleAddEventToProductParams {
|
||||
event: EventsSchema | undefined;
|
||||
addEvent: (productUuid: string, event: EventsSchema) => void;
|
||||
selectedProduct: {
|
||||
productUuid: string;
|
||||
productName: string;
|
||||
}
|
||||
clearSelectedAsset?: () => void;
|
||||
projectId: string;
|
||||
}
|
||||
|
||||
export const handleAddEventToProduct = ({
|
||||
event,
|
||||
addEvent,
|
||||
selectedProduct,
|
||||
clearSelectedAsset,
|
||||
projectId
|
||||
}: HandleAddEventToProductParams) => {
|
||||
if (event && selectedProduct.productUuid) {
|
||||
addEvent(selectedProduct.productUuid, event);
|
||||
|
||||
upsertProductOrEventApi({
|
||||
productName: selectedProduct.productName,
|
||||
productUuid: selectedProduct.productUuid,
|
||||
projectId: projectId ||'',
|
||||
eventDatas: event
|
||||
}).then((data) => {
|
||||
// console.log(data);
|
||||
})
|
||||
|
||||
if (clearSelectedAsset) {
|
||||
clearSelectedAsset();
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
import * as THREE from 'three';
|
||||
import { Group } from '../../../../../types/world/worldTypes';
|
||||
|
||||
function PointsCalculator(
|
||||
type: string,
|
||||
model: Group,
|
||||
rotation: THREE.Vector3 = new THREE.Vector3()
|
||||
): { points?: THREE.Vector3[] } | null {
|
||||
if (!model) return null;
|
||||
|
||||
const box = new THREE.Box3().setFromObject(model);
|
||||
|
||||
const size = new THREE.Vector3();
|
||||
box.getSize(size);
|
||||
const center = new THREE.Vector3();
|
||||
box.getCenter(center);
|
||||
|
||||
const rotationMatrix = new THREE.Matrix4().makeRotationFromEuler(new THREE.Euler(rotation.x, rotation.y, rotation.z));
|
||||
|
||||
const localTopMiddle = new THREE.Vector3(0, size.y / 2, 0);
|
||||
const worldTopMiddle = localTopMiddle.clone().applyMatrix4(rotationMatrix).add(center);
|
||||
|
||||
if (type === 'Conveyor') {
|
||||
const isWidthLonger = size.x > size.z;
|
||||
const longerSize = isWidthLonger ? size.x : size.z;
|
||||
const shorterSize = isWidthLonger ? size.z : size.x;
|
||||
const halfLongerSize = longerSize / 2;
|
||||
const halfShorterSize = shorterSize / 2;
|
||||
|
||||
const localEndPoint1 = new THREE.Vector3(isWidthLonger ? -halfLongerSize + halfShorterSize : 0, size.y / 2, isWidthLonger ? 0 : -halfLongerSize + halfShorterSize);
|
||||
|
||||
const localEndPoint2 = new THREE.Vector3(isWidthLonger ? halfLongerSize - halfShorterSize : 0, size.y / 2, isWidthLonger ? 0 : halfLongerSize - halfShorterSize);
|
||||
|
||||
const worldEndPoint1 = localEndPoint1.applyMatrix4(rotationMatrix).add(center);
|
||||
const worldEndPoint2 = localEndPoint2.applyMatrix4(rotationMatrix).add(center);
|
||||
|
||||
return {
|
||||
points: [worldEndPoint1, worldTopMiddle, worldEndPoint2]
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
points: [worldTopMiddle]
|
||||
};
|
||||
}
|
||||
|
||||
export default PointsCalculator;
|
||||
12
app/src/modules/simulation/events/points/points.tsx
Normal file
12
app/src/modules/simulation/events/points/points.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import React from 'react'
|
||||
import PointsCreator from './creator/pointsCreator'
|
||||
|
||||
function Points() {
|
||||
return (
|
||||
<>
|
||||
<PointsCreator />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Points
|
||||
@@ -0,0 +1,526 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useFrame, useThree } from "@react-three/fiber";
|
||||
import * as THREE from "three";
|
||||
import { useSubModuleStore } from "../../../../store/useModuleStore";
|
||||
import { useSelectedAction, useSelectedAsset } from "../../../../store/simulation/useSimulationStore";
|
||||
import { useProductStore } from "../../../../store/simulation/useProductStore";
|
||||
import { useEventsStore } from "../../../../store/simulation/useEventsStore";
|
||||
import { handleAddEventToProduct } from "../points/functions/handleAddEventToProduct";
|
||||
import { QuadraticBezierLine } from "@react-three/drei";
|
||||
import { upsertProductOrEventApi } from "../../../../services/simulation/products/UpsertProductOrEventApi";
|
||||
import { useDeleteTool } from "../../../../store/builder/store";
|
||||
import { usePlayButtonStore } from "../../../../store/usePlayButtonStore";
|
||||
import { ArrowOnQuadraticBezier, Arrows } from "../arrows/arrows";
|
||||
import { useProductContext } from "../../products/productContext";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
interface ConnectionLine {
|
||||
id: string;
|
||||
startPointUuid: string;
|
||||
endPointUuid: string;
|
||||
trigger: TriggerSchema;
|
||||
}
|
||||
|
||||
function TriggerConnector() {
|
||||
const { gl, raycaster, scene, pointer, camera } = useThree();
|
||||
const { subModule } = useSubModuleStore();
|
||||
const { selectedProductStore } = useProductContext();
|
||||
const { products, getPointByUuid, getIsEventInProduct, getActionByUuid, addTrigger, removeTrigger, addEvent, getEventByModelUuid, getPointUuidByActionUuid, getProductById } = useProductStore();
|
||||
const { selectedAsset, clearSelectedAsset } = useSelectedAsset();
|
||||
const { selectedProduct } = selectedProductStore();
|
||||
const [hoveredLineKey, setHoveredLineKey] = useState<string | null>(null);
|
||||
const groupRefs = useRef<Record<string, any>>({});
|
||||
const [helperlineColor, setHelperLineColor] = useState<string>("red");
|
||||
const [currentLine, setCurrentLine] = useState<{ start: THREE.Vector3; end: THREE.Vector3; mid: THREE.Vector3; } | null>(null);
|
||||
const { deleteTool } = useDeleteTool();
|
||||
const { isPlaying } = usePlayButtonStore();
|
||||
const { selectedAction } = useSelectedAction();
|
||||
const { projectId } = useParams();
|
||||
|
||||
const [firstSelectedPoint, setFirstSelectedPoint] = useState<{
|
||||
productUuid: string;
|
||||
modelUuid: string;
|
||||
pointUuid: string;
|
||||
actionUuid?: string;
|
||||
} | null>(null);
|
||||
|
||||
const [connections, setConnections] = useState<ConnectionLine[]>([]);
|
||||
|
||||
const updateBackend = (
|
||||
productName: string,
|
||||
productUuid: string,
|
||||
projectId: string,
|
||||
eventData: EventsSchema
|
||||
) => {
|
||||
upsertProductOrEventApi({
|
||||
productName: productName,
|
||||
productUuid: productUuid,
|
||||
projectId: projectId,
|
||||
eventDatas: eventData
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const newConnections: ConnectionLine[] = [];
|
||||
|
||||
const product = getProductById(selectedProduct.productUuid);
|
||||
if (!product || products.length === 0) return;
|
||||
|
||||
product.eventDatas.forEach(event => {
|
||||
// Handle Conveyor points
|
||||
if (event.type === "transfer" && 'points' in event) {
|
||||
event.points.forEach(point => {
|
||||
if (point.action?.triggers) {
|
||||
point.action.triggers.forEach(trigger => {
|
||||
if (trigger.triggeredAsset && trigger.triggeredAsset.triggeredPoint) {
|
||||
newConnections.push({
|
||||
id: `${point.uuid}-${trigger.triggeredAsset.triggeredPoint.pointUuid}-${trigger.triggerUuid}`,
|
||||
startPointUuid: point.uuid,
|
||||
endPointUuid: trigger.triggeredAsset.triggeredPoint.pointUuid,
|
||||
trigger
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
// Handle Vehicle point
|
||||
else if (event.type === "vehicle" && 'point' in event) {
|
||||
const point = event.point;
|
||||
if (point.action?.triggers) {
|
||||
point.action.triggers.forEach(trigger => {
|
||||
if (trigger.triggeredAsset && trigger.triggeredAsset.triggeredPoint) {
|
||||
newConnections.push({
|
||||
id: `${point.uuid}-${trigger.triggeredAsset.triggeredPoint.pointUuid}-${trigger.triggerUuid}`,
|
||||
startPointUuid: point.uuid,
|
||||
endPointUuid: trigger.triggeredAsset.triggeredPoint.pointUuid,
|
||||
trigger
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
// Handle Robotic Arm points
|
||||
else if (event.type === "roboticArm" && 'point' in event) {
|
||||
const point = event.point;
|
||||
point.actions?.forEach(action => {
|
||||
action.triggers?.forEach(trigger => {
|
||||
if (trigger.triggeredAsset && trigger.triggeredAsset.triggeredPoint) {
|
||||
newConnections.push({
|
||||
id: `${point.uuid}-${trigger.triggeredAsset.triggeredPoint.pointUuid}-${trigger.triggerUuid}`,
|
||||
startPointUuid: point.uuid,
|
||||
endPointUuid: trigger.triggeredAsset.triggeredPoint.pointUuid,
|
||||
trigger
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
// Handle Machine point
|
||||
else if (event.type === "machine" && 'point' in event) {
|
||||
const point = event.point;
|
||||
if (point.action?.triggers) {
|
||||
point.action.triggers.forEach(trigger => {
|
||||
if (trigger.triggeredAsset && trigger.triggeredAsset.triggeredPoint) {
|
||||
newConnections.push({
|
||||
id: `${point.uuid}-${trigger.triggeredAsset.triggeredPoint.pointUuid}-${trigger.triggerUuid}`,
|
||||
startPointUuid: point.uuid,
|
||||
endPointUuid: trigger.triggeredAsset.triggeredPoint.pointUuid,
|
||||
trigger
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
// Handle StorageUnit point
|
||||
else if (event.type === "storageUnit" && 'point' in event) {
|
||||
const point = event.point;
|
||||
if (point.action?.triggers) {
|
||||
point.action.triggers.forEach(trigger => {
|
||||
if (trigger.triggeredAsset && trigger.triggeredAsset.triggeredPoint) {
|
||||
newConnections.push({
|
||||
id: `${point.uuid}-${trigger.triggeredAsset.triggeredPoint.pointUuid}-${trigger.triggerUuid}`,
|
||||
startPointUuid: point.uuid,
|
||||
endPointUuid: trigger.triggeredAsset.triggeredPoint.pointUuid,
|
||||
trigger
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
setConnections(newConnections);
|
||||
}, [products, selectedProduct.productUuid]);
|
||||
|
||||
useEffect(() => {
|
||||
const canvasElement = gl.domElement;
|
||||
|
||||
let drag = false;
|
||||
let isRightMouseDown = false;
|
||||
|
||||
const onMouseDown = (evt: MouseEvent) => {
|
||||
if (selectedAsset) {
|
||||
clearSelectedAsset();
|
||||
}
|
||||
if (evt.button === 2) {
|
||||
isRightMouseDown = true;
|
||||
drag = false;
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseUp = (evt: MouseEvent) => {
|
||||
if (evt.button === 2) {
|
||||
isRightMouseDown = false;
|
||||
}
|
||||
}
|
||||
|
||||
const onMouseMove = () => {
|
||||
if (isRightMouseDown) {
|
||||
drag = true;
|
||||
}
|
||||
};
|
||||
|
||||
const handleRightClick = (evt: MouseEvent) => {
|
||||
if (drag) return;
|
||||
evt.preventDefault();
|
||||
|
||||
const intersects = raycaster
|
||||
.intersectObjects(scene.children, true)
|
||||
.filter(
|
||||
(intersect) =>
|
||||
intersect.object.name === ('Event-Sphere')
|
||||
);
|
||||
if (intersects.length === 0) {
|
||||
setFirstSelectedPoint(null);
|
||||
return;
|
||||
};
|
||||
|
||||
const currentObject = intersects[0].object;
|
||||
if (!currentObject || currentObject.name !== 'Event-Sphere') {
|
||||
setFirstSelectedPoint(null);
|
||||
return;
|
||||
};
|
||||
|
||||
const modelUuid = currentObject.userData.modelUuid;
|
||||
const pointUuid = currentObject.userData.pointUuid;
|
||||
|
||||
if (firstSelectedPoint && firstSelectedPoint.pointUuid === pointUuid) {
|
||||
setFirstSelectedPoint(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedProduct && getIsEventInProduct(selectedProduct.productUuid, modelUuid)) {
|
||||
|
||||
const point = getPointByUuid(
|
||||
selectedProduct.productUuid,
|
||||
modelUuid,
|
||||
pointUuid
|
||||
);
|
||||
|
||||
const event = getEventByModelUuid(selectedProduct.productUuid, modelUuid);
|
||||
const clickedPointUuid = getPointUuidByActionUuid(selectedProduct.productUuid, selectedAction.actionId || '');
|
||||
|
||||
if (!point || !event) {
|
||||
setFirstSelectedPoint(null);
|
||||
return;
|
||||
};
|
||||
|
||||
let actionUuid: string | undefined;
|
||||
if ('action' in point && point.action) {
|
||||
actionUuid = point.action.actionUuid;
|
||||
} else if ('actions' in point && point.actions.length === 1) {
|
||||
actionUuid = point.actions[0].actionUuid;
|
||||
}
|
||||
|
||||
if (!firstSelectedPoint) {
|
||||
if (point.uuid !== clickedPointUuid) return;
|
||||
setFirstSelectedPoint({
|
||||
productUuid: selectedProduct.productUuid,
|
||||
modelUuid,
|
||||
pointUuid,
|
||||
actionUuid: selectedAction.actionId || ''
|
||||
});
|
||||
} else {
|
||||
const trigger: TriggerSchema = {
|
||||
triggerUuid: THREE.MathUtils.generateUUID(),
|
||||
triggerName: `Trigger ${firstSelectedPoint.pointUuid.slice(0, 4)} → ${pointUuid.slice(0, 4)}`,
|
||||
triggerType: "onComplete",
|
||||
delay: 0,
|
||||
triggeredAsset: {
|
||||
triggeredModel: {
|
||||
modelName: event.modelName || 'Unknown',
|
||||
modelUuid: modelUuid
|
||||
},
|
||||
triggeredPoint: {
|
||||
pointName: 'Point',
|
||||
pointUuid: point.uuid
|
||||
},
|
||||
triggeredAction: actionUuid ? {
|
||||
actionName: getActionByUuid(selectedProduct.productUuid, actionUuid)?.actionName || 'Action',
|
||||
actionUuid: actionUuid
|
||||
} : null
|
||||
}
|
||||
};
|
||||
|
||||
if (firstSelectedPoint.actionUuid) {
|
||||
const event = addTrigger(selectedProduct.productUuid, firstSelectedPoint.actionUuid, trigger);
|
||||
|
||||
if (event) {
|
||||
updateBackend(
|
||||
selectedProduct.productName,
|
||||
selectedProduct.productUuid,
|
||||
projectId || '',
|
||||
event
|
||||
);
|
||||
}
|
||||
}
|
||||
setFirstSelectedPoint(null);
|
||||
}
|
||||
} else if (!getIsEventInProduct(selectedProduct.productUuid, modelUuid) && firstSelectedPoint) {
|
||||
handleAddEventToProduct({
|
||||
event: useEventsStore.getState().getEventByModelUuid(modelUuid),
|
||||
addEvent,
|
||||
selectedProduct,
|
||||
projectId: projectId || ''
|
||||
})
|
||||
|
||||
const point = getPointByUuid(
|
||||
selectedProduct.productUuid,
|
||||
modelUuid,
|
||||
pointUuid
|
||||
);
|
||||
|
||||
const event = getEventByModelUuid(selectedProduct.productUuid, modelUuid);
|
||||
|
||||
if (!point || !event) {
|
||||
setFirstSelectedPoint(null);
|
||||
return;
|
||||
};
|
||||
|
||||
let actionUuid: string | undefined;
|
||||
if ('action' in point && point.action) {
|
||||
actionUuid = point.action.actionUuid;
|
||||
} else if ('actions' in point && point.actions.length === 1) {
|
||||
actionUuid = point.actions[0].actionUuid;
|
||||
}
|
||||
|
||||
const trigger: TriggerSchema = {
|
||||
triggerUuid: THREE.MathUtils.generateUUID(),
|
||||
triggerName: `Trigger ${firstSelectedPoint.pointUuid.slice(0, 4)} → ${pointUuid.slice(0, 4)}`,
|
||||
triggerType: "onComplete",
|
||||
delay: 0,
|
||||
triggeredAsset: {
|
||||
triggeredModel: {
|
||||
modelName: event.modelName || 'Unknown',
|
||||
modelUuid: modelUuid
|
||||
},
|
||||
triggeredPoint: {
|
||||
pointName: 'Point',
|
||||
pointUuid: point.uuid
|
||||
},
|
||||
triggeredAction: actionUuid ? {
|
||||
actionName: getActionByUuid(selectedProduct.productUuid, actionUuid)?.actionName || 'Action',
|
||||
actionUuid: actionUuid
|
||||
} : null
|
||||
}
|
||||
};
|
||||
|
||||
if (firstSelectedPoint.actionUuid) {
|
||||
const event = addTrigger(selectedProduct.productUuid, firstSelectedPoint.actionUuid, trigger);
|
||||
|
||||
if (event) {
|
||||
updateBackend(
|
||||
selectedProduct.productName,
|
||||
selectedProduct.productUuid,
|
||||
projectId || '',
|
||||
event
|
||||
);
|
||||
}
|
||||
}
|
||||
setFirstSelectedPoint(null);
|
||||
} else if (firstSelectedPoint) {
|
||||
setFirstSelectedPoint(null);
|
||||
}
|
||||
};
|
||||
|
||||
if (subModule === 'mechanics' && !deleteTool && selectedAction.actionId && selectedAction.actionName) {
|
||||
canvasElement.addEventListener("mousedown", onMouseDown);
|
||||
canvasElement.addEventListener("mouseup", onMouseUp);
|
||||
canvasElement.addEventListener("mousemove", onMouseMove);
|
||||
canvasElement.addEventListener('contextmenu', handleRightClick);
|
||||
} else {
|
||||
setFirstSelectedPoint(null);
|
||||
}
|
||||
|
||||
return () => {
|
||||
canvasElement.removeEventListener("mousedown", onMouseDown);
|
||||
canvasElement.removeEventListener("mouseup", onMouseUp);
|
||||
canvasElement.removeEventListener("mousemove", onMouseMove);
|
||||
canvasElement.removeEventListener('contextmenu', handleRightClick);
|
||||
};
|
||||
|
||||
}, [gl, subModule, selectedProduct, firstSelectedPoint, deleteTool, selectedAction]);
|
||||
|
||||
useFrame(() => {
|
||||
if (firstSelectedPoint) {
|
||||
raycaster.setFromCamera(pointer, camera);
|
||||
const intersects = raycaster.intersectObjects(scene.children, true).filter(
|
||||
(intersect) =>
|
||||
!intersect.object.name.includes("Roof") &&
|
||||
!intersect.object.name.includes("MeasurementReference") &&
|
||||
!intersect.object.name.includes("agv-collider") &&
|
||||
!intersect.object.name.includes("zonePlane") &&
|
||||
!intersect.object.name.includes("SelectionGroup") &&
|
||||
!intersect.object.name.includes("selectionAssetGroup") &&
|
||||
!intersect.object.name.includes("SelectionGroupBoundingBoxLine") &&
|
||||
!intersect.object.name.includes("SelectionGroupBoundingBox") &&
|
||||
!intersect.object.name.includes("SelectionGroupBoundingLine") &&
|
||||
intersect.object.type !== "GridHelper" &&
|
||||
!intersect.object.name.includes("ArrowWithTube") &&
|
||||
!intersect.object.parent?.name.includes("Zone") &&
|
||||
intersect.object.type !== "Line2"
|
||||
);
|
||||
|
||||
let point: THREE.Vector3 | null = null;
|
||||
|
||||
if (intersects.length > 0) {
|
||||
point = intersects[0].point;
|
||||
if (point.y < 0.05) {
|
||||
point = new THREE.Vector3(point.x, 0.05, point.z);
|
||||
}
|
||||
}
|
||||
|
||||
const sphereIntersects = raycaster.intersectObjects(scene.children, true).filter((intersect) => intersect.object.name === ('Event-Sphere'));
|
||||
|
||||
if (sphereIntersects.length > 0 && sphereIntersects[0].object.uuid === firstSelectedPoint.pointUuid) {
|
||||
setHelperLineColor('red');
|
||||
setCurrentLine(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const startPoint = getWorldPositionFromScene(firstSelectedPoint.pointUuid);
|
||||
|
||||
if (point && startPoint) {
|
||||
if (sphereIntersects.length > 0) {
|
||||
point = sphereIntersects[0].object.getWorldPosition(new THREE.Vector3());
|
||||
}
|
||||
const distance = startPoint.distanceTo(point);
|
||||
const heightFactor = Math.max(0.5, distance * 0.2);
|
||||
const midPoint = new THREE.Vector3(
|
||||
(startPoint.x + point.x) / 2,
|
||||
Math.max(startPoint.y, point.y) + heightFactor,
|
||||
(startPoint.z + point.z) / 2
|
||||
);
|
||||
|
||||
const endPoint = point;
|
||||
|
||||
setCurrentLine({
|
||||
start: startPoint,
|
||||
mid: midPoint,
|
||||
end: endPoint,
|
||||
});
|
||||
|
||||
setHelperLineColor(sphereIntersects.length > 0 ? "#6cf542" : "red");
|
||||
} else {
|
||||
setCurrentLine(null);
|
||||
}
|
||||
} else {
|
||||
setCurrentLine(null);
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
const getWorldPositionFromScene = (pointUuid: string): THREE.Vector3 | null => {
|
||||
const pointObj = scene.getObjectByProperty("uuid", pointUuid);
|
||||
if (!pointObj) return null;
|
||||
|
||||
const worldPosition = new THREE.Vector3();
|
||||
pointObj.getWorldPosition(worldPosition);
|
||||
return worldPosition;
|
||||
};
|
||||
|
||||
const removeConnection = (connection: ConnectionLine) => {
|
||||
if (connection.trigger.triggerUuid) {
|
||||
const event = removeTrigger(selectedProduct.productUuid, connection.trigger.triggerUuid);
|
||||
if (event) {
|
||||
updateBackend(
|
||||
selectedProduct.productName,
|
||||
selectedProduct.productUuid,
|
||||
projectId || '',
|
||||
event
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<group name="simulationConnectionGroup" visible={!isPlaying}>
|
||||
{connections.map((connection) => {
|
||||
const startPoint = getWorldPositionFromScene(connection.startPointUuid);
|
||||
const endPoint = getWorldPositionFromScene(connection.endPointUuid);
|
||||
|
||||
if (!startPoint || !endPoint) return null;
|
||||
|
||||
const distance = startPoint.distanceTo(endPoint);
|
||||
const heightFactor = Math.max(0.5, distance * 0.2);
|
||||
const midPoint = new THREE.Vector3(
|
||||
(startPoint.x + endPoint.x) / 2,
|
||||
Math.max(startPoint.y, endPoint.y) + heightFactor,
|
||||
(startPoint.z + endPoint.z) / 2
|
||||
);
|
||||
|
||||
return (
|
||||
<QuadraticBezierLine
|
||||
key={connection.id}
|
||||
ref={(el) => (groupRefs.current[connection.id] = el!)}
|
||||
start={startPoint.toArray()}
|
||||
end={endPoint.toArray()}
|
||||
mid={midPoint.toArray()}
|
||||
color={deleteTool && hoveredLineKey === connection.id ? "red" : "#42a5f5"}
|
||||
lineWidth={4}
|
||||
dashed={deleteTool && hoveredLineKey === connection.id ? false : true}
|
||||
dashSize={0.75}
|
||||
dashScale={20}
|
||||
onPointerOver={() => setHoveredLineKey(connection.id)}
|
||||
onPointerOut={() => setHoveredLineKey(null)}
|
||||
onClick={() => {
|
||||
if (deleteTool) {
|
||||
setHoveredLineKey(null);
|
||||
setCurrentLine(null);
|
||||
removeConnection(connection);
|
||||
}
|
||||
}}
|
||||
userData={connection.trigger}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
<Arrows connections={connections} />
|
||||
|
||||
{currentLine && (
|
||||
<>
|
||||
<QuadraticBezierLine
|
||||
start={currentLine.start.toArray()}
|
||||
end={currentLine.end.toArray()}
|
||||
mid={currentLine.mid.toArray()}
|
||||
color={helperlineColor}
|
||||
lineWidth={4}
|
||||
dashed
|
||||
dashSize={1}
|
||||
dashScale={20}
|
||||
/>
|
||||
<ArrowOnQuadraticBezier
|
||||
start={currentLine.start.toArray()}
|
||||
mid={currentLine.mid.toArray()}
|
||||
end={currentLine.end.toArray()}
|
||||
color={helperlineColor}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
export default TriggerConnector;
|
||||
@@ -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 MachineCallback = {
|
||||
machineId: string;
|
||||
callback: () => void;
|
||||
};
|
||||
|
||||
export function useMachineEventManager() {
|
||||
const { machineStore } = useSceneContext();
|
||||
const { getMachineById } = machineStore();
|
||||
const callbacksRef = useRef<MachineCallback[]>([]);
|
||||
const isMonitoringRef = useRef(false);
|
||||
const { isPlaying } = usePlayButtonStore();
|
||||
const { isPaused } = usePauseButtonStore();
|
||||
const { isReset } = useResetButtonStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (isReset) {
|
||||
callbacksRef.current = [];
|
||||
}
|
||||
}, [isReset])
|
||||
|
||||
// Add a new machine to monitor
|
||||
const addMachineToMonitor = (machineId: string, callback: () => void) => {
|
||||
// Avoid duplicates
|
||||
if (!callbacksRef.current.some((entry) => entry.machineId === machineId)) {
|
||||
callbacksRef.current.push({ machineId, callback });
|
||||
}
|
||||
|
||||
// Start monitoring if not already running
|
||||
if (!isMonitoringRef.current) {
|
||||
isMonitoringRef.current = true;
|
||||
}
|
||||
};
|
||||
|
||||
// Remove a machine from monitoring
|
||||
const removeMachineFromMonitor = (machineId: string) => {
|
||||
callbacksRef.current = callbacksRef.current.filter(
|
||||
(entry) => entry.machineId !== machineId
|
||||
);
|
||||
|
||||
// Stop monitoring if no more machines to track
|
||||
if (callbacksRef.current.length === 0) {
|
||||
isMonitoringRef.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Check machine states every frame
|
||||
useFrame(() => {
|
||||
if (!isMonitoringRef.current || callbacksRef.current.length === 0 || !isPlaying || isPaused) return;
|
||||
|
||||
callbacksRef.current.forEach(({ machineId, callback }) => {
|
||||
const machine = getMachineById(machineId);
|
||||
if (machine && machine.isActive === false && machine.state === 'idle' && !machine.currentAction) {
|
||||
callback();
|
||||
removeMachineFromMonitor(machineId); // Remove after triggering
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
callbacksRef.current = [];
|
||||
isMonitoringRef.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
addMachineToMonitor,
|
||||
removeMachineFromMonitor,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useAnimationPlaySpeed, usePauseButtonStore, usePlayButtonStore, useResetButtonStore } from '../../../../../store/usePlayButtonStore';
|
||||
import { useSceneContext } from '../../../../scene/sceneContext';
|
||||
|
||||
interface MachineAnimatorProps {
|
||||
currentPhase: string;
|
||||
handleCallBack: () => void;
|
||||
reset: () => void;
|
||||
machineStatus: (modelId: string, status: string) => void;
|
||||
processingTime: number;
|
||||
machineUuid: string
|
||||
}
|
||||
|
||||
const MachineAnimator = ({ currentPhase, handleCallBack, processingTime, machineUuid, machineStatus, reset }: MachineAnimatorProps) => {
|
||||
const animationStarted = useRef<boolean>(false);
|
||||
const isPausedRef = useRef<boolean>(false);
|
||||
const startTimeRef = useRef<number>(0);
|
||||
const animationFrameId = useRef<number | null>(null);
|
||||
const pauseTimeRef = useRef<number | null>(null);
|
||||
const { isPaused } = usePauseButtonStore();
|
||||
const { machineStore } = useSceneContext();
|
||||
const { removeCurrentAction } = machineStore();
|
||||
const { isReset } = useResetButtonStore();
|
||||
const { isPlaying } = usePlayButtonStore();
|
||||
const { speed } = useAnimationPlaySpeed();
|
||||
const isPlayingRef = useRef<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
isPausedRef.current = isPaused;
|
||||
}, [isPaused]);
|
||||
|
||||
useEffect(() => {
|
||||
isPlayingRef.current = isPlaying;
|
||||
}, [isPlaying]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isReset || !isPlaying) {
|
||||
reset();
|
||||
startTimeRef.current = 0;
|
||||
isPausedRef.current = false;
|
||||
pauseTimeRef.current = 0;
|
||||
animationFrameId.current = null;
|
||||
animationStarted.current = false;
|
||||
removeCurrentAction(machineUuid)
|
||||
}
|
||||
}, [isReset, isPlaying])
|
||||
|
||||
useEffect(() => {
|
||||
if (currentPhase === 'processing' && !animationStarted.current && machineUuid) {
|
||||
animationStarted.current = true;
|
||||
startTimeRef.current = performance.now();
|
||||
animationFrameId.current = requestAnimationFrame(step);
|
||||
}
|
||||
|
||||
}, [currentPhase]);
|
||||
|
||||
function step(time: number) {
|
||||
|
||||
if (isPausedRef.current) {
|
||||
if (!pauseTimeRef.current) {
|
||||
pauseTimeRef.current = performance.now();
|
||||
}
|
||||
animationFrameId.current = requestAnimationFrame(step);
|
||||
return;
|
||||
}
|
||||
|
||||
if (pauseTimeRef.current) {
|
||||
const pauseDuration = performance.now() - pauseTimeRef.current;
|
||||
startTimeRef.current += pauseDuration;
|
||||
pauseTimeRef.current = null;
|
||||
}
|
||||
|
||||
const elapsed = time - startTimeRef.current;
|
||||
const processedTime = (processingTime * 1000) / speed;
|
||||
|
||||
if (elapsed < processedTime) {
|
||||
machineStatus(machineUuid, "Machine is currently processing the task");
|
||||
animationFrameId.current = requestAnimationFrame(step);
|
||||
} else {
|
||||
animationStarted.current = false;
|
||||
if (animationFrameId.current !== null) {
|
||||
removeCurrentAction(machineUuid);
|
||||
cancelAnimationFrame(animationFrameId.current);
|
||||
animationFrameId.current = null;
|
||||
handleCallBack();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default MachineAnimator;
|
||||
@@ -0,0 +1,140 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useAnimationPlaySpeed, usePauseButtonStore, usePlayButtonStore } from '../../../../../store/usePlayButtonStore';
|
||||
import MachineAnimator from '../animator/machineAnimator';
|
||||
import { useProductStore } from '../../../../../store/simulation/useProductStore';
|
||||
import { useTriggerHandler } from '../../../triggers/triggerHandler/useTriggerHandler';
|
||||
import { useSceneContext } from '../../../../scene/sceneContext';
|
||||
import { useProductContext } from '../../../products/productContext';
|
||||
|
||||
function MachineInstance({ machineDetail }: { readonly machineDetail: MachineStatus }) {
|
||||
const [currentPhase, setCurrentPhase] = useState<string>('idle');
|
||||
let isIncrememtable = useRef<boolean>(true);
|
||||
const idleTimeRef = useRef<number>(0);
|
||||
const activeTimeRef = useRef<number>(0);
|
||||
const previousTimeRef = useRef<number | null>(null);
|
||||
const animationFrameIdRef = useRef<number | null>(null);
|
||||
const isSpeedRef = useRef<number>(0);
|
||||
const isPausedRef = useRef<boolean>(false);
|
||||
const { isPlaying } = usePlayButtonStore();
|
||||
const { machineStore } = useSceneContext();
|
||||
const { selectedProductStore } = useProductContext();
|
||||
const { machines, setMachineState, setMachineActive, incrementIdleTime, incrementActiveTime, resetTime } = machineStore();
|
||||
const { selectedProduct } = selectedProductStore();
|
||||
const { getActionByUuid } = useProductStore();
|
||||
const { triggerPointActions } = useTriggerHandler();
|
||||
const { speed } = useAnimationPlaySpeed();
|
||||
const { isPaused } = usePauseButtonStore();
|
||||
useEffect(() => {
|
||||
isPausedRef.current = isPaused;
|
||||
}, [isPaused]);
|
||||
|
||||
useEffect(() => {
|
||||
isSpeedRef.current = speed;
|
||||
}, [speed]);
|
||||
|
||||
const reset = () => {
|
||||
setCurrentPhase("idle");
|
||||
setMachineState(machineDetail.modelUuid, 'idle');
|
||||
setMachineActive(machineDetail.modelUuid, false);
|
||||
isIncrememtable.current = true;
|
||||
isPausedRef.current = false;
|
||||
resetTime(machineDetail.modelUuid)
|
||||
activeTimeRef.current = 0
|
||||
idleTimeRef.current = 0
|
||||
previousTimeRef.current = null
|
||||
if (animationFrameIdRef.current !== null) {
|
||||
cancelAnimationFrame(animationFrameIdRef.current)
|
||||
animationFrameIdRef.current = null
|
||||
}
|
||||
}
|
||||
|
||||
function machineStatus(modelId: string, status: string) {
|
||||
// console.log(`${modelId} , ${status}`);
|
||||
}
|
||||
function animate(currentTime: number) {
|
||||
if (previousTimeRef.current === null) {
|
||||
previousTimeRef.current = currentTime;
|
||||
}
|
||||
|
||||
const deltaTime = (currentTime - previousTimeRef.current) / 1000;
|
||||
previousTimeRef.current = currentTime;
|
||||
|
||||
if (machineDetail.isActive) {
|
||||
if (!isPausedRef.current) {
|
||||
activeTimeRef.current += deltaTime * isSpeedRef.current;
|
||||
// console.log(' activeTimeRef.current: ', activeTimeRef.current);
|
||||
}
|
||||
} else {
|
||||
if (!isPausedRef.current) {
|
||||
idleTimeRef.current += deltaTime * isSpeedRef.current;
|
||||
// console.log('idleTimeRef.curre: ', idleTimeRef.current);
|
||||
}
|
||||
}
|
||||
animationFrameIdRef.current = requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!isPlaying) return
|
||||
if (!machineDetail.isActive) {
|
||||
const roundedActiveTime = Math.round(activeTimeRef.current);
|
||||
// console.log('Final Active Time:', roundedActiveTime, 'seconds');
|
||||
incrementActiveTime(machineDetail.modelUuid, roundedActiveTime);
|
||||
activeTimeRef.current = 0;
|
||||
} else {
|
||||
const roundedIdleTime = Math.round(idleTimeRef.current);
|
||||
// console.log('Final Idle Time:', roundedIdleTime, 'seconds');
|
||||
incrementIdleTime(machineDetail.modelUuid, roundedIdleTime);
|
||||
idleTimeRef.current = 0;
|
||||
}
|
||||
|
||||
if (animationFrameIdRef.current === null) {
|
||||
animationFrameIdRef.current = requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (animationFrameIdRef.current !== null) {
|
||||
cancelAnimationFrame(animationFrameIdRef.current);
|
||||
animationFrameIdRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [machineDetail, isPlaying]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isPlaying) {
|
||||
if (!machineDetail.isActive && machineDetail.state === "idle" && currentPhase == "idle" && !machineDetail.currentAction) {
|
||||
machineStatus(machineDetail.modelUuid, 'Machine is idle and waiting for next instruction.')
|
||||
} else if (!machineDetail.isActive && machineDetail.state === "idle" && currentPhase == "idle" && machineDetail.currentAction) {
|
||||
setCurrentPhase("processing");
|
||||
setMachineState(machineDetail.modelUuid, 'running');
|
||||
setMachineActive(machineDetail.modelUuid, true);
|
||||
machineStatus(machineDetail.modelUuid, "Machine started processing")
|
||||
}
|
||||
}
|
||||
}, [currentPhase, isPlaying, machines])
|
||||
|
||||
function handleCallBack() {
|
||||
if (currentPhase == "processing") {
|
||||
setMachineState(machineDetail.modelUuid, 'idle');
|
||||
setMachineActive(machineDetail.modelUuid, false);
|
||||
setCurrentPhase("idle")
|
||||
isIncrememtable.current = true;
|
||||
machineStatus(machineDetail.modelUuid, "Machine has completed the processing")
|
||||
|
||||
if (machineDetail.currentAction) {
|
||||
const action = getActionByUuid(selectedProduct.productUuid, machineDetail.currentAction.actionUuid);
|
||||
if (action && machineDetail.currentAction.materialId) {
|
||||
triggerPointActions(action, machineDetail.currentAction.materialId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<MachineAnimator processingTime={machineDetail.point.action.processTime} handleCallBack={handleCallBack} currentPhase={currentPhase} machineUuid={machineDetail.modelUuid} machineStatus={machineStatus} reset={reset} />
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default MachineInstance
|
||||
@@ -0,0 +1,24 @@
|
||||
import React from "react";
|
||||
import MachineInstance from "./machineInstance/machineInstance";
|
||||
import MachineContentUi from "../../ui3d/MachineContentUi";
|
||||
import { useSceneContext } from "../../../scene/sceneContext";
|
||||
import { useViewSceneStore } from "../../../../store/builder/store";
|
||||
|
||||
function MachineInstances() {
|
||||
const { machineStore } = useSceneContext();
|
||||
const { machines } = machineStore();
|
||||
const { viewSceneLabels } = useViewSceneStore();
|
||||
|
||||
return (
|
||||
<>
|
||||
{machines.map((machine: MachineStatus) => (
|
||||
<React.Fragment key={machine.modelUuid}>
|
||||
<MachineInstance machineDetail={machine} />
|
||||
{viewSceneLabels && <MachineContentUi machine={machine} />}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default MachineInstances;
|
||||
14
app/src/modules/simulation/machine/machine.tsx
Normal file
14
app/src/modules/simulation/machine/machine.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import MachineInstances from './instances/machineInstances'
|
||||
|
||||
function Machine() {
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
<MachineInstances />
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Machine
|
||||
@@ -0,0 +1,121 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import * as THREE from 'three';
|
||||
import { useThree, useFrame } from '@react-three/fiber';
|
||||
import { usePauseButtonStore, usePlayButtonStore, useResetButtonStore } from '../../../../store/usePlayButtonStore';
|
||||
import { CameraControls } from '@react-three/drei';
|
||||
import { useSceneContext } from '../../../scene/sceneContext';
|
||||
|
||||
const collisionWorker = new Worker(new URL('../../../../services/simulation/webWorkers/collisionWorker.ts', import.meta.url));
|
||||
|
||||
export default function MaterialCollisionDetector() {
|
||||
const { materialStore } = useSceneContext();
|
||||
const { materials } = materialStore();
|
||||
const { scene, controls, camera } = useThree();
|
||||
const positionsRef = useRef<Record<string, THREE.Vector3>>({});
|
||||
const sizesRef = useRef<Record<string, number>>({});
|
||||
const { isPlaying } = usePlayButtonStore();
|
||||
const { isPaused, setIsPaused } = usePauseButtonStore();
|
||||
const { isReset } = useResetButtonStore();
|
||||
const collisionStateRef = useRef<Record<string, boolean>>({});
|
||||
|
||||
useEffect(() => {
|
||||
if (isReset) {
|
||||
collisionStateRef.current = {};
|
||||
}
|
||||
}, [isReset])
|
||||
|
||||
useEffect(() => {
|
||||
collisionWorker.onmessage = (e) => {
|
||||
const { collisions } = e.data;
|
||||
handleCollisions(collisions);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useFrame(() => {
|
||||
if (materials.length < 2 || !isPlaying || isPaused) return;
|
||||
|
||||
const { positions, sizes } = getCurrentMaterialData();
|
||||
positionsRef.current = positions;
|
||||
sizesRef.current = sizes;
|
||||
|
||||
collisionWorker.postMessage({
|
||||
materials: materials,
|
||||
positions: positions,
|
||||
sizes: sizes
|
||||
});
|
||||
});
|
||||
|
||||
const getCurrentMaterialData = () => {
|
||||
const positions: Record<string, THREE.Vector3> = {};
|
||||
const sizes: Record<string, number> = {};
|
||||
|
||||
materials.forEach(material => {
|
||||
if (material.isVisible) {
|
||||
const obj = scene.getObjectByProperty('uuid', material.materialId);
|
||||
if (obj) {
|
||||
const position = new THREE.Vector3();
|
||||
obj.getWorldPosition(position);
|
||||
positions[material.materialId] = position;
|
||||
|
||||
const boundingBox = new THREE.Box3().setFromObject(obj);
|
||||
const sizeVector = new THREE.Vector3();
|
||||
boundingBox.getSize(sizeVector);
|
||||
sizes[material.materialId] = Math.max(sizeVector.x, sizeVector.y, sizeVector.z) / 2;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { positions, sizes };
|
||||
};
|
||||
|
||||
const handleCollisions = (currentCollisions: Array<{
|
||||
materialId1: string;
|
||||
materialId2: string;
|
||||
distance: number;
|
||||
}>) => {
|
||||
const newCollisionState: Record<string, boolean> = {};
|
||||
|
||||
currentCollisions.forEach(collision => {
|
||||
const key = `${collision.materialId1}|-|${collision.materialId2}`;
|
||||
newCollisionState[key] = true;
|
||||
});
|
||||
|
||||
Object.keys(collisionStateRef.current).forEach(key => {
|
||||
if (!newCollisionState[key]) {
|
||||
const [id1, id2] = key.split('|-|');
|
||||
echo.error(`Collision ended between ${id1} and ${id2}`);
|
||||
}
|
||||
});
|
||||
|
||||
currentCollisions.forEach(collision => {
|
||||
const key = `${collision.materialId1}|-|${collision.materialId2}`;
|
||||
if (!collisionStateRef.current[key]) {
|
||||
setIsPaused(true);
|
||||
echo.error(`Collision started between ${collision.materialId1} and ${collision.materialId2}`);
|
||||
const obj = scene.getObjectByProperty('uuid', collision.materialId1)
|
||||
if (obj) {
|
||||
const collisionPos = new THREE.Vector3();
|
||||
obj.getWorldPosition(collisionPos);
|
||||
|
||||
const currentPos = new THREE.Vector3().copy(camera.position);
|
||||
|
||||
const target = new THREE.Vector3();
|
||||
if (!controls) return;
|
||||
(controls as CameraControls).getTarget(target);
|
||||
const direction = new THREE.Vector3().subVectors(target, currentPos).normalize();
|
||||
|
||||
const offsetDistance = 5;
|
||||
const newCameraPos = new THREE.Vector3().copy(collisionPos).sub(direction.multiplyScalar(offsetDistance));
|
||||
|
||||
camera.position.copy(newCameraPos);
|
||||
(controls as CameraControls).setLookAt(newCameraPos.x, newCameraPos.y, newCameraPos.z, collisionPos.x, collisionPos.y, collisionPos.z, true);
|
||||
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
collisionStateRef.current = newCollisionState;
|
||||
};
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import * as THREE from 'three';
|
||||
import { useFrame, useThree } from '@react-three/fiber';
|
||||
import { usePauseButtonStore, usePlayButtonStore } from '../../../../../store/usePlayButtonStore';
|
||||
import { useSceneContext } from '../../../../scene/sceneContext';
|
||||
|
||||
interface MaterialAnimatorProps {
|
||||
matRef: React.RefObject<THREE.Mesh>;
|
||||
material: MaterialSchema;
|
||||
currentSpeed: number;
|
||||
onAnimationComplete?: () => void;
|
||||
}
|
||||
|
||||
function MaterialAnimator({
|
||||
matRef,
|
||||
material,
|
||||
currentSpeed,
|
||||
onAnimationComplete
|
||||
}: MaterialAnimatorProps) {
|
||||
const { scene } = useThree();
|
||||
const [targetPosition, setTargetPosition] = useState<THREE.Vector3 | null>(null);
|
||||
const [isAnimating, setIsAnimating] = useState(false);
|
||||
const { conveyorStore } = useSceneContext();
|
||||
const { getConveyorById } = conveyorStore();
|
||||
const animationState = useRef({
|
||||
startTime: 0,
|
||||
startPosition: new THREE.Vector3(),
|
||||
totalDistance: 0,
|
||||
pausedTime: 0,
|
||||
isPaused: false,
|
||||
lastFrameTime: 0
|
||||
});
|
||||
|
||||
const { isPlaying } = usePlayButtonStore();
|
||||
const { isPaused: isGlobalPaused } = usePauseButtonStore();
|
||||
|
||||
const conveyor = getConveyorById(material.current.modelUuid);
|
||||
const shouldPause = isGlobalPaused || material.isPaused || conveyor?.isPaused;
|
||||
|
||||
const getWorldPosition = (uuid: string): THREE.Vector3 | null => {
|
||||
const obj = scene.getObjectByProperty('uuid', uuid);
|
||||
if (!obj) return null;
|
||||
const position = new THREE.Vector3();
|
||||
obj.getWorldPosition(position);
|
||||
return position;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isPlaying || !material.next?.pointUuid) {
|
||||
if (material.current.pointUuid) {
|
||||
const newTarget = getWorldPosition(material.current.pointUuid);
|
||||
if (newTarget && matRef.current && !material.isPaused) {
|
||||
animationState.current.startPosition.copy(matRef.current.position);
|
||||
animationState.current.totalDistance = animationState.current.startPosition.distanceTo(newTarget);
|
||||
animationState.current.startTime = performance.now() - animationState.current.pausedTime;
|
||||
animationState.current.pausedTime = 0;
|
||||
animationState.current.isPaused = false;
|
||||
setTargetPosition(newTarget);
|
||||
setIsAnimating(true);
|
||||
}
|
||||
} else {
|
||||
setIsAnimating(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const newTarget = getWorldPosition(material.next.pointUuid);
|
||||
if (newTarget && matRef.current && !material.isPaused) {
|
||||
animationState.current.startPosition.copy(matRef.current.position);
|
||||
animationState.current.totalDistance = animationState.current.startPosition.distanceTo(newTarget);
|
||||
animationState.current.startTime = performance.now() - animationState.current.pausedTime;
|
||||
animationState.current.pausedTime = 0;
|
||||
animationState.current.isPaused = false;
|
||||
setTargetPosition(newTarget);
|
||||
setIsAnimating(true);
|
||||
}
|
||||
}, [material, isPlaying]);
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldPause) {
|
||||
// Pause the animation
|
||||
animationState.current.isPaused = true;
|
||||
setIsAnimating(false);
|
||||
animationState.current.pausedTime = performance.now() - animationState.current.startTime;
|
||||
} else {
|
||||
// Resume the animation
|
||||
animationState.current.isPaused = false;
|
||||
if (isPlaying && targetPosition && !isAnimating) {
|
||||
animationState.current.startTime = performance.now() - animationState.current.pausedTime;
|
||||
setIsAnimating(true);
|
||||
}
|
||||
}
|
||||
}, [shouldPause, isPlaying]);
|
||||
|
||||
useFrame(() => {
|
||||
if (!matRef.current || !targetPosition || !isAnimating || animationState.current.isPaused || !isPlaying) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentTime = performance.now();
|
||||
const elapsed = (currentTime - animationState.current.startTime) / 1000;
|
||||
const progress = Math.min(1, (currentSpeed * elapsed) / animationState.current.totalDistance);
|
||||
|
||||
matRef.current.position.lerpVectors(
|
||||
animationState.current.startPosition,
|
||||
targetPosition,
|
||||
progress
|
||||
);
|
||||
|
||||
if (progress >= 1) {
|
||||
matRef.current.position.copy(targetPosition);
|
||||
setIsAnimating(false);
|
||||
onAnimationComplete?.();
|
||||
animationState.current = {
|
||||
startTime: 0,
|
||||
startPosition: new THREE.Vector3(),
|
||||
totalDistance: 0,
|
||||
pausedTime: 0,
|
||||
isPaused: false,
|
||||
lastFrameTime: 0
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default React.memo(MaterialAnimator);
|
||||
@@ -0,0 +1,112 @@
|
||||
import { useMemo, useRef } from 'react'
|
||||
import * as THREE from 'three';
|
||||
import MaterialAnimator from '../animator/materialAnimator';
|
||||
import { useProductStore } from '../../../../../store/simulation/useProductStore';
|
||||
import { MaterialModel } from '../material/materialModel';
|
||||
import { useThree } from '@react-three/fiber';
|
||||
import { useAnimationPlaySpeed } from '../../../../../store/usePlayButtonStore';
|
||||
import { useTriggerHandler } from '../../../triggers/triggerHandler/useTriggerHandler';
|
||||
import { useProductContext } from '../../../products/productContext';
|
||||
|
||||
function MaterialInstance({ material }: { readonly material: MaterialSchema }) {
|
||||
const matRef: any = useRef();
|
||||
const { scene } = useThree();
|
||||
const { selectedProductStore } = useProductContext();
|
||||
const { getModelUuidByPointUuid, getPointByUuid, getEventByModelUuid, getActionByUuid, getTriggerByUuid, getActionByPointUuid } = useProductStore();
|
||||
const { selectedProduct } = selectedProductStore();
|
||||
const { speed } = useAnimationPlaySpeed();
|
||||
const { triggerPointActions } = useTriggerHandler();
|
||||
|
||||
const getWorldPositionFromScene = (pointUuid: string): THREE.Vector3 | null => {
|
||||
const pointObj = scene.getObjectByProperty("uuid", pointUuid);
|
||||
if (!pointObj) return null;
|
||||
|
||||
const worldPosition = new THREE.Vector3();
|
||||
pointObj.getWorldPosition(worldPosition);
|
||||
return worldPosition;
|
||||
};
|
||||
|
||||
const { position, rotation, currentSpeed } = useMemo(() => {
|
||||
if (!material.current?.pointUuid) {
|
||||
return { position: new THREE.Vector3(0, 0, 0), rotation: new THREE.Vector3(0, 0, 0), currentSpeed: 1 };
|
||||
}
|
||||
|
||||
const modelUuid = getModelUuidByPointUuid(selectedProduct.productUuid, material.current.pointUuid);
|
||||
if (!modelUuid) {
|
||||
return { position: new THREE.Vector3(0, 0, 0), rotation: new THREE.Vector3(0, 0, 0), currentSpeed: 1 };
|
||||
}
|
||||
|
||||
const currentSpeed = getCurrentSpeed(selectedProduct.productUuid, modelUuid);
|
||||
|
||||
const point = getPointByUuid(selectedProduct.productUuid, modelUuid, material.current.pointUuid);
|
||||
if (!point) {
|
||||
return { position: new THREE.Vector3(0, 0, 0), rotation: new THREE.Vector3(0, 0, 0), currentSpeed: currentSpeed || 1 };
|
||||
}
|
||||
|
||||
const position = getWorldPositionFromScene(point.uuid);
|
||||
if (position) {
|
||||
return { position: position, rotation: new THREE.Vector3(0, 0, 0), currentSpeed: currentSpeed || 1 };
|
||||
}
|
||||
|
||||
return {
|
||||
position: new THREE.Vector3(...point.position),
|
||||
rotation: new THREE.Vector3(...point.rotation),
|
||||
currentSpeed: currentSpeed || 1
|
||||
};
|
||||
}, [material, getPointByUuid]);
|
||||
|
||||
|
||||
function getCurrentSpeed(productUuid: string, modelUuid: string) {
|
||||
const event = getEventByModelUuid(productUuid, modelUuid)
|
||||
if (event) {
|
||||
if (event.type === 'transfer') {
|
||||
return event.speed;
|
||||
}
|
||||
|
||||
if (event.type === 'vehicle') {
|
||||
return event.speed;
|
||||
}
|
||||
|
||||
if (event.type === 'machine') {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (event.type === 'roboticArm') {
|
||||
return event.speed;
|
||||
}
|
||||
|
||||
if (event.type === 'storageUnit') {
|
||||
return 1;
|
||||
}
|
||||
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
const callTrigger = () => {
|
||||
if (!material.next) return;
|
||||
const action = getActionByPointUuid(selectedProduct.productUuid, material.next.pointUuid);
|
||||
if (action) {
|
||||
triggerPointActions(action, material.materialId);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
{material.isRendered &&
|
||||
<MaterialModel materialId={material.materialId} matRef={matRef} materialType={material.materialType} visible={material.isVisible} position={position} />
|
||||
}
|
||||
|
||||
<MaterialAnimator
|
||||
matRef={matRef}
|
||||
material={material}
|
||||
currentSpeed={currentSpeed * speed}
|
||||
onAnimationComplete={() => { callTrigger() }}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default MaterialInstance
|
||||
@@ -0,0 +1,44 @@
|
||||
import { useGLTF } from '@react-three/drei'
|
||||
import { useMemo } from 'react';
|
||||
import * as THREE from 'three';
|
||||
|
||||
import defaultMaterial from '../../../../../assets/gltf-glb/materials/default.glb';
|
||||
import material1 from '../../../../../assets/gltf-glb/materials/material1.glb';
|
||||
import material2 from '../../../../../assets/gltf-glb/materials/material2.glb';
|
||||
import material3 from '../../../../../assets/gltf-glb/materials/material3.glb';
|
||||
|
||||
const modelPaths: Record<string, string> = {
|
||||
'Default material': defaultMaterial,
|
||||
'Material 1': material1,
|
||||
'Material 2': material2,
|
||||
'Material 3': material3,
|
||||
};
|
||||
|
||||
type ModelType = keyof typeof modelPaths;
|
||||
|
||||
interface ModelProps extends React.ComponentProps<'group'> {
|
||||
materialId: string;
|
||||
materialType: ModelType;
|
||||
matRef: React.Ref<THREE.Group<THREE.Object3DEventMap>>
|
||||
}
|
||||
|
||||
export function MaterialModel({ materialId, materialType, matRef, ...props }: Readonly<ModelProps>) {
|
||||
const path = modelPaths[materialType] || modelPaths['Default material'];
|
||||
const gltf = useGLTF(path);
|
||||
const cloned = useMemo(() => gltf?.scene.clone(), [gltf]);
|
||||
|
||||
if (!cloned) return null;
|
||||
|
||||
return (
|
||||
<group uuid={materialId} ref={matRef} {...props} dispose={null}>
|
||||
<primitive
|
||||
object={cloned}
|
||||
scale={[0.4, 0.4, 0.4]}
|
||||
/>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
Object.values(modelPaths).forEach((path) => {
|
||||
useGLTF.preload(path);
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import MaterialInstance from './instance/materialInstance'
|
||||
import { useSceneContext } from '../../../scene/sceneContext';
|
||||
|
||||
function MaterialInstances() {
|
||||
const { materialStore } = useSceneContext();
|
||||
const { materials, materialHistory } = materialStore();
|
||||
|
||||
useEffect(() => {
|
||||
// console.log('materials: ', materials);
|
||||
}, [materials])
|
||||
|
||||
useEffect(() => {
|
||||
// console.log('materialHistory: ', materialHistory);
|
||||
}, [materialHistory])
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
{materials.map((material: MaterialSchema) =>
|
||||
<MaterialInstance key={material.materialId} material={material} />
|
||||
)}
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default MaterialInstances
|
||||
34
app/src/modules/simulation/materials/materials.tsx
Normal file
34
app/src/modules/simulation/materials/materials.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { useEffect } from 'react'
|
||||
import MaterialInstances from './instances/materialInstances'
|
||||
import { usePlayButtonStore, useResetButtonStore } from '../../../store/usePlayButtonStore';
|
||||
// import MaterialCollisionDetector from './collisionDetection/materialCollitionDetector';
|
||||
import { useSceneContext } from '../../scene/sceneContext';
|
||||
|
||||
function Materials() {
|
||||
const { materialStore } = useSceneContext();
|
||||
const { clearMaterials } = materialStore();
|
||||
const { isPlaying } = usePlayButtonStore();
|
||||
const { isReset } = useResetButtonStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (isReset || !isPlaying) {
|
||||
clearMaterials();
|
||||
}
|
||||
}, [isReset, isPlaying]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
{isPlaying &&
|
||||
|
||||
<MaterialInstances />
|
||||
|
||||
}
|
||||
|
||||
{/* <MaterialCollisionDetector /> */}
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Materials
|
||||
37
app/src/modules/simulation/products/productContext.tsx
Normal file
37
app/src/modules/simulation/products/productContext.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { createContext, useContext, useMemo } from 'react';
|
||||
import { createSelectedProductStore, SelectedProductType } from '../../../store/simulation/useSimulationStore';
|
||||
|
||||
type ProductContextValue = {
|
||||
selectedProductStore: SelectedProductType,
|
||||
};
|
||||
|
||||
const ProductContext = createContext<ProductContextValue | null>(null);
|
||||
|
||||
export function ProductProvider({
|
||||
children,
|
||||
}: {
|
||||
readonly children: React.ReactNode;
|
||||
}) {
|
||||
const selectedProductStore = useMemo(() => createSelectedProductStore(), []);
|
||||
|
||||
const contextValue = useMemo(() => (
|
||||
{
|
||||
selectedProductStore
|
||||
}
|
||||
), [selectedProductStore]);
|
||||
|
||||
return (
|
||||
<ProductContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</ProductContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
// Base hook to get the context
|
||||
export function useProductContext() {
|
||||
const context = useContext(ProductContext);
|
||||
if (!context) {
|
||||
throw new Error('useProductContext must be used within a ProductProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
155
app/src/modules/simulation/products/products.tsx
Normal file
155
app/src/modules/simulation/products/products.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
import * as THREE from 'three';
|
||||
import { useEffect } from 'react';
|
||||
import { useProductStore } from '../../../store/simulation/useProductStore';
|
||||
import { upsertProductOrEventApi } from '../../../services/simulation/products/UpsertProductOrEventApi';
|
||||
import { getAllProductsApi } from '../../../services/simulation/products/getallProductsApi';
|
||||
import { usePlayButtonStore, useResetButtonStore } from '../../../store/usePlayButtonStore';
|
||||
import { useSceneContext } from '../../scene/sceneContext';
|
||||
import { useProductContext } from './productContext';
|
||||
import { useComparisonProduct, useMainProduct } from '../../../store/simulation/useSimulationStore';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
function Products() {
|
||||
const { armBotStore, machineStore, conveyorStore, vehicleStore, storageUnitStore, layout } = useSceneContext();
|
||||
const { products, getProductById, addProduct, setProducts } = useProductStore();
|
||||
const { selectedProductStore } = useProductContext();
|
||||
const { selectedProduct, setSelectedProduct } = selectedProductStore();
|
||||
const { addVehicle, clearvehicles } = vehicleStore();
|
||||
const { addArmBot, clearArmBots } = armBotStore();
|
||||
const { addMachine, clearMachines } = machineStore();
|
||||
const { addConveyor, clearConveyors } = conveyorStore();
|
||||
const { setCurrentMaterials, clearStorageUnits, updateCurrentLoad, addStorageUnit } = storageUnitStore();
|
||||
const { isReset } = useResetButtonStore();
|
||||
const { isPlaying } = usePlayButtonStore();
|
||||
const { mainProduct } = useMainProduct();
|
||||
const { comparisonProduct } = useComparisonProduct();
|
||||
const { projectId } = useParams();
|
||||
|
||||
useEffect(() => {
|
||||
if (layout === 'Main Layout' && mainProduct) {
|
||||
setSelectedProduct(mainProduct.productUuid, mainProduct.productName);
|
||||
}
|
||||
}, [mainProduct])
|
||||
|
||||
useEffect(() => {
|
||||
if (layout === 'Comparison Layout' && comparisonProduct) {
|
||||
setSelectedProduct(comparisonProduct.productUuid, comparisonProduct.productName);
|
||||
}
|
||||
}, [comparisonProduct])
|
||||
|
||||
useEffect(() => {
|
||||
getAllProductsApi(projectId || '').then((data) => {
|
||||
if (data.length === 0) {
|
||||
const id = THREE.MathUtils.generateUUID();
|
||||
const name = 'Product 1';
|
||||
addProduct(name, id);
|
||||
console.log(name, id, projectId);
|
||||
upsertProductOrEventApi({
|
||||
productName: name,
|
||||
productUuid: id,
|
||||
projectId: projectId || ''
|
||||
})
|
||||
if (layout === 'Main Layout') {
|
||||
setSelectedProduct(id, name);
|
||||
}
|
||||
} else {
|
||||
setProducts(data);
|
||||
if (layout === 'Main Layout') {
|
||||
setSelectedProduct(data[0].productUuid, data[0].productName);
|
||||
}
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedProduct.productUuid) {
|
||||
const product = getProductById(selectedProduct.productUuid);
|
||||
if (product) {
|
||||
clearvehicles();
|
||||
product.eventDatas.forEach(events => {
|
||||
if (events.type === 'vehicle') {
|
||||
addVehicle(selectedProduct.productUuid, events);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [selectedProduct, products, isReset, isPlaying]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedProduct.productUuid) {
|
||||
const product = getProductById(selectedProduct.productUuid);
|
||||
if (product) {
|
||||
clearArmBots();
|
||||
product.eventDatas.forEach(events => {
|
||||
if (events.type === 'roboticArm') {
|
||||
addArmBot(selectedProduct.productUuid, events);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [selectedProduct, products, isReset, isPlaying]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedProduct.productUuid) {
|
||||
const product = getProductById(selectedProduct.productUuid);
|
||||
if (product) {
|
||||
clearConveyors();
|
||||
product.eventDatas.forEach(events => {
|
||||
if (events.type === 'transfer') {
|
||||
addConveyor(selectedProduct.productUuid, events);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [selectedProduct, products, isReset, isPlaying]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedProduct.productUuid) {
|
||||
const product = getProductById(selectedProduct.productUuid);
|
||||
if (product) {
|
||||
clearMachines();
|
||||
product.eventDatas.forEach(events => {
|
||||
if (events.type === 'machine') {
|
||||
addMachine(selectedProduct.productUuid, events);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [selectedProduct, products, isReset, isPlaying]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedProduct.productUuid) {
|
||||
const product = getProductById(selectedProduct.productUuid);
|
||||
if (product) {
|
||||
clearStorageUnits();
|
||||
product.eventDatas.forEach(event => {
|
||||
if (event.type === 'storageUnit') {
|
||||
addStorageUnit(selectedProduct.productUuid, event);
|
||||
|
||||
if (event.point.action.actionType === 'retrieve') {
|
||||
const storageAction = event.point.action;
|
||||
const materials = Array.from({ length: storageAction.storageCapacity }, () => ({
|
||||
materialType: storageAction.materialType || 'Default material',
|
||||
materialId: THREE.MathUtils.generateUUID()
|
||||
}));
|
||||
|
||||
setCurrentMaterials(event.modelUuid, materials);
|
||||
updateCurrentLoad(event.modelUuid, storageAction.storageCapacity);
|
||||
} else {
|
||||
setCurrentMaterials(event.modelUuid, []);
|
||||
updateCurrentLoad(event.modelUuid, 0);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [selectedProduct, products, isReset, isPlaying]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Products
|
||||
@@ -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;
|
||||
67
app/src/modules/simulation/simulation.tsx
Normal file
67
app/src/modules/simulation/simulation.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useEventsStore } from '../../store/simulation/useEventsStore';
|
||||
import { useProductStore } from '../../store/simulation/useProductStore';
|
||||
import Vehicles from './vehicle/vehicles';
|
||||
import Points from './events/points/points';
|
||||
import Conveyor from './conveyor/conveyor';
|
||||
import RoboticArm from './roboticArm/roboticArm';
|
||||
import Materials from './materials/materials';
|
||||
import Machine from './machine/machine';
|
||||
import StorageUnit from './storageUnit/storageUnit';
|
||||
import Simulator from './simulator/simulator';
|
||||
import Products from './products/products';
|
||||
import Trigger from './triggers/trigger';
|
||||
import useModuleStore from '../../store/useModuleStore';
|
||||
import SimulationAnalysis from './analysis/simulationAnalysis';
|
||||
|
||||
function Simulation() {
|
||||
const { activeModule } = useModuleStore();
|
||||
const { events } = useEventsStore();
|
||||
const { products } = useProductStore();
|
||||
|
||||
useEffect(() => {
|
||||
// console.log('events: ', events);
|
||||
}, [events])
|
||||
|
||||
useEffect(() => {
|
||||
// console.log('products: ', products);
|
||||
}, [products])
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
<Products />
|
||||
|
||||
{activeModule === 'simulation' &&
|
||||
|
||||
<>
|
||||
|
||||
<Points />
|
||||
|
||||
<Materials />
|
||||
|
||||
<Trigger />
|
||||
|
||||
<Conveyor />
|
||||
|
||||
<Vehicles />
|
||||
|
||||
<RoboticArm />
|
||||
|
||||
<Machine />
|
||||
|
||||
<StorageUnit />
|
||||
|
||||
<Simulator />
|
||||
|
||||
<SimulationAnalysis />
|
||||
|
||||
</>
|
||||
|
||||
}
|
||||
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Simulation;
|
||||
@@ -0,0 +1,189 @@
|
||||
import { useSceneContext } from "../../../scene/sceneContext";
|
||||
import { extractTriggersFromPoint } from "./extractTriggersFromPoint";
|
||||
|
||||
export function getRoboticArmSequencesInProduct(
|
||||
product: {
|
||||
productName: string;
|
||||
productUuid: string;
|
||||
eventDatas: EventsSchema[];
|
||||
}
|
||||
): EventsSchema[][][] {
|
||||
// Get all machine sequences for this product
|
||||
const machineSequences = determineExecutionMachineSequences([product]);
|
||||
|
||||
const allRoboticArmSequences: EventsSchema[][][] = [];
|
||||
|
||||
// Process each machine sequence separately
|
||||
for (const machineSequence of machineSequences) {
|
||||
const roboticArmSequencesForThisMachineSequence: EventsSchema[][] = [];
|
||||
let currentRoboticArmSequence: EventsSchema[] = [];
|
||||
|
||||
for (const event of machineSequence) {
|
||||
if (event.type === 'roboticArm') {
|
||||
// Add robotic arm to current sequence
|
||||
currentRoboticArmSequence.push(event);
|
||||
} else if (event.type === 'vehicle') {
|
||||
// Vehicle encountered - split the sequence
|
||||
if (currentRoboticArmSequence.length > 0) {
|
||||
roboticArmSequencesForThisMachineSequence.push([...currentRoboticArmSequence]);
|
||||
currentRoboticArmSequence = [];
|
||||
}
|
||||
}
|
||||
// Other machine types continue the current sequence
|
||||
}
|
||||
|
||||
// Add any remaining robotic arms in the current sequence
|
||||
if (currentRoboticArmSequence.length > 0) {
|
||||
roboticArmSequencesForThisMachineSequence.push([...currentRoboticArmSequence]);
|
||||
}
|
||||
|
||||
if (roboticArmSequencesForThisMachineSequence.length > 0) {
|
||||
allRoboticArmSequences.push(roboticArmSequencesForThisMachineSequence);
|
||||
}
|
||||
}
|
||||
|
||||
return allRoboticArmSequences;
|
||||
}
|
||||
|
||||
export function findRoboticArmSubsequence(
|
||||
product: {
|
||||
productName: string;
|
||||
productUuid: string;
|
||||
eventDatas: EventsSchema[];
|
||||
},
|
||||
roboticArmModelUuid: string
|
||||
): {
|
||||
allSequences: EventsSchema[][][];
|
||||
parentSequence: EventsSchema[][];
|
||||
currentSubSequence: EventsSchema[];
|
||||
} | null {
|
||||
const allSequences = getRoboticArmSequencesInProduct(product);
|
||||
|
||||
for (const parentSequence of allSequences) {
|
||||
for (const currentSubSequence of parentSequence) {
|
||||
const hasTargetRoboticArm = currentSubSequence.some(
|
||||
event => event.type === 'roboticArm' && event.modelUuid === roboticArmModelUuid
|
||||
);
|
||||
|
||||
if (hasTargetRoboticArm) {
|
||||
return {
|
||||
allSequences,
|
||||
parentSequence,
|
||||
currentSubSequence
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// React component/hook that uses the pure functions
|
||||
export function useCheckActiveRoboticArmsInSubsequence() {
|
||||
const {armBotStore} = useSceneContext();
|
||||
const { getArmBotById } = armBotStore();
|
||||
|
||||
return function (product: {
|
||||
productName: string;
|
||||
productUuid: string;
|
||||
eventDatas: EventsSchema[];
|
||||
}, roboticArmModelUuid: string) {
|
||||
const result = findRoboticArmSubsequence(product, roboticArmModelUuid);
|
||||
|
||||
if (!result) return null;
|
||||
|
||||
const hasActiveRoboticArm = result.currentSubSequence.some(event => {
|
||||
if (event.type === 'roboticArm' && event.modelUuid !== roboticArmModelUuid) {
|
||||
const armBot = getArmBotById(event.modelUuid);
|
||||
return armBot?.isActive;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
return {
|
||||
...result,
|
||||
hasActiveRoboticArm
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// Helper function to get machine sequences (simplified from your example)
|
||||
function determineExecutionMachineSequences(products: productsSchema): EventsSchema[][] {
|
||||
const pointToEventMap = new Map<string, EventsSchema>();
|
||||
const allPoints: PointsScheme[] = [];
|
||||
|
||||
// First pass: map points to their corresponding events
|
||||
products.forEach(product => {
|
||||
product.eventDatas.forEach(event => {
|
||||
if (event.type === 'transfer') {
|
||||
event.points.forEach(point => {
|
||||
pointToEventMap.set(point.uuid, event);
|
||||
allPoints.push(point);
|
||||
});
|
||||
} else if (
|
||||
event.type === 'vehicle' ||
|
||||
event.type === 'machine' ||
|
||||
event.type === 'storageUnit' ||
|
||||
event.type === 'roboticArm'
|
||||
) {
|
||||
pointToEventMap.set(event.point.uuid, event);
|
||||
allPoints.push(event.point);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Build dependency graph
|
||||
const dependencyGraph = new Map<string, string[]>();
|
||||
const triggeredPoints = new Set<string>();
|
||||
|
||||
allPoints.forEach(point => {
|
||||
const triggers = extractTriggersFromPoint(point);
|
||||
const dependencies: string[] = [];
|
||||
|
||||
triggers.forEach(trigger => {
|
||||
const targetUuid = trigger.triggeredAsset?.triggeredPoint?.pointUuid;
|
||||
if (targetUuid && pointToEventMap.has(targetUuid)) {
|
||||
dependencies.push(targetUuid);
|
||||
triggeredPoints.add(targetUuid);
|
||||
}
|
||||
});
|
||||
|
||||
dependencyGraph.set(point.uuid, dependencies);
|
||||
});
|
||||
|
||||
// Find root points (points that aren't triggered by others)
|
||||
const rootPoints = allPoints.filter(point =>
|
||||
!triggeredPoints.has(point.uuid) &&
|
||||
dependencyGraph.get(point.uuid)?.length
|
||||
);
|
||||
|
||||
const executionSequences: EventsSchema[][] = [];
|
||||
|
||||
function buildSequence(startUuid: string): EventsSchema[] {
|
||||
const sequence: EventsSchema[] = [];
|
||||
const visited = new Set<string>();
|
||||
|
||||
function traverse(uuid: string) {
|
||||
if (visited.has(uuid)) return;
|
||||
visited.add(uuid);
|
||||
|
||||
const event = pointToEventMap.get(uuid);
|
||||
if (event && !sequence.includes(event)) {
|
||||
sequence.push(event);
|
||||
}
|
||||
|
||||
const nextPoints = dependencyGraph.get(uuid) || [];
|
||||
nextPoints.forEach(nextUuid => traverse(nextUuid));
|
||||
}
|
||||
|
||||
traverse(startUuid);
|
||||
return sequence;
|
||||
}
|
||||
|
||||
// Build sequences from root points
|
||||
rootPoints.forEach(root => {
|
||||
executionSequences.push(buildSequence(root.uuid));
|
||||
});
|
||||
|
||||
return executionSequences;
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { extractTriggersFromPoint } from "./extractTriggersFromPoint";
|
||||
|
||||
export async function determineExecutionMachineSequences(products: productsSchema): Promise<EventsSchema[][]> {
|
||||
const pointToEventMap = new Map<string, EventsSchema>();
|
||||
const allPoints: PointsScheme[] = [];
|
||||
|
||||
// First pass: map points to their corresponding events
|
||||
products.forEach(product => {
|
||||
product.eventDatas.forEach(event => {
|
||||
if (event.type === 'transfer') {
|
||||
event.points.forEach(point => {
|
||||
pointToEventMap.set(point.uuid, event);
|
||||
allPoints.push(point);
|
||||
});
|
||||
} else if (
|
||||
event.type === 'vehicle' ||
|
||||
event.type === 'machine' ||
|
||||
event.type === 'storageUnit' ||
|
||||
event.type === 'roboticArm'
|
||||
) {
|
||||
pointToEventMap.set(event.point.uuid, event);
|
||||
allPoints.push(event.point);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Build dependency graph
|
||||
const dependencyGraph = new Map<string, string[]>();
|
||||
const reverseDependencyGraph = new Map<string, string[]>();
|
||||
const triggeredPoints = new Set<string>();
|
||||
|
||||
allPoints.forEach(point => {
|
||||
const triggers = extractTriggersFromPoint(point);
|
||||
const dependencies: string[] = [];
|
||||
|
||||
triggers.forEach(trigger => {
|
||||
const targetUuid = trigger.triggeredAsset?.triggeredPoint?.pointUuid;
|
||||
if (targetUuid && pointToEventMap.has(targetUuid)) {
|
||||
dependencies.push(targetUuid);
|
||||
triggeredPoints.add(targetUuid);
|
||||
|
||||
if (!reverseDependencyGraph.has(targetUuid)) {
|
||||
reverseDependencyGraph.set(targetUuid, []);
|
||||
}
|
||||
reverseDependencyGraph.get(targetUuid)!.push(point.uuid);
|
||||
}
|
||||
});
|
||||
|
||||
dependencyGraph.set(point.uuid, dependencies);
|
||||
});
|
||||
|
||||
// Find root points (points that trigger others but are not triggered themselves)
|
||||
const rootPoints = allPoints.filter(point => {
|
||||
const hasOutgoingTriggers = extractTriggersFromPoint(point).some(
|
||||
t => t.triggeredAsset?.triggeredPoint?.pointUuid
|
||||
);
|
||||
return hasOutgoingTriggers && !triggeredPoints.has(point.uuid);
|
||||
});
|
||||
|
||||
const executionSequences: EventsSchema[][] = [];
|
||||
|
||||
function buildSequence(startUuid: string): EventsSchema[] {
|
||||
const sequence: EventsSchema[] = [];
|
||||
const visited = new Set<string>();
|
||||
|
||||
function traverse(uuid: string) {
|
||||
if (visited.has(uuid)) return;
|
||||
visited.add(uuid);
|
||||
|
||||
const event = pointToEventMap.get(uuid);
|
||||
if (event && !sequence.includes(event)) {
|
||||
sequence.push(event);
|
||||
}
|
||||
|
||||
const nextPoints = dependencyGraph.get(uuid) || [];
|
||||
nextPoints.forEach(nextUuid => traverse(nextUuid));
|
||||
}
|
||||
|
||||
traverse(startUuid);
|
||||
return sequence;
|
||||
}
|
||||
|
||||
// Build sequences from root points
|
||||
rootPoints.forEach(root => {
|
||||
executionSequences.push(buildSequence(root.uuid));
|
||||
});
|
||||
|
||||
// Handle any isolated triggered points
|
||||
const processedEvents = new Set(
|
||||
executionSequences.flat().map(event => event)
|
||||
);
|
||||
|
||||
allPoints.forEach(point => {
|
||||
const event = pointToEventMap.get(point.uuid);
|
||||
if (triggeredPoints.has(point.uuid) && event && !processedEvents.has(event)) {
|
||||
executionSequences.push(buildSequence(point.uuid));
|
||||
}
|
||||
});
|
||||
|
||||
return executionSequences;
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import { extractTriggersFromPoint } from "./extractTriggersFromPoint";
|
||||
|
||||
export function determineExecutionOrder(products: productsSchema): PointsScheme[] {
|
||||
// Create maps for all events and points
|
||||
const eventMap = new Map<string, EventsSchema>();
|
||||
const pointMap = new Map<string, PointsScheme>();
|
||||
const allPoints: PointsScheme[] = [];
|
||||
|
||||
// First pass: collect all points
|
||||
products.forEach(product => {
|
||||
product.eventDatas.forEach(event => {
|
||||
eventMap.set(event.modelUuid, event);
|
||||
|
||||
if (event.type === 'transfer') {
|
||||
event.points.forEach(point => {
|
||||
pointMap.set(point.uuid, point);
|
||||
allPoints.push(point);
|
||||
});
|
||||
} else if (event.type === 'vehicle' ||
|
||||
event.type === 'machine' ||
|
||||
event.type === 'storageUnit' ||
|
||||
event.type === 'roboticArm') {
|
||||
pointMap.set(event.point.uuid, event.point);
|
||||
allPoints.push(event.point);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Build dependency graphs
|
||||
const graph = new Map<string, string[]>();
|
||||
const reverseGraph = new Map<string, string[]>();
|
||||
const allTriggeredPoints = new Set<string>();
|
||||
|
||||
allPoints.forEach(point => {
|
||||
const triggers = extractTriggersFromPoint(point);
|
||||
const dependencies: string[] = [];
|
||||
|
||||
triggers.forEach(trigger => {
|
||||
const targetUuid = trigger.triggeredAsset?.triggeredPoint?.pointUuid;
|
||||
if (targetUuid && pointMap.has(targetUuid)) {
|
||||
dependencies.push(targetUuid);
|
||||
allTriggeredPoints.add(targetUuid);
|
||||
|
||||
if (!reverseGraph.has(targetUuid)) {
|
||||
reverseGraph.set(targetUuid, []);
|
||||
}
|
||||
reverseGraph.get(targetUuid)!.push(point.uuid);
|
||||
}
|
||||
});
|
||||
|
||||
graph.set(point.uuid, dependencies);
|
||||
});
|
||||
|
||||
// Identify root points (points that trigger others but aren't triggered themselves)
|
||||
const rootPoints = allPoints
|
||||
.filter(point => !allTriggeredPoints.has(point.uuid))
|
||||
.filter(point => {
|
||||
// Only include roots that actually have triggers pointing FROM them
|
||||
const triggers = extractTriggersFromPoint(point);
|
||||
return triggers.some(t => t.triggeredAsset?.triggeredPoint?.pointUuid);
|
||||
});
|
||||
|
||||
// If no root points found but we have triggered points, find the earliest triggers
|
||||
if (rootPoints.length === 0 && allTriggeredPoints.size > 0) {
|
||||
// This handles cases where we have circular dependencies
|
||||
// but still want to include the triggered points
|
||||
const minTriggerCount = Math.min(
|
||||
...Array.from(allTriggeredPoints)
|
||||
.map(uuid => (graph.get(uuid) || []).length)
|
||||
);
|
||||
const potentialRoots = Array.from(allTriggeredPoints)
|
||||
.filter(uuid => (graph.get(uuid) || []).length === minTriggerCount);
|
||||
|
||||
rootPoints.push(...potentialRoots.map(uuid => pointMap.get(uuid)!));
|
||||
}
|
||||
|
||||
// Topological sort only for triggered points
|
||||
const visited = new Set<string>();
|
||||
const temp = new Set<string>();
|
||||
const order: string[] = [];
|
||||
let hasCycle = false;
|
||||
|
||||
function visit(node: string) {
|
||||
if (temp.has(node)) {
|
||||
hasCycle = true;
|
||||
return;
|
||||
}
|
||||
if (visited.has(node)) return;
|
||||
|
||||
temp.add(node);
|
||||
|
||||
const dependencies = reverseGraph.get(node) || [];
|
||||
for (const dep of dependencies) {
|
||||
visit(dep);
|
||||
}
|
||||
|
||||
temp.delete(node);
|
||||
visited.add(node);
|
||||
order.push(node);
|
||||
}
|
||||
|
||||
// Start processing from root points
|
||||
rootPoints.forEach(root => visit(root.uuid));
|
||||
|
||||
// Convert UUIDs back to points and filter out untriggered points
|
||||
const triggeredPoints = order
|
||||
.map(uuid => pointMap.get(uuid)!)
|
||||
.filter(point => allTriggeredPoints.has(point.uuid) ||
|
||||
rootPoints.some(root => root.uuid === point.uuid));
|
||||
|
||||
return triggeredPoints;
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { extractTriggersFromPoint } from "./extractTriggersFromPoint";
|
||||
|
||||
export async function determineExecutionSequences(products: productsSchema): Promise<PointsScheme[][]> {
|
||||
// Create maps for all points
|
||||
const pointMap = new Map<string, PointsScheme>();
|
||||
const allPoints: PointsScheme[] = [];
|
||||
|
||||
// First pass: collect all points
|
||||
products.forEach(product => {
|
||||
product.eventDatas.forEach(event => {
|
||||
if (event.type === 'transfer') {
|
||||
event.points.forEach(point => {
|
||||
pointMap.set(point.uuid, point);
|
||||
allPoints.push(point);
|
||||
});
|
||||
} else if (event.type === 'vehicle' ||
|
||||
event.type === 'machine' ||
|
||||
event.type === 'storageUnit' ||
|
||||
event.type === 'roboticArm') {
|
||||
pointMap.set(event.point.uuid, event.point);
|
||||
allPoints.push(event.point);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Build complete dependency graph
|
||||
const dependencyGraph = new Map<string, string[]>();
|
||||
const reverseDependencyGraph = new Map<string, string[]>();
|
||||
const triggeredPoints = new Set<string>();
|
||||
|
||||
allPoints.forEach(point => {
|
||||
const triggers = extractTriggersFromPoint(point);
|
||||
const dependencies: string[] = [];
|
||||
|
||||
triggers.forEach(trigger => {
|
||||
const targetUuid = trigger.triggeredAsset?.triggeredPoint?.pointUuid;
|
||||
if (targetUuid && pointMap.has(targetUuid)) {
|
||||
dependencies.push(targetUuid);
|
||||
triggeredPoints.add(targetUuid);
|
||||
|
||||
if (!reverseDependencyGraph.has(targetUuid)) {
|
||||
reverseDependencyGraph.set(targetUuid, []);
|
||||
}
|
||||
reverseDependencyGraph.get(targetUuid)!.push(point.uuid);
|
||||
}
|
||||
});
|
||||
|
||||
dependencyGraph.set(point.uuid, dependencies);
|
||||
});
|
||||
|
||||
// Identify independent root points (points that trigger others but aren't triggered themselves)
|
||||
const rootPoints = allPoints.filter(point => {
|
||||
const hasOutgoingTriggers = extractTriggersFromPoint(point).some(
|
||||
t => t.triggeredAsset?.triggeredPoint?.pointUuid
|
||||
);
|
||||
return hasOutgoingTriggers && !triggeredPoints.has(point.uuid);
|
||||
});
|
||||
|
||||
// For each root point, build its complete trigger chain
|
||||
const executionSequences: PointsScheme[][] = [];
|
||||
|
||||
function buildSequence(startUuid: string): PointsScheme[] {
|
||||
const sequence: PointsScheme[] = [];
|
||||
const visited = new Set<string>();
|
||||
|
||||
function traverse(uuid: string) {
|
||||
if (visited.has(uuid)) return;
|
||||
visited.add(uuid);
|
||||
|
||||
const point = pointMap.get(uuid);
|
||||
if (point) {
|
||||
sequence.push(point);
|
||||
}
|
||||
|
||||
// Follow forward dependencies
|
||||
const nextPoints = dependencyGraph.get(uuid) || [];
|
||||
nextPoints.forEach(nextUuid => traverse(nextUuid));
|
||||
}
|
||||
|
||||
traverse(startUuid);
|
||||
return sequence;
|
||||
}
|
||||
|
||||
// Build sequences for all root points
|
||||
rootPoints.forEach(root => {
|
||||
executionSequences.push(buildSequence(root.uuid));
|
||||
});
|
||||
|
||||
// Handle any triggered points not reachable from roots (isolated chains)
|
||||
const processedPoints = new Set(
|
||||
executionSequences.flat().map(p => p.uuid)
|
||||
);
|
||||
|
||||
allPoints.forEach(point => {
|
||||
if (triggeredPoints.has(point.uuid) && !processedPoints.has(point.uuid)) {
|
||||
executionSequences.push(buildSequence(point.uuid));
|
||||
}
|
||||
});
|
||||
|
||||
return executionSequences;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
|
||||
export function extractTriggersFromPoint(point: PointsScheme): TriggerSchema[] {
|
||||
if ('actions' in point) {
|
||||
return point.actions.flatMap(action => action.triggers);
|
||||
} else if ('action' in point) {
|
||||
return point.action.triggers;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
import { extractTriggersFromPoint } from "./extractTriggersFromPoint";
|
||||
|
||||
// Gets all conveyor sequences split by non-transfer events
|
||||
export function getConveyorSequencesInProduct(
|
||||
product: {
|
||||
productName: string;
|
||||
productUuid: string;
|
||||
eventDatas: EventsSchema[];
|
||||
}
|
||||
): EventsSchema[][][] {
|
||||
const machineSequences = determineExecutionMachineSequences([product]);
|
||||
const allConveyorSequences: EventsSchema[][][] = [];
|
||||
|
||||
for (const machineSequence of machineSequences) {
|
||||
const conveyorSequencesForMachine: EventsSchema[][] = [];
|
||||
let currentSequence: EventsSchema[] = [];
|
||||
|
||||
for (const event of machineSequence) {
|
||||
if (event.type === 'transfer') {
|
||||
currentSequence.push(event);
|
||||
} else {
|
||||
// Split sequence when non-transfer event is encountered
|
||||
if (currentSequence.length > 0) {
|
||||
conveyorSequencesForMachine.push([...currentSequence]);
|
||||
currentSequence = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add the last sequence if it exists
|
||||
if (currentSequence.length > 0) {
|
||||
conveyorSequencesForMachine.push([...currentSequence]);
|
||||
}
|
||||
|
||||
if (conveyorSequencesForMachine.length > 0) {
|
||||
allConveyorSequences.push(conveyorSequencesForMachine);
|
||||
}
|
||||
}
|
||||
|
||||
return allConveyorSequences;
|
||||
}
|
||||
|
||||
// Finds the subsequence containing a specific conveyor
|
||||
export function findConveyorSubsequence(
|
||||
product: {
|
||||
productName: string;
|
||||
productUuid: string;
|
||||
eventDatas: EventsSchema[];
|
||||
},
|
||||
conveyorModelUuid: string
|
||||
): {
|
||||
allSequences: EventsSchema[][][];
|
||||
parentSequence: EventsSchema[][];
|
||||
currentSubSequence: EventsSchema[];
|
||||
} | null {
|
||||
const allSequences = getConveyorSequencesInProduct(product);
|
||||
|
||||
for (const parentSequence of allSequences) {
|
||||
for (const currentSubSequence of parentSequence) {
|
||||
const hasTargetConveyor = currentSubSequence.some(
|
||||
event => event.type === 'transfer' && event.modelUuid === conveyorModelUuid
|
||||
);
|
||||
|
||||
if (hasTargetConveyor) {
|
||||
return {
|
||||
allSequences,
|
||||
parentSequence,
|
||||
currentSubSequence
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Helper function to get machine sequences (simplified from your example)
|
||||
function determineExecutionMachineSequences(products: productsSchema): EventsSchema[][] {
|
||||
const pointToEventMap = new Map<string, EventsSchema>();
|
||||
const allPoints: PointsScheme[] = [];
|
||||
|
||||
// First pass: map points to their corresponding events
|
||||
products.forEach(product => {
|
||||
product.eventDatas.forEach(event => {
|
||||
if (event.type === 'transfer') {
|
||||
event.points.forEach(point => {
|
||||
pointToEventMap.set(point.uuid, event);
|
||||
allPoints.push(point);
|
||||
});
|
||||
} else if (
|
||||
event.type === 'vehicle' ||
|
||||
event.type === 'machine' ||
|
||||
event.type === 'storageUnit' ||
|
||||
event.type === 'roboticArm'
|
||||
) {
|
||||
pointToEventMap.set(event.point.uuid, event);
|
||||
allPoints.push(event.point);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Build dependency graph
|
||||
const dependencyGraph = new Map<string, string[]>();
|
||||
const triggeredPoints = new Set<string>();
|
||||
|
||||
allPoints.forEach(point => {
|
||||
const triggers = extractTriggersFromPoint(point);
|
||||
const dependencies: string[] = [];
|
||||
|
||||
triggers.forEach(trigger => {
|
||||
const targetUuid = trigger.triggeredAsset?.triggeredPoint?.pointUuid;
|
||||
if (targetUuid && pointToEventMap.has(targetUuid)) {
|
||||
dependencies.push(targetUuid);
|
||||
triggeredPoints.add(targetUuid);
|
||||
}
|
||||
});
|
||||
|
||||
dependencyGraph.set(point.uuid, dependencies);
|
||||
});
|
||||
|
||||
// Find root points (points that aren't triggered by others)
|
||||
const rootPoints = allPoints.filter(point =>
|
||||
!triggeredPoints.has(point.uuid) &&
|
||||
dependencyGraph.get(point.uuid)?.length
|
||||
);
|
||||
|
||||
const executionSequences: EventsSchema[][] = [];
|
||||
|
||||
function buildSequence(startUuid: string): EventsSchema[] {
|
||||
const sequence: EventsSchema[] = [];
|
||||
const visited = new Set<string>();
|
||||
|
||||
function traverse(uuid: string) {
|
||||
if (visited.has(uuid)) return;
|
||||
visited.add(uuid);
|
||||
|
||||
const event = pointToEventMap.get(uuid);
|
||||
if (event && !sequence.includes(event)) {
|
||||
sequence.push(event);
|
||||
}
|
||||
|
||||
const nextPoints = dependencyGraph.get(uuid) || [];
|
||||
nextPoints.forEach(nextUuid => traverse(nextUuid));
|
||||
}
|
||||
|
||||
traverse(startUuid);
|
||||
return sequence;
|
||||
}
|
||||
|
||||
// Build sequences from root points
|
||||
rootPoints.forEach(root => {
|
||||
executionSequences.push(buildSequence(root.uuid));
|
||||
});
|
||||
|
||||
return executionSequences;
|
||||
}
|
||||
38
app/src/modules/simulation/simulator/simulator.tsx
Normal file
38
app/src/modules/simulation/simulator/simulator.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useProductStore } from '../../../store/simulation/useProductStore';
|
||||
import { useActionHandler } from '../actions/useActionHandler';
|
||||
import { usePlayButtonStore, useResetButtonStore } from '../../../store/usePlayButtonStore';
|
||||
import { determineExecutionOrder } from './functions/determineExecutionOrder';
|
||||
import { useProductContext } from '../products/productContext';
|
||||
|
||||
function Simulator() {
|
||||
const { selectedProductStore } = useProductContext();
|
||||
const { products, getProductById } = useProductStore();
|
||||
const { handleAction } = useActionHandler();
|
||||
const { selectedProduct } = selectedProductStore();
|
||||
const { isPlaying } = usePlayButtonStore();
|
||||
const { isReset } = useResetButtonStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isPlaying || isReset || !selectedProduct.productUuid) return;
|
||||
|
||||
const product = getProductById(selectedProduct.productUuid);
|
||||
if (!product) return;
|
||||
|
||||
const executionOrder = determineExecutionOrder([product]);
|
||||
executionOrder.forEach(point => {
|
||||
const action = 'actions' in point ? point.actions[0] : point.action;
|
||||
handleAction(action);
|
||||
});
|
||||
}, [products, isPlaying, isReset, selectedProduct]);
|
||||
|
||||
return (
|
||||
|
||||
<>
|
||||
|
||||
</>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
export default Simulator;
|
||||
61
app/src/modules/simulation/spatialUI/arm/PickDropPoints.tsx
Normal file
61
app/src/modules/simulation/spatialUI/arm/PickDropPoints.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import React, { useRef } from "react";
|
||||
import * as THREE from "three";
|
||||
import { ThreeEvent } from "@react-three/fiber";
|
||||
|
||||
interface PickDropProps {
|
||||
position: number[];
|
||||
modelUuid: string;
|
||||
pointUuid: string;
|
||||
actionType: "pick" | "drop";
|
||||
actionUuid: string;
|
||||
gltfScene: THREE.Group;
|
||||
|
||||
handlePointerDown: (e: ThreeEvent<PointerEvent>) => void;
|
||||
isSelected: boolean;
|
||||
}
|
||||
|
||||
const PickDropPoints: React.FC<PickDropProps> = ({
|
||||
position,
|
||||
modelUuid,
|
||||
pointUuid,
|
||||
actionType,
|
||||
actionUuid,
|
||||
gltfScene,
|
||||
handlePointerDown,
|
||||
isSelected,
|
||||
}) => {
|
||||
const groupRef = useRef<THREE.Group>(null);
|
||||
return (
|
||||
<group
|
||||
ref={groupRef}
|
||||
position={
|
||||
Array.isArray(position) && position.length === 3
|
||||
? new THREE.Vector3(...position)
|
||||
: new THREE.Vector3(0, 0, 0)
|
||||
}
|
||||
onPointerDown={(e) => {
|
||||
|
||||
e.stopPropagation(); // Prevent event bubbling
|
||||
if (!isSelected) return;
|
||||
handlePointerDown(e);
|
||||
}}
|
||||
userData={{ modelUuid, pointUuid, actionType, actionUuid }}
|
||||
>
|
||||
<primitive
|
||||
object={(() => {
|
||||
const cloned = gltfScene.clone();
|
||||
cloned.traverse((child: any) => {
|
||||
if (child.isMesh) {
|
||||
child.userData = { modelUuid, pointUuid, actionType, actionUuid };
|
||||
}
|
||||
});
|
||||
return cloned;
|
||||
})()}
|
||||
position={[0, 0, 0]}
|
||||
scale={[0.5, 0.5, 0.5]}
|
||||
/>
|
||||
</group>
|
||||
);
|
||||
};
|
||||
|
||||
export default PickDropPoints;
|
||||
222
app/src/modules/simulation/spatialUI/arm/armBotUI.tsx
Normal file
222
app/src/modules/simulation/spatialUI/arm/armBotUI.tsx
Normal file
@@ -0,0 +1,222 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useSelectedAction, useSelectedEventSphere } from '../../../../store/simulation/useSimulationStore';
|
||||
import { useGLTF } from '@react-three/drei';
|
||||
import { useThree } from '@react-three/fiber';
|
||||
import { useProductStore } from '../../../../store/simulation/useProductStore';
|
||||
import PickDropPoints from './PickDropPoints';
|
||||
import useDraggableGLTF from './useDraggableGLTF';
|
||||
import * as THREE from 'three';
|
||||
|
||||
import armPick from "../../../../assets/gltf-glb/ui/arm_ui_pick.glb";
|
||||
import armDrop from "../../../../assets/gltf-glb/ui/arm_ui_drop.glb";
|
||||
import { upsertProductOrEventApi } from '../../../../services/simulation/products/UpsertProductOrEventApi';
|
||||
import { useSceneContext } from '../../../scene/sceneContext';
|
||||
import { useProductContext } from '../../products/productContext';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
type Positions = {
|
||||
pick: [number, number, number];
|
||||
drop: [number, number, number];
|
||||
default: [number, number, number];
|
||||
};
|
||||
|
||||
const ArmBotUI = () => {
|
||||
const { getEventByModelUuid, updateAction, getActionByUuid } = useProductStore();
|
||||
const { selectedEventSphere } = useSelectedEventSphere();
|
||||
const { selectedProductStore } = useProductContext();
|
||||
const { selectedProduct } = selectedProductStore();
|
||||
const { scene } = useThree();
|
||||
const { selectedAction } = useSelectedAction();
|
||||
const { armBotStore } = useSceneContext();
|
||||
const { armBots } = armBotStore();
|
||||
const { projectId } = useParams();
|
||||
|
||||
const armUiPick = useGLTF(armPick) as any;
|
||||
const armUiDrop = useGLTF(armDrop) as any;
|
||||
|
||||
const [startPosition, setStartPosition] = useState<[number, number, number] | null>([0, 0, 0]);
|
||||
const [endPosition, setEndPosition] = useState<[number, number, number] | null>([0, 0, 0]);
|
||||
const [selectedArmBotData, setSelectedArmBotData] = useState<any>(null);
|
||||
|
||||
const updateBackend = (
|
||||
productName: string,
|
||||
productUuid: string,
|
||||
projectId: string,
|
||||
eventData: EventsSchema
|
||||
) => {
|
||||
upsertProductOrEventApi({
|
||||
productName: productName,
|
||||
productUuid: productUuid,
|
||||
projectId: projectId,
|
||||
eventDatas: eventData
|
||||
})
|
||||
}
|
||||
|
||||
// Fetch and setup selected ArmBot data
|
||||
useEffect(() => {
|
||||
if (selectedEventSphere) {
|
||||
const selectedArmBot = getEventByModelUuid(selectedProduct.productUuid, selectedEventSphere.userData.modelUuid);
|
||||
|
||||
if (selectedArmBot?.type === "roboticArm" && selectedAction.actionId) {
|
||||
setSelectedArmBotData(selectedArmBot);
|
||||
const defaultPositions = getDefaultPositions(selectedArmBot.modelUuid);
|
||||
const matchingAction = getActionByUuid(selectedProduct.productUuid, selectedAction.actionId);
|
||||
if (matchingAction && (matchingAction as RoboticArmPointSchema["actions"][0]).process) {
|
||||
const startPoint = (matchingAction as RoboticArmPointSchema["actions"][0]).process.startPoint;
|
||||
const pickPosition = (!startPoint || (Array.isArray(startPoint) && startPoint.every(v => v === 0)))
|
||||
? defaultPositions.pick
|
||||
: startPoint;
|
||||
|
||||
const endPoint = (matchingAction as RoboticArmPointSchema["actions"][0]).process.endPoint;
|
||||
const dropPosition = (!endPoint || (Array.isArray(endPoint) && endPoint.every(v => v === 0)))
|
||||
? defaultPositions.drop
|
||||
: endPoint;
|
||||
|
||||
setStartPosition(pickPosition);
|
||||
setEndPosition(dropPosition);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [armBots, selectedEventSphere, selectedProduct, getEventByModelUuid, selectedAction]);
|
||||
|
||||
function getDefaultPositions(modelUuid: string): Positions {
|
||||
const modelData = getEventByModelUuid(selectedProduct.productUuid, modelUuid);
|
||||
|
||||
if (modelData?.type === "roboticArm") {
|
||||
const baseX = modelData.point.position?.[0] || 0;
|
||||
const baseY = modelData.point.position?.[1] || 0;;
|
||||
const baseZ = modelData.point.position?.[2] || 0;
|
||||
return {
|
||||
pick: [baseX, baseY, baseZ + 0.5],
|
||||
drop: [baseX, baseY, baseZ - 0.5],
|
||||
default: [baseX, baseY, baseZ],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
pick: [0.5, 1.5, 0],
|
||||
drop: [-0.5, 1.5, 0],
|
||||
default: [0, 1.5, 0],
|
||||
};
|
||||
}
|
||||
|
||||
function getLocalPosition(parentUuid: string, worldPosArray: [number, number, number] | null): [number, number, number] | null {
|
||||
if (worldPosArray) {
|
||||
const worldPos = new THREE.Vector3(...worldPosArray);
|
||||
const parentObject = scene.getObjectByProperty('uuid', parentUuid);
|
||||
|
||||
if (parentObject) {
|
||||
const localPos = worldPos.clone();
|
||||
parentObject.worldToLocal(localPos);
|
||||
return [localPos.x, localPos.y, localPos.z];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const updatePointToState = (obj: THREE.Object3D) => {
|
||||
const { modelUuid, actionType, actionUuid } = obj.userData;
|
||||
const newPosition = new THREE.Vector3();
|
||||
obj.getWorldPosition(newPosition);
|
||||
const worldPositionArray = newPosition.toArray() as [number, number, number];
|
||||
|
||||
if (selectedEventSphere) {
|
||||
const selectedArmBot = getEventByModelUuid(selectedProduct.productUuid, selectedEventSphere.userData.modelUuid);
|
||||
|
||||
const armBot = selectedArmBot?.modelUuid === modelUuid ? selectedArmBot : null;
|
||||
if (!armBot) return;
|
||||
|
||||
if (armBot.type === "roboticArm") {
|
||||
armBot?.point?.actions?.map((action) => {
|
||||
if (action.actionUuid === actionUuid) {
|
||||
const updatedProcess = { ...action.process };
|
||||
|
||||
if (actionType === "pick") {
|
||||
updatedProcess.startPoint = getLocalPosition(modelUuid, worldPositionArray);
|
||||
setStartPosition(updatedProcess.startPoint)
|
||||
|
||||
} else if (actionType === "drop") {
|
||||
updatedProcess.endPoint = getLocalPosition(modelUuid, worldPositionArray);
|
||||
setEndPosition(updatedProcess.endPoint)
|
||||
}
|
||||
|
||||
const event = updateAction(selectedProduct.productUuid,
|
||||
actionUuid,
|
||||
{
|
||||
actionUuid: action.actionUuid,
|
||||
process: updatedProcess,
|
||||
}
|
||||
)
|
||||
|
||||
if (event) {
|
||||
updateBackend(
|
||||
selectedProduct.productName,
|
||||
selectedProduct.productUuid,
|
||||
projectId || '',
|
||||
event
|
||||
);
|
||||
}
|
||||
return {
|
||||
...action,
|
||||
process: updatedProcess,
|
||||
};
|
||||
}
|
||||
return action;
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { handlePointerDown } = useDraggableGLTF(updatePointToState);
|
||||
|
||||
if (!selectedArmBotData || !Array.isArray(selectedArmBotData.point?.actions)) {
|
||||
return null; // avoid rendering if no data yet
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{selectedArmBotData.point.actions.map((action: any) => {
|
||||
if (action.actionUuid === selectedAction.actionId) {
|
||||
return (
|
||||
<React.Fragment key={action.actionUuid}>
|
||||
<group
|
||||
position={new THREE.Vector3(...selectedArmBotData.position)}
|
||||
rotation={new THREE.Euler(...selectedArmBotData.rotation)}
|
||||
>
|
||||
{startPosition && endPosition && (
|
||||
<>
|
||||
<PickDropPoints
|
||||
position={startPosition}
|
||||
modelUuid={selectedArmBotData.modelUuid}
|
||||
pointUuid={selectedArmBotData.point.uuid}
|
||||
actionType="pick"
|
||||
actionUuid={action.actionUuid}
|
||||
gltfScene={armUiPick.scene}
|
||||
handlePointerDown={handlePointerDown}
|
||||
isSelected={true}
|
||||
/>
|
||||
<PickDropPoints
|
||||
position={endPosition}
|
||||
modelUuid={selectedArmBotData.modelUuid}
|
||||
pointUuid={selectedArmBotData.point.uuid}
|
||||
actionType="drop"
|
||||
actionUuid={action.actionUuid}
|
||||
gltfScene={armUiDrop.scene}
|
||||
handlePointerDown={handlePointerDown}
|
||||
isSelected={true}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</group>
|
||||
</React.Fragment>
|
||||
);
|
||||
} else {
|
||||
return null; // important! must return something
|
||||
}
|
||||
})}
|
||||
</>
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
export default ArmBotUI;
|
||||
200
app/src/modules/simulation/spatialUI/arm/useDraggableGLTF.ts
Normal file
200
app/src/modules/simulation/spatialUI/arm/useDraggableGLTF.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
import { useRef, useState } from "react";
|
||||
import * as THREE from "three";
|
||||
import { ThreeEvent, useThree } from "@react-three/fiber";
|
||||
import { useProductStore } from "../../../../store/simulation/useProductStore";
|
||||
import {
|
||||
useSelectedEventData,
|
||||
} from "../../../../store/simulation/useSimulationStore";
|
||||
import { useProductContext } from "../../products/productContext";
|
||||
|
||||
type OnUpdateCallback = (object: THREE.Object3D) => void;
|
||||
|
||||
export default function useDraggableGLTF(onUpdate: OnUpdateCallback) {
|
||||
const { getEventByModelUuid } = useProductStore();
|
||||
const { selectedEventData } = useSelectedEventData();
|
||||
const { selectedProductStore } = useProductContext();
|
||||
const { selectedProduct } = selectedProductStore();
|
||||
const { camera, gl, controls } = useThree();
|
||||
const activeObjRef = useRef<THREE.Object3D | null>(null);
|
||||
const planeRef = useRef<THREE.Plane>(new THREE.Plane(new THREE.Vector3(0, 1, 0), 0));
|
||||
const offsetRef = useRef<THREE.Vector3>(new THREE.Vector3());
|
||||
const initialPositionRef = useRef<THREE.Vector3>(new THREE.Vector3());
|
||||
|
||||
const raycaster = new THREE.Raycaster();
|
||||
const pointer = new THREE.Vector2();
|
||||
const [objectWorldPos, setObjectWorldPos] = useState(new THREE.Vector3());
|
||||
|
||||
const handlePointerDown = (e: ThreeEvent<PointerEvent>) => {
|
||||
e.stopPropagation();
|
||||
|
||||
let obj: THREE.Object3D | null = e.object;
|
||||
|
||||
// Traverse up until we find modelUuid in userData
|
||||
while (obj && !obj.userData?.modelUuid) {
|
||||
obj = obj.parent;
|
||||
}
|
||||
|
||||
if (!obj) return;
|
||||
|
||||
// Disable orbit controls while dragging
|
||||
if (controls) (controls as any).enabled = false;
|
||||
|
||||
activeObjRef.current = obj;
|
||||
initialPositionRef.current.copy(obj.position);
|
||||
|
||||
// Get world position
|
||||
setObjectWorldPos(obj.getWorldPosition(objectWorldPos));
|
||||
|
||||
// Set plane at the object's Y level
|
||||
planeRef.current.set(new THREE.Vector3(0, 1, 0), -objectWorldPos.y);
|
||||
|
||||
// Convert pointer to NDC
|
||||
const rect = gl.domElement.getBoundingClientRect();
|
||||
pointer.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
|
||||
pointer.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
|
||||
|
||||
// Raycast to intersection
|
||||
raycaster.setFromCamera(pointer, camera);
|
||||
const intersection = new THREE.Vector3();
|
||||
raycaster.ray.intersectPlane(planeRef.current, intersection);
|
||||
|
||||
// Calculate offset
|
||||
offsetRef.current.copy(objectWorldPos).sub(intersection);
|
||||
|
||||
// Start listening for drag
|
||||
gl.domElement.addEventListener("pointermove", handlePointerMove);
|
||||
gl.domElement.addEventListener("pointerup", handlePointerUp);
|
||||
};
|
||||
|
||||
const handlePointerMove = (e: PointerEvent) => {
|
||||
if (!activeObjRef.current) return;
|
||||
if (selectedEventData?.data.type === "roboticArm") {
|
||||
const selectedArmBot = getEventByModelUuid(
|
||||
selectedProduct.productUuid,
|
||||
selectedEventData.data.modelUuid
|
||||
);
|
||||
if (!selectedArmBot) return;
|
||||
// Check if Shift key is pressed
|
||||
const isShiftKeyPressed = e.shiftKey;
|
||||
|
||||
// Get the mouse position relative to the canvas
|
||||
const rect = gl.domElement.getBoundingClientRect();
|
||||
pointer.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
|
||||
pointer.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
|
||||
|
||||
// Update raycaster to point to the mouse position
|
||||
raycaster.setFromCamera(pointer, camera);
|
||||
|
||||
// Create a vector to store intersection point
|
||||
const intersection = new THREE.Vector3();
|
||||
const intersects = raycaster.ray.intersectPlane(
|
||||
planeRef.current,
|
||||
intersection
|
||||
);
|
||||
if (!intersects) return;
|
||||
|
||||
// Add offset for dragging
|
||||
intersection.add(offsetRef.current);
|
||||
|
||||
// Get the parent's world matrix if exists
|
||||
const parent = activeObjRef.current.parent;
|
||||
const targetPosition = new THREE.Vector3();
|
||||
|
||||
// OnPointerDown
|
||||
initialPositionRef.current.copy(objectWorldPos);
|
||||
|
||||
// OnPointerMove
|
||||
if (isShiftKeyPressed) {
|
||||
const { x: initialX, y: initialY } = initialPositionRef.current;
|
||||
const { x: objectX, z: objectZ } = objectWorldPos;
|
||||
|
||||
const deltaX = intersection.x - initialX;
|
||||
|
||||
targetPosition.set(objectX, initialY + deltaX, objectZ);
|
||||
} else {
|
||||
// For free movement
|
||||
targetPosition.copy(intersection);
|
||||
}
|
||||
|
||||
// CONSTRAIN MOVEMENT HERE:
|
||||
const centerX = selectedArmBot.position[0];
|
||||
const centerZ = selectedArmBot.position[2];
|
||||
const minDistance = 1.2;
|
||||
const maxDistance = 2;
|
||||
|
||||
const delta = new THREE.Vector3(targetPosition.x - centerX, 0, targetPosition.z - centerZ);
|
||||
|
||||
// Create quaternion from rotation
|
||||
const robotEuler = new THREE.Euler(selectedArmBot.rotation[0], selectedArmBot.rotation[1], selectedArmBot.rotation[2]);
|
||||
const robotQuaternion = new THREE.Quaternion().setFromEuler(robotEuler);
|
||||
|
||||
// Inverse rotate
|
||||
const inverseQuaternion = robotQuaternion.clone().invert();
|
||||
delta.applyQuaternion(inverseQuaternion);
|
||||
|
||||
// Angle in robot local space
|
||||
let relativeAngle = Math.atan2(delta.z, delta.x);
|
||||
let angleDeg = (relativeAngle * 180) / Math.PI;
|
||||
if (angleDeg < 0) {
|
||||
angleDeg += 360;
|
||||
}
|
||||
|
||||
// Clamp angle
|
||||
if (angleDeg < 0 || angleDeg > 270) {
|
||||
const distanceTo90 = Math.abs(angleDeg - 0);
|
||||
const distanceTo270 = Math.abs(angleDeg - 270);
|
||||
if (distanceTo90 < distanceTo270) {
|
||||
angleDeg = 0;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
relativeAngle = (angleDeg * Math.PI) / 180;
|
||||
}
|
||||
|
||||
// Distance clamp
|
||||
const distance = delta.length();
|
||||
const clampedDistance = Math.min(Math.max(distance, minDistance), maxDistance);
|
||||
|
||||
// Calculate local target
|
||||
const finalLocal = new THREE.Vector3(
|
||||
Math.cos(relativeAngle) * clampedDistance,
|
||||
0,
|
||||
Math.sin(relativeAngle) * clampedDistance
|
||||
);
|
||||
|
||||
// Rotate back to world space
|
||||
finalLocal.applyQuaternion(robotQuaternion);
|
||||
|
||||
targetPosition.x = centerX + finalLocal.x;
|
||||
targetPosition.z = centerZ + finalLocal.z;
|
||||
|
||||
|
||||
// Clamp Y axis if needed
|
||||
targetPosition.y = Math.min(Math.max(targetPosition.y, 0.6), 1.9);
|
||||
|
||||
// Convert to local if parent exists
|
||||
if (parent) {
|
||||
parent.worldToLocal(targetPosition);
|
||||
}
|
||||
|
||||
// Update the object position
|
||||
activeObjRef.current.position.copy(targetPosition);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePointerUp = () => {
|
||||
if (controls) (controls as any).enabled = true;
|
||||
|
||||
if (activeObjRef.current) {
|
||||
// Pass the updated position to the onUpdate callback to persist it
|
||||
onUpdate(activeObjRef.current);
|
||||
}
|
||||
|
||||
gl.domElement.removeEventListener("pointermove", handlePointerMove);
|
||||
gl.domElement.removeEventListener("pointerup", handlePointerUp);
|
||||
|
||||
activeObjRef.current = null;
|
||||
};
|
||||
|
||||
return { handlePointerDown };
|
||||
}
|
||||
0
app/src/modules/simulation/spatialUI/temp.md
Normal file
0
app/src/modules/simulation/spatialUI/temp.md
Normal file
131
app/src/modules/simulation/spatialUI/vehicle/useDraggableGLTF.ts
Normal file
131
app/src/modules/simulation/spatialUI/vehicle/useDraggableGLTF.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { useRef } from "react";
|
||||
import * as THREE from "three";
|
||||
import { ThreeEvent, useThree } from "@react-three/fiber";
|
||||
|
||||
type OnUpdateCallback = (object: THREE.Object3D) => void;
|
||||
|
||||
export default function useDraggableGLTF(onUpdate: OnUpdateCallback) {
|
||||
const { camera, gl, controls, scene } = useThree();
|
||||
const activeObjRef = useRef<THREE.Object3D | null>(null);
|
||||
const planeRef = useRef<THREE.Plane>(
|
||||
new THREE.Plane(new THREE.Vector3(0, 1, 0), 0)
|
||||
);
|
||||
const offsetRef = useRef<THREE.Vector3>(new THREE.Vector3());
|
||||
const initialPositionRef = useRef<THREE.Vector3>(new THREE.Vector3());
|
||||
|
||||
const raycaster = new THREE.Raycaster();
|
||||
const pointer = new THREE.Vector2();
|
||||
|
||||
const handlePointerDown = (e: ThreeEvent<PointerEvent>) => {
|
||||
e.stopPropagation();
|
||||
|
||||
let obj: THREE.Object3D | null = e.object;
|
||||
|
||||
// Traverse up until we find modelUuid in userData
|
||||
while (obj && !obj.userData?.modelUuid) {
|
||||
obj = obj.parent;
|
||||
}
|
||||
|
||||
if (!obj) return;
|
||||
|
||||
// Disable orbit controls while dragging
|
||||
if (controls) (controls as any).enabled = false;
|
||||
|
||||
activeObjRef.current = obj;
|
||||
initialPositionRef.current.copy(obj.position);
|
||||
|
||||
// Get world position
|
||||
const objectWorldPos = new THREE.Vector3();
|
||||
obj.getWorldPosition(objectWorldPos);
|
||||
|
||||
// Set plane at the object's Y level
|
||||
planeRef.current.set(new THREE.Vector3(0, 1, 0), -objectWorldPos.y);
|
||||
|
||||
// Convert pointer to NDC
|
||||
const rect = gl.domElement.getBoundingClientRect();
|
||||
pointer.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
|
||||
pointer.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
|
||||
|
||||
// Raycast to intersection
|
||||
raycaster.setFromCamera(pointer, camera);
|
||||
const intersection = new THREE.Vector3();
|
||||
raycaster.ray.intersectPlane(planeRef.current, intersection);
|
||||
|
||||
// Calculate offset
|
||||
offsetRef.current.copy(objectWorldPos).sub(intersection);
|
||||
|
||||
// Start listening for drag
|
||||
gl.domElement.addEventListener("pointermove", handlePointerMove);
|
||||
gl.domElement.addEventListener("pointerup", handlePointerUp);
|
||||
};
|
||||
|
||||
const handlePointerMove = (e: PointerEvent) => {
|
||||
if (!activeObjRef.current) return;
|
||||
|
||||
// Check if Shift key is pressed
|
||||
const isShiftKeyPressed = e.shiftKey;
|
||||
|
||||
// Get the mouse position relative to the canvas
|
||||
const rect = gl.domElement.getBoundingClientRect();
|
||||
pointer.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
|
||||
pointer.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
|
||||
|
||||
// Update raycaster to point to the mouse position
|
||||
raycaster.setFromCamera(pointer, camera);
|
||||
|
||||
// Create a vector to store intersection point
|
||||
const intersection = new THREE.Vector3();
|
||||
const intersects = raycaster.ray.intersectPlane(planeRef.current, intersection);
|
||||
if (!intersects) return;
|
||||
|
||||
// Add offset for dragging
|
||||
intersection.add(offsetRef.current);
|
||||
console.log('intersection: ', intersection);
|
||||
|
||||
// Get the parent's world matrix if exists
|
||||
const parent = activeObjRef.current.parent;
|
||||
const targetPosition = new THREE.Vector3();
|
||||
|
||||
if (isShiftKeyPressed) {
|
||||
console.log('isShiftKeyPressed: ', isShiftKeyPressed);
|
||||
// For Y-axis only movement, maintain original X and Z
|
||||
console.log('initialPositionRef: ', initialPositionRef);
|
||||
console.log('intersection.y: ', intersection);
|
||||
targetPosition.set(
|
||||
initialPositionRef.current.x,
|
||||
intersection.y,
|
||||
initialPositionRef.current.z
|
||||
);
|
||||
} else {
|
||||
// For free movement
|
||||
targetPosition.copy(intersection);
|
||||
}
|
||||
|
||||
// Convert world position to local if object is nested inside a parent
|
||||
if (parent) {
|
||||
parent.worldToLocal(targetPosition);
|
||||
}
|
||||
|
||||
// Update object position
|
||||
activeObjRef.current.position.copy(targetPosition);
|
||||
};
|
||||
|
||||
const handlePointerUp = () => {
|
||||
if (controls) (controls as any).enabled = true;
|
||||
|
||||
if (activeObjRef.current) {
|
||||
// Pass the updated position to the onUpdate callback to persist it
|
||||
onUpdate(activeObjRef.current);
|
||||
}
|
||||
|
||||
gl.domElement.removeEventListener("pointermove", handlePointerMove);
|
||||
gl.domElement.removeEventListener("pointerup", handlePointerUp);
|
||||
|
||||
activeObjRef.current = null;
|
||||
};
|
||||
|
||||
return { handlePointerDown };
|
||||
}
|
||||
|
||||
|
||||
|
||||
380
app/src/modules/simulation/spatialUI/vehicle/vehicleUI.tsx
Normal file
380
app/src/modules/simulation/spatialUI/vehicle/vehicleUI.tsx
Normal file
@@ -0,0 +1,380 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import * as Types from "../../../../types/world/worldTypes";
|
||||
import { useGLTF } from "@react-three/drei";
|
||||
import { useFrame, useThree } from "@react-three/fiber";
|
||||
import { useSelectedEventSphere, useIsDragging, useIsRotating, } from "../../../../store/simulation/useSimulationStore";
|
||||
import { useProductStore } from "../../../../store/simulation/useProductStore";
|
||||
import { upsertProductOrEventApi } from "../../../../services/simulation/products/UpsertProductOrEventApi";
|
||||
import { DoubleSide, Group, Plane, Vector3 } from "three";
|
||||
|
||||
import startPoint from "../../../../assets/gltf-glb/ui/arrow_green.glb";
|
||||
import startEnd from "../../../../assets/gltf-glb/ui/arrow_red.glb";
|
||||
import { useSceneContext } from "../../../scene/sceneContext";
|
||||
import { useProductContext } from "../../products/productContext";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
const VehicleUI = () => {
|
||||
const { scene: startScene } = useGLTF(startPoint) as any;
|
||||
const { scene: endScene } = useGLTF(startEnd) as any;
|
||||
const startMarker = useRef<Group>(null);
|
||||
const endMarker = useRef<Group>(null);
|
||||
const prevMousePos = useRef({ x: 0, y: 0 });
|
||||
const { selectedEventSphere } = useSelectedEventSphere();
|
||||
const { selectedProductStore } = useProductContext();
|
||||
const { vehicleStore } = useSceneContext();
|
||||
const { selectedProduct } = selectedProductStore();
|
||||
const { vehicles, getVehicleById } = vehicleStore();
|
||||
const { updateEvent } = useProductStore();
|
||||
const [startPosition, setStartPosition] = useState<[number, number, number]>([0, 1, 0,]);
|
||||
|
||||
const [endPosition, setEndPosition] = useState<[number, number, number]>([0, 1, 0,]);
|
||||
const [startRotation, setStartRotation] = useState<[number, number, number]>([0, 0, 0,]);
|
||||
|
||||
const [endRotation, setEndRotation] = useState<[number, number, number]>([0, 0, 0,]);
|
||||
const [steeringRotation, setSteeringRotation] = useState<[number, number, number]>([0, 0, 0]);
|
||||
|
||||
const { isDragging, setIsDragging } = useIsDragging();
|
||||
const { isRotating, setIsRotating } = useIsRotating();
|
||||
const { raycaster } = useThree();
|
||||
const [point, setPoint] = useState<[number, number, number]>([0, 0, 0]);
|
||||
const plane = useRef(new Plane(new Vector3(0, 1, 0), 0));
|
||||
const [tubeRotation, setTubeRotation] = useState<boolean>(false);
|
||||
const tubeRef = useRef<Group>(null);
|
||||
const outerGroup = useRef<Group>(null);
|
||||
const state: Types.ThreeState = useThree();
|
||||
const controls: any = state.controls;
|
||||
const [selectedVehicleData, setSelectedVechicleData] = useState<{ position: [number, number, number]; rotation: [number, number, number]; }>({ position: [0, 0, 0], rotation: [0, 0, 0] });
|
||||
const CIRCLE_RADIUS = 0.8;
|
||||
const { projectId } = useParams();
|
||||
|
||||
const updateBackend = (
|
||||
productName: string,
|
||||
productUuid: string,
|
||||
projectId: string,
|
||||
eventData: EventsSchema
|
||||
) => {
|
||||
upsertProductOrEventApi({
|
||||
productName: productName,
|
||||
productUuid: productUuid,
|
||||
projectId: projectId,
|
||||
eventDatas: eventData,
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedEventSphere) return;
|
||||
const selectedVehicle = getVehicleById(
|
||||
selectedEventSphere.userData.modelUuid
|
||||
);
|
||||
|
||||
if (selectedVehicle) {
|
||||
setSelectedVechicleData({
|
||||
position: selectedVehicle.position,
|
||||
rotation: selectedVehicle.rotation,
|
||||
});
|
||||
setPoint(selectedVehicle.point.position);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (selectedVehicle?.point?.action) {
|
||||
const { pickUpPoint, unLoadPoint, steeringAngle } = selectedVehicle.point.action;
|
||||
|
||||
if (pickUpPoint && outerGroup.current) {
|
||||
const worldPos = new Vector3(
|
||||
pickUpPoint.position.x,
|
||||
pickUpPoint.position.y,
|
||||
pickUpPoint.position.z
|
||||
);
|
||||
const localPosition = outerGroup.current.worldToLocal(worldPos.clone());
|
||||
|
||||
setStartPosition([
|
||||
localPosition.x,
|
||||
selectedVehicle.point.position[1],
|
||||
localPosition.z,
|
||||
]);
|
||||
setStartRotation([
|
||||
pickUpPoint.rotation.x,
|
||||
pickUpPoint.rotation.y,
|
||||
pickUpPoint.rotation.z,
|
||||
]);
|
||||
} else {
|
||||
setStartPosition([0, selectedVehicle.point.position[1] + 0.1, 1.5]);
|
||||
setStartRotation([0, 0, 0]);
|
||||
}
|
||||
// end point
|
||||
if (unLoadPoint && outerGroup.current) {
|
||||
const worldPos = new Vector3(
|
||||
unLoadPoint.position.x,
|
||||
unLoadPoint.position.y,
|
||||
unLoadPoint.position.z
|
||||
);
|
||||
const localPosition = outerGroup.current.worldToLocal(worldPos);
|
||||
|
||||
setEndPosition([
|
||||
localPosition.x,
|
||||
selectedVehicle.point.position[1],
|
||||
localPosition.z,
|
||||
]);
|
||||
setEndRotation([
|
||||
unLoadPoint.rotation.x,
|
||||
unLoadPoint.rotation.y,
|
||||
unLoadPoint.rotation.z,
|
||||
]);
|
||||
} else {
|
||||
setEndPosition([0, selectedVehicle.point.position[1] + 0.1, -1.5]);
|
||||
setEndRotation([0, 0, 0]);
|
||||
}
|
||||
setSteeringRotation([0, steeringAngle, 0]);
|
||||
}
|
||||
}, 10);
|
||||
}, [selectedEventSphere, outerGroup.current, vehicles]);
|
||||
|
||||
const handlePointerDown = (
|
||||
e: any,
|
||||
state: "start" | "end",
|
||||
rotation: "start" | "end"
|
||||
) => {
|
||||
if (e.object.name === "handle") {
|
||||
const normalizedX = (e.clientX / window.innerWidth) * 2 - 1;
|
||||
const normalizedY = -(e.clientY / window.innerHeight) * 2 + 1;
|
||||
prevMousePos.current = { x: normalizedX, y: normalizedY };
|
||||
setIsRotating(rotation);
|
||||
if (controls) controls.enabled = false;
|
||||
setIsDragging(null);
|
||||
} else {
|
||||
setIsDragging(state);
|
||||
setIsRotating(null);
|
||||
if (controls) controls.enabled = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handlePointerUp = () => {
|
||||
controls.enabled = true;
|
||||
setIsDragging(null);
|
||||
setIsRotating(null);
|
||||
|
||||
if (selectedEventSphere?.userData.modelUuid) {
|
||||
const updatedVehicle = getVehicleById(selectedEventSphere.userData.modelUuid);
|
||||
|
||||
let globalStartPosition = null;
|
||||
let globalEndPosition = null;
|
||||
|
||||
if (outerGroup.current && startMarker.current && endMarker.current) {
|
||||
const worldPosStart = new Vector3(...startPosition);
|
||||
globalStartPosition = outerGroup.current.localToWorld(
|
||||
worldPosStart.clone()
|
||||
);
|
||||
const worldPosEnd = new Vector3(...endPosition);
|
||||
globalEndPosition = outerGroup.current.localToWorld(
|
||||
worldPosEnd.clone()
|
||||
);
|
||||
}
|
||||
if (updatedVehicle && globalEndPosition && globalStartPosition) {
|
||||
const event = updateEvent(
|
||||
selectedProduct.productUuid,
|
||||
selectedEventSphere.userData.modelUuid,
|
||||
{
|
||||
point: {
|
||||
...updatedVehicle.point,
|
||||
action: {
|
||||
...updatedVehicle.point?.action,
|
||||
pickUpPoint: {
|
||||
position: {
|
||||
x: globalStartPosition.x,
|
||||
y: 0,
|
||||
z: globalStartPosition.z,
|
||||
},
|
||||
rotation: { x: 0, y: startRotation[1], z: 0 },
|
||||
},
|
||||
unLoadPoint: {
|
||||
position: {
|
||||
x: globalEndPosition.x,
|
||||
y: 0,
|
||||
z: globalEndPosition.z,
|
||||
},
|
||||
rotation: { x: 0, y: endRotation[1], z: 0 },
|
||||
},
|
||||
steeringAngle: steeringRotation[1],
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (event) {
|
||||
updateBackend(
|
||||
selectedProduct.productName,
|
||||
selectedProduct.productUuid,
|
||||
projectId || '',
|
||||
event
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useFrame(() => {
|
||||
if (!isDragging || !plane.current || !raycaster || !outerGroup.current) return;
|
||||
const intersectPoint = new Vector3();
|
||||
const intersects = raycaster.ray.intersectPlane(
|
||||
plane.current,
|
||||
intersectPoint
|
||||
);
|
||||
if (!intersects) return;
|
||||
const localPoint = outerGroup?.current.worldToLocal(intersectPoint.clone());
|
||||
if (isDragging === "start") {
|
||||
setStartPosition([localPoint.x, point[1], localPoint.z]);
|
||||
} else if (isDragging === "end") {
|
||||
setEndPosition([localPoint.x, point[1], localPoint.z]);
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const handleGlobalPointerUp = () => {
|
||||
setIsDragging(null);
|
||||
setIsRotating(null);
|
||||
setTubeRotation(false);
|
||||
if (controls) controls.enabled = true;
|
||||
handlePointerUp();
|
||||
};
|
||||
|
||||
if (isDragging || isRotating || tubeRotation) {
|
||||
window.addEventListener("pointerup", handleGlobalPointerUp);
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("pointerup", handleGlobalPointerUp);
|
||||
};
|
||||
}, [isDragging, isRotating, startPosition, startRotation, endPosition, endRotation, tubeRotation, steeringRotation, outerGroup.current, tubeRef.current,]);
|
||||
|
||||
const prevSteeringY = useRef(0);
|
||||
|
||||
useFrame((state) => {
|
||||
if (tubeRotation) {
|
||||
const currentPointerX = state.pointer.x;
|
||||
const deltaX = currentPointerX - prevMousePos.current.x;
|
||||
prevMousePos.current.x = currentPointerX;
|
||||
|
||||
const marker = tubeRef.current;
|
||||
if (marker) {
|
||||
const rotationSpeed = 2;
|
||||
marker.rotation.y += deltaX * rotationSpeed;
|
||||
setSteeringRotation([
|
||||
marker.rotation.x,
|
||||
marker.rotation.y,
|
||||
marker.rotation.z,
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
prevSteeringY.current = 0;
|
||||
}
|
||||
});
|
||||
|
||||
useFrame((state) => {
|
||||
if (!isRotating) return;
|
||||
const currentPointerX = state.pointer.x;
|
||||
const deltaX = currentPointerX - prevMousePos.current.x;
|
||||
prevMousePos.current.x = currentPointerX;
|
||||
const marker =
|
||||
isRotating === "start" ? startMarker.current : endMarker.current;
|
||||
if (marker) {
|
||||
const rotationSpeed = 10;
|
||||
marker.rotation.y += deltaX * rotationSpeed;
|
||||
if (isRotating === "start") {
|
||||
setStartRotation([
|
||||
marker.rotation.x,
|
||||
marker.rotation.y,
|
||||
marker.rotation.z,
|
||||
]);
|
||||
} else {
|
||||
setEndRotation([
|
||||
marker.rotation.x,
|
||||
marker.rotation.y,
|
||||
marker.rotation.z,
|
||||
]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return selectedVehicleData ? (
|
||||
<group
|
||||
position={selectedVehicleData.position}
|
||||
rotation={selectedVehicleData.rotation}
|
||||
ref={outerGroup}
|
||||
>
|
||||
<group
|
||||
position={[0, 0, 0]}
|
||||
ref={tubeRef}
|
||||
rotation={steeringRotation}
|
||||
onPointerDown={(e) => {
|
||||
e.stopPropagation();
|
||||
setTubeRotation(true);
|
||||
prevMousePos.current.x = e.pointer.x;
|
||||
controls.enabled = false;
|
||||
}}
|
||||
onPointerMissed={() => {
|
||||
controls.enabled = true;
|
||||
setTubeRotation(false);
|
||||
}}
|
||||
onPointerUp={() => {
|
||||
controls.enabled = true;
|
||||
setTubeRotation(false);
|
||||
}}
|
||||
>
|
||||
(
|
||||
<mesh
|
||||
position={[0, point[1], 0]}
|
||||
rotation={[-Math.PI / 2, 0, 0]}
|
||||
name="steering"
|
||||
>
|
||||
<ringGeometry args={[CIRCLE_RADIUS, CIRCLE_RADIUS + 0.2, 36]} />
|
||||
<meshBasicMaterial color="yellow" side={DoubleSide} />
|
||||
</mesh>
|
||||
<mesh
|
||||
position={[0, point[1], CIRCLE_RADIUS + 0.24]}
|
||||
rotation={[Math.PI / 2, 0, 0]}
|
||||
>
|
||||
<coneGeometry args={[0.1, 0.3, 12]} />
|
||||
<meshBasicMaterial color="yellow" side={DoubleSide} />
|
||||
</mesh>
|
||||
)
|
||||
</group>
|
||||
|
||||
{/* Start Marker */}
|
||||
<primitive
|
||||
name="startMarker"
|
||||
object={startScene}
|
||||
ref={startMarker}
|
||||
position={startPosition}
|
||||
rotation={startRotation}
|
||||
onPointerDown={(e: any) => {
|
||||
e.stopPropagation();
|
||||
handlePointerDown(e, "start", "start");
|
||||
}}
|
||||
onPointerMissed={() => {
|
||||
controls.enabled = true;
|
||||
setIsDragging(null);
|
||||
setIsRotating(null);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* End Marker */}
|
||||
<primitive
|
||||
name="endMarker"
|
||||
object={endScene}
|
||||
ref={endMarker}
|
||||
position={endPosition}
|
||||
rotation={endRotation}
|
||||
onPointerDown={(e: any) => {
|
||||
e.stopPropagation();
|
||||
handlePointerDown(e, "end", "end");
|
||||
}}
|
||||
onPointerMissed={() => {
|
||||
controls.enabled = true;
|
||||
setIsDragging(null);
|
||||
setIsRotating(null);
|
||||
}}
|
||||
/>
|
||||
</group>
|
||||
) : null;
|
||||
};
|
||||
export default VehicleUI;
|
||||
@@ -0,0 +1,76 @@
|
||||
import React, { useEffect, useRef, useState, useMemo } from "react";
|
||||
import { MaterialModel } from "../../../materials/instances/material/materialModel";
|
||||
import { Object3D, Box3, Vector3 } from "three";
|
||||
import { useThree } from "@react-three/fiber";
|
||||
|
||||
const MaterialAnimator = ({
|
||||
storage,
|
||||
}: Readonly<{ storage: StorageUnitStatus }>) => {
|
||||
const meshRef = useRef<any>(null!);
|
||||
const [hasLoad, setHasLoad] = useState(false);
|
||||
const { scene } = useThree();
|
||||
const padding = 0.1;
|
||||
|
||||
useEffect(() => {
|
||||
setHasLoad(storage.currentLoad > 0);
|
||||
}, [storage.currentLoad]);
|
||||
|
||||
const storageModel = useMemo(() => {
|
||||
return scene.getObjectByProperty("uuid", storage.modelUuid) as Object3D;
|
||||
}, [scene, storage.modelUuid]);
|
||||
|
||||
const materialPositions = useMemo(() => {
|
||||
if (!storageModel || storage.currentMaterials.length === 0) return [];
|
||||
|
||||
const box = new Box3().setFromObject(storageModel);
|
||||
const size = new Vector3();
|
||||
box.getSize(size);
|
||||
|
||||
const matCount = storage.currentMaterials.length;
|
||||
|
||||
// Assumed size each material needs in world units
|
||||
const materialWidth = 0.45;
|
||||
const materialDepth = 0.45;
|
||||
const materialHeight = 0.3;
|
||||
|
||||
const cols = Math.floor(size.x / materialWidth);
|
||||
const rows = Math.floor(size.z / materialDepth);
|
||||
const itemsPerLayer = cols * rows;
|
||||
|
||||
const origin = new Vector3(
|
||||
box.min.x + materialWidth / 2,
|
||||
box.max.y + padding, // slightly above the surface
|
||||
box.min.z + materialDepth / 2
|
||||
);
|
||||
|
||||
return Array.from({ length: matCount }, (_, i) => {
|
||||
const layer = Math.floor(i / itemsPerLayer);
|
||||
const layerIndex = i % itemsPerLayer;
|
||||
const row = Math.floor(layerIndex / cols);
|
||||
const col = layerIndex % cols;
|
||||
|
||||
return new Vector3(
|
||||
origin.x + col * materialWidth,
|
||||
origin.y + layer * (materialHeight + padding),
|
||||
origin.z + row * materialDepth
|
||||
);
|
||||
});
|
||||
}, [storageModel, storage.currentMaterials]);
|
||||
|
||||
return (
|
||||
<group {...{ position: [0, -padding, 0] }}>
|
||||
{hasLoad &&
|
||||
storage.currentMaterials.map((mat, index) => (
|
||||
<MaterialModel
|
||||
key={`${index}-${mat.materialId}`}
|
||||
materialId={mat.materialId}
|
||||
matRef={meshRef}
|
||||
materialType={mat.materialType ?? "Default material"}
|
||||
position={materialPositions[index]}
|
||||
/>
|
||||
))}
|
||||
</group>
|
||||
);
|
||||
};
|
||||
|
||||
export default MaterialAnimator;
|
||||
@@ -0,0 +1,15 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import MaterialAnimator from '../animator/MaterialAnimator'
|
||||
|
||||
function StorageUnitInstance({ storageUnit }: Readonly<{ storageUnit: StorageUnitStatus }>) {
|
||||
|
||||
useEffect(()=>{
|
||||
// console.log('storageUnit: ', storageUnit);
|
||||
},[storageUnit])
|
||||
|
||||
return (
|
||||
<MaterialAnimator storage={storageUnit}/>
|
||||
)
|
||||
}
|
||||
|
||||
export default StorageUnitInstance
|
||||
@@ -0,0 +1,24 @@
|
||||
import React from "react";
|
||||
import StorageUnitInstance from "./storageUnitInstance/storageUnitInstance";
|
||||
import StorageContentUi from "../../ui3d/StorageContentUi";
|
||||
import { useSceneContext } from "../../../scene/sceneContext";
|
||||
import { useViewSceneStore } from "../../../../store/builder/store";
|
||||
|
||||
function StorageUnitInstances() {
|
||||
const { storageUnitStore } = useSceneContext();
|
||||
const { storageUnits } = storageUnitStore();
|
||||
const { viewSceneLabels } = useViewSceneStore();
|
||||
|
||||
return (
|
||||
<>
|
||||
{storageUnits.map((storageUnit: StorageUnitStatus) => (
|
||||
<React.Fragment key={storageUnit.modelUuid}>
|
||||
<StorageUnitInstance storageUnit={storageUnit} />
|
||||
{viewSceneLabels && <StorageContentUi storageUnit={storageUnit} />}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default StorageUnitInstances;
|
||||
14
app/src/modules/simulation/storageUnit/storageUnit.tsx
Normal file
14
app/src/modules/simulation/storageUnit/storageUnit.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import React from 'react'
|
||||
import StorageUnitInstances from './instances/storageUnitInstances'
|
||||
|
||||
function StorageUnit() {
|
||||
return (
|
||||
<>
|
||||
|
||||
<StorageUnitInstances />
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default StorageUnit
|
||||
14
app/src/modules/simulation/triggers/trigger.tsx
Normal file
14
app/src/modules/simulation/triggers/trigger.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import TriggerConnector from '../events/triggerConnections/triggerConnector'
|
||||
|
||||
function Trigger() {
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
<TriggerConnector />
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Trigger
|
||||
@@ -0,0 +1,862 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useActionHandler } from '../../actions/useActionHandler';
|
||||
import { useProductStore } from '../../../../store/simulation/useProductStore';
|
||||
import { useArmBotEventManager } from '../../roboticArm/eventManager/useArmBotEventManager';
|
||||
import { useConveyorEventManager } from '../../conveyor/eventManager/useConveyorEventManager';
|
||||
import { useVehicleEventManager } from '../../vehicle/eventManager/useVehicleEventManager';
|
||||
import { useMachineEventManager } from '../../machine/eventManager/useMachineEventManager';
|
||||
import { useSceneContext } from '../../../scene/sceneContext';
|
||||
import { useProductContext } from '../../products/productContext';
|
||||
|
||||
export function useTriggerHandler() {
|
||||
const { materialStore, armBotStore, machineStore, conveyorStore, vehicleStore, storageUnitStore } = useSceneContext();
|
||||
const { selectedProductStore } = useProductContext();
|
||||
const { handleAction } = useActionHandler();
|
||||
const { selectedProduct } = selectedProductStore();
|
||||
const { getEventByTriggerUuid, getEventByModelUuid, getActionByUuid, getModelUuidByActionUuid } = useProductStore();
|
||||
const { getArmBotById } = armBotStore();
|
||||
const { getConveyorById } = conveyorStore();
|
||||
const { addArmBotToMonitor } = useArmBotEventManager();
|
||||
const { addConveyorToMonitor } = useConveyorEventManager();
|
||||
const { addVehicleToMonitor } = useVehicleEventManager();
|
||||
const { addMachineToMonitor } = useMachineEventManager();
|
||||
const { getVehicleById } = vehicleStore();
|
||||
const { getMachineById } = machineStore();
|
||||
const { getStorageUnitById } = storageUnitStore();
|
||||
const { getMaterialById, setCurrentLocation, setNextLocation, setPreviousLocation, setIsPaused, setIsVisible, setEndTime } = materialStore();
|
||||
|
||||
const handleTrigger = (trigger: TriggerSchema, action: Action, materialId?: string) => {
|
||||
|
||||
const fromEvent = getEventByTriggerUuid(selectedProduct.productUuid, trigger.triggerUuid);
|
||||
|
||||
const toEvent = getEventByModelUuid(selectedProduct.productUuid, trigger.triggeredAsset?.triggeredModel.modelUuid || '');
|
||||
|
||||
if (fromEvent?.type === 'transfer') {
|
||||
if (toEvent?.type === 'transfer') {
|
||||
// Transfer to Transfer
|
||||
if (materialId && trigger.triggeredAsset && trigger.triggeredAsset.triggeredPoint && trigger.triggeredAsset.triggeredAction) {
|
||||
const material = getMaterialById(materialId);
|
||||
if (material) {
|
||||
if (material.next) {
|
||||
|
||||
setPreviousLocation(material.materialId, {
|
||||
modelUuid: material.current.modelUuid,
|
||||
pointUuid: material.current.pointUuid,
|
||||
actionUuid: material.current.actionUuid,
|
||||
})
|
||||
|
||||
setCurrentLocation(material.materialId, {
|
||||
modelUuid: material.next.modelUuid,
|
||||
pointUuid: material.next.pointUuid,
|
||||
actionUuid: trigger.triggeredAsset?.triggeredAction?.actionUuid,
|
||||
});
|
||||
|
||||
setNextLocation(material.materialId, {
|
||||
modelUuid: trigger.triggeredAsset.triggeredModel.modelUuid,
|
||||
pointUuid: trigger.triggeredAsset.triggeredPoint.pointUuid,
|
||||
});
|
||||
}
|
||||
handleAction(action, materialId);
|
||||
}
|
||||
}
|
||||
} else if (toEvent?.type === 'vehicle') {
|
||||
// Transfer to Vehicle
|
||||
if (materialId && trigger.triggeredAsset && trigger.triggeredAsset.triggeredPoint && trigger.triggeredAsset.triggeredAction) {
|
||||
const material = getMaterialById(materialId);
|
||||
if (material) {
|
||||
|
||||
// Handle current action of the material
|
||||
handleAction(action, materialId);
|
||||
|
||||
if (material.next) {
|
||||
const action = getActionByUuid(selectedProduct.productUuid, trigger.triggeredAsset.triggeredAction.actionUuid);
|
||||
const vehicle = getVehicleById(trigger.triggeredAsset?.triggeredModel.modelUuid);
|
||||
|
||||
setPreviousLocation(material.materialId, {
|
||||
modelUuid: material.current.modelUuid,
|
||||
pointUuid: material.current.pointUuid,
|
||||
actionUuid: material.current.actionUuid,
|
||||
})
|
||||
|
||||
setCurrentLocation(material.materialId, {
|
||||
modelUuid: material.next.modelUuid,
|
||||
pointUuid: material.next.pointUuid,
|
||||
actionUuid: trigger.triggeredAsset?.triggeredAction?.actionUuid,
|
||||
});
|
||||
|
||||
setNextLocation(material.materialId, null);
|
||||
|
||||
if (action) {
|
||||
|
||||
if (vehicle) {
|
||||
|
||||
if (vehicle.isActive === false && vehicle.state === 'idle' && vehicle.isPicking && vehicle.currentLoad < vehicle.point.action.loadCapacity) {
|
||||
|
||||
setIsVisible(materialId, false);
|
||||
|
||||
// Handle current action from vehicle
|
||||
handleAction(action, materialId);
|
||||
|
||||
} else {
|
||||
|
||||
// Handle current action using Event Manager
|
||||
addVehicleToMonitor(vehicle.modelUuid, () => {
|
||||
handleAction(action, materialId);
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else if (toEvent?.type === 'machine') {
|
||||
// Transfer to Machine
|
||||
|
||||
} else if (toEvent?.type === 'roboticArm') {
|
||||
// Transfer to Robotic Arm
|
||||
if (materialId && trigger.triggeredAsset && trigger.triggeredAsset.triggeredPoint && trigger.triggeredAsset.triggeredAction) {
|
||||
const material = getMaterialById(materialId);
|
||||
if (material) {
|
||||
|
||||
// Handle current action of the material
|
||||
handleAction(action, materialId);
|
||||
|
||||
if (material.next) {
|
||||
const action = getActionByUuid(selectedProduct.productUuid, trigger.triggeredAsset.triggeredAction.actionUuid);
|
||||
const armBot = getArmBotById(trigger.triggeredAsset?.triggeredModel.modelUuid);
|
||||
|
||||
setPreviousLocation(material.materialId, {
|
||||
modelUuid: material.current.modelUuid,
|
||||
pointUuid: material.current.pointUuid,
|
||||
actionUuid: material.current.actionUuid,
|
||||
})
|
||||
|
||||
setCurrentLocation(material.materialId, {
|
||||
modelUuid: material.next.modelUuid,
|
||||
pointUuid: material.next.pointUuid,
|
||||
actionUuid: trigger.triggeredAsset?.triggeredAction?.actionUuid,
|
||||
});
|
||||
|
||||
setNextLocation(material.materialId, null);
|
||||
|
||||
if (action) {
|
||||
|
||||
if (armBot) {
|
||||
if (action && action.triggers.length > 0 &&
|
||||
action.triggers[0]?.triggeredAsset?.triggeredModel.modelUuid &&
|
||||
action.triggers[0]?.triggeredAsset?.triggeredAction?.actionUuid &&
|
||||
action.triggers[0]?.triggeredAsset?.triggeredPoint?.pointUuid) {
|
||||
const model = getEventByModelUuid(selectedProduct.productUuid, action.triggers[0]?.triggeredAsset?.triggeredModel.modelUuid);
|
||||
|
||||
if (model?.type === 'vehicle') {
|
||||
const vehicle = getVehicleById(action.triggers[0]?.triggeredAsset?.triggeredModel.modelUuid);
|
||||
if (vehicle) {
|
||||
if (vehicle.isActive === false && vehicle.state === 'idle' && vehicle.isPicking && vehicle.currentLoad < vehicle.point.action.loadCapacity) {
|
||||
// Handle current action from vehicle
|
||||
setIsPaused(materialId, true);
|
||||
handleAction(action, materialId);
|
||||
|
||||
} else {
|
||||
|
||||
// Handle current action using Event Manager
|
||||
setIsPaused(materialId, true);
|
||||
|
||||
addVehicleToMonitor(vehicle.modelUuid,
|
||||
() => {
|
||||
handleAction(action, materialId);
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (model?.type === 'machine') {
|
||||
const armBot = getArmBotById(trigger.triggeredAsset?.triggeredModel.modelUuid);
|
||||
if (armBot) {
|
||||
if (armBot.isActive === false && armBot.state === 'idle') {
|
||||
const machine = getMachineById(action.triggers[0]?.triggeredAsset?.triggeredModel.modelUuid);
|
||||
if (machine) {
|
||||
if (machine.isActive === false && machine.state === 'idle' && !machine.currentAction) {
|
||||
setIsPaused(materialId, true);
|
||||
handleAction(action, materialId);
|
||||
} else {
|
||||
|
||||
// Handle current action using Event Manager
|
||||
setIsPaused(materialId, true);
|
||||
|
||||
addMachineToMonitor(machine.modelUuid,
|
||||
() => {
|
||||
handleAction(action, materialId);
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setIsPaused(materialId, true);
|
||||
addArmBotToMonitor(armBot.modelUuid,
|
||||
() => {
|
||||
const machine = getMachineById(action.triggers[0]?.triggeredAsset?.triggeredModel.modelUuid || '');
|
||||
if (machine) {
|
||||
if (machine.isActive === false && machine.state === 'idle' && !machine.currentAction) {
|
||||
setIsPaused(materialId, true);
|
||||
handleAction(action, materialId);
|
||||
} else {
|
||||
|
||||
// Handle current action using Event Manager
|
||||
setIsPaused(materialId, true);
|
||||
|
||||
addMachineToMonitor(machine.modelUuid,
|
||||
() => {
|
||||
handleAction(action, materialId);
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (armBot.isActive === false && armBot.state === 'idle') {
|
||||
|
||||
// Handle current action from arm bot
|
||||
setIsPaused(materialId, true);
|
||||
handleAction(action, materialId);
|
||||
|
||||
} else {
|
||||
|
||||
// Handle current action using Event Manager
|
||||
setIsPaused(materialId, true);
|
||||
addArmBotToMonitor(armBot.modelUuid,
|
||||
() => handleAction(action, materialId)
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (armBot.isActive === false && armBot.state === 'idle') {
|
||||
|
||||
// Handle current action from arm bot
|
||||
setIsPaused(materialId, true);
|
||||
handleAction(action, materialId);
|
||||
|
||||
} else {
|
||||
|
||||
// Handle current action using Event Manager
|
||||
setIsPaused(materialId, true);
|
||||
addArmBotToMonitor(armBot.modelUuid,
|
||||
() => handleAction(action, materialId)
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (toEvent?.type === 'storageUnit') {
|
||||
// Transfer to Storage Unit
|
||||
|
||||
}
|
||||
} else if (fromEvent?.type === 'vehicle') {
|
||||
if (toEvent?.type === 'transfer') {
|
||||
// Vehicle to Transfer
|
||||
if (materialId && trigger.triggeredAsset && trigger.triggeredAsset.triggeredPoint && trigger.triggeredAsset.triggeredAction) {
|
||||
const material = getMaterialById(materialId);
|
||||
if (material) {
|
||||
const action = getActionByUuid(selectedProduct.productUuid, trigger.triggeredAsset.triggeredAction.actionUuid);
|
||||
|
||||
setPreviousLocation(material.materialId, {
|
||||
modelUuid: material.current.modelUuid,
|
||||
pointUuid: material.current.pointUuid,
|
||||
actionUuid: material.current.actionUuid,
|
||||
})
|
||||
|
||||
setCurrentLocation(material.materialId, {
|
||||
modelUuid: trigger.triggeredAsset.triggeredModel.modelUuid,
|
||||
pointUuid: trigger.triggeredAsset.triggeredPoint.pointUuid,
|
||||
actionUuid: trigger.triggeredAsset?.triggeredAction?.actionUuid,
|
||||
});
|
||||
|
||||
setIsVisible(materialId, true);
|
||||
|
||||
if (action &&
|
||||
action.triggers[0]?.triggeredAsset?.triggeredModel.modelUuid &&
|
||||
action.triggers[0]?.triggeredAsset?.triggeredPoint?.pointUuid
|
||||
) {
|
||||
setNextLocation(material.materialId, {
|
||||
modelUuid: action.triggers[0]?.triggeredAsset?.triggeredModel.modelUuid,
|
||||
pointUuid: action.triggers[0]?.triggeredAsset?.triggeredPoint?.pointUuid,
|
||||
});
|
||||
|
||||
handleAction(action, materialId);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
} else if (toEvent?.type === 'vehicle') {
|
||||
// Vehicle to Vehicle
|
||||
|
||||
} else if (toEvent?.type === 'machine') {
|
||||
// Vehicle to Machine
|
||||
|
||||
} else if (toEvent?.type === 'roboticArm') {
|
||||
// Vehicle to Robotic Arm
|
||||
if (materialId && trigger.triggeredAsset && trigger.triggeredAsset.triggeredPoint && trigger.triggeredAsset.triggeredAction) {
|
||||
const material = getMaterialById(materialId);
|
||||
if (material) {
|
||||
const action = getActionByUuid(selectedProduct.productUuid, trigger.triggeredAsset.triggeredAction.actionUuid);
|
||||
const armBot = getArmBotById(trigger.triggeredAsset?.triggeredModel.modelUuid);
|
||||
|
||||
setPreviousLocation(material.materialId, {
|
||||
modelUuid: material.current.modelUuid,
|
||||
pointUuid: material.current.pointUuid,
|
||||
actionUuid: material.current.actionUuid,
|
||||
})
|
||||
|
||||
setCurrentLocation(material.materialId, {
|
||||
modelUuid: trigger.triggeredAsset.triggeredModel.modelUuid,
|
||||
pointUuid: trigger.triggeredAsset.triggeredPoint.pointUuid,
|
||||
actionUuid: trigger.triggeredAsset?.triggeredAction?.actionUuid,
|
||||
});
|
||||
|
||||
setNextLocation(material.materialId, null);
|
||||
|
||||
setIsVisible(materialId, false);
|
||||
|
||||
if (action && armBot) {
|
||||
|
||||
if (armBot.isActive === false && armBot.state === 'idle') {
|
||||
|
||||
// Handle current action from arm bot
|
||||
handleAction(action, materialId);
|
||||
|
||||
} else {
|
||||
|
||||
// Event Manager Needed
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else if (toEvent?.type === 'storageUnit') {
|
||||
// Vehicle to Storage Unit
|
||||
if (materialId && trigger.triggeredAsset && trigger.triggeredAsset.triggeredPoint && trigger.triggeredAsset.triggeredAction) {
|
||||
const material = getMaterialById(materialId);
|
||||
if (material) {
|
||||
const action = getActionByUuid(selectedProduct.productUuid, trigger.triggeredAsset.triggeredAction.actionUuid);
|
||||
const storageUnit = getStorageUnitById(trigger.triggeredAsset?.triggeredModel.modelUuid);
|
||||
|
||||
setPreviousLocation(material.materialId, {
|
||||
modelUuid: material.current.modelUuid,
|
||||
pointUuid: material.current.pointUuid,
|
||||
actionUuid: material.current.actionUuid,
|
||||
})
|
||||
|
||||
setCurrentLocation(material.materialId, {
|
||||
modelUuid: trigger.triggeredAsset.triggeredModel.modelUuid,
|
||||
pointUuid: trigger.triggeredAsset.triggeredPoint.pointUuid,
|
||||
actionUuid: trigger.triggeredAsset?.triggeredAction?.actionUuid,
|
||||
});
|
||||
|
||||
setNextLocation(material.materialId, null);
|
||||
|
||||
setIsVisible(materialId, false);
|
||||
|
||||
if (action && storageUnit) {
|
||||
|
||||
if (storageUnit.currentLoad < storageUnit.point.action.storageCapacity) {
|
||||
|
||||
// Handle current action from vehicle
|
||||
handleAction(action, materialId);
|
||||
|
||||
} else {
|
||||
|
||||
// Event Manager Needed
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (fromEvent?.type === 'machine') {
|
||||
if (toEvent?.type === 'transfer') {
|
||||
// Machine to Transfer
|
||||
|
||||
} else if (toEvent?.type === 'vehicle') {
|
||||
// Machine to Vehicle
|
||||
|
||||
} else if (toEvent?.type === 'machine') {
|
||||
// Machine to Machine
|
||||
|
||||
} else if (toEvent?.type === 'roboticArm') {
|
||||
// Machine to Robotic Arm
|
||||
if (materialId && trigger.triggeredAsset && trigger.triggeredAsset.triggeredPoint && trigger.triggeredAsset.triggeredAction) {
|
||||
const material = getMaterialById(materialId);
|
||||
|
||||
setIsPaused(materialId, true);
|
||||
|
||||
if (material) {
|
||||
const action = getActionByUuid(selectedProduct.productUuid, trigger.triggeredAsset.triggeredAction.actionUuid);
|
||||
const armBot = getArmBotById(trigger.triggeredAsset?.triggeredModel.modelUuid);
|
||||
|
||||
setPreviousLocation(material.materialId, {
|
||||
modelUuid: material.current.modelUuid,
|
||||
pointUuid: material.current.pointUuid,
|
||||
actionUuid: material.current.actionUuid,
|
||||
})
|
||||
|
||||
setCurrentLocation(material.materialId, {
|
||||
modelUuid: material.current.modelUuid,
|
||||
pointUuid: trigger.triggeredAsset.triggeredPoint.pointUuid,
|
||||
actionUuid: trigger.triggeredAsset?.triggeredAction?.actionUuid,
|
||||
});
|
||||
|
||||
setNextLocation(material.materialId, null);
|
||||
|
||||
setIsVisible(materialId, false);
|
||||
|
||||
if (action && armBot) {
|
||||
|
||||
if (armBot.isActive === false && armBot.state === 'idle') {
|
||||
|
||||
// Handle current action from arm bot
|
||||
const model = getEventByModelUuid(selectedProduct.productUuid, action.triggers[0]?.triggeredAsset?.triggeredModel.modelUuid || '');
|
||||
if (model?.type === 'transfer') {
|
||||
const conveyor = getConveyorById(action.triggers[0]?.triggeredAsset?.triggeredModel.modelUuid || '');
|
||||
if (conveyor) {
|
||||
const previousModel = getEventByModelUuid(selectedProduct.productUuid, material.previous?.modelUuid || '');
|
||||
if (previousModel) {
|
||||
if (previousModel.type === 'transfer' && previousModel.modelUuid === model.modelUuid) {
|
||||
handleAction(action, materialId)
|
||||
} else {
|
||||
addConveyorToMonitor(conveyor.modelUuid,
|
||||
() => {
|
||||
handleAction(action, materialId)
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
handleAction(action, materialId)
|
||||
}
|
||||
// handleAction(action, materialId)
|
||||
}
|
||||
} else {
|
||||
handleAction(action, materialId)
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
// Handle current action using Event Manager
|
||||
|
||||
addArmBotToMonitor(armBot.modelUuid,
|
||||
() => {
|
||||
const model = getEventByModelUuid(selectedProduct.productUuid, action.triggers[0]?.triggeredAsset?.triggeredModel.modelUuid || '');
|
||||
if (model?.type === 'transfer') {
|
||||
const conveyor = getConveyorById(action.triggers[0]?.triggeredAsset?.triggeredModel.modelUuid || '');
|
||||
if (conveyor) {
|
||||
const previousModel = getEventByModelUuid(selectedProduct.productUuid, material.previous?.modelUuid || '');
|
||||
if (previousModel) {
|
||||
if (previousModel.type === 'transfer' && previousModel.modelUuid === model.modelUuid) {
|
||||
handleAction(action, materialId)
|
||||
} else {
|
||||
addConveyorToMonitor(conveyor.modelUuid,
|
||||
() => {
|
||||
handleAction(action, materialId)
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
handleAction(action, materialId)
|
||||
}
|
||||
// handleAction(action, materialId)
|
||||
}
|
||||
} else {
|
||||
handleAction(action, materialId)
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else if (toEvent?.type === 'storageUnit') {
|
||||
// Machine to Storage Unit
|
||||
|
||||
}
|
||||
} else if (fromEvent?.type === 'roboticArm') {
|
||||
if (toEvent?.type === 'transfer') {
|
||||
// Robotic Arm to Transfer
|
||||
if (materialId && trigger.triggeredAsset && trigger.triggeredAsset.triggeredPoint && trigger.triggeredAsset.triggeredAction) {
|
||||
const material = getMaterialById(materialId);
|
||||
if (material) {
|
||||
|
||||
setPreviousLocation(material.materialId, {
|
||||
modelUuid: material.current.modelUuid,
|
||||
pointUuid: material.current.pointUuid,
|
||||
actionUuid: material.current.actionUuid,
|
||||
})
|
||||
|
||||
setCurrentLocation(material.materialId, {
|
||||
modelUuid: trigger.triggeredAsset.triggeredModel.modelUuid,
|
||||
pointUuid: trigger.triggeredAsset.triggeredPoint.pointUuid,
|
||||
actionUuid: trigger.triggeredAsset?.triggeredAction?.actionUuid,
|
||||
});
|
||||
|
||||
const action = getActionByUuid(selectedProduct.productUuid, trigger.triggeredAsset.triggeredAction.actionUuid);
|
||||
|
||||
if (action && action.triggers.length > 0 &&
|
||||
action.triggers[0]?.triggeredAsset?.triggeredModel.modelUuid &&
|
||||
action.triggers[0]?.triggeredAsset?.triggeredAction?.actionUuid &&
|
||||
action.triggers[0]?.triggeredAsset?.triggeredPoint?.pointUuid) {
|
||||
const model = getEventByModelUuid(selectedProduct.productUuid, action.triggers[0]?.triggeredAsset?.triggeredModel.modelUuid);
|
||||
|
||||
if (model?.type === 'roboticArm') {
|
||||
|
||||
handleAction(action, material.materialId);
|
||||
|
||||
} else if (model?.type === 'vehicle') {
|
||||
const nextAction = getActionByUuid(selectedProduct.productUuid, action.triggers[0]?.triggeredAsset?.triggeredAction.actionUuid);
|
||||
|
||||
if (action) {
|
||||
handleAction(action, material.materialId);
|
||||
|
||||
const vehicle = getVehicleById(action.triggers[0]?.triggeredAsset?.triggeredModel.modelUuid);
|
||||
|
||||
setPreviousLocation(material.materialId, {
|
||||
modelUuid: material.current.modelUuid,
|
||||
pointUuid: material.current.pointUuid,
|
||||
actionUuid: material.current.actionUuid,
|
||||
})
|
||||
|
||||
setCurrentLocation(material.materialId, {
|
||||
modelUuid: trigger.triggeredAsset?.triggeredModel.modelUuid,
|
||||
pointUuid: trigger.triggeredAsset?.triggeredPoint?.pointUuid,
|
||||
actionUuid: trigger.triggeredAsset?.triggeredAction?.actionUuid,
|
||||
});
|
||||
|
||||
setNextLocation(material.materialId, null);
|
||||
|
||||
if (nextAction) {
|
||||
|
||||
if (vehicle) {
|
||||
|
||||
if (vehicle.isActive === false && vehicle.state === 'idle' && vehicle.isPicking && vehicle.currentLoad < vehicle.point.action.loadCapacity) {
|
||||
|
||||
setPreviousLocation(material.materialId, {
|
||||
modelUuid: material.current.modelUuid,
|
||||
pointUuid: material.current.pointUuid,
|
||||
actionUuid: material.current.actionUuid,
|
||||
})
|
||||
|
||||
setCurrentLocation(material.materialId, {
|
||||
modelUuid: action.triggers[0]?.triggeredAsset?.triggeredModel.modelUuid || '',
|
||||
pointUuid: action.triggers[0]?.triggeredAsset?.triggeredPoint?.pointUuid || '',
|
||||
actionUuid: action.triggers[0]?.triggeredAsset?.triggeredAction?.actionUuid || '',
|
||||
});
|
||||
|
||||
setNextLocation(material.materialId, null);
|
||||
|
||||
setIsVisible(materialId, false);
|
||||
setIsPaused(materialId, false);
|
||||
|
||||
// Handle current action from vehicle
|
||||
handleAction(nextAction, materialId);
|
||||
|
||||
} else {
|
||||
|
||||
// Handle current action using Event Manager
|
||||
setIsPaused(materialId, true);
|
||||
|
||||
addVehicleToMonitor(vehicle.modelUuid,
|
||||
() => {
|
||||
setPreviousLocation(material.materialId, {
|
||||
modelUuid: material.current.modelUuid,
|
||||
pointUuid: material.current.pointUuid,
|
||||
actionUuid: material.current.actionUuid,
|
||||
})
|
||||
|
||||
setCurrentLocation(material.materialId, {
|
||||
modelUuid: action.triggers[0]?.triggeredAsset?.triggeredModel.modelUuid || '',
|
||||
pointUuid: action.triggers[0]?.triggeredAsset?.triggeredPoint?.pointUuid || '',
|
||||
actionUuid: action.triggers[0]?.triggeredAsset?.triggeredAction?.actionUuid || '',
|
||||
});
|
||||
|
||||
setNextLocation(material.materialId, null);
|
||||
|
||||
setIsPaused(materialId, false);
|
||||
setIsVisible(materialId, false);
|
||||
handleAction(nextAction, materialId);
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else if (model?.type === 'transfer') {
|
||||
const conveyor = getConveyorById(action.triggers[0]?.triggeredAsset?.triggeredModel.modelUuid);
|
||||
if (conveyor) {
|
||||
setNextLocation(material.materialId, {
|
||||
modelUuid: action.triggers[0]?.triggeredAsset?.triggeredModel.modelUuid,
|
||||
pointUuid: action.triggers[0]?.triggeredAsset?.triggeredPoint?.pointUuid,
|
||||
})
|
||||
handleAction(action, material.materialId);
|
||||
}
|
||||
} else {
|
||||
setNextLocation(material.materialId, {
|
||||
modelUuid: action.triggers[0]?.triggeredAsset?.triggeredModel.modelUuid,
|
||||
pointUuid: action.triggers[0]?.triggeredAsset?.triggeredPoint?.pointUuid,
|
||||
})
|
||||
|
||||
handleAction(action, material.materialId);
|
||||
}
|
||||
} else if (action) {
|
||||
setNextLocation(material.materialId, null)
|
||||
|
||||
handleAction(action, material.materialId);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
} else if (toEvent?.type === 'vehicle') {
|
||||
// Robotic Arm to Vehicle
|
||||
if (materialId && trigger.triggeredAsset && trigger.triggeredAsset.triggeredPoint && trigger.triggeredAsset.triggeredAction) {
|
||||
const material = getMaterialById(materialId);
|
||||
if (material) {
|
||||
|
||||
setIsPaused(material.materialId, false);
|
||||
|
||||
const action = getActionByUuid(selectedProduct.productUuid, trigger.triggeredAsset.triggeredAction.actionUuid);
|
||||
const vehicle = getVehicleById(trigger.triggeredAsset?.triggeredModel.modelUuid);
|
||||
|
||||
setNextLocation(material.materialId, null);
|
||||
|
||||
if (action) {
|
||||
|
||||
if (vehicle) {
|
||||
|
||||
if (vehicle.isActive === false && vehicle.state === 'idle' && vehicle.isPicking && vehicle.currentLoad < vehicle.point.action.loadCapacity) {
|
||||
|
||||
setIsVisible(materialId, false);
|
||||
|
||||
setPreviousLocation(material.materialId, {
|
||||
modelUuid: material.current.modelUuid,
|
||||
pointUuid: material.current.pointUuid,
|
||||
actionUuid: material.current.actionUuid,
|
||||
})
|
||||
|
||||
setCurrentLocation(material.materialId, {
|
||||
modelUuid: trigger.triggeredAsset.triggeredModel.modelUuid,
|
||||
pointUuid: trigger.triggeredAsset.triggeredPoint.pointUuid,
|
||||
actionUuid: trigger.triggeredAsset?.triggeredAction?.actionUuid,
|
||||
});
|
||||
|
||||
// Handle current action from vehicle
|
||||
handleAction(action, materialId);
|
||||
|
||||
} else {
|
||||
|
||||
// Event Manager Needed
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
} else if (toEvent?.type === 'machine') {
|
||||
// Robotic Arm to Machine
|
||||
|
||||
if (materialId && trigger.triggeredAsset && trigger.triggeredAsset.triggeredPoint && trigger.triggeredAsset.triggeredAction) {
|
||||
const material = getMaterialById(materialId);
|
||||
if (material) {
|
||||
|
||||
// setIsPaused(material.materialId, false);
|
||||
|
||||
const action = getActionByUuid(selectedProduct.productUuid, trigger.triggeredAsset.triggeredAction.actionUuid);
|
||||
const machine = getMachineById(trigger.triggeredAsset?.triggeredModel.modelUuid);
|
||||
setNextLocation(material.materialId, null);
|
||||
|
||||
if (action) {
|
||||
|
||||
if (machine) {
|
||||
|
||||
if (machine.isActive === false && machine.state === 'idle') {
|
||||
|
||||
setIsVisible(materialId, false);
|
||||
|
||||
setPreviousLocation(material.materialId, {
|
||||
modelUuid: material.current.modelUuid,
|
||||
pointUuid: material.current.pointUuid,
|
||||
actionUuid: material.current.actionUuid,
|
||||
})
|
||||
|
||||
setCurrentLocation(material.materialId, {
|
||||
modelUuid: material.current.modelUuid,
|
||||
pointUuid: trigger.triggeredAsset.triggeredPoint.pointUuid,
|
||||
actionUuid: trigger.triggeredAsset?.triggeredAction?.actionUuid,
|
||||
});
|
||||
|
||||
// Handle current action from machine
|
||||
handleAction(action, materialId);
|
||||
|
||||
} else {
|
||||
|
||||
// Event Manager Needed
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else if (toEvent?.type === 'roboticArm') {
|
||||
// Robotic Arm to Robotic Arm
|
||||
|
||||
} else if (toEvent?.type === 'storageUnit') {
|
||||
// Robotic Arm to Storage Unit
|
||||
if (materialId && trigger.triggeredAsset && trigger.triggeredAsset.triggeredPoint && trigger.triggeredAsset.triggeredAction) {
|
||||
const material = getMaterialById(materialId);
|
||||
if (material) {
|
||||
const action = getActionByUuid(selectedProduct.productUuid, trigger.triggeredAsset.triggeredAction.actionUuid);
|
||||
const storageUnit = getStorageUnitById(trigger.triggeredAsset?.triggeredModel.modelUuid);
|
||||
|
||||
setPreviousLocation(material.materialId, {
|
||||
modelUuid: material.current.modelUuid,
|
||||
pointUuid: material.current.pointUuid,
|
||||
actionUuid: material.current.actionUuid,
|
||||
})
|
||||
|
||||
setCurrentLocation(material.materialId, {
|
||||
modelUuid: trigger.triggeredAsset.triggeredModel.modelUuid,
|
||||
pointUuid: trigger.triggeredAsset.triggeredPoint.pointUuid,
|
||||
actionUuid: trigger.triggeredAsset?.triggeredAction?.actionUuid,
|
||||
});
|
||||
|
||||
setNextLocation(material.materialId, null);
|
||||
|
||||
setIsVisible(materialId, false);
|
||||
|
||||
if (action && storageUnit) {
|
||||
|
||||
if (storageUnit.currentLoad < storageUnit.point.action.storageCapacity) {
|
||||
|
||||
// Handle current action from vehicle
|
||||
handleAction(action, materialId);
|
||||
|
||||
} else {
|
||||
|
||||
// Event Manager Needed
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (fromEvent?.type === 'storageUnit') {
|
||||
if (toEvent?.type === 'transfer') {
|
||||
// Storage Unit to Transfer
|
||||
|
||||
} else if (toEvent?.type === 'vehicle') {
|
||||
// Storage Unit to Vehicle
|
||||
|
||||
} else if (toEvent?.type === 'machine') {
|
||||
// Storage Unit to Machine
|
||||
|
||||
} else if (toEvent?.type === 'roboticArm') {
|
||||
// Storage Unit to Robotic Arm
|
||||
|
||||
} else if (toEvent?.type === 'storageUnit') {
|
||||
// Storage Unit to Storage Unit
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleFinalAction = (action: Action, materialId?: string) => {
|
||||
if (!action) return;
|
||||
|
||||
const modelUuid = getModelUuidByActionUuid(selectedProduct.productUuid, action.actionUuid);
|
||||
if (!modelUuid) return;
|
||||
const finalModel = getEventByModelUuid(selectedProduct.productUuid, modelUuid);
|
||||
if (!finalModel) return;
|
||||
|
||||
if (finalModel.type === 'transfer') {
|
||||
// Storage Unit to Transfer
|
||||
if (!materialId) return;
|
||||
const material = getMaterialById(materialId);
|
||||
if (material) {
|
||||
const currentTime = performance.now();
|
||||
|
||||
setPreviousLocation(material.materialId, {
|
||||
modelUuid: material.current.modelUuid,
|
||||
pointUuid: material.current.pointUuid,
|
||||
actionUuid: material.current.actionUuid,
|
||||
})
|
||||
|
||||
setCurrentLocation(material.materialId, {
|
||||
modelUuid: material.next?.modelUuid || '',
|
||||
pointUuid: material.next?.pointUuid || '',
|
||||
actionUuid: action.actionUuid,
|
||||
});
|
||||
|
||||
setNextLocation(material.materialId, null);
|
||||
|
||||
setEndTime(material.materialId, currentTime);
|
||||
|
||||
handleAction(action, material.materialId);
|
||||
}
|
||||
|
||||
} else if (finalModel.type === 'vehicle') {
|
||||
// Storage Unit to Vehicle
|
||||
|
||||
} else if (finalModel.type === 'machine') {
|
||||
// Storage Unit to Machine
|
||||
|
||||
} else if (finalModel.type === 'roboticArm') {
|
||||
// Storage Unit to Robotic Arm
|
||||
|
||||
} else if (finalModel.type === 'storageUnit') {
|
||||
// Storage Unit to Storage Unit
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const triggerPointActions = useCallback((action: Action, materialId?: string) => {
|
||||
if (!action) return;
|
||||
|
||||
if (action.triggers.length > 0) {
|
||||
|
||||
action.triggers.forEach(trigger => {
|
||||
switch (trigger.triggerType) {
|
||||
case 'onStart':
|
||||
break;
|
||||
case 'onComplete':
|
||||
handleTrigger(trigger, action, materialId);
|
||||
break;
|
||||
case 'onStop':
|
||||
break;
|
||||
case 'onError':
|
||||
break;
|
||||
case 'delay':
|
||||
break;
|
||||
default:
|
||||
console.warn(`Unknown trigger type: ${trigger.triggerType}`);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
handleFinalAction(action, materialId);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
triggerPointActions
|
||||
};
|
||||
}
|
||||
34
app/src/modules/simulation/ui3d/MachineContentUi.tsx
Normal file
34
app/src/modules/simulation/ui3d/MachineContentUi.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Html } from "@react-three/drei";
|
||||
import React from "react";
|
||||
import AssetDetailsCard from "../../../components/ui/simulation/AssetDetailsCard";
|
||||
import { Vector3 } from "three";
|
||||
|
||||
type MachineContentUiProps = {
|
||||
machine: MachineStatus;
|
||||
};
|
||||
|
||||
const MachineContentUi: React.FC<MachineContentUiProps> = ({ machine }) => {
|
||||
return (
|
||||
<Html
|
||||
// data
|
||||
position={
|
||||
new Vector3(
|
||||
machine.position[0],
|
||||
machine.point.position[1],
|
||||
machine.position[2]
|
||||
)
|
||||
}
|
||||
// class none
|
||||
// other
|
||||
zIndexRange={[1, 0]}
|
||||
prepend
|
||||
sprite
|
||||
center
|
||||
distanceFactor={20}
|
||||
>
|
||||
<AssetDetailsCard name={machine.modelName} status={machine.state} />
|
||||
</Html>
|
||||
);
|
||||
};
|
||||
|
||||
export default MachineContentUi;
|
||||
34
app/src/modules/simulation/ui3d/RoboticArmContentUi.tsx
Normal file
34
app/src/modules/simulation/ui3d/RoboticArmContentUi.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Html } from "@react-three/drei";
|
||||
import React from "react";
|
||||
import AssetDetailsCard from "../../../components/ui/simulation/AssetDetailsCard";
|
||||
import { Vector3 } from "three";
|
||||
|
||||
type RoboticArmContentUiProps = {
|
||||
roboticArm: ArmBotStatus;
|
||||
};
|
||||
|
||||
const RoboticArmContentUi: React.FC<RoboticArmContentUiProps> = ({ roboticArm }) => {
|
||||
return (
|
||||
<Html
|
||||
// data
|
||||
position={
|
||||
new Vector3(
|
||||
roboticArm.position[0],
|
||||
roboticArm.point.position[1],
|
||||
roboticArm.position[2]
|
||||
)
|
||||
}
|
||||
// class none
|
||||
// other
|
||||
zIndexRange={[1, 0]}
|
||||
prepend
|
||||
sprite
|
||||
center
|
||||
distanceFactor={20}
|
||||
>
|
||||
<AssetDetailsCard name={roboticArm.modelName} status={roboticArm.state} />
|
||||
</Html>
|
||||
);
|
||||
};
|
||||
|
||||
export default RoboticArmContentUi;
|
||||
40
app/src/modules/simulation/ui3d/StorageContentUi.tsx
Normal file
40
app/src/modules/simulation/ui3d/StorageContentUi.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Html } from "@react-three/drei";
|
||||
import React from "react";
|
||||
import AssetDetailsCard from "../../../components/ui/simulation/AssetDetailsCard";
|
||||
import { Vector3 } from "three";
|
||||
|
||||
type StorageContentUiProps = {
|
||||
storageUnit: StorageUnitStatus;
|
||||
};
|
||||
|
||||
const StorageContentUi: React.FC<StorageContentUiProps> = ({ storageUnit }) => {
|
||||
return (
|
||||
<Html
|
||||
// data
|
||||
position={
|
||||
new Vector3(
|
||||
storageUnit.position[0],
|
||||
storageUnit.point.position[1],
|
||||
storageUnit.position[2]
|
||||
)
|
||||
}
|
||||
// class none
|
||||
// other
|
||||
zIndexRange={[1, 0]}
|
||||
prepend
|
||||
sprite
|
||||
center
|
||||
distanceFactor={20}
|
||||
>
|
||||
<AssetDetailsCard
|
||||
name={storageUnit.modelName}
|
||||
status={storageUnit.state}
|
||||
count={storageUnit.currentLoad}
|
||||
enableStatue={false}
|
||||
totalCapacity={storageUnit.point.action.storageCapacity}
|
||||
/>
|
||||
</Html>
|
||||
);
|
||||
};
|
||||
|
||||
export default StorageContentUi;
|
||||
52
app/src/modules/simulation/ui3d/VehicleContentUi.tsx
Normal file
52
app/src/modules/simulation/ui3d/VehicleContentUi.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Html } from "@react-three/drei";
|
||||
import { Object3D, Vector3 } from "three";
|
||||
import AssetDetailsCard from "../../../components/ui/simulation/AssetDetailsCard";
|
||||
import { useFrame, useThree } from "@react-three/fiber";
|
||||
import { useState } from "react";
|
||||
|
||||
type VehicleContentUiProps = {
|
||||
vehicle: VehicleStatus;
|
||||
};
|
||||
|
||||
const VehicleContentUi: React.FC<VehicleContentUiProps> = ({ vehicle }) => {
|
||||
const { scene } = useThree();
|
||||
const [htmlPosition, setHtmlPosition] = useState<[number, number, number]>([
|
||||
0, 0, 0,
|
||||
]);
|
||||
const offset = new Vector3(0, 0.85, 0);
|
||||
useFrame(() => {
|
||||
const agvModel = scene.getObjectByProperty(
|
||||
"uuid",
|
||||
vehicle.modelUuid
|
||||
) as Object3D;
|
||||
if (agvModel) {
|
||||
const worldPosition = offset.clone().applyMatrix4(agvModel.matrixWorld);
|
||||
setHtmlPosition([worldPosition.x, worldPosition.y, worldPosition.z]);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Html
|
||||
// data
|
||||
position={
|
||||
new Vector3(htmlPosition[0], vehicle.point.position[1], htmlPosition[2])
|
||||
}
|
||||
// class none
|
||||
// other
|
||||
zIndexRange={[1, 0]}
|
||||
prepend
|
||||
sprite
|
||||
center
|
||||
distanceFactor={20}
|
||||
>
|
||||
<AssetDetailsCard
|
||||
name={vehicle.modelName}
|
||||
status={vehicle.state}
|
||||
count={vehicle.currentLoad}
|
||||
totalCapacity={vehicle.point.action.loadCapacity}
|
||||
/>
|
||||
</Html>
|
||||
);
|
||||
};
|
||||
|
||||
export default VehicleContentUi;
|
||||
@@ -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