first commit

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

View File

@@ -0,0 +1,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,
};
}

View File

@@ -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
};
}

View File

@@ -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,
};
}

View File

@@ -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
};
}

View File

@@ -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,
};
}

View File

@@ -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
};
}

View File

@@ -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
};
}

View File

@@ -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,
};
}

View File

@@ -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,
};
}

View File

@@ -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
};
}

View File

@@ -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,
};
}

View File

@@ -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,
};
}

View File

@@ -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
};
}

View 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,
};
}

View File

@@ -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,
};
}

View File

@@ -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
};
}

View 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 (
<></>
)
}

View File

@@ -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 (
<></>
)
}

View 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

View File

@@ -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 (
<>
</>
);
}

View File

@@ -0,0 +1,14 @@
import React from 'react'
import ConveyorInstances from './instances/conveyorInstances'
function Conveyor() {
return (
<>
<ConveyorInstances />
</>
)
}
export default Conveyor

View File

@@ -0,0 +1,76 @@
import { useEffect, useRef } from 'react';
import { useFrame } from '@react-three/fiber';
import { usePauseButtonStore, usePlayButtonStore, useResetButtonStore } from '../../../../store/usePlayButtonStore';
import { useSceneContext } from '../../../scene/sceneContext';
type 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,
};
}

View File

@@ -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

View File

@@ -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

View 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>
);
}

View File

@@ -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;

View File

@@ -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();
}
}
};

View File

@@ -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;

View File

@@ -0,0 +1,12 @@
import React from 'react'
import PointsCreator from './creator/pointsCreator'
function Points() {
return (
<>
<PointsCreator />
</>
)
}
export default Points

View File

@@ -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;

View File

@@ -0,0 +1,76 @@
import { useEffect, useRef } from 'react';
import { useFrame } from '@react-three/fiber';
import { usePauseButtonStore, usePlayButtonStore, useResetButtonStore } from '../../../../store/usePlayButtonStore';
import { useSceneContext } from '../../../scene/sceneContext';
type 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,
};
}

View File

@@ -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;

View File

@@ -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

View File

@@ -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;

View File

@@ -0,0 +1,14 @@
import MachineInstances from './instances/machineInstances'
function Machine() {
return (
<>
<MachineInstances />
</>
)
}
export default Machine

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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

View File

@@ -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);
});

View File

@@ -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

View 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

View 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;
}

View 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

View File

@@ -0,0 +1,76 @@
import { useEffect, useRef } from 'react';
import { useFrame } from '@react-three/fiber';
import { usePauseButtonStore, usePlayButtonStore, useResetButtonStore } from '../../../../store/usePlayButtonStore';
import { useSceneContext } from '../../../scene/sceneContext';
type 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,
};
}

View File

@@ -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'}
/>
)}
</>
);
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View 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;

View 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;

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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 [];
}

View File

@@ -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;
}

View 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;

View 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;

View 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;

View 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 };
}

View 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 };
}

View 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;

View File

@@ -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;

View File

@@ -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

View File

@@ -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;

View File

@@ -0,0 +1,14 @@
import React from 'react'
import StorageUnitInstances from './instances/storageUnitInstances'
function StorageUnit() {
return (
<>
<StorageUnitInstances />
</>
)
}
export default StorageUnit

View File

@@ -0,0 +1,14 @@
import TriggerConnector from '../events/triggerConnections/triggerConnector'
function Trigger() {
return (
<>
<TriggerConnector />
</>
)
}
export default Trigger

View File

@@ -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
};
}

View 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;

View 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;

View 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;

View 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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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