v2 #78

Merged
Vishnu merged 5 commits from v2 into main 2025-05-05 09:21:34 +00:00
24 changed files with 990 additions and 555 deletions
app/src
components
layout/sidebarRight/properties/eventProperties
ui/simulation
modules
collaboration/functions
simulation
store/simulation
types

View File

@ -237,7 +237,7 @@ function ConveyorMechanics() {
{activeOption === "spawn" && ( {activeOption === "spawn" && (
<SpawnAction <SpawnAction
onChangeCount={handleSpawnCountChange} onChangeCount={handleSpawnCountChange}
options={["Default material", "Material 1", "Material 2"]} options={["Default material", "Material 1", "Material 2", "Material 3"]}
defaultOption={currentMaterial} defaultOption={currentMaterial}
onSelect={handleMaterialSelect} onSelect={handleMaterialSelect}
onChangeInterval={handleSpawnIntervalChange} onChangeInterval={handleSpawnIntervalChange}
@ -253,7 +253,7 @@ function ConveyorMechanics() {
)} )}
{activeOption === "swap" && ( {activeOption === "swap" && (
<SwapAction <SwapAction
options={["Default material", "Material 1", "Material 2"]} options={["Default material", "Material 1", "Material 2", "Material 3"]}
defaultOption={currentMaterial} defaultOption={currentMaterial}
onSelect={handleMaterialSelect} onSelect={handleMaterialSelect}
/> />

View File

@ -129,7 +129,7 @@ function MachineMechanics() {
max={60} max={60}
defaultValue="1" defaultValue="1"
onChange={handleProcessTimeChange} onChange={handleProcessTimeChange}
swapOptions={["Default material", "Material 1", "Material 2"]} swapOptions={["Default material", "Material 1", "Material 2", "Material 3"]}
swapDefaultOption={currentMaterial} swapDefaultOption={currentMaterial}
onSwapSelect={handleMaterialSelect} onSwapSelect={handleMaterialSelect}
/> />

View File

@ -359,7 +359,8 @@ const Trigger = ({ selectedPointData, type }: TriggerProps) => {
<LabledDropdown <LabledDropdown
label="Trigger Type" label="Trigger Type"
defaultOption={selectedTrigger.triggerType} defaultOption={selectedTrigger.triggerType}
options={["onComplete", "onStart", "onStop", "delay", "onError"]} // options={["onComplete", "onStart", "onStop", "delay", "onError"]}
options={["onComplete"]}
onSelect={handleTriggerTypeChange} onSelect={handleTriggerTypeChange}
/> />

View File

@ -38,9 +38,17 @@ const SimulationPlayer: React.FC = () => {
const { isReset, setReset } = useResetButtonStore(); const { isReset, setReset } = useResetButtonStore();
const { subModule } = useSubModuleStore(); const { subModule } = useSubModuleStore();
useEffect(() => {
if (isReset) {
setTimeout(()=>{
setReset(false);
},0)
}
}, [isReset])
// Button functions // Button functions
const handleReset = () => { const handleReset = () => {
setReset(!isReset); setReset(true);
setSpeed(1); setSpeed(1);
}; };
const handlePlayStop = () => { const handlePlayStop = () => {
@ -271,11 +279,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>
@ -314,9 +321,8 @@ const SimulationPlayer: React.FC = () => {
<div className="custom-slider-wrapper"> <div className="custom-slider-wrapper">
<div className="custom-slider"> <div className="custom-slider">
<button <button
className={`slider-handle ${ className={`slider-handle ${isDragging ? "dragging" : ""
isDragging ? "dragging" : "" }`}
}`}
style={{ left: `${calculateHandlePosition()}%` }} style={{ left: `${calculateHandlePosition()}%` }}
onMouseDown={handleMouseDown} onMouseDown={handleMouseDown}
> >

View File

@ -28,6 +28,4 @@ export default async function setCameraView({
controls?.setLookAt(...newPosition.toArray(), newPosition.x, 0, newPosition.z, true); controls?.setLookAt(...newPosition.toArray(), newPosition.x, 0, newPosition.z, true);
} }
// Optionally you can log
console.log(`Camera view updated by ${username ?? 'unknown user'}`);
} }

View File

@ -7,7 +7,7 @@ import { useSelectedProduct } from "../../../../../store/simulation/useSimulatio
export function useSpawnHandler() { export function useSpawnHandler() {
const { addMaterial } = useMaterialStore(); const { addMaterial } = useMaterialStore();
const { getModelUuidByActionUuid } = useProductStore(); const { getModelUuidByActionUuid, getPointUuidByActionUuid } = useProductStore();
const { selectedProduct } = useSelectedProduct(); const { selectedProduct } = useSelectedProduct();
const lastSpawnTime = useRef<number | null>(null); const lastSpawnTime = useRef<number | null>(null);
const startTime = useRef<number | null>(null); const startTime = useRef<number | null>(null);
@ -16,7 +16,7 @@ export function useSpawnHandler() {
material: string; material: string;
intervalMs: number; intervalMs: number;
totalCount: number; totalCount: number;
point: ConveyorPointSchema; action: ConveyorAction;
} | null>(null); } | null>(null);
const clearCurrentSpawn = useCallback(() => { const clearCurrentSpawn = useCallback(() => {
@ -30,9 +30,10 @@ export function useSpawnHandler() {
// console.log(`${materialUuid}, ${status}`); // console.log(`${materialUuid}, ${status}`);
} }
const createNewMaterial = useCallback((materialType: string, point: ConveyorPointSchema) => { const createNewMaterial = useCallback((materialType: string, action: ConveyorAction) => {
const modelUuid = getModelUuidByActionUuid(selectedProduct.productId, point.action.actionUuid); const modelUuid = getModelUuidByActionUuid(selectedProduct.productId, action.actionUuid);
if (!modelUuid || !point.uuid) return; const pointUuid = getPointUuidByActionUuid(selectedProduct.productId, action.actionUuid);
if (!modelUuid || !pointUuid) return;
const newMaterial: MaterialSchema = { const newMaterial: MaterialSchema = {
materialId: THREE.MathUtils.generateUUID(), materialId: THREE.MathUtils.generateUUID(),
@ -43,13 +44,24 @@ export function useSpawnHandler() {
isRendered: true, isRendered: true,
current: { current: {
modelUuid: modelUuid, modelUuid: modelUuid,
pointUuid: point.uuid, pointUuid: pointUuid,
actionUuid: point.action?.actionUuid || '' actionUuid: action?.actionUuid || ''
}, },
weight: 1, weight: 1,
cost: 1 cost: 1
}; };
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,
actionUuid: action.triggers[0].triggeredAsset?.triggeredAction?.actionUuid
}
}
addMaterial(newMaterial); addMaterial(newMaterial);
return newMaterial; return newMaterial;
}, [addMaterial]); }, [addMaterial]);
@ -58,14 +70,14 @@ export function useSpawnHandler() {
if (!spawnParams.current || !startTime.current) return; if (!spawnParams.current || !startTime.current) return;
const currentTime = performance.now(); const currentTime = performance.now();
const { material, intervalMs, totalCount, point } = spawnParams.current; const { material, intervalMs, totalCount, action } = spawnParams.current;
const isFirstSpawn = lastSpawnTime.current === null; const isFirstSpawn = lastSpawnTime.current === null;
const elapsed = currentTime - startTime.current; const elapsed = currentTime - startTime.current;
// First spawn // First spawn
if (isFirstSpawn) { if (isFirstSpawn) {
if (elapsed >= intervalMs) { if (elapsed >= intervalMs) {
const createdMaterial = createNewMaterial(material, point); const createdMaterial = createNewMaterial(material, action);
if (createdMaterial) { if (createdMaterial) {
spawnLogStatus(createdMaterial.materialId, `[${elapsed.toFixed(2)}ms] Spawned ${material} (1/${totalCount})`); spawnLogStatus(createdMaterial.materialId, `[${elapsed.toFixed(2)}ms] Spawned ${material} (1/${totalCount})`);
} }
@ -84,9 +96,9 @@ export function useSpawnHandler() {
const timeSinceLast = currentTime - lastSpawnTime.current; const timeSinceLast = currentTime - lastSpawnTime.current;
if (timeSinceLast >= intervalMs) { if (timeSinceLast >= intervalMs) {
const count = spawnCountRef.current + 1; const count = spawnCountRef.current + 1;
const createdMaterial = createNewMaterial(material, point); const createdMaterial = createNewMaterial(material, action);
if (createdMaterial) { if (createdMaterial) {
spawnLogStatus(createdMaterial.materialId, `[${elapsed.toFixed(2)}ms] Spawned ${material} (1/${totalCount})`); spawnLogStatus(createdMaterial.materialId, `[${elapsed.toFixed(2)}ms] Spawned ${material} (${count}/${totalCount})`);
} }
lastSpawnTime.current = currentTime; lastSpawnTime.current = currentTime;
spawnCountRef.current = count; spawnCountRef.current = count;
@ -98,10 +110,10 @@ export function useSpawnHandler() {
} }
}); });
const handleSpawn = useCallback((point: ConveyorPointSchema) => { const handleSpawn = useCallback((action: ConveyorAction) => {
if (!point.action || point.action.actionType !== 'spawn') return; if (!action || action.actionType !== 'spawn') return;
const { material, spawnInterval = 0, spawnCount = 1 } = point.action; const { material, spawnInterval = 0, spawnCount = 1 } = action;
const intervalMs = spawnInterval * 1000; const intervalMs = spawnInterval * 1000;
clearCurrentSpawn(); clearCurrentSpawn();
@ -110,7 +122,7 @@ export function useSpawnHandler() {
material, material,
intervalMs, intervalMs,
totalCount: spawnCount, totalCount: spawnCount,
point: point action: action
}; };
startTime.current = performance.now(); startTime.current = performance.now();

View File

@ -1,40 +1,66 @@
import { useEffect } from "react"; import { useEffect, useCallback, useRef } from "react";
import { useSpawnHandler } from "./actionHandler/spawnActionHandler"; import { useSpawnHandler } from "./actionHandler/useSpawnHandler";
// Conveyor Actions export function useConveyorActions() {
export function useConveyorActions(point: ConveyorPointSchema) { const { handleSpawn, clearCurrentSpawn } = useSpawnHandler();
const { handleSpawn } = useSpawnHandler();
const handleDefaultAction = useCallback((action: ConveyorAction) => {
// console.log(`Default conveyor action ${action.actionUuid}`);
}, []);
const handleSpawnAction = useCallback((action: ConveyorAction) => {
handleSpawn(action);
}, [handleSpawn]);
const handleSwapAction = useCallback((action: ConveyorAction) => {
console.log(`Swapping to material ${action.material}`);
}, []);
const handleDelayAction = useCallback((action: ConveyorAction) => {
const delayMs = (action.delay || 0) * 1000;
console.log(`Delaying for ${delayMs}ms`);
}, []);
const handleDespawnAction = useCallback((action: ConveyorAction) => {
console.log(`Despawning material`);
}, []);
const handleConveyorAction = useCallback((action: ConveyorAction) => {
if (!action) return;
switch (action.actionType) {
case 'default':
handleDefaultAction(action);
break;
case 'spawn':
handleSpawnAction(action);
break;
case 'swap':
handleSwapAction(action);
break;
case 'delay':
handleDelayAction(action);
break;
case 'despawn':
handleDespawnAction(action);
break;
default:
console.warn(`Unknown conveyor action type: ${action.actionType}`);
}
}, [handleDefaultAction, handleSpawnAction, handleSwapAction, handleDelayAction, handleDespawnAction]);
const cleanup = useCallback(() => {
clearCurrentSpawn();
}, [clearCurrentSpawn]);
useEffect(() => { useEffect(() => {
if (!point.action) return;
const { actionType, material, delay, spawnInterval, spawnCount } = point.action;
const handleAction = () => {
switch (actionType) {
case 'default':
console.log(`Default conveyor action at point ${point.uuid}`);
break;
case 'spawn':
console.log(`Spawning material ${material} at point ${point.uuid}`);
handleSpawn(point);
break;
case 'swap':
console.log(`Swapping to material ${material} at point ${point.uuid}`);
break;
case 'delay':
console.log(`Delaying for ${delay}ms at point ${point.uuid}`);
break;
case 'despawn':
console.log(`Despawning material at point ${point.uuid}`);
break;
}
};
handleAction();
return () => { return () => {
// Cleanup if needed cleanup();
}; };
}, [point]); }, [cleanup]);
return {
handleConveyorAction,
cleanup
};
} }

View File

@ -1,23 +1,35 @@
import { useEffect } from 'react'; import { useEffect, useCallback, useRef, useState } from 'react';
export function useMachineActions() {
const handleProcessAction = useCallback((action: MachineAction) => {
if (!action || action.actionType !== 'process') return;
console.log(`Machine processing for ${action.processTime}ms`);
}, []);
const handleMachineAction = useCallback((action: MachineAction) => {
if (!action) return;
switch (action.actionType) {
case 'process':
handleProcessAction(action);
break;
default:
console.warn(`Unknown machine action type: ${action.actionType}`);
}
}, [handleProcessAction]);
const cleanup = useCallback(() => {
}, []);
// Machine Actions
export function useMachineActions(point: MachinePointSchema) {
useEffect(() => { useEffect(() => {
if (!point.action) return;
const { actionType, processTime, swapMaterial } = point.action;
const handleAction = () => {
if (actionType === 'process') {
console.log(`Machine processing for ${processTime}ms at point ${point.uuid}`);
if (swapMaterial) {
console.log(`Swapping material to ${swapMaterial}`);
}
}
};
return () => { return () => {
// Cleanup if needed cleanup();
}; };
}, [point]); }, [cleanup]);
return {
handleMachineAction,
cleanup,
};
} }

View File

@ -1,26 +1,32 @@
import { useEffect } from 'react'; import { useEffect, useCallback } from 'react';
export function useRoboticArmActions() {
const handlePickAndPlace = useCallback((action: RoboticArmAction) => {
console.log(`Robotic arm pick and place`);
}, []);
const handleRoboticArmAction = useCallback((action: RoboticArmAction) => {
switch (action.actionType) {
case 'pickAndPlace':
handlePickAndPlace(action);
break;
default:
console.warn(`Unknown robotic arm action type: ${action.actionType}`);
}
}, [handlePickAndPlace]);
const cleanup = useCallback(() => {
}, []);
// Robotic Arm Actions
export function useRoboticArmActions(point: RoboticArmPointSchema) {
useEffect(() => { useEffect(() => {
point.actions.forEach(action => {
const { actionType, process } = action;
const handleAction = () => {
if (actionType === 'pickAndPlace') {
console.log(`Robotic arm pick and place at point ${point.uuid}`);
if (process.startPoint) {
console.log(`Start point: ${process.startPoint}`);
}
if (process.endPoint) {
console.log(`End point: ${process.endPoint}`);
}
}
};
});
return () => { return () => {
// Cleanup if needed cleanup();
}; };
}, [point]); }, [cleanup]);
return {
handleRoboticArmAction,
cleanup
};
} }

View File

@ -1,22 +1,33 @@
import { useEffect } from 'react'; import { useEffect, useCallback, useRef, useState } from 'react';
export function useStorageActions() {
const handleStoreAction = useCallback((action: StorageAction) => {
if (!action || action.actionType !== 'store') return;
}, []);
const handleStorageAction = useCallback((action: StorageAction) => {
if (!action) return;
switch (action.actionType) {
case 'store':
handleStoreAction(action);
break;
default:
console.warn(`Unknown storage action type: ${action.actionType}`);
}
}, [handleStoreAction]);
const cleanup = useCallback(() => {
}, []);
// Storage Actions
export function useStorageActions(point: StoragePointSchema) {
useEffect(() => { useEffect(() => {
if (!point.action) return;
const { actionType, materials, storageCapacity } = point.action;
const handleAction = () => {
if (actionType === 'store') {
console.log(`Storage action at point ${point.uuid}`);
console.log(`Materials: ${materials.map(m => m.materialName).join(', ')}`);
console.log(`Capacity: ${storageCapacity}`);
}
};
return () => { return () => {
// Cleanup if needed cleanup();
}; };
}, [point]); }, [cleanup]);
return {
handleStorageAction,
cleanup
};
} }

View File

@ -3,23 +3,59 @@ import { useMachineActions } from "./machine/useMachineActions";
import { useRoboticArmActions } from "./roboticArm/useRoboticArmActions"; import { useRoboticArmActions } from "./roboticArm/useRoboticArmActions";
import { useStorageActions } from "./storageUnit/useStorageUnitActions"; import { useStorageActions } from "./storageUnit/useStorageUnitActions";
import { useVehicleActions } from "./vehicle/useVehicleActions"; import { useVehicleActions } from "./vehicle/useVehicleActions";
import { useCallback, useEffect } from "react";
// Master hook that selects the appropriate action handler export function useActionHandler() {
export function useActionHandler(point: PointsScheme) { const { handleConveyorAction, cleanup: cleanupConveyor } = useConveyorActions();
if ('actions' in point) { const { handleVehicleAction, cleanup: cleanupVehicle } = useVehicleActions();
// Robotic Arm const { handleRoboticArmAction, cleanup: cleanupRoboticArm } = useRoboticArmActions();
useRoboticArmActions(point); const { handleMachineAction, cleanup: cleanupMachine } = useMachineActions();
} else if (point.action.actionType === 'travel') { const { handleStorageAction, cleanup: cleanupStorage } = useStorageActions();
// Vehicle
useVehicleActions(point as VehiclePointSchema); const handleAction = useCallback((action: Action) => {
} else if (point.action.actionType === 'process') { if (!action) return;
// Machine
useMachineActions(point as MachinePointSchema); try {
} else if (point.action.actionType === 'store') { switch (action.actionType) {
// Storage case 'default': case 'spawn': case 'swap': case 'delay': case 'despawn':
useStorageActions(point as StoragePointSchema); handleConveyorAction(action as ConveyorAction);
} else { break;
// Conveyor case 'travel':
useConveyorActions(point as ConveyorPointSchema); handleVehicleAction(action as VehicleAction);
} break;
case 'pickAndPlace':
handleRoboticArmAction(action as RoboticArmAction);
break;
case 'process':
handleMachineAction(action as MachineAction);
break;
case 'store':
handleStorageAction(action as StorageAction);
break;
default:
console.warn(`Unknown action type: ${(action as Action).actionType}`);
}
} catch (error) {
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]);
return {
handleAction,
cleanup
};
} }

View File

@ -1,26 +1,37 @@
import { useEffect } from 'react'; import { useEffect, useCallback } from 'react';
export function useVehicleActions() {
const handleTravelAction = useCallback((action: VehicleAction) => {
if (!action || action.actionType !== 'travel') return;
console.log(`Vehicle travel action ${action.actionUuid}`);
}, []);
const handleVehicleAction = useCallback((action: VehicleAction) => {
if (!action) return;
switch (action.actionType) {
case 'travel':
handleTravelAction(action);
break;
default:
console.warn(`Unknown vehicle action type: ${action.actionType}`);
}
}, [handleTravelAction]);
const cleanup = useCallback(() => {
}, []);
// Vehicle Actions
export function useVehicleActions(point: VehiclePointSchema) {
useEffect(() => { useEffect(() => {
if (!point.action) return;
const { actionType, unLoadDuration, loadCapacity, steeringAngle } = point.action;
const handleAction = () => {
if (actionType === 'travel') {
console.log(`Vehicle travel action at point ${point.uuid}`);
if (point.action.pickUpPoint) {
console.log(`Pick up at: ${JSON.stringify(point.action.pickUpPoint)}`);
}
if (point.action.unLoadPoint) {
console.log(`Unload at: ${JSON.stringify(point.action.unLoadPoint)}`);
}
}
};
return () => { return () => {
// Cleanup if needed cleanup();
}; };
}, [point]); }, [cleanup]);
return {
handleVehicleAction,
cleanup
};
} }

View File

@ -1,9 +1,74 @@
import React from 'react' import React, { useEffect, useState, useRef } from 'react';
import * as THREE from 'three';
import { useFrame, useThree } from '@react-three/fiber';
function MaterialAnimator() { interface MaterialAnimatorProps {
return ( matRef: React.RefObject<THREE.Mesh>;
<></> material: MaterialSchema;
) speed: number; // units per second
onAnimationComplete?: () => void;
} }
export default MaterialAnimator function MaterialAnimator({
matRef,
material,
speed,
onAnimationComplete
}: MaterialAnimatorProps) {
const { scene } = useThree();
const [targetPosition, setTargetPosition] = useState<THREE.Vector3 | null>(null);
const [isAnimating, setIsAnimating] = useState(false);
const animationStartTime = useRef<number>(0);
const startPosition = useRef<THREE.Vector3>(new THREE.Vector3());
const totalDistance = useRef<number>(0);
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 (!material.next?.pointUuid) {
setTargetPosition(null);
setIsAnimating(false);
return;
}
const newTarget = getWorldPosition(material.next.pointUuid);
if (newTarget && matRef.current) {
startPosition.current.copy(matRef.current.position);
totalDistance.current = startPosition.current.distanceTo(newTarget);
animationStartTime.current = performance.now();
setTargetPosition(newTarget);
setIsAnimating(true);
}
}, [material.next?.pointUuid]);
useFrame(() => {
if (!matRef.current || !targetPosition || !isAnimating) return;
// Calculate exact position based on constant speed
const elapsed = (performance.now() - animationStartTime.current) / 1000;
const progress = Math.min(1, (speed * elapsed) / totalDistance.current);
matRef.current.position.lerpVectors(
startPosition.current,
targetPosition,
progress
);
// Check if animation is complete
if (progress >= 1) {
matRef.current.position.copy(targetPosition);
setIsAnimating(false);
onAnimationComplete?.();
}
});
return null;
}
export default React.memo(MaterialAnimator);

View File

@ -1,10 +1,82 @@
import React, { useEffect, useState } from 'react' import React, { useEffect, useMemo, useRef, useState } from 'react'
import * as THREE from 'three'; import * as THREE from 'three';
import MaterialAnimator from '../animator/materialAnimator' import MaterialAnimator from '../animator/materialAnimator';
import { useProductStore } from '../../../../../store/simulation/useProductStore';
import { useSelectedProduct } from '../../../../../store/simulation/useSimulationStore';
import { MaterialModel } from '../material/materialModel';
import { useThree } from '@react-three/fiber';
function MaterialInstance({ material }: { material: MaterialSchema }) { function MaterialInstance({ material }: { material: MaterialSchema }) {
const [position, setPosition] = useState<THREE.Vector3>(); const matRef: any = useRef();
const [rotation, setRotation] = useState<THREE.Vector3>(); const { scene } = useThree();
const { getModelUuidByPointUuid, getPointByUuid, getEventByModelUuid } = useProductStore();
const { selectedProduct } = useSelectedProduct();
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, speed } = useMemo(() => {
if (!material.current?.pointUuid) {
return { position: new THREE.Vector3(0, 0, 0), rotation: new THREE.Vector3(0, 0, 0), speed: 1 };
}
const modelUuid = getModelUuidByPointUuid(selectedProduct.productId, material.current.pointUuid);
if (!modelUuid) {
return { position: new THREE.Vector3(0, 0, 0), rotation: new THREE.Vector3(0, 0, 0), speed: 1 };
}
const speed = getCurrentSpeed(selectedProduct.productId, modelUuid);
const point = getPointByUuid(selectedProduct.productId, modelUuid, material.current.pointUuid);
if (!point) {
return { position: new THREE.Vector3(0, 0, 0), rotation: new THREE.Vector3(0, 0, 0), speed: 1 };
}
const position = getWorldPositionFromScene(point.uuid);
if (position) {
return { position: position, rotation: new THREE.Vector3(0, 0, 0), speed: 1 };
}
return {
position: new THREE.Vector3(...point.position),
rotation: new THREE.Vector3(...point.rotation),
speed: speed || 1
};
}, [material, getPointByUuid]);
function getCurrentSpeed(productId: string, modelUuid: string) {
const event = getEventByModelUuid(productId, 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;
}
}
useEffect(() => { useEffect(() => {
// console.log('material: ', material); // console.log('material: ', material);
@ -13,8 +85,14 @@ function MaterialInstance({ material }: { material: MaterialSchema }) {
return ( return (
<> <>
<MaterialAnimator /> <MaterialModel matRef={matRef} materialType={material.materialType} position={position} />
<MaterialAnimator
matRef={matRef}
material={material}
speed={speed}
onAnimationComplete={() => { console.log('123');}}
/>
</> </>
) )
} }

View File

@ -0,0 +1,43 @@
import { useGLTF } from '@react-three/drei'
import { useMemo } from 'react';
import * as THREE from 'three';
import defaultMaterial from '../../../../../assets/gltf-glb/default.glb';
import material1 from '../../../../../assets/gltf-glb/material1.glb';
import material2 from '../../../../../assets/gltf-glb/material2.glb';
import material3 from '../../../../../assets/gltf-glb/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'> {
materialType: ModelType;
matRef: React.Ref<THREE.Group<THREE.Object3DEventMap>>
}
export function MaterialModel({ materialType, matRef, ...props }: 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 ref={matRef} {...props} dispose={null}>
<primitive
object={cloned}
scale={[0.25, 0.25, 0.25]}
/>
</group>
);
}
Object.values(modelPaths).forEach((path) => {
useGLTF.preload(path);
});

View File

@ -3,240 +3,260 @@ import { useFrame } from '@react-three/fiber';
import * as THREE from 'three'; import * as THREE from 'three';
import { Line } from '@react-three/drei'; import { Line } from '@react-three/drei';
import { import {
useAnimationPlaySpeed, useAnimationPlaySpeed,
usePauseButtonStore, usePauseButtonStore,
usePlayButtonStore, usePlayButtonStore,
useResetButtonStore useResetButtonStore
} from '../../../../../store/usePlayButtonStore'; } from '../../../../../store/usePlayButtonStore';
function RoboticArmAnimator({ function RoboticArmAnimator({
HandleCallback, HandleCallback,
restPosition, restPosition,
ikSolver, ikSolver,
targetBone, targetBone,
armBot, armBot,
logStatus, logStatus,
path path
}: any) { }: any) {
const progressRef = useRef(0); const progressRef = useRef(0);
const curveRef = useRef<THREE.Vector3[] | null>(null); const curveRef = useRef<THREE.Vector3[] | null>(null);
const [currentPath, setCurrentPath] = useState<[number, number, number][]>([]); const [currentPath, setCurrentPath] = useState<[number, number, number][]>([]);
const [circlePoints, setCirclePoints] = useState<[number, number, number][]>([]); const [circlePoints, setCirclePoints] = useState<[number, number, number][]>([]);
const [customCurvePoints, setCustomCurvePoints] = useState<THREE.Vector3[] | null>(null); const [customCurvePoints, setCustomCurvePoints] = useState<THREE.Vector3[] | null>(null);
let curveHeight = 1.75 let curveHeight = 1.75
const totalDistanceRef = useRef(0); const totalDistanceRef = useRef(0);
const startTimeRef = useRef<number | null>(null); const startTimeRef = useRef<number | null>(null);
const segmentDistancesRef = useRef<number[]>([]); const segmentDistancesRef = useRef<number[]>([]);
// Zustand stores // Zustand stores
const { isPlaying } = usePlayButtonStore(); const { isPlaying } = usePlayButtonStore();
const { isPaused } = usePauseButtonStore(); const { isPaused } = usePauseButtonStore();
const { isReset, setReset } = useResetButtonStore(); const { isReset, setReset } = useResetButtonStore();
const { speed } = useAnimationPlaySpeed(); const { speed } = useAnimationPlaySpeed();
// Update path state whenever `path` prop changes // Update path state whenever `path` prop changes
useEffect(() => { useEffect(() => {
setCurrentPath(path); setCurrentPath(path);
}, [path]); }, [path]);
// Handle circle points based on armBot position // Handle circle points based on armBot position
useEffect(() => { useEffect(() => {
const points = generateRingPoints(1.6, 64) const points = generateRingPoints(1.6, 64)
setCirclePoints(points); setCirclePoints(points);
}, [armBot.position]); }, [armBot.position]);
useEffect(() => { //Handle Reset Animation
if (isReset || !isPlaying) { useEffect(() => {
progressRef.current = 0; if (isReset ) {
curveRef.current = null; progressRef.current = 0;
setCurrentPath([]); curveRef.current = null;
setCustomCurvePoints(null); setCurrentPath([]);
totalDistanceRef.current = 0; setCustomCurvePoints(null);
startTimeRef.current = null; totalDistanceRef.current = 0;
segmentDistancesRef.current = []; startTimeRef.current = null;
setReset(false); 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;
} }
}, [isReset, isPlaying]) // 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
function generateRingPoints(radius: any, segments: any) { const findNearest = (target: [number, number, number]) => {
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;
}
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
};
// 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 findNearest = (target: [number, number, number]) => {
return circlePoints.reduce((nearest, point) => { return circlePoints.reduce((nearest, point) => {
const distance = Math.hypot(target[0] - point[0], target[1] - point[1], target[2] - point[2]); 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]); const nearestDistance = Math.hypot(target[0] - nearest[0], target[1] - nearest[1], target[2] - nearest[2]);
return distance < nearestDistance ? point : nearest; return distance < nearestDistance ? point : nearest;
}, circlePoints[0]); }, circlePoints[0]);
}; };
// Handle nearest points and final path (including arc points)
useEffect(() => {
if (circlePoints.length > 0 && currentPath.length > 0) {
const nearestToStart = findNearest(raisedStart); const start = currentPath[0];
const nearestToEnd = findNearest(raisedEnd); const end = currentPath[currentPath.length - 1];
const indexOfNearestStart = findNearestIndex(nearestToStart, circlePoints); const raisedStart = [start[0], start[1] + 0.5, start[2]] as [number, number, number];
const indexOfNearestEnd = findNearestIndex(nearestToEnd, circlePoints); const raisedEnd = [end[0], end[1] + 0.5, end[2]] as [number, number, number];
const clockwiseDistance = (indexOfNearestEnd - indexOfNearestStart + 64) % 64; // const findNearest = (target: [number, number, number]) => {
const counterClockwiseDistance = (indexOfNearestStart - indexOfNearestEnd + 64) % 64; // return circlePoints.reduce((nearest, point) => {
const clockwiseIsShorter = clockwiseDistance <= counterClockwiseDistance; // 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]);
// };
let arcPoints: [number, number, number][] = []; const nearestToStart = findNearest(raisedStart);
const nearestToEnd = findNearest(raisedEnd);
const indexOfNearestStart = findNearestIndex(nearestToStart, circlePoints);
const indexOfNearestEnd = findNearestIndex(nearestToEnd, circlePoints);
const clockwiseDistance = (indexOfNearestEnd - indexOfNearestStart + 64) % 64;
const counterClockwiseDistance = (indexOfNearestStart - indexOfNearestEnd + 64) % 64;
const clockwiseIsShorter = clockwiseDistance <= counterClockwiseDistance;
let arcPoints: [number, number, number][] = [];
if (clockwiseIsShorter) {
if (indexOfNearestStart <= indexOfNearestEnd) {
arcPoints = circlePoints.slice(indexOfNearestStart, indexOfNearestEnd + 1);
} else {
arcPoints = [
...circlePoints.slice(indexOfNearestStart, 64),
...circlePoints.slice(0, indexOfNearestEnd + 1)
];
}
} else {
if (indexOfNearestStart >= indexOfNearestEnd) {
for (let i = indexOfNearestStart; i !== (indexOfNearestEnd - 1 + 64) % 64; i = (i - 1 + 64) % 64) {
arcPoints.push(circlePoints[i]);
}
} else {
for (let i = indexOfNearestStart; i !== (indexOfNearestEnd - 1 + 64) % 64; i = (i - 1 + 64) % 64) {
arcPoints.push(circlePoints[i]);
}
}
}
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;
const movementSpeed = speed * armBot.speed;
const totalMoveTime = totalDistance / movementSpeed;
const segmentTimes = segmentDistances.map(distance => (distance / totalDistance) * totalMoveTime);
setCustomCurvePoints(pathVectors);
if (clockwiseIsShorter) {
if (indexOfNearestStart <= indexOfNearestEnd) {
arcPoints = circlePoints.slice(indexOfNearestStart, indexOfNearestEnd + 1);
} else {
arcPoints = [
...circlePoints.slice(indexOfNearestStart, 64),
...circlePoints.slice(0, indexOfNearestEnd + 1)
];
} }
} else { }, [circlePoints, currentPath]);
if (indexOfNearestStart >= indexOfNearestEnd) {
for (let i = indexOfNearestStart; i !== (indexOfNearestEnd - 1 + 64) % 64; i = (i - 1 + 64) % 64) { // Frame update for animation
arcPoints.push(circlePoints[i]); useFrame((state, delta) => {
} if (!ikSolver) return;
} else {
for (let i = indexOfNearestStart; i !== (indexOfNearestEnd - 1 + 64) % 64; i = (i - 1 + 64) % 64) { const bone = ikSolver.mesh.skeleton.bones.find((b: any) => b.name === targetBone);
arcPoints.push(circlePoints[i]); if (!bone) return;
}
if (isPlaying) {
if (isReset) {
bone.position.copy(restPosition);
setCustomCurvePoints([]);
ikSolver.update();
}
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) {
bone.position.copy(restPosition);
} }
}
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;
const movementSpeed = speed * armBot.speed;
const totalMoveTime = totalDistance / movementSpeed;
const segmentTimes = segmentDistances.map(distance => (distance / totalDistance) * totalMoveTime);
setCustomCurvePoints(pathVectors);
}
}, [circlePoints, currentPath]);
// Frame update for animation
useFrame((state, delta) => {
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; // distances between each pair of points
const totalDistance = totalDistanceRef.current;
progressRef.current += delta * (speed * armBot.speed);
const coveredDistance = progressRef.current;
let index = 0;
let accumulatedDistance = 0;
// Find which segment we are currently in
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(); ikSolver.update();
} });
} else if ((!isPlaying && currentPath.length === 0) || isReset) {
bone.position.copy(restPosition);
ikSolver.update();
}
});
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}>
<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"
lineWidth={5} lineWidth={5}
dashed={false} dashed={false}
/> />
</mesh> </mesh>
)} )}
<mesh position={[armBot.position[0], armBot.position[1] + 1.5, armBot.position[2]]} rotation={[-Math.PI / 2, 0, 0]}> <mesh position={[armBot.position[0], armBot.position[1] + 1.5, armBot.position[2]]} rotation={[-Math.PI / 2, 0, 0]}>
<ringGeometry args={[1.59, 1.61, 64]} /> <ringGeometry args={[1.59, 1.61, 64]} />
<meshBasicMaterial color="green" side={THREE.DoubleSide} /> <meshBasicMaterial color="green" side={THREE.DoubleSide} />
</mesh> </mesh>
</> </>
); );
} }
export default RoboticArmAnimator; export default RoboticArmAnimator;

View File

@ -97,24 +97,24 @@ function RoboticArmInstance({ armBot }: { armBot: ArmBotStatus }) {
useEffect(() => { useEffect(() => {
if (isReset || !isPlaying) { if (isReset || !isPlaying) {
logStatus(armBot.modelUuid, "Simulation Play Reset Successfully") logStatus(armBot.modelUuid, "Simulation Play Reset Successfully")
removeCurrentAction(armBot.modelUuid)
setArmBotActive(armBot.modelUuid, false) setArmBotActive(armBot.modelUuid, false)
setArmBotState(armBot.modelUuid, "idle") setArmBotState(armBot.modelUuid, "idle")
setCurrentPhase("init"); setCurrentPhase("init");
setPath([])
removeCurrentAction(armBot.modelUuid)
isPausedRef.current = false isPausedRef.current = false
pauseTimeRef.current = null pauseTimeRef.current = null
startTime = 0 startTime = 0
const targetBones = ikSolver?.mesh.skeleton.bones.find( const targetBones = ikSolver?.mesh.skeleton.bones.find((b: any) => b.name === targetBone
(b: any) => b.name === targetBone
); );
if (targetBones) { if (targetBones && isPlaying) {
let curve = createCurveBetweenTwoPoints(targetBones.position, restPosition) let curve = createCurveBetweenTwoPoints(targetBones.position, restPosition)
if (curve) { if (curve) {
setPath(curve.points.map(point => [point.x, point.y, point.z])); setPath(curve.points.map(point => [point.x, point.y, point.z]));
logStatus(armBot.modelUuid, "Moving armBot from initial point to rest position.")
} }
} }
setReset(false); // setReset(false);
logStatus(armBot.modelUuid, "Moving armBot from initial point to rest position.")
} }
}, [isReset, isPlaying]) }, [isReset, isPlaying])
@ -127,7 +127,6 @@ function RoboticArmInstance({ armBot }: { armBot: ArmBotStatus }) {
(b: any) => b.name === targetBone (b: any) => b.name === targetBone
); );
if (isPlaying) { if (isPlaying) {
//Moving armBot from initial point to rest position. //Moving armBot from initial point to rest position.
if (!armBot?.isActive && armBot?.state == "idle" && currentPhase == "init") { if (!armBot?.isActive && armBot?.state == "idle" && currentPhase == "init") {
@ -204,16 +203,16 @@ function RoboticArmInstance({ armBot }: { armBot: ArmBotStatus }) {
// logStatus(armBot.modelUuid, "Moving armBot from end point to rest position.") // logStatus(armBot.modelUuid, "Moving armBot from end point to rest position.")
} }
} else { } else {
logStatus(armBot.modelUuid, "Simulation Play Exited") // logStatus(armBot.modelUuid, "Simulation Play Exited")
setArmBotActive(armBot.modelUuid, false) // setArmBotActive(armBot.modelUuid, false)
setArmBotState(armBot.modelUuid, "idle") // setArmBotState(armBot.modelUuid, "idle")
setCurrentPhase("init"); // setCurrentPhase("init");
setPath([]) // setPath([])
isPausedRef.current = false // isPausedRef.current = false
pauseTimeRef.current = null // pauseTimeRef.current = null
isPausedRef.current = false // isPausedRef.current = false
startTime = 0 // startTime = 0
removeCurrentAction(armBot.modelUuid) // removeCurrentAction(armBot.modelUuid)
} }
}, [currentPhase, armBot, isPlaying, ikSolver]) }, [currentPhase, armBot, isPlaying, ikSolver])
@ -260,6 +259,7 @@ function RoboticArmInstance({ armBot }: { armBot: ArmBotStatus }) {
} }
const logStatus = (id: string, status: string) => { const logStatus = (id: string, status: string) => {
// //
} }

View File

@ -1,114 +1,21 @@
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';
function Simulator() { function Simulator() {
const { products } = useProductStore(); const { products } = useProductStore();
const { handleAction } = useActionHandler();
const executionOrder = determineExecutionOrder(products); useEffect(() => {
const executionOrder = determineExecutionOrder(products);
executionOrder.forEach(point => { executionOrder.forEach(point => {
useActionHandler(point); if ('actions' in point) {
}); handleAction(point.actions[0]);
} else {
function determineExecutionSequences(products: productsSchema): PointsScheme[][] { handleAction(point.action);
// 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));
} }
}); });
}, [products, handleAction]);
return executionSequences;
}
function determineExecutionOrder(products: productsSchema): PointsScheme[] { function determineExecutionOrder(products: productsSchema): PointsScheme[] {
// Create maps for all events and points // Create maps for all events and points
@ -223,6 +130,106 @@ function Simulator() {
return triggeredPoints; 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[] { function extractTriggersFromPoint(point: PointsScheme): TriggerSchema[] {
if ('actions' in point) { if ('actions' in point) {
return point.actions.flatMap(action => action.triggers); return point.actions.flatMap(action => action.triggers);

View File

@ -0,0 +1,38 @@
import { useCallback, useEffect, useRef } from 'react';
import { useActionHandler } from '../../actions/useActionHandler';
import { useProductStore } from '../../../../store/simulation/useProductStore';
export function useTriggerHandler() {
const { getActionByUuid } = useProductStore();
const { handleAction } = useActionHandler();
const handleTrigger = (trigger: TriggerSchema) => {
}
const triggerPointActions = useCallback((action: Action) => {
if (!action) return;
action.triggers.forEach(trigger => {
switch (trigger.triggerType) {
case 'onStart':
break;
case 'onComplete':
handleTrigger(trigger);
break;
case 'onStop':
break;
case 'onError':
break;
case 'delay':
break;
default:
console.warn(`Unknown trigger type: ${trigger.triggerType}`);
}
});
}, []);
return {
triggerPointActions
};
}

View File

@ -84,6 +84,7 @@ export const useArmBotStore = create<ArmBotStore>()(
armBot.currentAction = { armBot.currentAction = {
actionUuid: action.actionUuid, actionUuid: action.actionUuid,
actionName: action.actionName, actionName: action.actionName,
materialType: null
}; };
} }
} }

View File

@ -78,6 +78,7 @@ export const useMachineStore = create<MachineStore>()(
armBot.currentAction = { armBot.currentAction = {
actionUuid: action.actionUuid, actionUuid: action.actionUuid,
actionName: action.actionName, actionName: action.actionName,
materialType: null
}; };
} }
} }

View File

@ -63,7 +63,9 @@ type ProductsStore = {
getEventByModelUuid: (productId: string, modelUuid: string) => EventsSchema | undefined; getEventByModelUuid: (productId: string, modelUuid: string) => EventsSchema | undefined;
getPointByUuid: (productId: string, modelUuid: string, pointUuid: string) => ConveyorPointSchema | VehiclePointSchema | RoboticArmPointSchema | MachinePointSchema | StoragePointSchema | undefined; getPointByUuid: (productId: string, modelUuid: string, pointUuid: string) => ConveyorPointSchema | VehiclePointSchema | RoboticArmPointSchema | MachinePointSchema | StoragePointSchema | undefined;
getActionByUuid: (productId: string, actionUuid: string) => (ConveyorPointSchema['action'] | VehiclePointSchema['action'] | RoboticArmPointSchema['actions'][0] | MachinePointSchema['action'] | StoragePointSchema['action']) | undefined; getActionByUuid: (productId: string, actionUuid: string) => (ConveyorPointSchema['action'] | VehiclePointSchema['action'] | RoboticArmPointSchema['actions'][0] | MachinePointSchema['action'] | StoragePointSchema['action']) | undefined;
getModelUuidByPointUuid: (productId: string, actionUuid: string) => (string) | undefined;
getModelUuidByActionUuid: (productId: string, actionUuid: string) => (string) | undefined; getModelUuidByActionUuid: (productId: string, actionUuid: string) => (string) | undefined;
getPointUuidByActionUuid: (productId: string, actionUuid: string) => (string) | undefined;
getTriggerByUuid: (productId: string, triggerUuid: string) => TriggerSchema | undefined; getTriggerByUuid: (productId: string, triggerUuid: string) => TriggerSchema | undefined;
getIsEventInProduct: (productId: string, modelUuid: string) => boolean; getIsEventInProduct: (productId: string, modelUuid: string) => boolean;
}; };
@ -574,6 +576,27 @@ export const useProductStore = create<ProductsStore>()(
return undefined; return undefined;
}, },
getModelUuidByPointUuid: (productId, pointUuid) => {
const product = get().products.find(p => p.productId === productId);
if (!product) return undefined;
for (const event of product.eventDatas) {
if ('points' in event) {
for (const point of (event as ConveyorEventSchema).points) {
if (point.uuid === pointUuid) {
return event.modelUuid;
}
}
} else if ('point' in event) {
const point = (event as any).point;
if (point.uuid === pointUuid) {
return event.modelUuid;
}
}
}
return undefined;
},
getModelUuidByActionUuid: (productId, actionUuid) => { getModelUuidByActionUuid: (productId, actionUuid) => {
const product = get().products.find(p => p.productId === productId); const product = get().products.find(p => p.productId === productId);
if (!product) return undefined; if (!product) return undefined;
@ -598,6 +621,30 @@ export const useProductStore = create<ProductsStore>()(
return undefined; return undefined;
}, },
getPointUuidByActionUuid: (productId, actionUuid) => {
const product = get().products.find(p => p.productId === productId);
if (!product) return undefined;
for (const event of product.eventDatas) {
if ('points' in event) {
for (const point of (event as ConveyorEventSchema).points) {
if (point.action?.actionUuid === actionUuid) {
return point.uuid;
}
}
} else if ('point' in event) {
const point = (event as any).point;
if ('action' in point && point.action?.actionUuid === actionUuid) {
return point.uuid;
} else if ('actions' in point) {
const action = point.actions.find((a: any) => a.actionUuid === actionUuid);
if (action) return point.uuid;
}
}
}
return undefined;
},
getTriggerByUuid: (productId, triggerUuid) => { getTriggerByUuid: (productId, triggerUuid) => {
const product = get().products.find(p => p.productId === productId); const product = get().products.find(p => p.productId === productId);
if (!product) return undefined; if (!product) return undefined;

View File

@ -2,15 +2,6 @@
import { create } from 'zustand'; import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer'; import { immer } from 'zustand/middleware/immer';
interface VehicleStatus extends VehicleEventSchema {
productId: string;
isActive: boolean;
idleTime: number;
activeTime: number;
currentLoad: number;
distanceTraveled: number;
}
interface VehiclesStore { interface VehiclesStore {
vehicles: VehicleStatus[]; vehicles: VehicleStatus[];
@ -27,6 +18,7 @@ interface VehiclesStore {
incrementVehicleLoad: (modelUuid: string, incrementBy: number) => void; incrementVehicleLoad: (modelUuid: string, incrementBy: number) => void;
decrementVehicleLoad: (modelUuid: string, decrementBy: number) => void; decrementVehicleLoad: (modelUuid: string, decrementBy: number) => void;
setVehicleState: (modelUuid: string, newState: VehicleStatus['state']) => void; setVehicleState: (modelUuid: string, newState: VehicleStatus['state']) => void;
setMaterialType: (modelUuid: string, materialType: string | null) => void;
incrementActiveTime: (modelUuid: string, incrementBy: number) => void; incrementActiveTime: (modelUuid: string, incrementBy: number) => void;
incrementIdleTime: (modelUuid: string, incrementBy: number) => void; incrementIdleTime: (modelUuid: string, incrementBy: number) => void;
@ -51,6 +43,7 @@ export const useVehicleStore = create<VehiclesStore>()(
idleTime: 0, idleTime: 0,
activeTime: 0, activeTime: 0,
currentLoad: 0, currentLoad: 0,
materialType: null,
distanceTraveled: 0, distanceTraveled: 0,
}); });
} }
@ -123,6 +116,15 @@ export const useVehicleStore = create<VehiclesStore>()(
}); });
}, },
setMaterialType: (modelUuid, materialType) => {
set((state) => {
const vehicle = state.vehicles.find(v => v.modelUuid === modelUuid);
if (vehicle) {
vehicle.materialType = materialType;
}
});
},
incrementActiveTime: (modelUuid, incrementBy) => { incrementActiveTime: (modelUuid, incrementBy) => {
set((state) => { set((state) => {
const vehicle = state.vehicles.find(v => v.modelUuid === modelUuid); const vehicle = state.vehicles.find(v => v.modelUuid === modelUuid);

View File

@ -22,74 +22,35 @@ interface ConveyorPointSchema {
uuid: string; uuid: string;
position: [number, number, number]; position: [number, number, number];
rotation: [number, number, number]; rotation: [number, number, number];
action: { action: ConveyorAction;
actionUuid: string;
actionName: string;
actionType: "default" | "spawn" | "swap" | "delay" | "despawn";
material: string;
delay: number;
spawnInterval: number;
spawnCount: number;
triggers: TriggerSchema[];
};
} }
interface VehiclePointSchema { interface VehiclePointSchema {
uuid: string; uuid: string;
position: [number, number, number]; position: [number, number, number];
rotation: [number, number, number]; rotation: [number, number, number];
action: { action: VehicleAction;
actionUuid: string;
actionName: string;
actionType: "travel";
unLoadDuration: number;
loadCapacity: number;
steeringAngle: number;
pickUpPoint: { position: { x: number; y: number, z: number }, rotation: { x: number; y: number, z: number } } | null;
unLoadPoint: { position: { x: number; y: number, z: number }, rotation: { x: number; y: number, z: number } } | null;
triggers: TriggerSchema[];
};
} }
interface RoboticArmPointSchema { interface RoboticArmPointSchema {
uuid: string; uuid: string;
position: [number, number, number]; position: [number, number, number];
rotation: [number, number, number]; rotation: [number, number, number];
actions: { actions: RoboticArmAction[];
actionUuid: string;
actionName: string;
actionType: "pickAndPlace";
process: { startPoint: [number, number, number] | null; endPoint: [number, number, number] | null; };
triggers: TriggerSchema[];
}[];
} }
interface MachinePointSchema { interface MachinePointSchema {
uuid: string; uuid: string;
position: [number, number, number]; position: [number, number, number];
rotation: [number, number, number]; rotation: [number, number, number];
action: { action: MachineAction;
actionUuid: string;
actionName: string;
actionType: "process";
processTime: number;
swapMaterial: string;
triggers: TriggerSchema[];
};
} }
interface StoragePointSchema { interface StoragePointSchema {
uuid: string; uuid: string;
position: [number, number, number]; position: [number, number, number];
rotation: [number, number, number]; rotation: [number, number, number];
action: { action: StorageAction;
actionUuid: string;
actionName: string;
actionType: "store";
materials: { materialName: string; materialId: string; }[];
storageCapacity: number;
triggers: TriggerSchema[];
};
} }
interface ConveyorEventSchema extends AssetEventSchema { interface ConveyorEventSchema extends AssetEventSchema {
@ -120,6 +81,57 @@ interface StorageEventSchema extends AssetEventSchema {
point: StoragePointSchema; point: StoragePointSchema;
} }
interface ConveyorAction {
actionUuid: string;
actionName: string;
actionType: "default" | "spawn" | "swap" | "delay" | "despawn";
material: string;
delay: number;
spawnInterval: number;
spawnCount: number;
triggers: TriggerSchema[];
}
interface VehicleAction {
actionUuid: string;
actionName: string;
actionType: "travel";
unLoadDuration: number;
loadCapacity: number;
steeringAngle: number;
pickUpPoint: { position: { x: number; y: number, z: number }, rotation: { x: number; y: number, z: number } } | null;
unLoadPoint: { position: { x: number; y: number, z: number }, rotation: { x: number; y: number, z: number } } | null;
triggers: TriggerSchema[];
}
interface RoboticArmAction {
actionUuid: string;
actionName: string;
actionType: "pickAndPlace";
process: { startPoint: [number, number, number] | null; endPoint: [number, number, number] | null; };
triggers: TriggerSchema[];
}
interface MachineAction {
actionUuid: string;
actionName: string;
actionType: "process";
processTime: number;
swapMaterial: string;
triggers: TriggerSchema[];
}
interface StorageAction {
actionUuid: string;
actionName: string;
actionType: "store";
materials: { materialName: string; materialId: string; }[];
storageCapacity: number;
triggers: TriggerSchema[];
}
type Action = ConveyorAction | VehicleAction | RoboticArmAction | MachineAction | StorageAction;
type PointsScheme = ConveyorPointSchema | VehiclePointSchema | RoboticArmPointSchema | MachinePointSchema | StoragePointSchema; type PointsScheme = ConveyorPointSchema | VehiclePointSchema | RoboticArmPointSchema | MachinePointSchema | StoragePointSchema;
type EventsSchema = ConveyorEventSchema | VehicleEventSchema | RoboticArmEventSchema | MachineEventSchema | StorageEventSchema; type EventsSchema = ConveyorEventSchema | VehicleEventSchema | RoboticArmEventSchema | MachineEventSchema | StorageEventSchema;
@ -136,7 +148,6 @@ interface ConveyorStatus extends ConveyorEventSchema {
isActive: boolean; isActive: boolean;
idleTime: number; idleTime: number;
activeTime: number; activeTime: number;
} }
interface MachineStatus extends MachineEventSchema { interface MachineStatus extends MachineEventSchema {
@ -147,6 +158,7 @@ interface MachineStatus extends MachineEventSchema {
currentAction?: { currentAction?: {
actionUuid: string; actionUuid: string;
actionName: string; actionName: string;
materialType: string | null;
}; };
} }
@ -158,6 +170,7 @@ interface ArmBotStatus extends RoboticArmEventSchema {
currentAction?: { currentAction?: {
actionUuid: string; actionUuid: string;
actionName: string; actionName: string;
materialType: string | null;
}; };
} }
@ -167,6 +180,7 @@ interface VehicleStatus extends VehicleEventSchema {
idleTime: number; idleTime: number;
activeTime: number; activeTime: number;
currentLoad: number; currentLoad: number;
materialType: string | null;
distanceTraveled: number; distanceTraveled: number;
} }