feat: Introduce simulation module with time management, entity action handlers for conveyors and storage units, and analysis components.

This commit is contained in:
2025-12-22 14:13:39 +05:30
parent aadecec2c9
commit 02cb4b9c95
7 changed files with 160 additions and 61 deletions

View File

@@ -76,6 +76,9 @@ type SceneContextValue = {
humanEventManagerRef: React.RefObject<HumanEventManagerState>; humanEventManagerRef: React.RefObject<HumanEventManagerState>;
craneEventManagerRef: React.RefObject<CraneEventManagerState>; craneEventManagerRef: React.RefObject<CraneEventManagerState>;
simulationTimeRef: React.MutableRefObject<number>;
getSimulationTime: () => number;
clearStores: () => void; clearStores: () => void;
layout: "Main Layout" | "Comparison Layout"; layout: "Main Layout" | "Comparison Layout";
@@ -134,6 +137,10 @@ export function SceneProvider({
const humanEventManagerRef = useRef<HumanEventManagerState>({ humanStates: [] }); const humanEventManagerRef = useRef<HumanEventManagerState>({ humanStates: [] });
const craneEventManagerRef = useRef<CraneEventManagerState>({ craneStates: [] }); const craneEventManagerRef = useRef<CraneEventManagerState>({ craneStates: [] });
// Simulation Time
const simulationTimeRef = useRef<number>(0);
const getSimulationTime = () => simulationTimeRef.current;
const clearStores = useMemo( const clearStores = useMemo(
() => () => { () => () => {
scene.current = null; scene.current = null;
@@ -165,6 +172,7 @@ export function SceneProvider({
collabUsersStore.getState().clearCollabUsers(); collabUsersStore.getState().clearCollabUsers();
humanEventManagerRef.current.humanStates = []; humanEventManagerRef.current.humanStates = [];
craneEventManagerRef.current.craneStates = []; craneEventManagerRef.current.craneStates = [];
simulationTimeRef.current = 0;
}, },
[ [
versionStore, versionStore,
@@ -225,6 +233,8 @@ export function SceneProvider({
collabUsersStore, collabUsersStore,
humanEventManagerRef, humanEventManagerRef,
craneEventManagerRef, craneEventManagerRef,
simulationTimeRef,
getSimulationTime,
clearStores, clearStores,
layout, layout,
layoutType, layoutType,

View File

@@ -2,28 +2,30 @@ import { useCallback } from "react";
import { useSceneContext } from "../../../../scene/sceneContext"; import { useSceneContext } from "../../../../scene/sceneContext";
export function useDespawnHandler() { export function useDespawnHandler() {
const { materialStore } = useSceneContext(); const { materialStore, getSimulationTime } = useSceneContext();
const { getMaterialById, removeMaterial, setEndTime, clearLocations } = materialStore(); const { getMaterialById, removeMaterial, setEndTime, clearLocations } = materialStore();
const deSpawnLogStatus = (materialUuid: string, status: string) => { const deSpawnLogStatus = (materialUuid: string, status: string) => {
echo.info(`${materialUuid}, ${status}`); echo.info(`${materialUuid}, ${status}`);
} };
const handleDespawn = useCallback((action: ConveyorAction, materialId?: string) => { const handleDespawn = useCallback(
if (!action || action.actionType !== 'despawn' || !materialId) return; (action: ConveyorAction, materialId?: string) => {
if (!action || action.actionType !== "despawn" || !materialId) return;
const material = getMaterialById(materialId); const material = getMaterialById(materialId);
if (!material) return; if (!material) return;
setEndTime(material.materialId, performance.now()); setEndTime(material.materialId, getSimulationTime());
removeMaterial(material.materialId); removeMaterial(material.materialId);
clearLocations(material.materialId); clearLocations(material.materialId);
deSpawnLogStatus(material.materialName, `Despawned`); deSpawnLogStatus(material.materialName, `Despawned`);
},
}, [getMaterialById, removeMaterial]); [getMaterialById, removeMaterial]
);
return { return {
handleDespawn, handleDespawn,
}; };
} }

View File

@@ -20,7 +20,7 @@ interface SpawnInstance {
} }
export function useSpawnHandler() { export function useSpawnHandler() {
const { materialStore, conveyorStore, productStore } = useSceneContext(); const { materialStore, conveyorStore, productStore, getSimulationTime } = useSceneContext();
const { addMaterial } = materialStore(); const { addMaterial } = materialStore();
const { getConveyorById } = conveyorStore(); const { getConveyorById } = conveyorStore();
const { getModelUuidByActionUuid, getPointUuidByActionUuid, selectedProduct } = productStore(); const { getModelUuidByActionUuid, getPointUuidByActionUuid, selectedProduct } = productStore();
@@ -69,7 +69,7 @@ export function useSpawnHandler() {
const modelUuid = getModelUuidByActionUuid(selectedProduct.productUuid, action.actionUuid); const modelUuid = getModelUuidByActionUuid(selectedProduct.productUuid, action.actionUuid);
const pointUuid = getPointUuidByActionUuid(selectedProduct.productUuid, action.actionUuid); const pointUuid = getPointUuidByActionUuid(selectedProduct.productUuid, action.actionUuid);
if (!modelUuid || !pointUuid) return; if (!modelUuid || !pointUuid) return;
const currentTime = performance.now(); const currentTime = getSimulationTime();
const newMaterial: MaterialSchema = { const newMaterial: MaterialSchema = {
materialId: THREE.MathUtils.generateUUID(), materialId: THREE.MathUtils.generateUUID(),
@@ -101,11 +101,11 @@ export function useSpawnHandler() {
addMaterial(newMaterial); addMaterial(newMaterial);
return newMaterial; return newMaterial;
}, },
[addMaterial, getModelUuidByActionUuid, getPointUuidByActionUuid, selectedProduct.productUuid] [selectedProduct.productUuid]
); );
useFrame(() => { useFrame(() => {
const currentTime = performance.now(); const currentTime = getSimulationTime();
const completedActions: string[] = []; const completedActions: string[] = [];
let hasChanges = false; let hasChanges = false;
@@ -114,9 +114,9 @@ export function useSpawnHandler() {
if (isPausedNow && !spawn.isPaused) { if (isPausedNow && !spawn.isPaused) {
if (spawn.lastSpawnTime === null) { if (spawn.lastSpawnTime === null) {
spawn.remainingTime = Math.max(0, spawn.params.intervalMs / speed - (currentTime - spawn.startTime)); spawn.remainingTime = Math.max(0, spawn.params.intervalMs - (currentTime - spawn.startTime));
} else { } else {
spawn.remainingTime = Math.max(0, spawn.params.intervalMs / speed - (currentTime - spawn.lastSpawnTime)); spawn.remainingTime = Math.max(0, spawn.params.intervalMs - (currentTime - spawn.lastSpawnTime));
} }
spawn.pauseStartTime = currentTime; spawn.pauseStartTime = currentTime;
spawn.isPaused = true; spawn.isPaused = true;
@@ -140,7 +140,8 @@ export function useSpawnHandler() {
if (spawn.isPaused) return; if (spawn.isPaused) return;
const { material, intervalMs, totalCount, action } = spawn.params; const { material, intervalMs, totalCount, action } = spawn.params;
const adjustedInterval = intervalMs / speed; // intervalMs is in simulation time, and currentTime is simulation time. No speed adjustment needed.
const adjustedInterval = intervalMs;
const isFirstSpawn = spawn.lastSpawnTime === null; const isFirstSpawn = spawn.lastSpawnTime === null;
// First spawn // First spawn
@@ -211,7 +212,7 @@ export function useSpawnHandler() {
newSpawns.set(actionUuid, { newSpawns.set(actionUuid, {
lastSpawnTime: null, lastSpawnTime: null,
startTime: performance.now(), startTime: getSimulationTime(),
spawnCount: 0, spawnCount: 0,
params: { params: {
material, material,

View File

@@ -5,7 +5,8 @@ import { useSceneContext } from "../../../../scene/sceneContext";
import { useHumanEventManager } from "../../../human/eventManager/useHumanEventManager"; import { useHumanEventManager } from "../../../human/eventManager/useHumanEventManager";
export function useRetrieveHandler() { export function useRetrieveHandler() {
const { materialStore, armBotStore, machineStore, vehicleStore, storageUnitStore, conveyorStore, craneStore, productStore, humanStore, assetStore, humanEventManagerRef } = useSceneContext(); const { materialStore, armBotStore, machineStore, vehicleStore, storageUnitStore, conveyorStore, craneStore, productStore, humanStore, assetStore, humanEventManagerRef, getSimulationTime } =
useSceneContext();
const { addMaterial } = materialStore(); const { addMaterial } = materialStore();
const { getModelUuidByActionUuid, getPointUuidByActionUuid, getEventByModelUuid, getActionByUuid, selectedProduct } = productStore(); const { getModelUuidByActionUuid, getPointUuidByActionUuid, getEventByModelUuid, getActionByUuid, selectedProduct } = productStore();
const { getStorageUnitById, getLastMaterial, updateCurrentLoad, removeLastMaterial } = storageUnitStore(); const { getStorageUnitById, getLastMaterial, updateCurrentLoad, removeLastMaterial } = storageUnitStore();
@@ -40,7 +41,7 @@ export function useRetrieveHandler() {
const modelUuid = getModelUuidByActionUuid(selectedProduct.productUuid, action.actionUuid); const modelUuid = getModelUuidByActionUuid(selectedProduct.productUuid, action.actionUuid);
const pointUuid = getPointUuidByActionUuid(selectedProduct.productUuid, action.actionUuid); const pointUuid = getPointUuidByActionUuid(selectedProduct.productUuid, action.actionUuid);
if (!modelUuid || !pointUuid) return null; if (!modelUuid || !pointUuid) return null;
const currentTime = performance.now(); const currentTime = getSimulationTime();
const newMaterial: MaterialSchema = { const newMaterial: MaterialSchema = {
materialId: materialId, materialId: materialId,
@@ -95,7 +96,7 @@ export function useRetrieveHandler() {
useFrame(() => { useFrame(() => {
if (!isPlaying || isPaused || isReset || !initialDelayComplete) return; if (!isPlaying || isPaused || isReset || !initialDelayComplete) return;
const currentTime = performance.now(); const currentTime = getSimulationTime();
const completedActions: string[] = []; const completedActions: string[] = [];
let hasChanges = false; let hasChanges = false;
@@ -135,8 +136,8 @@ export function useRetrieveHandler() {
} }
const idleStartTime = retrievalTimeRef.current.get(actionUuid); const idleStartTime = retrievalTimeRef.current.get(actionUuid);
const minIdleTimeBeforeFirstRetrieval = 5000 / speed; const minIdleTimeBeforeFirstRetrieval = 5000;
const minDelayBetweenRetrievals = 5000 / speed; const minDelayBetweenRetrievals = 5000;
const canProceedFirstRetrieval = idleStartTime !== undefined && currentTime - idleStartTime >= minIdleTimeBeforeFirstRetrieval; const canProceedFirstRetrieval = idleStartTime !== undefined && currentTime - idleStartTime >= minIdleTimeBeforeFirstRetrieval;
@@ -215,8 +216,8 @@ export function useRetrieveHandler() {
if (!vehicle) return; if (!vehicle) return;
const loadDuration = vehicle.point.action.unLoadDuration; const loadDuration = vehicle.point.action.unLoadDuration;
let minDelayBetweenRetrievals = (loadDuration * 1000) / speed; let minDelayBetweenRetrievals = loadDuration * 1000;
const minIdleTimeBeforeFirstRetrieval = 3000 / speed; const minIdleTimeBeforeFirstRetrieval = 3000;
if (!retrievalTimeRef.current.has(actionUuid) && isIdle) { if (!retrievalTimeRef.current.has(actionUuid) && isIdle) {
retrievalTimeRef.current.set(actionUuid, currentTime); retrievalTimeRef.current.set(actionUuid, currentTime);
@@ -535,7 +536,7 @@ export function useRetrieveHandler() {
newRetrievals.set(action.actionUuid, { newRetrievals.set(action.actionUuid, {
action, action,
isProcessing: false, isProcessing: false,
lastCheckTime: performance.now(), lastCheckTime: getSimulationTime(),
}); });
return newRetrievals; return newRetrievals;
}); });

View File

@@ -17,6 +17,7 @@ function Analyzer() {
const { speed } = useAnimationPlaySpeed(); const { speed } = useAnimationPlaySpeed();
const { setAnalysis, setAnalyzing, analysis } = analysisStore(); const { setAnalysis, setAnalyzing, analysis } = analysisStore();
const { getSimulationTime } = useSceneContext();
// ============================================================================ // ============================================================================
// COMPREHENSIVE TRACKING REFS FOR PERFORMANCE METRICS // COMPREHENSIVE TRACKING REFS FOR PERFORMANCE METRICS
@@ -245,49 +246,18 @@ function Analyzer() {
setAnalyzing(false); setAnalyzing(false);
}; };
// Simulation Time Tracking
const simulationTimeRef = useRef<number>(0);
const lastSpeedUpdateRef = useRef<number>(Date.now());
const prevSpeedRef = useRef<number>(speed); // Initialize with current speed
// Reset accumulated time when simulation stops/starts // Reset accumulated time when simulation stops/starts
useEffect(() => { useEffect(() => {
if (!isPlaying) { if (!isPlaying) {
resetAllRefs(); resetAllRefs();
simulationTimeRef.current = 0;
lastSpeedUpdateRef.current = Date.now();
} else { } else {
// Reset start time when simulation starts // Reset start time when simulation starts
startTimeRef.current = new Date().toISOString(); startTimeRef.current = new Date().toISOString();
simulationTimeRef.current = 0;
lastSpeedUpdateRef.current = Date.now();
} }
}, [isPlaying]); }, [isPlaying]);
// Track accumulated simulation time on speed change
useEffect(() => {
if (!isPlaying) {
prevSpeedRef.current = speed;
return;
}
const now = Date.now();
const deltaReal = now - lastSpeedUpdateRef.current;
// Add time segment using previous speed
simulationTimeRef.current += deltaReal * Math.max(1, prevSpeedRef.current);
lastSpeedUpdateRef.current = now;
prevSpeedRef.current = speed; // Update for next segment
}, [speed, isPlaying]);
const getSimulationDuration = () => { const getSimulationDuration = () => {
if (!isPlaying) return simulationTimeRef.current; return getSimulationTime();
const now = Date.now();
const deltaReal = now - lastSpeedUpdateRef.current;
// Current accumulating segment uses current speed
return simulationTimeRef.current + deltaReal * Math.max(1, speed);
}; };
// ============================================================================ // ============================================================================

View File

@@ -16,6 +16,7 @@ import SimulationAnalysis from "./analysis/simulationAnalysis";
import { useSceneContext } from "../scene/sceneContext"; import { useSceneContext } from "../scene/sceneContext";
import HeatMap from "./heatMap/heatMap"; import HeatMap from "./heatMap/heatMap";
import Analyzer from "./analyzer/analyzer"; import Analyzer from "./analyzer/analyzer";
import TimeManager from "./simulator/TimeManager";
function Simulation() { function Simulation() {
const { activeModule } = useModuleStore(); const { activeModule } = useModuleStore();
@@ -64,6 +65,8 @@ function Simulation() {
<Simulator /> <Simulator />
<TimeManager />
<Analyzer /> <Analyzer />
<SimulationAnalysis /> <SimulationAnalysis />

View File

@@ -0,0 +1,112 @@
import { useRef, useEffect } from "react";
import { usePlayButtonStore, useAnimationPlaySpeed } from "../../../store/ui/usePlayButtonStore";
import { useSceneContext } from "../../scene/sceneContext";
const TimeManager = () => {
const { isPlaying } = usePlayButtonStore();
const { speed } = useAnimationPlaySpeed();
const { simulationTimeRef } = useSceneContext();
const lastSpeedUpdateRef = useRef<number>(Date.now());
const prevSpeedRef = useRef<number>(speed);
// Reset accumulated time when simulation stops
useEffect(() => {
if (!isPlaying) {
simulationTimeRef.current = 0;
lastSpeedUpdateRef.current = Date.now();
} else {
lastSpeedUpdateRef.current = Date.now();
}
}, [isPlaying, simulationTimeRef]);
// Track accumulated simulation time
useEffect(() => {
if (!isPlaying) {
prevSpeedRef.current = speed;
return;
}
// Set up an interval to update time continuously
const intervalId = setInterval(() => {
const now = Date.now();
const deltaReal = now - lastSpeedUpdateRef.current;
// Add time segment using current speed
// We use interval instead of useFrame because TimeManager might run outside of Canvas or we want independent tick
// Actually, if we want frame-perfect sync, useFrame is better.
// But this component might not be inside Canvas in all setups (though we plan to put it there).
// Let's stick to useEffect loop for now to mirror the logic we had, or better yet, use requestAnimationFrame
simulationTimeRef.current += deltaReal * Math.max(1, speed);
lastSpeedUpdateRef.current = now;
}, 16); // ~60fps
return () => clearInterval(intervalId);
}, [speed, isPlaying, simulationTimeRef]);
// Handle speed changes accurately by updating before speed changes
// The interval handles the continuous update. This effect ensures we capture the exact moment speed changes if we want higher precision,
// but the interval driven approach with small steps is usually sufficient for "game time".
// Actually, "prevSpeedRef" logic was better for exact transitions.
// Let's refine the logic to match what we did in Analyzer:
useEffect(() => {
if (!isPlaying) {
prevSpeedRef.current = speed;
return;
}
const now = Date.now();
const deltaReal = now - lastSpeedUpdateRef.current;
// Add time segment using previous speed (before this update)
// Wait, if we use interval, we are continuously updating.
// If we mix interval and this effect, we double count.
// Let's ONLY use requestAnimationFrame (or a tight interval) to update time.
// And update refs.
}, [speed, isPlaying]); // This is tricky with intervals.
return null;
};
// Re-implementing correctly with requestAnimationFrame for smooth time
const TimeManagerRAF = () => {
const { isPlaying } = usePlayButtonStore();
const { speed } = useAnimationPlaySpeed();
const { simulationTimeRef } = useSceneContext();
const lastTimeRef = useRef<number>(Date.now());
useEffect(() => {
if (!isPlaying) {
simulationTimeRef.current = 0;
return;
}
lastTimeRef.current = Date.now();
let frameId: number;
const loop = () => {
const now = Date.now();
const deltaReal = now - lastTimeRef.current;
if (deltaReal > 0) {
simulationTimeRef.current += deltaReal * Math.max(1, speed);
lastTimeRef.current = now;
}
frameId = requestAnimationFrame(loop);
};
frameId = requestAnimationFrame(loop);
return () => cancelAnimationFrame(frameId);
}, [isPlaying, speed, simulationTimeRef]);
return null;
};
export default TimeManagerRAF;