v2 #82

Merged
Vishnu merged 9 commits from v2 into main 2025-05-07 13:26:15 +00:00
24 changed files with 674 additions and 422 deletions

View File

@ -38,7 +38,7 @@ function RoboticArmMechanics() {
} else { } else {
clearSelectedAction(); clearSelectedAction();
} }
}, [selectedAction, selectedEventData, selectedProduct]); }, [selectedEventData, selectedProduct]);
const updateBackend = ( const updateBackend = (
productName: string, productName: string,

View File

@ -9,7 +9,7 @@ import LabledDropdown from "../../../../../ui/inputs/LabledDropdown";
import RenameInput from "../../../../../ui/inputs/RenameInput"; import RenameInput from "../../../../../ui/inputs/RenameInput";
import { handleResize } from "../../../../../../functions/handleResizePannel"; import { handleResize } from "../../../../../../functions/handleResizePannel";
import { useProductStore } from "../../../../../../store/simulation/useProductStore"; import { useProductStore } from "../../../../../../store/simulation/useProductStore";
import { useSelectedProduct } from "../../../../../../store/simulation/useSimulationStore"; import { useSelectedAction, useSelectedProduct } from "../../../../../../store/simulation/useSimulationStore";
import { upsertProductOrEventApi } from "../../../../../../services/simulation/UpsertProductOrEventApi"; import { upsertProductOrEventApi } from "../../../../../../services/simulation/UpsertProductOrEventApi";
type TriggerProps = { type TriggerProps = {
@ -25,6 +25,7 @@ const Trigger = ({ selectedPointData, type }: TriggerProps) => {
const [selectedTrigger, setSelectedTrigger] = useState<TriggerSchema | undefined>(); const [selectedTrigger, setSelectedTrigger] = useState<TriggerSchema | undefined>();
const [activeOption, setActiveOption] = useState<"onComplete" | "onStart" | "onStop" | "delay" | "onError">("onComplete"); const [activeOption, setActiveOption] = useState<"onComplete" | "onStart" | "onStop" | "delay" | "onError">("onComplete");
const triggersContainerRef = useRef<HTMLDivElement>(null); const triggersContainerRef = useRef<HTMLDivElement>(null);
const { selectedAction } = useSelectedAction();
const email = localStorage.getItem('email') const email = localStorage.getItem('email')
const organization = (email!.split("@")[1]).split(".")[0]; const organization = (email!.split("@")[1]).split(".")[0];
@ -36,12 +37,12 @@ const Trigger = ({ selectedPointData, type }: TriggerProps) => {
if (type === 'Conveyor' || type === 'Vehicle' || type === 'Machine' || type === 'StorageUnit') { if (type === 'Conveyor' || type === 'Vehicle' || type === 'Machine' || type === 'StorageUnit') {
actionUuid = (selectedPointData as ConveyorPointSchema | VehiclePointSchema | MachinePointSchema | StoragePointSchema).action?.actionUuid; actionUuid = (selectedPointData as ConveyorPointSchema | VehiclePointSchema | MachinePointSchema | StoragePointSchema).action?.actionUuid;
} else if (type === 'RoboticArm') { } else if (type === 'RoboticArm' && selectedAction) {
actionUuid = (selectedPointData as RoboticArmPointSchema).actions[0]?.actionUuid; actionUuid = selectedAction.actionId;
} }
setCurrentAction(actionUuid); setCurrentAction(actionUuid);
}, [selectedPointData, selectedProduct, type]); }, [selectedPointData, selectedProduct, type, selectedAction]);
const updateBackend = ( const updateBackend = (
productName: string, productName: string,

View File

@ -7,7 +7,6 @@ interface SkeletonUIProps {
// Define the SkeletonUI component // Define the SkeletonUI component
const SkeletonUI: React.FC<SkeletonUIProps> = ({ type }) => { const SkeletonUI: React.FC<SkeletonUIProps> = ({ type }) => {
console.log("type: ", type);
// Function to render skeleton content based on 'type' // Function to render skeleton content based on 'type'
const renderSkeleton = () => { const renderSkeleton = () => {

View File

@ -40,9 +40,9 @@ const SimulationPlayer: React.FC = () => {
useEffect(() => { useEffect(() => {
if (isReset) { if (isReset) {
setTimeout(()=>{ setTimeout(() => {
setReset(false); setReset(false);
},0) }, 0)
} }
}, [isReset]) }, [isReset])
@ -282,11 +282,10 @@ const SimulationPlayer: React.FC = () => {
</div> </div>
{index < intervals.length - 1 && ( {index < intervals.length - 1 && (
<div <div
className={`line ${ className={`line ${progress >= ((index + 1) / totalSegments) * 100
progress >= ((index + 1) / totalSegments) * 100
? "filled" ? "filled"
: "" : ""
}`} }`}
></div> ></div>
)} )}
</React.Fragment> </React.Fragment>

View File

@ -1,22 +1,27 @@
import { useCallback, useEffect, useRef } from "react"; import { useCallback, useEffect, useRef } from "react";
import { useFrame } from "@react-three/fiber"; import { useFrame } from "@react-three/fiber";
import { usePlayButtonStore, usePauseButtonStore, useResetButtonStore } from "../../../../../store/usePlayButtonStore"; import { usePlayButtonStore, usePauseButtonStore, useResetButtonStore, useAnimationPlaySpeed } from "../../../../../store/usePlayButtonStore";
import { useMaterialStore } from "../../../../../store/simulation/useMaterialStore"; import { useMaterialStore } from "../../../../../store/simulation/useMaterialStore";
interface DelayInstance { interface DelayInstance {
initialDelay: number;
delayEndTime: number; delayEndTime: number;
materialId?: string; materialId?: string;
action: ConveyorAction; action: ConveyorAction;
isPaused: boolean; isPaused: boolean;
remainingTime: number; remainingTime: number;
lastUpdateTime: number;
elapsedTime: number;
} }
export function useDelayHandler() { export function useDelayHandler() {
const { isPlaying } = usePlayButtonStore(); const { isPlaying } = usePlayButtonStore();
const { isPaused } = usePauseButtonStore(); const { isPaused } = usePauseButtonStore();
const { isReset } = useResetButtonStore(); const { isReset } = useResetButtonStore();
const { speed } = useAnimationPlaySpeed();
const { setIsPaused } = useMaterialStore(); const { setIsPaused } = useMaterialStore();
const activeDelays = useRef<Map<string, DelayInstance>>(new Map()); const activeDelays = useRef<Map<string, DelayInstance>>(new Map());
const lastUpdateTimeRef = useRef<number>(performance.now());
const cleanupDelay = useCallback(() => { const cleanupDelay = useCallback(() => {
activeDelays.current.clear(); activeDelays.current.clear();
@ -30,34 +35,33 @@ export function useDelayHandler() {
const delayLogStatus = (materialUuid: string, status: string) => { const delayLogStatus = (materialUuid: string, status: string) => {
// console.log(`${materialUuid}, ${status}`); // console.log(`${materialUuid}, ${status}`);
} };
useEffect(() => { useFrame(() => {
const currentTime = performance.now(); const currentTime = performance.now();
const deltaTime = currentTime - lastUpdateTimeRef.current;
lastUpdateTimeRef.current = currentTime;
const completedDelays: string[] = [];
activeDelays.current.forEach((delay) => { activeDelays.current.forEach((delay, key) => {
if (isPaused && !delay.isPaused) { if (isPaused && !delay.isPaused) {
delay.remainingTime = Math.max(0, delay.delayEndTime - currentTime); delay.remainingTime = Math.max(0, delay.delayEndTime - currentTime);
delay.isPaused = true; delay.isPaused = true;
} else if (!isPaused && delay.isPaused) { } else if (!isPaused && delay.isPaused) {
delay.delayEndTime = currentTime + delay.remainingTime; delay.delayEndTime = currentTime + delay.remainingTime;
delay.isPaused = false; delay.isPaused = false;
delay.remainingTime = 0; delay.lastUpdateTime = currentTime;
} delay.elapsedTime = 0;
}); } else if (!delay.isPaused && isPlaying) {
}, [isPaused]); delay.elapsedTime += deltaTime * speed;
useFrame(() => { if (delay.elapsedTime >= delay.initialDelay) {
if (!isPlaying || isPaused) return; if (delay.materialId) {
delayLogStatus(delay.materialId, `Delay completed`);
const currentTime = performance.now(); setIsPaused(delay.materialId, false);
const completedDelays: string[] = []; }
completedDelays.push(key);
activeDelays.current.forEach((delay, key) => { }
if (!delay.isPaused && currentTime >= delay.delayEndTime && delay.materialId) {
delayLogStatus(delay.materialId, `Delay completed}`);
setIsPaused(delay.materialId, false);
completedDelays.push(key);
} }
}); });
@ -67,29 +71,31 @@ export function useDelayHandler() {
}); });
const handleDelay = useCallback((action: ConveyorAction, materialId?: string) => { const handleDelay = useCallback((action: ConveyorAction, materialId?: string) => {
if (!action || action.actionType !== 'delay' || !isPlaying || !materialId) return; if (!action || action.actionType !== 'delay' || !materialId) return;
const delayMs = (action.delay || 0) * 1000; const delayMs = (action.delay || 0) * 1000;
if (delayMs <= 0) return; if (delayMs <= 0) return;
const key = materialId ? `${materialId}-${action.actionUuid}` : action.actionUuid; const key = materialId ? `${materialId}-${action.actionUuid}` : action.actionUuid;
// If this material already has a delay, cancel it
if (activeDelays.current.has(key)) { if (activeDelays.current.has(key)) {
activeDelays.current.delete(key); activeDelays.current.delete(key);
} }
const now = performance.now();
activeDelays.current.set(key, { activeDelays.current.set(key, {
delayEndTime: performance.now() + delayMs, initialDelay: delayMs,
delayEndTime: now + delayMs,
materialId, materialId,
action, action,
isPaused: false, isPaused: false,
remainingTime: 0 remainingTime: 0,
lastUpdateTime: now,
elapsedTime: 0
}); });
delayLogStatus(materialId, `Started ${delayMs * 1000}s delay`); delayLogStatus(materialId, `Started ${delayMs / 1000}s delay`);
setIsPaused(materialId, true); setIsPaused(materialId, true);
}, [isPlaying]); }, [isPlaying]);
useEffect(() => { useEffect(() => {

View File

@ -0,0 +1,26 @@
import { useCallback } from "react";
import { useMaterialStore } from "../../../../../store/simulation/useMaterialStore";
export function useDespawnHandler() {
const { addMaterial, getMaterialById, removeMaterial } = useMaterialStore();
const deSpawnLogStatus = (materialUuid: string, status: string) => {
// console.log(`${materialUuid}, ${status}`);
}
const handleDespawn = useCallback((action: ConveyorAction, materialId?: string) => {
if (!action || action.actionType !== 'despawn' || !materialId) return;
const material = getMaterialById(materialId);
if (!material) return;
removeMaterial(material.materialId);
deSpawnLogStatus(material.materialId, `Despawned`);
}, [addMaterial, getMaterialById, removeMaterial]);
return {
handleDespawn,
};
}

View File

@ -1,4 +1,4 @@
import { useCallback, useEffect, useRef } from "react"; import { useCallback, useEffect, useState } from "react";
import * as THREE from 'three'; import * as THREE from 'three';
import { useFrame } from "@react-three/fiber"; import { useFrame } from "@react-three/fiber";
import { useMaterialStore } from "../../../../../store/simulation/useMaterialStore"; import { useMaterialStore } from "../../../../../store/simulation/useMaterialStore";
@ -32,10 +32,23 @@ export function useSpawnHandler() {
const { isReset } = useResetButtonStore(); const { isReset } = useResetButtonStore();
const { selectedProduct } = useSelectedProduct(); const { selectedProduct } = useSelectedProduct();
const activeSpawns = useRef<Map<string, SpawnInstance>>(new Map()); const [activeSpawns, setActiveSpawns] = useState<Map<string, SpawnInstance>>(new Map());
const getConveyorPausedState = useCallback((action: ConveyorAction) => {
const modelUuid = getModelUuidByActionUuid(selectedProduct.productId, action.actionUuid);
if (!modelUuid) return false;
const conveyor = getConveyorById(modelUuid);
if (!conveyor) return false;
return conveyor.isPaused;
}, [getConveyorById, getModelUuidByActionUuid, selectedProduct.productId]);
const shouldPauseSpawn = useCallback((action: ConveyorAction) => {
return isPaused || getConveyorPausedState(action);
}, [isPaused, getConveyorPausedState]);
const clearAllSpawns = useCallback(() => { const clearAllSpawns = useCallback(() => {
activeSpawns.current.clear(); setActiveSpawns(new Map());
}, []); }, []);
useEffect(() => { useEffect(() => {
@ -52,6 +65,7 @@ export function useSpawnHandler() {
const modelUuid = getModelUuidByActionUuid(selectedProduct.productId, action.actionUuid); const modelUuid = getModelUuidByActionUuid(selectedProduct.productId, action.actionUuid);
const pointUuid = getPointUuidByActionUuid(selectedProduct.productId, action.actionUuid); const pointUuid = getPointUuidByActionUuid(selectedProduct.productId, action.actionUuid);
if (!modelUuid || !pointUuid) return; if (!modelUuid || !pointUuid) return;
const currentTime = performance.now();
const newMaterial: MaterialSchema = { const newMaterial: MaterialSchema = {
materialId: THREE.MathUtils.generateUUID(), materialId: THREE.MathUtils.generateUUID(),
@ -61,13 +75,12 @@ export function useSpawnHandler() {
isVisible: true, isVisible: true,
isPaused: false, isPaused: false,
isRendered: true, isRendered: true,
startTime:currentTime,
current: { current: {
modelUuid: modelUuid, modelUuid: modelUuid,
pointUuid: pointUuid, pointUuid: pointUuid,
actionUuid: action.actionUuid actionUuid: action.actionUuid
}, },
weight: 1,
cost: 1
}; };
if (action.triggers[0].triggeredAsset?.triggeredModel.modelUuid && if (action.triggers[0].triggeredAsset?.triggeredModel.modelUuid &&
@ -84,92 +97,96 @@ export function useSpawnHandler() {
return newMaterial; return newMaterial;
}, [addMaterial, getModelUuidByActionUuid, getPointUuidByActionUuid, selectedProduct.productId]); }, [addMaterial, getModelUuidByActionUuid, getPointUuidByActionUuid, selectedProduct.productId]);
useFrame(() => {
const getConveyorPausedState = useCallback((action: ConveyorAction) => {
const modelUuid = getModelUuidByActionUuid(selectedProduct.productId, action.actionUuid);
if (!modelUuid) return false;
const conveyor = getConveyorById(modelUuid);
return conveyor?.isPaused ?? false;
}, [getConveyorById, getModelUuidByActionUuid, selectedProduct.productId]);
useEffect(() => {
const currentTime = performance.now(); const currentTime = performance.now();
const completedActions: string[] = [];
let hasChanges = false;
activeSpawns.current.forEach((spawn) => { activeSpawns.forEach(spawn => {
if (isPaused && !spawn.isPaused) { const isPausedNow = shouldPauseSpawn(spawn.params.action);
if (isPausedNow && !spawn.isPaused) {
if (spawn.lastSpawnTime === null) { if (spawn.lastSpawnTime === null) {
spawn.remainingTime = Math.max(0, spawn.params.intervalMs - (currentTime - spawn.startTime)); spawn.remainingTime = Math.max(0, (spawn.params.intervalMs / speed) - (currentTime - spawn.startTime));
} else { } else {
spawn.remainingTime = Math.max(0, spawn.params.intervalMs - (currentTime - spawn.lastSpawnTime)); spawn.remainingTime = Math.max(0, (spawn.params.intervalMs / speed) - (currentTime - spawn.lastSpawnTime));
} }
spawn.pauseStartTime = currentTime; spawn.pauseStartTime = currentTime;
spawn.isPaused = true; spawn.isPaused = true;
} else if (!isPaused && spawn.isPaused) { hasChanges = true;
if (spawn.remainingTime > 0) { } else if (!isPausedNow && spawn.isPaused) {
if (spawn.lastSpawnTime === null) { const pauseDuration = currentTime - spawn.pauseStartTime;
spawn.startTime = currentTime - (spawn.params.intervalMs - spawn.remainingTime); if (spawn.lastSpawnTime === null) {
} else { spawn.startTime += pauseDuration;
spawn.lastSpawnTime = currentTime - (spawn.params.intervalMs - spawn.remainingTime); } else {
} spawn.lastSpawnTime += pauseDuration;
} }
spawn.isPaused = false; spawn.isPaused = false;
spawn.pauseStartTime = 0; spawn.pauseStartTime = 0;
spawn.remainingTime = 0; spawn.remainingTime = 0;
hasChanges = true;
} }
}); });
}, [isPaused]);
useFrame(() => { if (isPlaying && !isReset && !isPaused) {
if (!isPlaying || isPaused || isReset) return; activeSpawns.forEach((spawn, actionUuid) => {
if (spawn.isPaused) return;
const currentTime = performance.now(); const { material, intervalMs, totalCount, action } = spawn.params;
const completedActions: string[] = []; const adjustedInterval = intervalMs / speed;
const isFirstSpawn = spawn.lastSpawnTime === null;
activeSpawns.current.forEach((spawn, actionUuid) => { // First spawn
const { material, intervalMs, totalCount, action } = spawn.params; if (isFirstSpawn) {
const isFirstSpawn = spawn.lastSpawnTime === null; const elapsed = currentTime - spawn.startTime;
if (elapsed >= adjustedInterval) {
const createdMaterial = createNewMaterial(material, action);
if (createdMaterial) {
spawnLogStatus(createdMaterial.materialId, `[${elapsed.toFixed(2)}ms] Spawned ${material} (1/${totalCount})`);
}
spawn.lastSpawnTime = currentTime;
spawn.spawnCount = 1;
hasChanges = true;
// First spawn if (totalCount <= 1) {
if (isFirstSpawn) { completedActions.push(actionUuid);
const elapsed = currentTime - spawn.startTime; }
if (elapsed >= intervalMs) {
const createdMaterial = createNewMaterial(material, action);
if (createdMaterial) {
spawnLogStatus(createdMaterial.materialId, `[${elapsed.toFixed(2)}ms] Spawned ${material} (1/${totalCount})`);
} }
spawn.lastSpawnTime = currentTime; return;
spawn.spawnCount = 1; }
if (totalCount <= 1) { // Subsequent spawns
completedActions.push(actionUuid); 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.materialId, `[${timeSinceLast.toFixed(2)}ms] Spawned ${material} (${count}/${totalCount})`);
}
spawn.lastSpawnTime = currentTime;
spawn.spawnCount = count;
hasChanges = true;
if (count >= totalCount) {
completedActions.push(actionUuid);
}
} }
} }
return; });
} }
// Subsequent spawns if (hasChanges || completedActions.length > 0) {
if (spawn.lastSpawnTime !== null) { setActiveSpawns(prevSpawns => {
const timeSinceLast = currentTime - spawn.lastSpawnTime; const newSpawns = new Map(prevSpawns);
if (timeSinceLast >= intervalMs) {
const count = spawn.spawnCount + 1;
const createdMaterial = createNewMaterial(material, action);
if (createdMaterial) {
spawnLogStatus(createdMaterial.materialId, `[${timeSinceLast.toFixed(2)}ms] Spawned ${material} (${count}/${totalCount})`);
}
spawn.lastSpawnTime = currentTime;
spawn.spawnCount = count;
if (count >= totalCount) { completedActions.forEach(actionUuid => {
completedActions.push(actionUuid); newSpawns.delete(actionUuid);
} });
}
}
});
completedActions.forEach(actionUuid => { return newSpawns;
activeSpawns.current.delete(actionUuid); });
}); }
}); });
const handleSpawn = useCallback((action: ConveyorAction) => { const handleSpawn = useCallback((action: ConveyorAction) => {
@ -178,23 +195,29 @@ export function useSpawnHandler() {
const { material, spawnInterval = 0, spawnCount = 1, actionUuid } = action; const { material, spawnInterval = 0, spawnCount = 1, actionUuid } = action;
const intervalMs = spawnInterval * 1000; const intervalMs = spawnInterval * 1000;
if (activeSpawns.current.has(actionUuid)) { setActiveSpawns(prevSpawns => {
activeSpawns.current.delete(actionUuid); const newSpawns = new Map(prevSpawns);
}
activeSpawns.current.set(actionUuid, { if (newSpawns.has(actionUuid)) {
lastSpawnTime: null, newSpawns.delete(actionUuid);
startTime: performance.now(), }
spawnCount: 0,
params: { newSpawns.set(actionUuid, {
material, lastSpawnTime: null,
intervalMs, startTime: performance.now(),
totalCount: spawnCount, spawnCount: 0,
action: action params: {
}, material,
pauseStartTime: 0, intervalMs,
remainingTime: 0, totalCount: spawnCount,
isPaused: false action: action
},
pauseStartTime: 0,
remainingTime: 0,
isPaused: false
});
return newSpawns;
}); });
}, []); }, []);

View File

@ -1,21 +1,15 @@
import { useCallback } from "react"; import { useCallback } from "react";
import { useMaterialStore } from "../../../../../store/simulation/useMaterialStore"; import { useMaterialStore } from "../../../../../store/simulation/useMaterialStore";
import { useProductStore } from "../../../../../store/simulation/useProductStore";
import { useSelectedProduct } from "../../../../../store/simulation/useSimulationStore";
import { usePlayButtonStore } from "../../../../../store/usePlayButtonStore";
export function useSwapHandler() { export function useSwapHandler() {
const { addMaterial, getMaterialById, setMaterial } = useMaterialStore(); const { getMaterialById, setMaterial } = useMaterialStore();
const { getPointUuidByActionUuid } = useProductStore();
const { selectedProduct } = useSelectedProduct();
const { isPlaying } = usePlayButtonStore();
const swapLogStatus = (materialUuid: string, status: string) => { const swapLogStatus = (materialUuid: string, status: string) => {
// console.log(`${materialUuid}, ${status}`); // console.log(`${materialUuid}, ${status}`);
} }
const handleSwap = useCallback((action: ConveyorAction, materialId?: string) => { const handleSwap = useCallback((action: ConveyorAction, materialId?: string) => {
if (!action || action.actionType !== 'swap' || !isPlaying || !materialId) return; if (!action || action.actionType !== 'swap' || !materialId) return;
const { material: newMaterialType } = action; const { material: newMaterialType } = action;
const material = getMaterialById(materialId); const material = getMaterialById(materialId);
@ -24,7 +18,7 @@ export function useSwapHandler() {
setMaterial(material.materialId, newMaterialType); setMaterial(material.materialId, newMaterialType);
swapLogStatus(material.materialId, `Swapped to ${newMaterialType}`); swapLogStatus(material.materialId, `Swapped to ${newMaterialType}`);
}, [addMaterial, getMaterialById, getPointUuidByActionUuid, isPlaying, setMaterial, selectedProduct.productId]); }, [getMaterialById, setMaterial]);
return { return {
handleSwap, handleSwap,

View File

@ -2,10 +2,12 @@ import { useEffect, useCallback } from "react";
import { useSpawnHandler } from "./actionHandler/useSpawnHandler"; import { useSpawnHandler } from "./actionHandler/useSpawnHandler";
import { useSwapHandler } from "./actionHandler/useSwapHandler"; import { useSwapHandler } from "./actionHandler/useSwapHandler";
import { useDelayHandler } from "./actionHandler/useDelayHandler"; import { useDelayHandler } from "./actionHandler/useDelayHandler";
import { useDespawnHandler } from "./actionHandler/useDespawnHandler";
export function useConveyorActions() { export function useConveyorActions() {
const { handleSpawn, clearCurrentSpawn } = useSpawnHandler(); const { handleSpawn, clearCurrentSpawn } = useSpawnHandler();
const { handleSwap } = useSwapHandler(); const { handleSwap } = useSwapHandler();
const { handleDespawn } = useDespawnHandler();
const { handleDelay, cleanupDelay } = useDelayHandler(); const { handleDelay, cleanupDelay } = useDelayHandler();
const handleDefaultAction = useCallback((action: ConveyorAction) => { const handleDefaultAction = useCallback((action: ConveyorAction) => {
@ -23,9 +25,9 @@ export function useConveyorActions() {
handleDelay(action, materialId); handleDelay(action, materialId);
}, [handleDelay]); }, [handleDelay]);
const handleDespawnAction = useCallback((action: ConveyorAction) => { const handleDespawnAction = useCallback((action: ConveyorAction, materialId?: string) => {
console.log(`Despawning material`); handleDespawn(action, materialId)
}, []); }, [handleDespawn]);
const handleConveyorAction = useCallback((action: ConveyorAction, materialId?: string) => { const handleConveyorAction = useCallback((action: ConveyorAction, materialId?: string) => {
if (!action) return; if (!action) return;
@ -44,7 +46,7 @@ export function useConveyorActions() {
handleDelayAction(action, materialId); handleDelayAction(action, materialId);
break; break;
case 'despawn': case 'despawn':
handleDespawnAction(action); handleDespawnAction(action, materialId);
break; break;
default: default:
console.warn(`Unknown conveyor action type: ${action.actionType}`); console.warn(`Unknown conveyor action type: ${action.actionType}`);

View File

@ -46,12 +46,24 @@ function MaterialAnimator({
useEffect(() => { useEffect(() => {
if (!isPlaying || !material.next?.pointUuid) { if (!isPlaying || !material.next?.pointUuid) {
setIsAnimating(false); 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; return;
} }
const newTarget = getWorldPosition(material.next.pointUuid); const newTarget = getWorldPosition(material.next.pointUuid);
if (newTarget && matRef.current) { if (newTarget && matRef.current && !material.isPaused) {
animationState.current.startPosition.copy(matRef.current.position); animationState.current.startPosition.copy(matRef.current.position);
animationState.current.totalDistance = animationState.current.startPosition.distanceTo(newTarget); animationState.current.totalDistance = animationState.current.startPosition.distanceTo(newTarget);
animationState.current.startTime = performance.now() - animationState.current.pausedTime; animationState.current.startTime = performance.now() - animationState.current.pausedTime;
@ -60,7 +72,7 @@ function MaterialAnimator({
setTargetPosition(newTarget); setTargetPosition(newTarget);
setIsAnimating(true); setIsAnimating(true);
} }
}, [material.next?.pointUuid, isPlaying]); }, [material, isPlaying]);
useEffect(() => { useEffect(() => {
if (shouldPause) { if (shouldPause) {

View File

@ -83,14 +83,6 @@ function MaterialInstance({ material }: { material: MaterialSchema }) {
} }
} }
useEffect(() => {
// console.log('material: ', material);
if (material.current && material.next) {
// console.log('current: ', material.current.pointUuid);
// console.log('next: ', material.next.pointUuid);
}
}, [material])
const callTrigger = () => { const callTrigger = () => {
if (!material.next) return; if (!material.next) return;
const fromModel = getEventByModelUuid(selectedProduct.productId, material.next.modelUuid); const fromModel = getEventByModelUuid(selectedProduct.productId, material.next.modelUuid);

View File

@ -3,12 +3,16 @@ import MaterialInstance from './instance/materialInstance'
import { useMaterialStore } from '../../../../store/simulation/useMaterialStore'; import { useMaterialStore } from '../../../../store/simulation/useMaterialStore';
function MaterialInstances() { function MaterialInstances() {
const { materials } = useMaterialStore(); const { materials, materialHistory } = useMaterialStore();
useEffect(() => { useEffect(() => {
// console.log('materials: ', materials); // console.log('materials: ', materials);
}, [materials]) }, [materials])
useEffect(() => {
// console.log('materialHistory: ', materialHistory);
}, [materialHistory])
return ( return (
<> <>

View File

@ -8,6 +8,7 @@ import { getAllProductsApi } from '../../../services/simulation/getallProductsAp
import { useVehicleStore } from '../../../store/simulation/useVehicleStore'; import { useVehicleStore } from '../../../store/simulation/useVehicleStore';
import { useArmBotStore } from '../../../store/simulation/useArmBotStore'; import { useArmBotStore } from '../../../store/simulation/useArmBotStore';
import { useConveyorStore } from '../../../store/simulation/useConveyorStore'; import { useConveyorStore } from '../../../store/simulation/useConveyorStore';
import { useResetButtonStore } from '../../../store/usePlayButtonStore';
function Products() { function Products() {
const { products, getProductById, addProduct, setProducts } = useProductStore(); const { products, getProductById, addProduct, setProducts } = useProductStore();
@ -15,6 +16,7 @@ function Products() {
const { addVehicle, clearvehicles } = useVehicleStore(); const { addVehicle, clearvehicles } = useVehicleStore();
const { addArmBot, clearArmBots } = useArmBotStore(); const { addArmBot, clearArmBots } = useArmBotStore();
const { addConveyor, clearConveyors } = useConveyorStore(); const { addConveyor, clearConveyors } = useConveyorStore();
const { isReset } = useResetButtonStore();
useEffect(() => { useEffect(() => {
const email = localStorage.getItem('email') const email = localStorage.getItem('email')
@ -45,7 +47,7 @@ function Products() {
}); });
} }
} }
}, [selectedProduct, products]); }, [selectedProduct, products, isReset]);
useEffect(() => { useEffect(() => {
if (selectedProduct.productId) { if (selectedProduct.productId) {
@ -59,7 +61,7 @@ function Products() {
}); });
} }
} }
}, [selectedProduct, products]); }, [selectedProduct, products, isReset]);
useEffect(() => { useEffect(() => {
if (selectedProduct.productId) { if (selectedProduct.productId) {
@ -73,7 +75,7 @@ function Products() {
}); });
} }
} }
}, [selectedProduct, products]); }, [selectedProduct, products, isReset]);
return ( return (
<> <>

View File

@ -14,7 +14,7 @@ export default function MaterialAnimator({ ikSolver, armBot, currentPhase }: Mat
const [isRendered, setIsRendered] = useState<boolean>(false); const [isRendered, setIsRendered] = useState<boolean>(false);
useEffect(() => { useEffect(() => {
if (currentPhase === "start-to-end") { if (currentPhase === "start-to-end" || currentPhase === "dropping") {
setIsRendered(true); setIsRendered(true);
} else { } else {
setIsRendered(false); setIsRendered(false);
@ -40,7 +40,7 @@ export default function MaterialAnimator({ ikSolver, armBot, currentPhase }: Mat
const direction = new THREE.Vector3(); const direction = new THREE.Vector3();
direction.subVectors(boneWorldPos, boneTargetWorldPos).normalize(); direction.subVectors(boneWorldPos, boneTargetWorldPos).normalize();
const downwardDirection = direction.clone().negate(); const downwardDirection = direction.clone().negate();
const adjustedPosition = boneWorldPos.clone().addScaledVector(downwardDirection, -0.01); const adjustedPosition = boneWorldPos.clone().addScaledVector(downwardDirection, -0.01);
//set position //set position

View File

@ -266,7 +266,7 @@ function RoboticArmAnimator({ HandleCallback, restPosition, ikSolver, targetBone
return ( return (
<> <>
{customCurvePoints && customCurvePoints?.length >= 2 && currentPath && isPlaying && ( {customCurvePoints && customCurvePoints?.length >= 2 && currentPath && isPlaying && (
<mesh rotation={armBot.rotation} position={armBot.position}> <mesh rotation={armBot.rotation} position={armBot.position} visible={false}>
<Line <Line
points={customCurvePoints.map((p) => [p.x, p.y, p.z] as [number, number, number])} points={customCurvePoints.map((p) => [p.x, p.y, p.z] as [number, number, number])}
color="green" color="green"
@ -278,13 +278,50 @@ function RoboticArmAnimator({ HandleCallback, restPosition, ikSolver, targetBone
<group <group
position={[armBot.position[0], armBot.position[1] + 1.5, armBot.position[2]]} position={[armBot.position[0], armBot.position[1] + 1.5, armBot.position[2]]}
rotation={[armBot.rotation[0], armBot.rotation[1], armBot.rotation[2]]} rotation={[armBot.rotation[0], armBot.rotation[1], armBot.rotation[2]]}
visible={false}
> >
{/* Green ring */} {/* Green ring */}
<mesh rotation={[-Math.PI / 2, 0, 0]}> <mesh rotation={[-Math.PI / 2, 0, 0]} visible={false}>
<ringGeometry args={[CIRCLE_RADIUS, CIRCLE_RADIUS + 0.02, 64]} /> <ringGeometry args={[CIRCLE_RADIUS, CIRCLE_RADIUS + 0.02, 64]} />
<meshBasicMaterial color="green" side={THREE.DoubleSide} /> <meshBasicMaterial color="green" side={THREE.DoubleSide} />
</mesh> </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> </group>
</> </>

View File

@ -1,13 +1,16 @@
import React, { useEffect, useRef, useState } from 'react' import React, { useEffect, useRef, useState } from 'react'
import * as THREE from "three";
import { useThree } from "@react-three/fiber";
import IKInstance from '../ikInstance/ikInstance'; import IKInstance from '../ikInstance/ikInstance';
import RoboticArmAnimator from '../animator/roboticArmAnimator'; import RoboticArmAnimator from '../animator/roboticArmAnimator';
import { usePauseButtonStore, usePlayButtonStore, useResetButtonStore } from '../../../../../store/usePlayButtonStore';
import { useArmBotStore } from '../../../../../store/simulation/useArmBotStore';
import armModel from "../../../../../assets/gltf-glb/rigged/ik_arm_1.glb";
import { useThree } from "@react-three/fiber";
import * as THREE from "three";
import MaterialAnimator from '../animator/materialAnimator'; import MaterialAnimator from '../animator/materialAnimator';
import armModel from "../../../../../assets/gltf-glb/rigged/ik_arm_1.glb";
import { usePauseButtonStore, usePlayButtonStore, useResetButtonStore } from '../../../../../store/usePlayButtonStore';
import { useMaterialStore } from '../../../../../store/simulation/useMaterialStore';
import { useArmBotStore } from '../../../../../store/simulation/useArmBotStore';
import { useProductStore } from '../../../../../store/simulation/useProductStore';
import { useSelectedProduct } from '../../../../../store/simulation/useSimulationStore';
import { useTriggerHandler } from '../../../triggers/triggerHandler/useTriggerHandler';
function RoboticArmInstance({ armBot }: { armBot: ArmBotStatus }) { function RoboticArmInstance({ armBot }: { armBot: ArmBotStatus }) {
@ -21,8 +24,12 @@ function RoboticArmInstance({ armBot }: { armBot: ArmBotStatus }) {
const pauseTimeRef = useRef<number | null>(null); const pauseTimeRef = useRef<number | null>(null);
const isPausedRef = useRef<boolean>(false); const isPausedRef = useRef<boolean>(false);
let startTime: number; let startTime: number;
//zustand
const { addCurrentAction, setArmBotActive, setArmBotState, removeCurrentAction } = useArmBotStore(); const { setArmBotActive, setArmBotState, removeCurrentAction } = useArmBotStore();
const { setIsVisible } = useMaterialStore();
const { selectedProduct } = useSelectedProduct();
const { getActionByUuid } = useProductStore();
const { triggerPointActions } = useTriggerHandler();
const { isPlaying } = usePlayButtonStore(); const { isPlaying } = usePlayButtonStore();
const { isReset } = useResetButtonStore(); const { isReset } = useResetButtonStore();
const { isPaused } = usePauseButtonStore(); const { isPaused } = usePauseButtonStore();
@ -65,6 +72,10 @@ function RoboticArmInstance({ armBot }: { armBot: ArmBotStatus }) {
if (curve) { if (curve) {
logStatus(armBot.modelUuid, "picking the object"); logStatus(armBot.modelUuid, "picking the object");
setPath(curve.points.map(point => [point.x, point.y, point.z])) setPath(curve.points.map(point => [point.x, point.y, point.z]))
if (armBot.currentAction) {
setIsVisible(armBot.currentAction.materialId || '', false);
}
} }
} }
logStatus(armBot.modelUuid, "Moving armBot from start point to end position.") logStatus(armBot.modelUuid, "Moving armBot from start point to end position.")
@ -80,6 +91,18 @@ function RoboticArmInstance({ armBot }: { armBot: ArmBotStatus }) {
if (curve) { if (curve) {
logStatus(armBot.modelUuid, "dropping the object"); logStatus(armBot.modelUuid, "dropping the object");
setPath(curve.points.map(point => [point.x, point.y, point.z])); setPath(curve.points.map(point => [point.x, point.y, point.z]));
if (armBot.currentAction) {
setIsVisible(armBot.currentAction.materialId || '', true);
}
if (armBot.currentAction) {
const action = getActionByUuid(selectedProduct.productId, armBot.currentAction.actionUuid);
if (action && armBot.currentAction.materialId) {
triggerPointActions(action, armBot.currentAction.materialId)
removeCurrentAction(armBot.modelUuid)
}
}
} }
} }
logStatus(armBot.modelUuid, "Moving armBot from end point to rest position.") logStatus(armBot.modelUuid, "Moving armBot from end point to rest position.")
@ -137,10 +160,6 @@ function RoboticArmInstance({ armBot }: { armBot: ArmBotStatus }) {
//Waiting for trigger. //Waiting for trigger.
else if (armBot && !armBot.isActive && armBot.state === "idle" && currentPhase === "rest" && !armBot.currentAction) { else if (armBot && !armBot.isActive && armBot.state === "idle" && currentPhase === "rest" && !armBot.currentAction) {
logStatus(armBot.modelUuid, "Waiting to trigger CurrentAction") logStatus(armBot.modelUuid, "Waiting to trigger CurrentAction")
const timeoutId = setTimeout(() => {
addCurrentAction(armBot.modelUuid, armBot.point.actions[0].actionUuid, 'Material 2');
}, 3000);
return () => clearTimeout(timeoutId);
} }
//Moving to pickup point //Moving to pickup point
else if (armBot && !armBot.isActive && armBot.state === "idle" && currentPhase === "rest" && armBot.currentAction) { else if (armBot && !armBot.isActive && armBot.state === "idle" && currentPhase === "rest" && armBot.currentAction) {
@ -219,7 +238,6 @@ function RoboticArmInstance({ armBot }: { armBot: ArmBotStatus }) {
setArmBotState(armBot.modelUuid, "idle") setArmBotState(armBot.modelUuid, "idle")
setCurrentPhase("rest"); setCurrentPhase("rest");
setPath([]) setPath([])
removeCurrentAction(armBot.modelUuid)
} }
} }
const logStatus = (id: string, status: string) => { const logStatus = (id: string, status: string) => {
@ -228,12 +246,12 @@ function RoboticArmInstance({ armBot }: { armBot: ArmBotStatus }) {
return ( return (
<> <>
{!isReset && isPlaying && ( {!isReset && isPlaying && (
<> <>
<IKInstance modelUrl={armModel} setIkSolver={setIkSolver} ikSolver={ikSolver} armBot={armBot} groupRef={groupRef} /> <IKInstance modelUrl={armModel} setIkSolver={setIkSolver} ikSolver={ikSolver} armBot={armBot} groupRef={groupRef} />
<RoboticArmAnimator HandleCallback={HandleCallback} restPosition={restPosition} ikSolver={ikSolver} targetBone={targetBone} armBot={armBot} <RoboticArmAnimator HandleCallback={HandleCallback} restPosition={restPosition} ikSolver={ikSolver} targetBone={targetBone} armBot={armBot}
logStatus={logStatus} path={path} currentPhase={currentPhase} /> logStatus={logStatus} path={path} currentPhase={currentPhase} />
</> </>
)} )}
<MaterialAnimator ikSolver={ikSolver} armBot={armBot} currentPhase={currentPhase} /> <MaterialAnimator ikSolver={ikSolver} armBot={armBot} currentPhase={currentPhase} />
</> </>
) )

View File

@ -0,0 +1,114 @@
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') {
pointMap.set(event.point.uuid, event.point);
allPoints.push(event.point);
} else if (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 function determineExecutionSequences(products: productsSchema): 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

@ -2,6 +2,7 @@ import { useEffect } from 'react';
import { useProductStore } from '../../../store/simulation/useProductStore'; import { useProductStore } from '../../../store/simulation/useProductStore';
import { useActionHandler } from '../actions/useActionHandler'; import { useActionHandler } from '../actions/useActionHandler';
import { usePlayButtonStore, useResetButtonStore } from '../../../store/usePlayButtonStore'; import { usePlayButtonStore, useResetButtonStore } from '../../../store/usePlayButtonStore';
import { determineExecutionOrder } from './functions/determineExecutionOrder';
function Simulator() { function Simulator() {
const { products } = useProductStore(); const { products } = useProductStore();
@ -11,237 +12,21 @@ function Simulator() {
useEffect(() => { useEffect(() => {
if (!isPlaying || isReset) return; if (!isPlaying || isReset) return;
const executionOrder = determineExecutionOrder(products); const executionOrder = determineExecutionOrder(products);
executionOrder.map(point => { executionOrder.map(point => {
const action = 'actions' in point ? point.actions[0] : point.action; const action = 'actions' in point ? point.actions[0] : point.action;
handleAction(action); handleAction(action);
}); });
}, [products, handleAction, isPlaying, isReset]); }, [products, isPlaying, isReset]);
function determineExecutionOrder(products: productsSchema): PointsScheme[] { return (
// 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') {
pointMap.set(event.point.uuid, event.point);
allPoints.push(event.point);
} else if (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;
}
function determineExecutionSequences(products: productsSchema): 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;
}
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 [];
}
return <></>;
} }
export default Simulator; export default Simulator;

View File

@ -3,12 +3,14 @@ import { useActionHandler } from '../../actions/useActionHandler';
import { useProductStore } from '../../../../store/simulation/useProductStore'; import { useProductStore } from '../../../../store/simulation/useProductStore';
import { useSelectedProduct } from '../../../../store/simulation/useSimulationStore'; import { useSelectedProduct } from '../../../../store/simulation/useSimulationStore';
import { useMaterialStore } from '../../../../store/simulation/useMaterialStore'; import { useMaterialStore } from '../../../../store/simulation/useMaterialStore';
import { useArmBotStore } from '../../../../store/simulation/useArmBotStore';
export function useTriggerHandler() { export function useTriggerHandler() {
const { getEventByTriggerUuid, getEventByModelUuid } = useProductStore();
const { handleAction } = useActionHandler(); const { handleAction } = useActionHandler();
const { setCurrentLocation, setNextLocation, getMaterialById } = useMaterialStore();
const { selectedProduct } = useSelectedProduct(); const { selectedProduct } = useSelectedProduct();
const { getEventByTriggerUuid, getEventByModelUuid, getActionByUuid, getModelUuidByActionUuid } = useProductStore();
const { addCurrentAction, getArmBotById } = useArmBotStore();
const { setCurrentLocation, setNextLocation, getMaterialById, setIsPaused, setEndTime } = useMaterialStore();
const handleTrigger = (trigger: TriggerSchema, action: Action, materialId: string) => { const handleTrigger = (trigger: TriggerSchema, action: Action, materialId: string) => {
@ -46,7 +48,38 @@ export function useTriggerHandler() {
} else if (toEvent?.type === 'roboticArm') { } else if (toEvent?.type === 'roboticArm') {
// Transfer to Robotic Arm // Transfer to Robotic Arm
if (trigger.triggeredAsset && trigger.triggeredAsset.triggeredPoint && trigger.triggeredAsset.triggeredAction) {
const material = getMaterialById(materialId);
if (material) {
if (material.next) {
const armBot = getArmBotById(trigger.triggeredAsset?.triggeredModel.modelUuid);
if (armBot) {
if (armBot.isActive === false && armBot.state === 'idle') {
setCurrentLocation(material.materialId, {
modelUuid: material.next.modelUuid,
pointUuid: material.next.pointUuid,
actionUuid: trigger.triggeredAsset?.triggeredAction?.actionUuid,
});
setNextLocation(material.materialId, null);
setIsPaused(material.materialId, true);
addCurrentAction(
trigger.triggeredAsset?.triggeredModel.modelUuid,
trigger.triggeredAsset?.triggeredAction?.actionUuid,
material.materialType,
material.materialId
);
} else {
// Event Manager Needed
}
}
}
handleAction(action, materialId);
}
}
} else if (toEvent?.type === 'storageUnit') { } else if (toEvent?.type === 'storageUnit') {
// Transfer to Storage Unit // Transfer to Storage Unit
@ -88,6 +121,31 @@ export function useTriggerHandler() {
} else if (fromEvent?.type === 'roboticArm') { } else if (fromEvent?.type === 'roboticArm') {
if (toEvent?.type === 'transfer') { if (toEvent?.type === 'transfer') {
// Robotic Arm to Transfer // Robotic Arm to Transfer
if (trigger.triggeredAsset && trigger.triggeredAsset.triggeredPoint && trigger.triggeredAsset.triggeredAction) {
const material = getMaterialById(materialId);
if (material) {
setIsPaused(material.materialId, false);
setCurrentLocation(material.materialId, {
modelUuid: trigger.triggeredAsset.triggeredModel.modelUuid,
pointUuid: trigger.triggeredAsset.triggeredPoint.pointUuid,
actionUuid: trigger.triggeredAsset?.triggeredAction?.actionUuid,
});
const action = getActionByUuid(selectedProduct.productId, trigger.triggeredAsset.triggeredAction.actionUuid);
if (action && action.triggers.length > 0 &&
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, material.materialId);
}
}
}
} else if (toEvent?.type === 'vehicle') { } else if (toEvent?.type === 'vehicle') {
// Robotic Arm to Vehicle // Robotic Arm to Vehicle
@ -122,26 +180,75 @@ export function useTriggerHandler() {
} }
} }
const handleFinalAction = (action: Action, materialId: string) => {
if (!action) return;
const modelUuid = getModelUuidByActionUuid(selectedProduct.productId, action.actionUuid);
if (!modelUuid) return;
const finalModel = getEventByModelUuid(selectedProduct.productId, modelUuid);
if (!finalModel) return;
const material = getMaterialById(materialId);
if (finalModel.type === 'transfer') {
// Storage Unit to Transfer
if (material) {
const currentTime = performance.now();
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) => { const triggerPointActions = useCallback((action: Action, materialId: string) => {
if (!action) return; if (!action) return;
action.triggers.forEach(trigger => { if (action.triggers.length > 0) {
switch (trigger.triggerType) {
case 'onStart': action.triggers.forEach(trigger => {
break; switch (trigger.triggerType) {
case 'onComplete': case 'onStart':
handleTrigger(trigger, action, materialId); break;
break; case 'onComplete':
case 'onStop': handleTrigger(trigger, action, materialId);
break; break;
case 'onError': case 'onStop':
break; break;
case 'delay': case 'onError':
break; break;
default: case 'delay':
console.warn(`Unknown trigger type: ${trigger.triggerType}`); break;
} default:
}); console.warn(`Unknown trigger type: ${trigger.triggerType}`);
}
});
} else {
handleFinalAction(action, materialId);
}
}, []); }, []);
return { return {

View File

@ -12,7 +12,7 @@ interface ArmBotStore {
) => void; ) => void;
clearArmBots: () => void; clearArmBots: () => void;
addCurrentAction: (modelUuid: string, actionUuid: string, materialType: string) => void; addCurrentAction: (modelUuid: string, actionUuid: string, materialType: string, materialId: string) => void;
removeCurrentAction: (modelUuid: string) => void; removeCurrentAction: (modelUuid: string) => void;
addAction: (modelUuid: string, action: RoboticArmPointSchema['actions'][number]) => void; addAction: (modelUuid: string, action: RoboticArmPointSchema['actions'][number]) => void;
@ -75,7 +75,7 @@ export const useArmBotStore = create<ArmBotStore>()(
}); });
}, },
addCurrentAction: (modelUuid, actionUuid, materialType) => { addCurrentAction: (modelUuid, actionUuid, materialType, materialId) => {
set((state) => { set((state) => {
const armBot = state.armBots.find(a => a.modelUuid === modelUuid); const armBot = state.armBots.find(a => a.modelUuid === modelUuid);
if (armBot) { if (armBot) {
@ -84,7 +84,8 @@ export const useArmBotStore = create<ArmBotStore>()(
armBot.currentAction = { armBot.currentAction = {
actionUuid: action.actionUuid, actionUuid: action.actionUuid,
actionName: action.actionName, actionName: action.actionName,
materialType: materialType materialType: materialType,
materialId:materialId
}; };
} }
} }

View File

@ -3,6 +3,7 @@ import { immer } from 'zustand/middleware/immer';
type MaterialsStore = { type MaterialsStore = {
materials: MaterialsSchema; materials: MaterialsSchema;
materialHistory: MaterialHistorySchema;
addMaterial: (material: MaterialSchema) => MaterialSchema | undefined; addMaterial: (material: MaterialSchema) => MaterialSchema | undefined;
removeMaterial: (materialId: string) => MaterialSchema | undefined; removeMaterial: (materialId: string) => MaterialSchema | undefined;
@ -20,20 +21,20 @@ type MaterialsStore = {
setNextLocation: ( setNextLocation: (
materialId: string, materialId: string,
location?: { location: {
modelUuid: string; modelUuid: string;
pointUuid: string; pointUuid: string;
} | null } | null
) => MaterialSchema | undefined; ) => MaterialSchema | undefined;
setMaterial: (materialId: string, materialType: string) => MaterialSchema | undefined; setMaterial: (materialId: string, materialType: string) => MaterialSchema | undefined;
setStartTime: (materialId: string, startTime: string) => MaterialSchema | undefined; setStartTime: (materialId: string, startTime: number) => MaterialSchema | undefined;
setEndTime: (materialId: string, endTime: string) => MaterialSchema | undefined; setEndTime: (materialId: string, endTime: number) => MaterialSchema | undefined;
setCost: (materialId: string, cost: number) => MaterialSchema | undefined; setCost: (materialId: string, cost: number) => MaterialSchema | undefined;
setWeight: (materialId: string, weight: number) => MaterialSchema | undefined; setWeight: (materialId: string, weight: number) => MaterialSchema | undefined;
setIsActive: (materialId: string, isActive: boolean) => MaterialSchema | undefined; setIsActive: (materialId: string, isActive: boolean) => MaterialSchema | undefined;
setIsVisible: (materialId: string, isVisible: boolean) => MaterialSchema | undefined; setIsVisible: (materialId: string, isVisible: boolean) => MaterialSchema | undefined;
setIsPaused: (materialId: string, isPlaying: boolean) => MaterialSchema | undefined; setIsPaused: (materialId: string, isPaused: boolean) => MaterialSchema | undefined;
setIsRendered: (materialId: string, isRendered: boolean) => MaterialSchema | undefined; setIsRendered: (materialId: string, isRendered: boolean) => MaterialSchema | undefined;
getMaterialById: (materialId: string) => MaterialSchema | undefined; getMaterialById: (materialId: string) => MaterialSchema | undefined;
@ -41,11 +42,13 @@ type MaterialsStore = {
getMaterialByCurrentPointUuid: (currentPointUuid: string) => MaterialSchema | undefined; getMaterialByCurrentPointUuid: (currentPointUuid: string) => MaterialSchema | undefined;
getMaterialsByPoint: (pointUuid: string) => MaterialSchema[]; getMaterialsByPoint: (pointUuid: string) => MaterialSchema[];
getMaterialsByModel: (modelUuid: string) => MaterialSchema[]; getMaterialsByModel: (modelUuid: string) => MaterialSchema[];
getMaterialHistory: () => MaterialHistorySchema;
}; };
export const useMaterialStore = create<MaterialsStore>()( export const useMaterialStore = create<MaterialsStore>()(
immer((set, get) => ({ immer((set, get) => ({
materials: [], materials: [],
materialHistory: [],
addMaterial: (material) => { addMaterial: (material) => {
let updatedMaterial: MaterialSchema | undefined; let updatedMaterial: MaterialSchema | undefined;
@ -60,8 +63,13 @@ export const useMaterialStore = create<MaterialsStore>()(
set((state) => { set((state) => {
const material = state.materials.find(m => m.materialId === materialId); const material = state.materials.find(m => m.materialId === materialId);
if (material) { if (material) {
state.materials.filter(m => m.materialId !== material.materialId); state.materials = state.materials.filter(m => m.materialId !== materialId);
updatedMaterial = JSON.parse(JSON.stringify(material)); updatedMaterial = JSON.parse(JSON.stringify(material));
state.materialHistory.push({
material,
removedAt: new Date().toISOString()
});
} }
}); });
return updatedMaterial; return updatedMaterial;
@ -102,7 +110,7 @@ export const useMaterialStore = create<MaterialsStore>()(
set((state) => { set((state) => {
const material = state.materials.find(m => m.materialId === materialId); const material = state.materials.find(m => m.materialId === materialId);
if (material) { if (material) {
material.next = location || undefined; material.next = location;
updatedMaterial = JSON.parse(JSON.stringify(material)); updatedMaterial = JSON.parse(JSON.stringify(material));
} }
}); });
@ -242,5 +250,9 @@ export const useMaterialStore = create<MaterialsStore>()(
m.next?.modelUuid === modelUuid m.next?.modelUuid === modelUuid
); );
}, },
getMaterialHistory: () => {
return get().materialHistory;
},
})) }))
); );

View File

@ -171,6 +171,7 @@ interface ArmBotStatus extends RoboticArmEventSchema {
actionUuid: string; actionUuid: string;
actionName: string; actionName: string;
materialType: string | null; materialType: string | null;
materialId: string | null;
}; };
} }
@ -201,8 +202,8 @@ interface MaterialSchema {
isVisible: boolean; isVisible: boolean;
isPaused: boolean; isPaused: boolean;
isRendered: boolean; isRendered: boolean;
startTime?: string; startTime?: number;
endTime?: string; endTime?: number;
cost?: number; cost?: number;
weight?: number; weight?: number;
@ -215,7 +216,14 @@ interface MaterialSchema {
next?: { next?: {
modelUuid: string; modelUuid: string;
pointUuid: string; pointUuid: string;
}; } | null;
} }
type MaterialsSchema = MaterialSchema[]; type MaterialsSchema = MaterialSchema[];
interface MaterialHistoryEntry {
material: MaterialSchema;
removedAt: string;
}
type MaterialHistorySchema = MaterialHistoryEntry[];