Files
Dwinzo_dev/app/src/modules/simulation/process/processAnimator.tsx
2025-04-04 18:02:53 +05:30

799 lines
23 KiB
TypeScript

import React, { useRef, useState, useEffect, useMemo } from "react";
import {
useAnimationPlaySpeed,
usePauseButtonStore,
usePlayButtonStore,
useResetButtonStore,
} from "../../../store/usePlayButtonStore";
import { GLTFLoader } from "three-stdlib";
import { useLoader, useFrame } from "@react-three/fiber";
import * as THREE from "three";
import { GLTF } from "three-stdlib";
import crate from "../../../assets/gltf-glb/crate_box.glb";
import box from "../../../assets/gltf-glb/box.glb";
interface PointAction {
uuid: string;
name: string;
type: "Inherit" | "Spawn" | "Despawn" | "Delay" | "Swap";
objectType: string;
material: string;
delay: string | number;
spawnInterval: string | number;
isUsed: boolean;
}
interface ProcessPoint {
uuid: string;
position: number[];
rotation: number[];
actions: PointAction[];
connections: {
source: { pathUUID: string; pointUUID: string };
targets: { pathUUID: string; pointUUID: string }[];
};
}
interface ProcessPath {
modeluuid: string;
modelName: string;
points: ProcessPoint[];
pathPosition: number[];
pathRotation: number[];
speed: number;
}
interface ProcessData {
id: string;
paths: ProcessPath[];
animationPath: { x: number; y: number; z: number }[];
pointActions: PointAction[][];
speed: number;
customMaterials?: Record<string, THREE.Material>;
renderAs?: "box" | "custom";
}
interface AnimationState {
currentIndex: number;
progress: number;
isAnimating: boolean;
speed: number;
isDelaying: boolean;
delayStartTime: number;
currentDelayDuration: number;
delayComplete: boolean;
currentPathIndex: number;
}
interface SpawnedObject {
ref: React.RefObject<THREE.Group | THREE.Mesh>;
state: AnimationState;
visible: boolean;
material: THREE.Material;
spawnTime: number;
currentMaterialType: string;
position: THREE.Vector3;
}
interface ProcessAnimationState {
spawnedObjects: { [objectId: string]: SpawnedObject };
nextSpawnTime: number;
objectIdCounter: number;
isProcessDelaying: boolean;
processDelayStartTime: number;
processDelayDuration: number;
}
const ProcessAnimator: React.FC<{ processes: ProcessData[] }> = ({
processes,
}) => {
const gltf = useLoader(GLTFLoader, crate) as GLTF;
const { isPlaying, setIsPlaying } = usePlayButtonStore();
const { isPaused, setIsPaused } = usePauseButtonStore();
const { isReset, setReset } = useResetButtonStore();
const groupRef = useRef<THREE.Group>(null);
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, ProcessAnimationState>>({});
const { speed, setSpeed } = useAnimationPlaySpeed();
const prevIsPlaying = useRef<boolean | null>(null);
const [internalResetFlag, setInternalResetFlag] = useState(false);
const [animationStates, setAnimationStates] = useState<
Record<string, ProcessAnimationState>
>({});
// Store the speed in a ref to access the latest value in animation frames
const speedRef = useRef<number>(speed);
// Update the ref when speed changes
useEffect(() => {
speedRef.current = speed;
}, [speed]);
useEffect(() => {
if (prevIsPlaying.current !== null) {
setAnimationStates({});
}
// Update ref to current isPlaying after effect
prevIsPlaying.current = isPlaying;
// setAnimationStates({});
}, [isPlaying]);
// Sync ref with state
useEffect(() => {
animationStatesRef.current = animationStates;
}, [animationStates]);
// Base materials
const baseMaterials = useMemo(
() => ({
Box: new THREE.MeshStandardMaterial({ color: 0x8b4513 }),
Crate: new THREE.MeshStandardMaterial({ color: 0x00ff00 }),
Default: new THREE.MeshStandardMaterial({ color: 0x00ff00 }),
}),
[]
);
// Replace your reset effect with this:
useEffect(() => {
if (isReset) {
// 1. Mark that we're doing an internal reset
setInternalResetFlag(true);
// 2. Pause the animation first
setIsPlaying(false);
setIsPaused(false);
// 3. Reset all animation states
setAnimationStates({});
animationStatesRef.current = {};
// 4. Reset timing references
clockRef.current = new THREE.Clock();
elapsedBeforePauseRef.current = 0;
pauseTimeRef.current = 0;
// 5. Clear the external reset flag
setReset(false);
// 6. After state updates are complete, restart
setTimeout(() => {
setInternalResetFlag(false);
setIsPlaying(true);
}, 0);
}
}, [isReset, setReset, setIsPlaying, setIsPaused]);
// Handle pause state changes
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 when processes or play state changes
useEffect(() => {
if (isPlaying && !internalResetFlag) {
const newStates: Record<string, ProcessAnimationState> = {};
processes.forEach((process) => {
newStates[process.id] = {
spawnedObjects: {},
nextSpawnTime: 0,
objectIdCounter: 0,
isProcessDelaying: false,
processDelayStartTime: 0,
processDelayDuration: 0,
};
});
setAnimationStates(newStates);
animationStatesRef.current = newStates;
clockRef.current.start();
}
}, [isPlaying, processes, internalResetFlag]);
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]
);
};
const createSpawnedObject = (
process: ProcessData,
currentTime: number,
materialType: string,
spawnPoint: ProcessPoint
): SpawnedObject => {
const processMaterials = {
...baseMaterials,
...(process.customMaterials || {}),
};
const spawnPosition = findAnimationPathPoint(process, spawnPoint);
const material =
processMaterials[materialType as keyof typeof processMaterials] ||
baseMaterials.Default;
if (debugRef.current) {
console.log(`Creating object with material: ${materialType}`, material);
}
return {
ref: React.createRef(),
state: {
currentIndex: 0,
progress: 0,
isAnimating: true,
speed: process.speed || 1, // Process base speed (will be multiplied by global speed)
isDelaying: false,
delayStartTime: 0,
currentDelayDuration: 0,
delayComplete: false,
currentPathIndex: 0,
},
visible: true,
material: material,
currentMaterialType: materialType,
spawnTime: currentTime,
position: spawnPosition,
};
};
const handleMaterialSwap = (
processId: string,
objectId: string,
materialType: string
) => {
if (debugRef.current) {
console.log(`Attempting material swap to: ${materialType}`);
}
setAnimationStates((prev) => {
const processState = prev[processId];
if (!processState || !processState.spawnedObjects[objectId]) {
if (debugRef.current) console.log("Object not found for swap");
return prev;
}
const process = processes.find((p) => p.id === processId);
if (!process) {
if (debugRef.current) console.log("Process not found");
return prev;
}
const processMaterials = {
...baseMaterials,
...(process.customMaterials || {}),
};
const newMaterial =
processMaterials[materialType as keyof typeof processMaterials];
if (!newMaterial) {
if (debugRef.current) console.log(`Material ${materialType} not found`);
return prev;
}
if (debugRef.current) {
console.log(`Swapping material for ${objectId} to ${materialType}`);
}
return {
...prev,
[processId]: {
...processState,
spawnedObjects: {
...processState.spawnedObjects,
[objectId]: {
...processState.spawnedObjects[objectId],
material: newMaterial,
currentMaterialType: materialType,
},
},
},
};
});
};
const handlePointActions = (
processId: string,
objectId: string,
actions: PointAction[] = [],
currentTime: number
): boolean => {
let shouldStopAnimation = false;
actions.forEach((action) => {
if (!action.isUsed) return;
if (debugRef.current) {
console.log(`Processing action: ${action.type} for ${objectId}`);
}
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);
}
break;
default:
break;
}
});
return shouldStopAnimation;
};
const hasNonInheritActions = (actions: PointAction[] = []): boolean => {
return actions.some((action) => action.isUsed && action.type !== "Inherit");
};
const getPointDataForAnimationIndex = (
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;
};
useFrame(() => {
if (!isPlaying || isPaused) return;
const currentTime =
clockRef.current.getElapsedTime() - elapsedBeforePauseRef.current;
setAnimationStates((prev) => {
const newStates = { ...prev };
processes.forEach((process) => {
const processState = newStates[process.id];
if (!processState) return;
if (processState.isProcessDelaying) {
// Apply global speed to delays (faster speed = shorter delays)
const effectiveDelayTime =
processState.processDelayDuration / speedRef.current;
if (
currentTime - processState.processDelayStartTime >=
effectiveDelayTime
) {
newStates[process.id] = {
...processState,
isProcessDelaying: false,
spawnedObjects: Object.entries(
processState.spawnedObjects
).reduce(
(acc, [id, obj]) => ({
...acc,
[id]: {
...obj,
state: {
...obj.state,
isDelaying: false,
delayComplete: true,
isAnimating: true,
progress:
obj.state.progress === 0 ? 0.001 : obj.state.progress,
},
},
}),
{}
),
};
}
return;
}
const spawnPoint = findSpawnPoint(process);
if (!spawnPoint || !spawnPoint.actions) return;
const spawnAction = spawnPoint.actions.find(
(a) => a.isUsed && a.type === "Spawn"
);
if (!spawnAction) return;
const spawnInterval =
typeof spawnAction.spawnInterval === "number"
? spawnAction.spawnInterval
: parseFloat(spawnAction.spawnInterval as string) || 0;
// Apply global speed to spawn intervals (faster speed = more frequent spawns)
const effectiveSpawnInterval = spawnInterval / speedRef.current;
if (currentTime >= processState.nextSpawnTime) {
const objectId = `obj-${process.id}-${processState.objectIdCounter}`;
const newObject = createSpawnedObject(
process,
currentTime,
spawnAction.material || "Default",
spawnPoint
);
newStates[process.id] = {
...processState,
spawnedObjects: {
...processState.spawnedObjects,
[objectId]: newObject,
},
objectIdCounter: processState.objectIdCounter + 1,
nextSpawnTime: currentTime + effectiveSpawnInterval,
};
}
});
return newStates;
});
});
useFrame((_, delta) => {
if (!isPlaying || isPaused) return;
const currentTime =
clockRef.current.getElapsedTime() - elapsedBeforePauseRef.current;
setAnimationStates((prev) => {
const newStates = { ...prev };
processes.forEach((process) => {
const processState = newStates[process.id];
if (!processState) return;
if (processState.isProcessDelaying) {
// Apply global speed to delays (faster speed = shorter delays)
const effectiveDelayTime =
processState.processDelayDuration / speedRef.current;
if (
currentTime - processState.processDelayStartTime >=
effectiveDelayTime
) {
newStates[process.id] = {
...processState,
isProcessDelaying: false,
spawnedObjects: Object.entries(
processState.spawnedObjects
).reduce(
(acc, [id, obj]) => ({
...acc,
[id]: {
...obj,
state: {
...obj.state,
isDelaying: false,
delayComplete: true,
isAnimating: true,
progress:
obj.state.progress === 0 ? 0.005 : obj.state.progress,
},
},
}),
{}
),
};
return newStates;
} else {
return newStates;
}
}
const path =
process.animationPath?.map((p) => new THREE.Vector3(p.x, p.y, p.z)) ||
[];
if (path.length < 2) return;
const updatedObjects = { ...processState.spawnedObjects };
Object.entries(processState.spawnedObjects).forEach(
([objectId, obj]) => {
if (!obj.visible) return;
const currentRef = gltf?.scene ? obj.ref.current : obj.ref.current;
if (!currentRef) return;
if (
obj.position &&
obj.state.currentIndex === 0 &&
obj.state.progress === 0
) {
currentRef.position.copy(obj.position);
}
const stateRef = obj.state;
if (stateRef.isDelaying) {
// Apply global speed to delays (faster speed = shorter delays)
const effectiveDelayTime =
stateRef.currentDelayDuration / speedRef.current;
if (currentTime - stateRef.delayStartTime >= effectiveDelayTime) {
stateRef.isDelaying = false;
stateRef.delayComplete = true;
stateRef.isAnimating = true;
if (stateRef.progress === 0) {
stateRef.progress = 0.005;
}
const nextPointIdx = stateRef.currentIndex + 1;
if (nextPointIdx < path.length) {
const slightProgress = Math.max(stateRef.progress, 0.005);
currentRef.position.lerpVectors(
path[stateRef.currentIndex],
nextPointIdx < path.length
? path[nextPointIdx]
: path[stateRef.currentIndex],
slightProgress
);
}
} else {
updatedObjects[objectId] = { ...obj, state: { ...stateRef } };
return;
}
}
if (!stateRef.isAnimating) return;
const currentPointData = getPointDataForAnimationIndex(
process,
stateRef.currentIndex
);
if (stateRef.progress === 0 && currentPointData?.actions) {
if (debugRef.current) {
console.log(
`At point ${stateRef.currentIndex} with actions:`,
currentPointData.actions
);
}
const shouldStop = handlePointActions(
process.id,
objectId,
currentPointData.actions,
currentTime
);
if (shouldStop) {
updatedObjects[objectId] = { ...obj, state: { ...stateRef } };
return;
}
}
const nextPointIdx = stateRef.currentIndex + 1;
const isLastPoint = nextPointIdx >= path.length;
if (isLastPoint) {
if (currentPointData?.actions) {
const shouldStop = !hasNonInheritActions(
currentPointData.actions
);
if (shouldStop) {
return;
}
}
}
if (!isLastPoint) {
const nextPoint = path[nextPointIdx];
const distance =
path[stateRef.currentIndex].distanceTo(nextPoint);
// Apply both process-specific speed and global speed multiplier
const effectiveSpeed = stateRef.speed * speedRef.current;
const movement = effectiveSpeed * delta;
if (stateRef.delayComplete && stateRef.progress < 0.01) {
stateRef.progress = 0.05;
stateRef.delayComplete = false;
} else {
stateRef.progress += movement / distance;
}
if (stateRef.progress >= 1) {
stateRef.currentIndex = nextPointIdx;
stateRef.progress = 0;
currentRef.position.copy(nextPoint);
const newPointData = getPointDataForAnimationIndex(
process,
stateRef.currentIndex
);
if (newPointData?.actions && debugRef.current) {
console.log(
`Reached new point with actions:`,
newPointData.actions
);
}
} else {
currentRef.position.lerpVectors(
path[stateRef.currentIndex],
nextPoint,
stateRef.progress
);
}
}
updatedObjects[objectId] = { ...obj, state: { ...stateRef } };
}
);
newStates[process.id] = {
...processState,
spawnedObjects: updatedObjects,
};
});
return newStates;
});
});
if (!processes || processes.length === 0) {
return null;
}
return (
<>
{Object.entries(animationStates).flatMap(([processId, processState]) =>
Object.entries(processState.spawnedObjects)
.filter(([_, obj]) => obj.visible)
.map(([objectId, obj]) => {
const process = processes.find((p) => p.id === processId);
const renderAs = process?.renderAs || "custom";
if (renderAs === "box") {
return (
<mesh
key={objectId}
ref={obj.ref as React.RefObject<THREE.Mesh>}
material={obj.material}
position={obj.position}
>
<boxGeometry args={[1, 1, 1]} />
</mesh>
);
}
if (gltf?.scene) {
// Clone the scene and apply the material to all meshes
const clonedScene = gltf.scene.clone();
clonedScene.traverse((child) => {
if (child instanceof THREE.Mesh) {
child.material = obj.material;
}
});
return (
<group
key={objectId}
ref={obj.ref as React.RefObject<THREE.Group>}
position={obj.position}
>
<primitive object={clonedScene} />
</group>
);
}
return null;
})
)}
</>
);
};
export default ProcessAnimator;