Dwinzo_dev/app/src/modules/simulation/process/processAnimator.tsx

525 lines
14 KiB
TypeScript
Raw Normal View History

2025-04-01 13:24:04 +00:00
import React, { useRef, useState, useEffect, useMemo } from "react";
import { usePlayButtonStore } 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 boxGltb from "../../../assets/gltf-glb/crate_box.glb";
interface PointAction {
uuid: string;
name: string;
type: "Inherit" | "Spawn" | "Despawn" | "Delay" | "Swap";
2025-04-02 05:40:44 +00:00
objectType: string;
2025-04-01 13:24:04 +00:00
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;
2025-04-03 04:58:13 +00:00
customMaterials?: Record<string, THREE.Material>;
renderAs?: "box" | "custom";
2025-04-01 13:24:04 +00:00
}
interface AnimationState {
currentIndex: number;
progress: number;
isAnimating: boolean;
speed: number;
isDelaying: boolean;
delayStartTime: number;
currentDelayDuration: number;
delayComplete: boolean;
currentPathIndex: number;
}
2025-04-03 04:58:13 +00:00
interface SpawnedObject {
ref: React.RefObject<THREE.Group | THREE.Mesh>;
state: AnimationState;
visible: boolean;
material: THREE.Material;
spawnTime: number;
currentMaterialType: string;
}
interface ProcessAnimationState {
spawnedObjects: { [objectId: string]: SpawnedObject };
nextSpawnTime: number;
objectIdCounter: number;
}
2025-04-01 13:24:04 +00:00
const ProcessAnimator: React.FC<{ processes: ProcessData[] }> = ({
processes,
}) => {
console.log("processes: ", processes);
const gltf = useLoader(GLTFLoader, boxGltb) as GLTF;
2025-04-03 04:58:13 +00:00
const { isPlaying } = usePlayButtonStore();
2025-04-01 13:24:04 +00:00
const groupRef = useRef<THREE.Group>(null);
2025-04-03 04:58:13 +00:00
const [animationStates, setAnimationStates] = useState<
Record<string, ProcessAnimationState>
>({});
// Base materials
const baseMaterials = useMemo(
2025-04-01 13:24:04 +00:00
() => ({
Wood: new THREE.MeshStandardMaterial({ color: 0x8b4513 }),
Box: new THREE.MeshStandardMaterial({
color: 0xcccccc,
metalness: 0.8,
roughness: 0.2,
}),
Crate: new THREE.MeshStandardMaterial({
color: 0x00aaff,
metalness: 0.1,
roughness: 0.5,
}),
Default: new THREE.MeshStandardMaterial({ color: 0x00ff00 }),
}),
[]
);
2025-04-03 04:58:13 +00:00
// Initialize animation states when processes or play state changes
useEffect(() => {
if (!isPlaying) {
setAnimationStates({});
return;
}
2025-04-02 05:40:44 +00:00
2025-04-03 04:58:13 +00:00
const newStates: Record<string, ProcessAnimationState> = {};
processes.forEach((process) => {
newStates[process.id] = {
spawnedObjects: {},
nextSpawnTime: 0,
objectIdCounter: 0,
};
});
setAnimationStates(newStates);
}, [isPlaying, processes]);
// Find spawn point in a process
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;
}
2025-04-02 05:40:44 +00:00
}
2025-04-01 13:24:04 +00:00
}
2025-04-02 05:40:44 +00:00
return null;
2025-04-01 13:24:04 +00:00
};
2025-04-03 04:58:13 +00:00
// Create a new spawned object
const createSpawnedObject = (
process: ProcessData,
currentTime: number,
materialType: string
): SpawnedObject => {
const processMaterials = {
...baseMaterials,
...(process.customMaterials || {}),
};
return {
ref: React.createRef(),
state: {
2025-04-01 13:24:04 +00:00
currentIndex: 0,
progress: 0,
isAnimating: true,
2025-04-03 04:58:13 +00:00
speed: process.speed || 1,
2025-04-01 13:24:04 +00:00
isDelaying: false,
delayStartTime: 0,
currentDelayDuration: 0,
delayComplete: false,
currentPathIndex: 0,
2025-04-03 04:58:13 +00:00
},
visible: true,
material:
processMaterials[materialType as keyof typeof processMaterials] ||
baseMaterials.Default,
currentMaterialType: materialType,
spawnTime: currentTime,
};
2025-04-01 13:24:04 +00:00
};
2025-04-03 04:58:13 +00:00
// Handle material swap for an object
const handleMaterialSwap = (
processId: string,
objectId: string,
materialType: string
) => {
setAnimationStates((prev) => {
const processState = prev[processId];
if (!processState || !processState.spawnedObjects[objectId]) return prev;
const process = processes.find((p) => p.id === processId);
const processMaterials = {
...baseMaterials,
...(process?.customMaterials || {}),
};
const newMaterial =
processMaterials[materialType as keyof typeof processMaterials] ||
baseMaterials.Default;
return {
...prev,
[processId]: {
...processState,
spawnedObjects: {
...processState.spawnedObjects,
[objectId]: {
...processState.spawnedObjects[objectId],
material: newMaterial,
currentMaterialType: materialType,
},
},
},
};
});
2025-04-01 13:24:04 +00:00
};
2025-04-03 04:58:13 +00:00
// Handle point actions for an object
2025-04-01 13:24:04 +00:00
const handlePointActions = (
2025-04-03 04:58:13 +00:00
processId: string,
objectId: string,
2025-04-01 13:24:04 +00:00
actions: PointAction[] = [],
2025-04-02 05:40:44 +00:00
currentTime: number
2025-04-03 04:58:13 +00:00
): boolean => {
2025-04-01 13:24:04 +00:00
let shouldStopAnimation = false;
actions.forEach((action) => {
if (!action.isUsed) return;
switch (action.type) {
case "Delay":
2025-04-03 04:58:13 +00:00
setAnimationStates((prev) => {
const processState = prev[processId];
if (
!processState ||
!processState.spawnedObjects[objectId] ||
processState.spawnedObjects[objectId].state.isDelaying
) {
return prev;
}
2025-04-01 13:24:04 +00:00
const delayDuration =
typeof action.delay === "number"
? action.delay
: parseFloat(action.delay as string) || 0;
if (delayDuration > 0) {
2025-04-03 04:58:13 +00:00
return {
...prev,
[processId]: {
...processState,
spawnedObjects: {
...processState.spawnedObjects,
[objectId]: {
...processState.spawnedObjects[objectId],
state: {
...processState.spawnedObjects[objectId].state,
isDelaying: true,
delayStartTime: currentTime,
currentDelayDuration: delayDuration,
delayComplete: false,
},
},
},
},
};
2025-04-01 13:24:04 +00:00
}
2025-04-03 04:58:13 +00:00
return prev;
});
shouldStopAnimation = true;
2025-04-01 13:24:04 +00:00
break;
case "Despawn":
2025-04-03 04:58:13 +00:00
setAnimationStates((prev) => {
const processState = prev[processId];
if (!processState) return prev;
const newSpawnedObjects = { ...processState.spawnedObjects };
delete newSpawnedObjects[objectId];
return {
...prev,
[processId]: {
...processState,
spawnedObjects: newSpawnedObjects,
},
};
});
2025-04-01 13:24:04 +00:00
shouldStopAnimation = true;
break;
case "Swap":
if (action.material) {
2025-04-03 04:58:13 +00:00
handleMaterialSwap(processId, objectId, action.material);
2025-04-01 13:24:04 +00:00
}
break;
2025-04-03 04:58:13 +00:00
default:
2025-04-01 13:24:04 +00:00
break;
}
});
return shouldStopAnimation;
};
2025-04-03 04:58:13 +00:00
// Check if point has non-inherit actions
const hasNonInheritActions = (actions: PointAction[] = []): boolean => {
return actions.some((action) => action.isUsed && action.type !== "Inherit");
};
2025-04-01 13:24:04 +00:00
2025-04-03 04:58:13 +00:00
// Get point data for current animation index
const getPointDataForAnimationIndex = (
process: ProcessData,
index: number
): ProcessPoint | null => {
if (!process.paths) return null;
2025-04-01 13:24:04 +00:00
2025-04-03 04:58:13 +00:00
let cumulativePoints = 0;
for (const path of process.paths) {
const pointCount = path.points?.length || 0;
2025-04-01 13:24:04 +00:00
2025-04-03 04:58:13 +00:00
if (index < cumulativePoints + pointCount) {
const pointIndex = index - cumulativePoints;
return path.points?.[pointIndex] || null;
2025-04-01 13:24:04 +00:00
}
2025-04-03 04:58:13 +00:00
cumulativePoints += pointCount;
2025-04-01 13:24:04 +00:00
}
2025-04-03 04:58:13 +00:00
return null;
};
// Spawn objects for all processes
useFrame((state) => {
if (!isPlaying) return;
const currentTime = state.clock.getElapsedTime();
setAnimationStates((prev) => {
const newStates = { ...prev };
processes.forEach((process) => {
const processState = newStates[process.id];
if (!processState) 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
: 0;
if (currentTime >= processState.nextSpawnTime) {
const objectId = `obj-${process.id}-${processState.objectIdCounter}`;
newStates[process.id] = {
...processState,
spawnedObjects: {
...processState.spawnedObjects,
[objectId]: createSpawnedObject(
process,
currentTime,
spawnAction.material || "Default"
),
},
objectIdCounter: processState.objectIdCounter + 1,
nextSpawnTime: currentTime + spawnInterval,
};
2025-04-01 13:24:04 +00:00
}
2025-04-03 04:58:13 +00:00
});
return newStates;
});
});
// Animate objects for all processes
useFrame((state, delta) => {
if (!isPlaying) return;
2025-04-01 13:24:04 +00:00
2025-04-03 04:58:13 +00:00
const currentTime = state.clock.getElapsedTime();
setAnimationStates((prev) => {
const newStates = { ...prev };
processes.forEach((process) => {
const processState = newStates[process.id];
if (!processState) return;
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 || !obj.state.isAnimating) return;
const currentRef = gltf?.scene ? obj.ref.current : obj.ref.current;
if (!currentRef) return;
const stateRef = obj.state;
// Get current point data
const currentPointData = getPointDataForAnimationIndex(
process,
stateRef.currentIndex
);
// Execute actions when arriving at a new point
if (stateRef.progress === 0 && currentPointData?.actions) {
const shouldStop = handlePointActions(
process.id,
objectId,
currentPointData.actions,
currentTime
);
if (shouldStop) return;
}
// Handle delays
if (stateRef.isDelaying) {
if (
currentTime - stateRef.delayStartTime >=
stateRef.currentDelayDuration
) {
stateRef.isDelaying = false;
stateRef.delayComplete = true;
} else {
return; // Keep waiting
}
}
const nextPointIdx = stateRef.currentIndex + 1;
const isLastPoint = nextPointIdx >= path.length;
if (isLastPoint) {
if (currentPointData?.actions) {
const shouldStop = !hasNonInheritActions(
currentPointData.actions
);
if (shouldStop) {
currentRef.position.copy(path[stateRef.currentIndex]);
delete updatedObjects[objectId];
return;
}
}
}
if (!isLastPoint) {
const nextPoint = path[nextPointIdx];
const distance =
path[stateRef.currentIndex].distanceTo(nextPoint);
const movement = stateRef.speed * delta;
stateRef.progress += movement / distance;
if (stateRef.progress >= 1) {
stateRef.currentIndex = nextPointIdx;
stateRef.progress = 0;
stateRef.delayComplete = false;
currentRef.position.copy(nextPoint);
} else {
currentRef.position.lerpVectors(
path[stateRef.currentIndex],
nextPoint,
stateRef.progress
);
}
}
updatedObjects[objectId] = { ...obj, state: { ...stateRef } };
}
2025-04-01 13:24:04 +00:00
);
2025-04-03 04:58:13 +00:00
newStates[process.id] = {
...processState,
spawnedObjects: updatedObjects,
};
});
return newStates;
});
2025-04-01 13:24:04 +00:00
});
if (!processes || processes.length === 0) {
return null;
}
2025-04-03 04:58:13 +00:00
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);
console.log("process: ", process);
const renderAs = process?.renderAs || "custom";
return renderAs === "box" ? (
<mesh
key={objectId}
ref={obj.ref as React.RefObject<THREE.Mesh>}
material={obj.material}
>
<boxGeometry args={[1, 1, 1]} />
</mesh>
) : (
gltf?.scene && (
<group
key={objectId}
ref={obj.ref as React.RefObject<THREE.Group>}
>
<primitive
object={gltf.scene.clone()}
material={obj.material}
/>
</group>
)
);
})
)}
</>
);
2025-04-01 13:24:04 +00:00
};
export default ProcessAnimator;