646 lines
24 KiB
TypeScript
646 lines
24 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from "react";
|
|
import * as THREE from "three";
|
|
import {
|
|
ProcessData,
|
|
ProcessAnimationState,
|
|
SpawnedObject,
|
|
AnimationState,
|
|
ProcessPoint,
|
|
PointAction,
|
|
Trigger,
|
|
} from "./types";
|
|
import {
|
|
useAnimationPlaySpeed,
|
|
usePauseButtonStore,
|
|
usePlayButtonStore,
|
|
useResetButtonStore,
|
|
} from "../../../store/usePlayButtonStore";
|
|
import { usePlayAgv } from "../../../store/store";
|
|
|
|
// Enhanced ProcessAnimationState with trigger tracking
|
|
interface EnhancedProcessAnimationState extends ProcessAnimationState {
|
|
triggerCounts: Record<string, number>;
|
|
triggerLogs: Array<{
|
|
timestamp: number;
|
|
pointId: string;
|
|
objectId: string;
|
|
triggerId: string;
|
|
}>;
|
|
}
|
|
|
|
interface ProcessContainerProps {
|
|
processes: ProcessData[];
|
|
setProcesses: React.Dispatch<React.SetStateAction<any[]>>;
|
|
agvRef: any;
|
|
}
|
|
|
|
interface PlayAgvState {
|
|
playAgv: Record<string, any>;
|
|
setPlayAgv: (data: any) => void;
|
|
}
|
|
|
|
interface ArmBotState {
|
|
uuid: string;
|
|
position: [number, number, number];
|
|
rotation: [number, number, number];
|
|
status: string;
|
|
material: string;
|
|
triggerId: string;
|
|
actions: { uuid: string; name: string; speed: number; processes: { triggerId: string; startPoint: string; endPoint: string }[]; };
|
|
}
|
|
|
|
export const useProcessAnimation = (
|
|
processes: ProcessData[],
|
|
setProcesses: React.Dispatch<React.SetStateAction<any[]>>,
|
|
agvRef: any,
|
|
armBots: ArmBotState[],
|
|
setArmBots: React.Dispatch<React.SetStateAction<ArmBotState[]>>
|
|
) => {
|
|
// State and refs initialization
|
|
const { isPlaying, setIsPlaying } = usePlayButtonStore();
|
|
const { isPaused, setIsPaused } = usePauseButtonStore();
|
|
const { isReset, setReset } = useResetButtonStore();
|
|
const debugRef = useRef<boolean>(false);
|
|
const clockRef = useRef<THREE.Clock>(new THREE.Clock());
|
|
const pauseTimeRef = useRef<number>(0);
|
|
const elapsedBeforePauseRef = useRef<number>(0);
|
|
const animationStatesRef = useRef<Record<string, EnhancedProcessAnimationState>>({});
|
|
const { speed } = useAnimationPlaySpeed();
|
|
const prevIsPlaying = useRef<boolean | null>(null);
|
|
const [internalResetFlag, setInternalResetFlag] = useState(false);
|
|
const [animationStates, setAnimationStates] = useState<Record<string, EnhancedProcessAnimationState>>({});
|
|
const speedRef = useRef<number>(speed);
|
|
const { PlayAgv, setPlayAgv } = usePlayAgv();
|
|
|
|
// Effect hooks
|
|
useEffect(() => {
|
|
speedRef.current = speed;
|
|
}, [speed]);
|
|
|
|
useEffect(() => {
|
|
if (prevIsPlaying.current !== null || !isPlaying) {
|
|
setAnimationStates({});
|
|
}
|
|
prevIsPlaying.current = isPlaying;
|
|
}, [isPlaying]);
|
|
|
|
useEffect(() => {
|
|
animationStatesRef.current = animationStates;
|
|
}, [animationStates]);
|
|
|
|
// Reset handler
|
|
useEffect(() => {
|
|
if (isReset) {
|
|
setInternalResetFlag(true);
|
|
setIsPlaying(false);
|
|
setIsPaused(false);
|
|
setAnimationStates({});
|
|
animationStatesRef.current = {};
|
|
clockRef.current = new THREE.Clock();
|
|
elapsedBeforePauseRef.current = 0;
|
|
pauseTimeRef.current = 0;
|
|
setReset(false);
|
|
setTimeout(() => {
|
|
setInternalResetFlag(false);
|
|
setIsPlaying(true);
|
|
}, 0);
|
|
}
|
|
}, [isReset, setReset, setIsPlaying, setIsPaused]);
|
|
|
|
// Pause handler
|
|
useEffect(() => {
|
|
if (isPaused) {
|
|
pauseTimeRef.current = clockRef.current.getElapsedTime();
|
|
} else if (pauseTimeRef.current > 0) {
|
|
const pausedDuration = clockRef.current.getElapsedTime() - pauseTimeRef.current;
|
|
elapsedBeforePauseRef.current += pausedDuration;
|
|
}
|
|
}, [isPaused]);
|
|
|
|
// Initialize animation states with trigger tracking
|
|
useEffect(() => {
|
|
if (isPlaying && !internalResetFlag) {
|
|
const newStates: Record<string, EnhancedProcessAnimationState> = {};
|
|
|
|
processes.forEach((process) => {
|
|
const triggerCounts: Record<string, number> = {};
|
|
|
|
// Initialize trigger counts for all On-Hit triggers
|
|
process.paths?.forEach((path) => {
|
|
path.points?.forEach((point) => {
|
|
point.triggers?.forEach((trigger: Trigger) => {
|
|
if (trigger.type === "On-Hit" && trigger.isUsed) {
|
|
triggerCounts[`${point.uuid}-${trigger.uuid}`] = 0;
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
newStates[process.id] = {
|
|
spawnedObjects: {},
|
|
nextSpawnTime: 0,
|
|
objectIdCounter: 0,
|
|
isProcessDelaying: false,
|
|
processDelayStartTime: 0,
|
|
processDelayDuration: 0,
|
|
triggerCounts,
|
|
triggerLogs: [],
|
|
};
|
|
});
|
|
|
|
setAnimationStates(newStates);
|
|
animationStatesRef.current = newStates;
|
|
clockRef.current.start();
|
|
}
|
|
}, [isPlaying, processes, internalResetFlag]);
|
|
|
|
useEffect(() => {
|
|
if (isPlaying && !internalResetFlag) {
|
|
const newStates: Record<string, EnhancedProcessAnimationState> = {};
|
|
|
|
// Initialize AGVs for each process first
|
|
processes.forEach((process) => {
|
|
// Find all vehicle paths for this process
|
|
const vehiclePaths = process.paths?.filter(
|
|
(path) => path.type === "Vehicle"
|
|
) || [];
|
|
|
|
// Initialize AGVs for each vehicle path
|
|
vehiclePaths.forEach((vehiclePath) => {
|
|
if (vehiclePath.points?.length > 0) {
|
|
const vehiclePoint = vehiclePath.points[0];
|
|
const action = vehiclePoint.actions?.[0];
|
|
const maxHitCount = action?.hitCount;
|
|
|
|
const vehicleId = vehiclePath.modeluuid;
|
|
const processId = process.id;
|
|
|
|
// Check if this AGV already exists
|
|
const existingAgv = agvRef.current.find(
|
|
(v: any) => v.vehicleId === vehicleId && v.processId === processId
|
|
);
|
|
|
|
if (!existingAgv) {
|
|
// Initialize the AGV in a stationed state
|
|
agvRef.current.push({
|
|
processId,
|
|
vehicleId,
|
|
maxHitCount: maxHitCount || 0,
|
|
isActive: false,
|
|
hitCount: 0,
|
|
status: 'stationed',
|
|
lastUpdated: 0
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
// Then initialize trigger counts as before
|
|
const triggerCounts: Record<string, number> = {};
|
|
process.paths?.forEach((path) => {
|
|
path.points?.forEach((point) => {
|
|
point.triggers?.forEach((trigger: Trigger) => {
|
|
if (trigger.type === "On-Hit" && trigger.isUsed) {
|
|
triggerCounts[`${point.uuid}-${trigger.uuid}`] = 0;
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
newStates[process.id] = {
|
|
spawnedObjects: {},
|
|
nextSpawnTime: 0,
|
|
objectIdCounter: 0,
|
|
isProcessDelaying: false,
|
|
processDelayStartTime: 0,
|
|
processDelayDuration: 0,
|
|
triggerCounts,
|
|
triggerLogs: [],
|
|
};
|
|
});
|
|
|
|
setAnimationStates(newStates);
|
|
animationStatesRef.current = newStates;
|
|
clockRef.current.start();
|
|
}
|
|
}, [isPlaying, processes, internalResetFlag]);
|
|
|
|
// Helper functions
|
|
const findSpawnPoint = (process: ProcessData): ProcessPoint | null => {
|
|
for (const path of process.paths || []) {
|
|
for (const point of path.points || []) {
|
|
const spawnAction = point.actions?.find(
|
|
(a) => a.isUsed && a.type === "Spawn"
|
|
);
|
|
if (spawnAction) {
|
|
return point;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
const findAnimationPathPoint = (
|
|
process: ProcessData,
|
|
spawnPoint: ProcessPoint
|
|
): THREE.Vector3 => {
|
|
if (process.animationPath && process.animationPath.length > 0) {
|
|
let pointIndex = 0;
|
|
for (const path of process.paths || []) {
|
|
for (let i = 0; i < (path.points?.length || 0); i++) {
|
|
const point = path.points?.[i];
|
|
if (point && point.uuid === spawnPoint.uuid) {
|
|
if (process.animationPath[pointIndex]) {
|
|
const p = process.animationPath[pointIndex];
|
|
return new THREE.Vector3(p.x, p.y, p.z);
|
|
}
|
|
}
|
|
pointIndex++;
|
|
}
|
|
}
|
|
}
|
|
return new THREE.Vector3(
|
|
spawnPoint.position[0],
|
|
spawnPoint.position[1],
|
|
spawnPoint.position[2]
|
|
);
|
|
};
|
|
|
|
// Optimized object creation
|
|
const createSpawnedObject = useCallback(
|
|
(
|
|
process: ProcessData,
|
|
currentTime: number,
|
|
materialType: string,
|
|
spawnPoint: ProcessPoint,
|
|
baseMaterials: Record<string, THREE.Material>
|
|
): SpawnedObject => {
|
|
const processMaterials = {
|
|
...baseMaterials,
|
|
...(process.customMaterials || {}),
|
|
};
|
|
|
|
const spawnPosition = findAnimationPathPoint(process, spawnPoint);
|
|
const material =
|
|
processMaterials[materialType as keyof typeof processMaterials] ||
|
|
baseMaterials.Default;
|
|
|
|
return {
|
|
ref: { current: null },
|
|
state: {
|
|
currentIndex: 0,
|
|
progress: 0,
|
|
isAnimating: true,
|
|
speed: process.speed || 1,
|
|
isDelaying: false,
|
|
delayStartTime: 0,
|
|
currentDelayDuration: 0,
|
|
delayComplete: false,
|
|
currentPathIndex: 0,
|
|
},
|
|
visible: true,
|
|
material: material,
|
|
currentMaterialType: materialType,
|
|
spawnTime: currentTime,
|
|
position: spawnPosition,
|
|
};
|
|
},
|
|
[]
|
|
);
|
|
|
|
// Material handling
|
|
const handleMaterialSwap = useCallback(
|
|
(
|
|
processId: string,
|
|
objectId: string,
|
|
materialType: string,
|
|
processes: ProcessData[],
|
|
baseMaterials: Record<string, THREE.Material>
|
|
) => {
|
|
setAnimationStates((prev) => {
|
|
const processState = prev[processId];
|
|
if (!processState || !processState.spawnedObjects[objectId])
|
|
return prev;
|
|
|
|
const process = processes.find((p) => p.id === processId);
|
|
if (!process) return prev;
|
|
|
|
const processMaterials = {
|
|
...baseMaterials,
|
|
...(process.customMaterials || {}),
|
|
};
|
|
|
|
const newMaterial =
|
|
processMaterials[materialType as keyof typeof processMaterials];
|
|
if (!newMaterial) return prev;
|
|
|
|
return {
|
|
...prev,
|
|
[processId]: {
|
|
...processState,
|
|
spawnedObjects: {
|
|
...processState.spawnedObjects,
|
|
[objectId]: {
|
|
...processState.spawnedObjects[objectId],
|
|
material: newMaterial,
|
|
currentMaterialType: materialType,
|
|
},
|
|
},
|
|
},
|
|
};
|
|
});
|
|
},
|
|
[]
|
|
);
|
|
|
|
// Point action handler with trigger counting
|
|
const handlePointActions = useCallback(
|
|
(
|
|
processId: string,
|
|
objectId: string,
|
|
actions: PointAction[] = [],
|
|
currentTime: number,
|
|
processes: ProcessData[],
|
|
baseMaterials: Record<string, THREE.Material>
|
|
): boolean => {
|
|
let shouldStopAnimation = false;
|
|
|
|
actions.forEach((action) => {
|
|
if (!action.isUsed) return;
|
|
|
|
switch (action.type) {
|
|
case "Delay":
|
|
setAnimationStates((prev) => {
|
|
const processState = prev[processId];
|
|
if (!processState || processState.isProcessDelaying) return prev;
|
|
|
|
const delayDuration =
|
|
typeof action.delay === "number"
|
|
? action.delay
|
|
: parseFloat(action.delay as string) || 0;
|
|
|
|
if (delayDuration > 0) {
|
|
return {
|
|
...prev,
|
|
[processId]: {
|
|
...processState,
|
|
isProcessDelaying: true,
|
|
processDelayStartTime: currentTime,
|
|
processDelayDuration: delayDuration,
|
|
spawnedObjects: {
|
|
...processState.spawnedObjects,
|
|
[objectId]: {
|
|
...processState.spawnedObjects[objectId],
|
|
state: {
|
|
...processState.spawnedObjects[objectId].state,
|
|
isAnimating: false,
|
|
isDelaying: true,
|
|
delayStartTime: currentTime,
|
|
currentDelayDuration: delayDuration,
|
|
delayComplete: false,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
}
|
|
return prev;
|
|
});
|
|
shouldStopAnimation = true;
|
|
break;
|
|
|
|
case "Despawn":
|
|
setAnimationStates((prev) => {
|
|
const processState = prev[processId];
|
|
if (!processState) return prev;
|
|
|
|
const newSpawnedObjects = { ...processState.spawnedObjects };
|
|
delete newSpawnedObjects[objectId];
|
|
|
|
return {
|
|
...prev,
|
|
[processId]: {
|
|
...processState,
|
|
spawnedObjects: newSpawnedObjects,
|
|
},
|
|
};
|
|
});
|
|
shouldStopAnimation = true;
|
|
break;
|
|
|
|
case "Swap":
|
|
if (action.material) {
|
|
handleMaterialSwap(
|
|
processId,
|
|
objectId,
|
|
action.material,
|
|
processes,
|
|
baseMaterials
|
|
);
|
|
}
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
});
|
|
|
|
return shouldStopAnimation;
|
|
},
|
|
[handleMaterialSwap]
|
|
);
|
|
|
|
const deferredArmBotUpdates = useRef<{ uuid: string; triggerId: string }[]>([]);
|
|
|
|
// Trigger counting system
|
|
const checkAndCountTriggers = useCallback(
|
|
(
|
|
processId: string,
|
|
objectId: string,
|
|
currentPointIndex: number,
|
|
processes: ProcessData[],
|
|
currentTime: number
|
|
) => {
|
|
setAnimationStates((prev) => {
|
|
const processState = prev[processId];
|
|
if (!processState) return prev;
|
|
|
|
const process = processes.find((p) => p.id === processId);
|
|
if (!process) return prev;
|
|
|
|
const point = getPointDataForAnimationIndex(process, currentPointIndex);
|
|
if (!point?.triggers) return prev;
|
|
|
|
const onHitTriggers = point.triggers.filter((t: Trigger) => t.type === "On-Hit" && t.isUsed);
|
|
|
|
if (onHitTriggers.length === 0) return prev;
|
|
|
|
let newTriggerCounts = { ...processState.triggerCounts };
|
|
const newTriggerLogs = [...processState.triggerLogs];
|
|
let shouldLog = false;
|
|
|
|
const vehiclePaths = process.paths.filter((path) => path.type === "Vehicle");
|
|
const armBotPaths = process.paths.filter((path) => path.type === "ArmBot");
|
|
|
|
const activeVehicles = vehiclePaths.filter((path) => {
|
|
const vehicleId = path.modeluuid;
|
|
const vehicleEntry = agvRef.current.find((v: any) => v.vehicleId === vehicleId && v.processId === processId);
|
|
return vehicleEntry?.isActive;
|
|
});
|
|
|
|
// Check if any ArmBot is active for this process
|
|
// const activeArmBots = armBotPaths.filter((path) => {
|
|
// const armBotId = path.modeluuid;
|
|
// const armBotEntry = armBots.find((a: any) => a.uuid === armBotId);
|
|
// return armBotEntry;
|
|
// });
|
|
|
|
// Only count triggers if no vehicles and no ArmBots are active for this process
|
|
|
|
if (activeVehicles.length === 0) {
|
|
onHitTriggers.forEach((trigger: Trigger) => {
|
|
const connections = point.connections?.targets || [];
|
|
|
|
connections.forEach((connection) => {
|
|
const connectedModelUUID = connection.modelUUID;
|
|
|
|
const matchingArmPath = armBotPaths.find((path) => path.modeluuid === connectedModelUUID);
|
|
|
|
if (matchingArmPath) {
|
|
deferredArmBotUpdates.current.push({
|
|
uuid: connectedModelUUID,
|
|
triggerId: trigger.uuid,
|
|
});
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
let processTotalHits = Object.values(newTriggerCounts).reduce((a, b) => a + b, 0);
|
|
|
|
// Handle logic for vehicles and ArmBots when a trigger is hit
|
|
if (shouldLog) {
|
|
vehiclePaths.forEach((vehiclePath) => {
|
|
if (vehiclePath.points?.length > 0) {
|
|
const vehiclePoint = vehiclePath.points[0];
|
|
const action = vehiclePoint.actions?.[0];
|
|
const maxHitCount = action?.hitCount;
|
|
|
|
if (maxHitCount !== undefined) {
|
|
const vehicleId = vehiclePath.modeluuid;
|
|
let vehicleEntry = agvRef.current.find(
|
|
(v: any) =>
|
|
v.vehicleId === vehicleId && v.processId === processId
|
|
);
|
|
|
|
if (!vehicleEntry) {
|
|
vehicleEntry = {
|
|
processId,
|
|
vehicleId,
|
|
maxHitCount: maxHitCount,
|
|
isActive: false,
|
|
hitCount: 0,
|
|
status: "stationed",
|
|
};
|
|
agvRef.current.push(vehicleEntry);
|
|
}
|
|
|
|
if (!vehicleEntry.isActive) {
|
|
vehicleEntry.hitCount++;
|
|
vehicleEntry.lastUpdated = currentTime;
|
|
|
|
if (vehicleEntry.hitCount >= vehicleEntry.maxHitCount) {
|
|
vehicleEntry.isActive = true;
|
|
newTriggerCounts = {};
|
|
processTotalHits = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
return {
|
|
...prev,
|
|
[processId]: {
|
|
...processState,
|
|
triggerCounts: newTriggerCounts,
|
|
triggerLogs: newTriggerLogs,
|
|
totalHits: processTotalHits,
|
|
},
|
|
};
|
|
});
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (deferredArmBotUpdates.current.length > 0) {
|
|
const updates = [...deferredArmBotUpdates.current];
|
|
deferredArmBotUpdates.current = [];
|
|
|
|
setArmBots((prev) =>
|
|
prev.map((bot) => {
|
|
const update = updates.find((u) => u.uuid === bot.uuid);
|
|
return update ? { ...bot, triggerId: update.triggerId } : bot;
|
|
})
|
|
);
|
|
}
|
|
}, [animationStates]);
|
|
|
|
// Utility functions
|
|
const hasNonInheritActions = useCallback(
|
|
(actions: PointAction[] = []): boolean => {
|
|
return actions.some(
|
|
(action) => action.isUsed && action.type !== "Inherit"
|
|
);
|
|
},
|
|
[]
|
|
);
|
|
|
|
const getPointDataForAnimationIndex = useCallback(
|
|
(process: ProcessData, index: number): ProcessPoint | null => {
|
|
if (!process.paths) return null;
|
|
|
|
let cumulativePoints = 0;
|
|
for (const path of process.paths) {
|
|
const pointCount = path.points?.length || 0;
|
|
|
|
if (index < cumulativePoints + pointCount) {
|
|
const pointIndex = index - cumulativePoints;
|
|
return path.points?.[pointIndex] || null;
|
|
}
|
|
|
|
cumulativePoints += pointCount;
|
|
}
|
|
|
|
return null;
|
|
},
|
|
[]
|
|
);
|
|
|
|
const getTriggerCounts = useCallback((processId: string) => {
|
|
return animationStatesRef.current[processId]?.triggerCounts || {};
|
|
}, []);
|
|
|
|
const getTriggerLogs = useCallback((processId: string) => {
|
|
return animationStatesRef.current[processId]?.triggerLogs || [];
|
|
}, []);
|
|
|
|
return {
|
|
animationStates,
|
|
setAnimationStates,
|
|
clockRef,
|
|
elapsedBeforePauseRef,
|
|
speedRef,
|
|
debugRef,
|
|
findSpawnPoint,
|
|
createSpawnedObject,
|
|
handlePointActions,
|
|
hasNonInheritActions,
|
|
getPointDataForAnimationIndex,
|
|
checkAndCountTriggers,
|
|
getTriggerCounts,
|
|
getTriggerLogs,
|
|
processes,
|
|
};
|
|
};
|