"updated animation"
This commit is contained in:
@@ -5,7 +5,6 @@ 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";
|
||||
import camera from "../../../assets/gltf-glb/camera face 2.gltf";
|
||||
|
||||
interface PointAction {
|
||||
uuid: string;
|
||||
@@ -44,6 +43,8 @@ interface ProcessData {
|
||||
animationPath: { x: number; y: number; z: number }[];
|
||||
pointActions: PointAction[][];
|
||||
speed: number;
|
||||
customMaterials?: Record<string, THREE.Material>;
|
||||
renderAs?: "box" | "custom";
|
||||
}
|
||||
|
||||
interface AnimationState {
|
||||
@@ -58,19 +59,35 @@ interface AnimationState {
|
||||
currentPathIndex: number;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const ProcessAnimator: React.FC<{ processes: ProcessData[] }> = ({
|
||||
processes,
|
||||
}) => {
|
||||
console.log("processes: ", processes);
|
||||
const gltf = useLoader(GLTFLoader, boxGltb) as GLTF;
|
||||
const { isPlaying, setIsPlaying } = usePlayButtonStore();
|
||||
|
||||
const { isPlaying } = usePlayButtonStore();
|
||||
const groupRef = useRef<THREE.Group>(null);
|
||||
const meshRef = useRef<THREE.Mesh>(null);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [currentPathIndex, setCurrentPathIndex] = useState(0);
|
||||
|
||||
const materials = useMemo(
|
||||
const [animationStates, setAnimationStates] = useState<
|
||||
Record<string, ProcessAnimationState>
|
||||
>({});
|
||||
|
||||
// Base materials
|
||||
const baseMaterials = useMemo(
|
||||
() => ({
|
||||
Wood: new THREE.MeshStandardMaterial({ color: 0x8b4513 }),
|
||||
Box: new THREE.MeshStandardMaterial({
|
||||
@@ -88,44 +105,211 @@ const ProcessAnimator: React.FC<{ processes: ProcessData[] }> = ({
|
||||
[]
|
||||
);
|
||||
|
||||
const [currentMaterial, setCurrentMaterial] = useState<THREE.Material>(
|
||||
materials.Default
|
||||
);
|
||||
// Initialize animation states when processes or play state changes
|
||||
useEffect(() => {
|
||||
if (!isPlaying) {
|
||||
setAnimationStates({});
|
||||
return;
|
||||
}
|
||||
|
||||
const { animationPath, currentProcess } = useMemo(() => {
|
||||
const defaultProcess = {
|
||||
animationPath: [],
|
||||
pointActions: [],
|
||||
speed: 1,
|
||||
paths: [],
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Create a new spawned object
|
||||
const createSpawnedObject = (
|
||||
process: ProcessData,
|
||||
currentTime: number,
|
||||
materialType: string
|
||||
): SpawnedObject => {
|
||||
const processMaterials = {
|
||||
...baseMaterials,
|
||||
...(process.customMaterials || {}),
|
||||
};
|
||||
const cp = processes?.[0] || defaultProcess;
|
||||
|
||||
return {
|
||||
animationPath:
|
||||
cp.animationPath?.map((p) => new THREE.Vector3(p.x, p.y, p.z)) || [],
|
||||
currentProcess: cp,
|
||||
ref: React.createRef(),
|
||||
state: {
|
||||
currentIndex: 0,
|
||||
progress: 0,
|
||||
isAnimating: true,
|
||||
speed: process.speed || 1,
|
||||
isDelaying: false,
|
||||
delayStartTime: 0,
|
||||
currentDelayDuration: 0,
|
||||
delayComplete: false,
|
||||
currentPathIndex: 0,
|
||||
},
|
||||
visible: true,
|
||||
material:
|
||||
processMaterials[materialType as keyof typeof processMaterials] ||
|
||||
baseMaterials.Default,
|
||||
currentMaterialType: materialType,
|
||||
spawnTime: currentTime,
|
||||
};
|
||||
}, [processes]);
|
||||
};
|
||||
|
||||
const animationStateRef = useRef<AnimationState>({
|
||||
currentIndex: 0,
|
||||
progress: 0,
|
||||
isAnimating: false,
|
||||
speed: currentProcess.speed,
|
||||
isDelaying: false,
|
||||
delayStartTime: 0,
|
||||
currentDelayDuration: 0,
|
||||
delayComplete: false,
|
||||
currentPathIndex: 0,
|
||||
});
|
||||
// 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 getPointDataForAnimationIndex = (index: number) => {
|
||||
if (!processes[0]?.paths) return null;
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// Handle point actions for an object
|
||||
const handlePointActions = (
|
||||
processId: string,
|
||||
objectId: string,
|
||||
actions: PointAction[] = [],
|
||||
currentTime: number
|
||||
): 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.spawnedObjects[objectId] ||
|
||||
processState.spawnedObjects[objectId].state.isDelaying
|
||||
) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
const delayDuration =
|
||||
typeof action.delay === "number"
|
||||
? action.delay
|
||||
: parseFloat(action.delay as string) || 0;
|
||||
|
||||
if (delayDuration > 0) {
|
||||
return {
|
||||
...prev,
|
||||
[processId]: {
|
||||
...processState,
|
||||
spawnedObjects: {
|
||||
...processState.spawnedObjects,
|
||||
[objectId]: {
|
||||
...processState.spawnedObjects[objectId],
|
||||
state: {
|
||||
...processState.spawnedObjects[objectId].state,
|
||||
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;
|
||||
};
|
||||
|
||||
// Check if point has non-inherit actions
|
||||
const hasNonInheritActions = (actions: PointAction[] = []): boolean => {
|
||||
return actions.some((action) => action.isUsed && action.type !== "Inherit");
|
||||
};
|
||||
|
||||
// Get point data for current animation index
|
||||
const getPointDataForAnimationIndex = (
|
||||
process: ProcessData,
|
||||
index: number
|
||||
): ProcessPoint | null => {
|
||||
if (!process.paths) return null;
|
||||
|
||||
let cumulativePoints = 0;
|
||||
console.log("cumulativePoints: ", cumulativePoints);
|
||||
|
||||
for (const path of processes[0].paths) {
|
||||
for (const path of process.paths) {
|
||||
const pointCount = path.points?.length || 0;
|
||||
|
||||
if (index < cumulativePoints + pointCount) {
|
||||
@@ -139,209 +323,202 @@ const ProcessAnimator: React.FC<{ processes: ProcessData[] }> = ({
|
||||
return null;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isPlaying) {
|
||||
setVisible(true);
|
||||
animationStateRef.current = {
|
||||
currentIndex: 0,
|
||||
progress: 0,
|
||||
isAnimating: true,
|
||||
speed: currentProcess.speed,
|
||||
isDelaying: false,
|
||||
delayStartTime: 0,
|
||||
currentDelayDuration: 0,
|
||||
delayComplete: false,
|
||||
currentPathIndex: 0,
|
||||
};
|
||||
|
||||
const currentRef = gltf?.scene ? groupRef.current : meshRef.current;
|
||||
if (currentRef && animationPath.length > 0) {
|
||||
currentRef.position.copy(animationPath[0]);
|
||||
}
|
||||
} else {
|
||||
animationStateRef.current.isAnimating = false;
|
||||
}
|
||||
}, [isPlaying, currentProcess, animationPath]);
|
||||
|
||||
const handleMaterialSwap = (materialType: string) => {
|
||||
setCurrentMaterial(
|
||||
materials[materialType as keyof typeof materials] || materials.Default
|
||||
);
|
||||
};
|
||||
|
||||
const hasNonInheritActions = (actions: PointAction[] = []) => {
|
||||
return actions.some((action) => action.isUsed && action.type !== "Inherit");
|
||||
};
|
||||
|
||||
const handlePointActions = (
|
||||
actions: PointAction[] = [],
|
||||
currentTime: number
|
||||
) => {
|
||||
let shouldStopAnimation = false;
|
||||
|
||||
actions.forEach((action) => {
|
||||
if (!action.isUsed) return;
|
||||
|
||||
switch (action.type) {
|
||||
case "Delay":
|
||||
if (
|
||||
!animationStateRef.current.isDelaying &&
|
||||
!animationStateRef.current.delayComplete
|
||||
) {
|
||||
const delayDuration =
|
||||
typeof action.delay === "number"
|
||||
? action.delay
|
||||
: parseFloat(action.delay as string) || 0;
|
||||
|
||||
if (delayDuration > 0) {
|
||||
animationStateRef.current.isDelaying = true;
|
||||
animationStateRef.current.delayStartTime = currentTime;
|
||||
animationStateRef.current.currentDelayDuration = delayDuration;
|
||||
shouldStopAnimation = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "Despawn":
|
||||
setVisible(false);
|
||||
setIsPlaying(false);
|
||||
animationStateRef.current.isAnimating = false;
|
||||
shouldStopAnimation = true;
|
||||
break;
|
||||
|
||||
case "Spawn":
|
||||
setVisible(true);
|
||||
break;
|
||||
|
||||
case "Swap":
|
||||
if (action.material) {
|
||||
handleMaterialSwap(action.material);
|
||||
}
|
||||
break;
|
||||
|
||||
case "Inherit":
|
||||
// Handle inherit logic if needed
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return shouldStopAnimation;
|
||||
};
|
||||
|
||||
useFrame((state, delta) => {
|
||||
const currentRef = gltf?.scene ? groupRef.current : meshRef.current;
|
||||
if (
|
||||
!currentRef ||
|
||||
!animationStateRef.current.isAnimating ||
|
||||
animationPath.length < 2
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// Spawn objects for all processes
|
||||
useFrame((state) => {
|
||||
if (!isPlaying) return;
|
||||
|
||||
const currentTime = state.clock.getElapsedTime();
|
||||
const path = animationPath;
|
||||
const stateRef = animationStateRef.current;
|
||||
setAnimationStates((prev) => {
|
||||
const newStates = { ...prev };
|
||||
|
||||
// Check if we need to switch paths (specific to your structure)
|
||||
if (stateRef.currentIndex === 3 && stateRef.currentPathIndex === 0) {
|
||||
setCurrentPathIndex(1);
|
||||
stateRef.currentPathIndex = 1;
|
||||
}
|
||||
processes.forEach((process) => {
|
||||
const processState = newStates[process.id];
|
||||
if (!processState) return;
|
||||
|
||||
// Get the current point data
|
||||
const currentPointData = getPointDataForAnimationIndex(
|
||||
stateRef.currentIndex
|
||||
);
|
||||
const spawnPoint = findSpawnPoint(process);
|
||||
if (!spawnPoint || !spawnPoint.actions) return;
|
||||
|
||||
// Execute actions when we first arrive at a point
|
||||
|
||||
if (stateRef.progress === 0 && currentPointData?.actions) {
|
||||
console.log(
|
||||
`Processing actions at point ${stateRef.currentIndex}`,
|
||||
currentPointData.actions
|
||||
);
|
||||
const shouldStop = handlePointActions(
|
||||
currentPointData.actions,
|
||||
currentTime
|
||||
);
|
||||
if (shouldStop) return;
|
||||
}
|
||||
|
||||
// Handle delays
|
||||
if (stateRef.isDelaying) {
|
||||
console.log(
|
||||
`Delaying... ${currentTime - stateRef.delayStartTime}/${
|
||||
stateRef.currentDelayDuration
|
||||
}`
|
||||
);
|
||||
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]);
|
||||
setIsPlaying(false);
|
||||
stateRef.isAnimating = false;
|
||||
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
|
||||
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,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return newStates;
|
||||
});
|
||||
});
|
||||
|
||||
// Animate objects for all processes
|
||||
useFrame((state, delta) => {
|
||||
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 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 } };
|
||||
}
|
||||
);
|
||||
|
||||
newStates[process.id] = {
|
||||
...processState,
|
||||
spawnedObjects: updatedObjects,
|
||||
};
|
||||
});
|
||||
|
||||
return newStates;
|
||||
});
|
||||
});
|
||||
|
||||
if (!processes || processes.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!gltf?.scene) {
|
||||
return visible ? (
|
||||
<mesh ref={meshRef} material={currentMaterial}>
|
||||
<boxGeometry args={[1, 1, 1]} />
|
||||
</mesh>
|
||||
) : 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);
|
||||
console.log("process: ", process);
|
||||
const renderAs = process?.renderAs || "custom";
|
||||
|
||||
return visible ? (
|
||||
<group ref={groupRef}>
|
||||
<primitive
|
||||
object={gltf.scene.clone()}
|
||||
scale={[1, 1, 1]}
|
||||
material={currentMaterial}
|
||||
/>
|
||||
</group>
|
||||
) : null;
|
||||
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>
|
||||
)
|
||||
);
|
||||
})
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProcessAnimator;
|
||||
|
||||
Reference in New Issue
Block a user