"updated animation"
This commit is contained in:
916
app/src/modules/simulation/process/animation.Worker.js
Normal file
916
app/src/modules/simulation/process/animation.Worker.js
Normal file
@@ -0,0 +1,916 @@
|
|||||||
|
// // animation-worker.js
|
||||||
|
// // This web worker handles animation calculations off the main thread
|
||||||
|
|
||||||
|
// /* eslint-disable no-restricted-globals */
|
||||||
|
// // The above disables the ESLint rule for this file since 'self' is valid in web workers
|
||||||
|
|
||||||
|
// // Store process data, animation states, and objects
|
||||||
|
// let processes = [];
|
||||||
|
// let animationStates = {};
|
||||||
|
// let lastTimestamp = 0;
|
||||||
|
|
||||||
|
// // Message handler for communication with main thread
|
||||||
|
// self.onmessage = function (event) {
|
||||||
|
// const { type, data } = event.data;
|
||||||
|
|
||||||
|
// switch (type) {
|
||||||
|
// case "initialize":
|
||||||
|
// processes = data.processes;
|
||||||
|
// initializeAnimationStates();
|
||||||
|
// break;
|
||||||
|
|
||||||
|
// case "update":
|
||||||
|
// const { timestamp, isPlaying } = data;
|
||||||
|
// if (isPlaying) {
|
||||||
|
// const delta = (timestamp - lastTimestamp) / 1000; // Convert to seconds
|
||||||
|
// updateAnimations(delta, timestamp);
|
||||||
|
// }
|
||||||
|
// lastTimestamp = timestamp;
|
||||||
|
// break;
|
||||||
|
|
||||||
|
// case "reset":
|
||||||
|
// resetAnimations();
|
||||||
|
// break;
|
||||||
|
|
||||||
|
// case "togglePlay":
|
||||||
|
// // If resuming from pause, recalculate the time delta
|
||||||
|
// lastTimestamp = data.timestamp;
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// // Initialize animation states for all processes
|
||||||
|
// function initializeAnimationStates() {
|
||||||
|
// animationStates = {};
|
||||||
|
|
||||||
|
// processes.forEach((process) => {
|
||||||
|
// animationStates[process.id] = {
|
||||||
|
// spawnedObjects: {},
|
||||||
|
// nextSpawnTime: 0,
|
||||||
|
// objectIdCounter: 0,
|
||||||
|
// };
|
||||||
|
// });
|
||||||
|
|
||||||
|
// // Send initial states back to main thread
|
||||||
|
// self.postMessage({
|
||||||
|
// type: "statesInitialized",
|
||||||
|
// data: { animationStates },
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Reset all animations
|
||||||
|
// function resetAnimations() {
|
||||||
|
// initializeAnimationStates();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Find spawn point in a process
|
||||||
|
// function findSpawnPoint(process) {
|
||||||
|
// 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, path };
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return null;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Create a new spawned object with proper initial position
|
||||||
|
// function createSpawnedObject(process, spawnPoint, currentTime, materialType) {
|
||||||
|
// // Extract spawn position from the actual spawn point
|
||||||
|
// const position = spawnPoint.point.position
|
||||||
|
// ? [...spawnPoint.point.position]
|
||||||
|
// : [0, 0, 0];
|
||||||
|
|
||||||
|
// // Get the path position and add it to the spawn point position
|
||||||
|
// const pathPosition = spawnPoint.path.pathPosition || [0, 0, 0];
|
||||||
|
// const absolutePosition = [
|
||||||
|
// position[0] + pathPosition[0],
|
||||||
|
// position[1] + pathPosition[1],
|
||||||
|
// position[2] + pathPosition[2],
|
||||||
|
// ];
|
||||||
|
|
||||||
|
// return {
|
||||||
|
// id: `obj-${process.id}-${animationStates[process.id].objectIdCounter}`,
|
||||||
|
// position: absolutePosition,
|
||||||
|
// state: {
|
||||||
|
// currentIndex: 0,
|
||||||
|
// progress: 0,
|
||||||
|
// isAnimating: true,
|
||||||
|
// speed: process.speed || 1,
|
||||||
|
// isDelaying: false,
|
||||||
|
// delayStartTime: 0,
|
||||||
|
// currentDelayDuration: 0,
|
||||||
|
// delayComplete: false,
|
||||||
|
// currentPathIndex: 0,
|
||||||
|
// // Store the spawn point index to start animation from correct path point
|
||||||
|
// spawnPointIndex: getPointIndexInProcess(process, spawnPoint.point),
|
||||||
|
// },
|
||||||
|
// visible: true,
|
||||||
|
// materialType: materialType || "Default",
|
||||||
|
// spawnTime: currentTime,
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Get the index of a point within the process animation path
|
||||||
|
// function getPointIndexInProcess(process, point) {
|
||||||
|
// if (!process.paths) return 0;
|
||||||
|
|
||||||
|
// let cumulativePoints = 0;
|
||||||
|
// for (const path of process.paths) {
|
||||||
|
// for (let i = 0; i < (path.points?.length || 0); i++) {
|
||||||
|
// if (path.points[i].uuid === point.uuid) {
|
||||||
|
// return cumulativePoints + i;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// cumulativePoints += path.points?.length || 0;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return 0;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Get point data for current animation index
|
||||||
|
// function getPointDataForAnimationIndex(process, index) {
|
||||||
|
// 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;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Convert process paths to Vector3 format
|
||||||
|
// function getProcessPath(process) {
|
||||||
|
// return process.animationPath?.map((p) => ({ x: p.x, y: p.y, z: p.z })) || [];
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Handle material swap for an object
|
||||||
|
// function handleMaterialSwap(processId, objectId, materialType) {
|
||||||
|
// const processState = animationStates[processId];
|
||||||
|
// if (!processState || !processState.spawnedObjects[objectId]) return;
|
||||||
|
|
||||||
|
// processState.spawnedObjects[objectId].materialType = materialType;
|
||||||
|
|
||||||
|
// // Notify main thread about material change
|
||||||
|
// self.postMessage({
|
||||||
|
// type: "materialChanged",
|
||||||
|
// data: {
|
||||||
|
// processId,
|
||||||
|
// objectId,
|
||||||
|
// materialType,
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Handle point actions for an object
|
||||||
|
// function handlePointActions(processId, objectId, actions = [], currentTime) {
|
||||||
|
// let shouldStopAnimation = false;
|
||||||
|
// const processState = animationStates[processId];
|
||||||
|
|
||||||
|
// if (!processState || !processState.spawnedObjects[objectId]) return false;
|
||||||
|
|
||||||
|
// const objectState = processState.spawnedObjects[objectId];
|
||||||
|
|
||||||
|
// actions.forEach((action) => {
|
||||||
|
// if (!action.isUsed) return;
|
||||||
|
|
||||||
|
// switch (action.type) {
|
||||||
|
// case "Delay":
|
||||||
|
// if (objectState.state.isDelaying) return;
|
||||||
|
|
||||||
|
// const delayDuration =
|
||||||
|
// typeof action.delay === "number"
|
||||||
|
// ? action.delay
|
||||||
|
// : parseFloat(action.delay || "0");
|
||||||
|
|
||||||
|
// if (delayDuration > 0) {
|
||||||
|
// objectState.state.isDelaying = true;
|
||||||
|
// objectState.state.delayStartTime = currentTime;
|
||||||
|
// objectState.state.currentDelayDuration = delayDuration;
|
||||||
|
// objectState.state.delayComplete = false;
|
||||||
|
// shouldStopAnimation = true;
|
||||||
|
// }
|
||||||
|
// break;
|
||||||
|
|
||||||
|
// case "Despawn":
|
||||||
|
// delete processState.spawnedObjects[objectId];
|
||||||
|
// shouldStopAnimation = true;
|
||||||
|
|
||||||
|
// // Notify main thread about despawn
|
||||||
|
// self.postMessage({
|
||||||
|
// type: "objectDespawned",
|
||||||
|
// data: {
|
||||||
|
// processId,
|
||||||
|
// objectId,
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
// break;
|
||||||
|
|
||||||
|
// case "Swap":
|
||||||
|
// if (action.material) {
|
||||||
|
// handleMaterialSwap(processId, objectId, action.material);
|
||||||
|
// }
|
||||||
|
// break;
|
||||||
|
|
||||||
|
// default:
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
// return shouldStopAnimation;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Check if point has non-inherit actions
|
||||||
|
// function hasNonInheritActions(actions = []) {
|
||||||
|
// return actions.some((action) => action.isUsed && action.type !== "Inherit");
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Calculate vector lerp (linear interpolation)
|
||||||
|
// function lerpVectors(v1, v2, alpha) {
|
||||||
|
// return {
|
||||||
|
// x: v1.x + (v2.x - v1.x) * alpha,
|
||||||
|
// y: v1.y + (v2.y - v1.y) * alpha,
|
||||||
|
// z: v1.z + (v2.z - v1.z) * alpha,
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Calculate vector distance
|
||||||
|
// function distanceBetweenVectors(v1, v2) {
|
||||||
|
// const dx = v2.x - v1.x;
|
||||||
|
// const dy = v2.y - v1.y;
|
||||||
|
// const dz = v2.z - v1.z;
|
||||||
|
// return Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Process spawn logic
|
||||||
|
// function processSpawns(currentTime) {
|
||||||
|
// processes.forEach((process) => {
|
||||||
|
// const processState = animationStates[process.id];
|
||||||
|
// if (!processState) return;
|
||||||
|
|
||||||
|
// const spawnPointData = findSpawnPoint(process);
|
||||||
|
// if (!spawnPointData || !spawnPointData.point.actions) return;
|
||||||
|
|
||||||
|
// const spawnAction = spawnPointData.point.actions.find(
|
||||||
|
// (a) => a.isUsed && a.type === "Spawn"
|
||||||
|
// );
|
||||||
|
// if (!spawnAction) return;
|
||||||
|
|
||||||
|
// const spawnInterval =
|
||||||
|
// typeof spawnAction.spawnInterval === "number"
|
||||||
|
// ? spawnAction.spawnInterval
|
||||||
|
// : parseFloat(spawnAction.spawnInterval || "0");
|
||||||
|
|
||||||
|
// if (currentTime >= processState.nextSpawnTime) {
|
||||||
|
// const newObject = createSpawnedObject(
|
||||||
|
// process,
|
||||||
|
// spawnPointData,
|
||||||
|
// currentTime,
|
||||||
|
// spawnAction.material || "Default"
|
||||||
|
// );
|
||||||
|
|
||||||
|
// processState.spawnedObjects[newObject.id] = newObject;
|
||||||
|
// processState.objectIdCounter++;
|
||||||
|
// processState.nextSpawnTime = currentTime + spawnInterval;
|
||||||
|
|
||||||
|
// // Notify main thread about new object
|
||||||
|
// self.postMessage({
|
||||||
|
// type: "objectSpawned",
|
||||||
|
// data: {
|
||||||
|
// processId: process.id,
|
||||||
|
// object: newObject,
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Update all animations
|
||||||
|
// function updateAnimations(delta, currentTime) {
|
||||||
|
// // First handle spawning of new objects
|
||||||
|
// processSpawns(currentTime);
|
||||||
|
|
||||||
|
// // Then animate existing objects
|
||||||
|
// processes.forEach((process) => {
|
||||||
|
// const processState = animationStates[process.id];
|
||||||
|
// if (!processState) return;
|
||||||
|
|
||||||
|
// const path = getProcessPath(process);
|
||||||
|
// if (path.length < 2) return;
|
||||||
|
|
||||||
|
// const updatedObjects = {};
|
||||||
|
// let hasChanges = false;
|
||||||
|
|
||||||
|
// Object.entries(processState.spawnedObjects).forEach(([objectId, obj]) => {
|
||||||
|
// if (!obj.visible || !obj.state.isAnimating) return;
|
||||||
|
|
||||||
|
// const stateRef = obj.state;
|
||||||
|
|
||||||
|
// // Use the spawnPointIndex as starting point if it's the initial movement
|
||||||
|
// if (stateRef.currentIndex === 0 && stateRef.progress === 0) {
|
||||||
|
// stateRef.currentIndex = stateRef.spawnPointIndex || 0;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 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 {
|
||||||
|
// updatedObjects[objectId] = { ...obj, state: { ...stateRef } };
|
||||||
|
// 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) {
|
||||||
|
// // Reached the end of path with no more actions
|
||||||
|
// delete processState.spawnedObjects[objectId];
|
||||||
|
|
||||||
|
// // Notify main thread to remove the object
|
||||||
|
// self.postMessage({
|
||||||
|
// type: "objectCompleted",
|
||||||
|
// data: {
|
||||||
|
// processId: process.id,
|
||||||
|
// objectId,
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (!isLastPoint) {
|
||||||
|
// const currentPos = path[stateRef.currentIndex];
|
||||||
|
// const nextPos = path[nextPointIdx];
|
||||||
|
// const distance = distanceBetweenVectors(currentPos, nextPos);
|
||||||
|
// const movement = stateRef.speed * delta;
|
||||||
|
|
||||||
|
// // Update progress based on distance and speed
|
||||||
|
// const oldProgress = stateRef.progress;
|
||||||
|
// stateRef.progress += movement / distance;
|
||||||
|
|
||||||
|
// if (stateRef.progress >= 1) {
|
||||||
|
// // Reached next point
|
||||||
|
// stateRef.currentIndex = nextPointIdx;
|
||||||
|
// stateRef.progress = 0;
|
||||||
|
// stateRef.delayComplete = false;
|
||||||
|
// obj.position = [nextPos.x, nextPos.y, nextPos.z];
|
||||||
|
// } else {
|
||||||
|
// // Interpolate position
|
||||||
|
// const lerpedPos = lerpVectors(currentPos, nextPos, stateRef.progress);
|
||||||
|
// obj.position = [lerpedPos.x, lerpedPos.y, lerpedPos.z];
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Only send updates when there's meaningful movement
|
||||||
|
// if (Math.abs(oldProgress - stateRef.progress) > 0.01) {
|
||||||
|
// hasChanges = true;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// updatedObjects[objectId] = { ...obj, state: { ...stateRef } };
|
||||||
|
// });
|
||||||
|
|
||||||
|
// // Update animation state with modified objects
|
||||||
|
// if (Object.keys(updatedObjects).length > 0) {
|
||||||
|
// processState.spawnedObjects = {
|
||||||
|
// ...processState.spawnedObjects,
|
||||||
|
// ...updatedObjects,
|
||||||
|
// };
|
||||||
|
|
||||||
|
// // Only send position updates when there are meaningful changes
|
||||||
|
// if (hasChanges) {
|
||||||
|
// self.postMessage({
|
||||||
|
// type: "positionsUpdated",
|
||||||
|
// data: {
|
||||||
|
// processId: process.id,
|
||||||
|
// objects: updatedObjects,
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// animation-worker.js
|
||||||
|
// This web worker handles animation calculations off the main thread
|
||||||
|
|
||||||
|
/* eslint-disable no-restricted-globals */
|
||||||
|
// The above disables the ESLint rule for this file since 'self' is valid in web workers
|
||||||
|
|
||||||
|
// Store process data, animation states, and objects
|
||||||
|
let processes = [];
|
||||||
|
let animationStates = {};
|
||||||
|
let lastTimestamp = 0;
|
||||||
|
let debugMode = true;
|
||||||
|
|
||||||
|
// Logger function for debugging
|
||||||
|
function log(...args) {
|
||||||
|
if (debugMode) {
|
||||||
|
self.postMessage({
|
||||||
|
type: "debug",
|
||||||
|
data: { message: args.join(' ') }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Message handler for communication with main thread
|
||||||
|
self.onmessage = function (event) {
|
||||||
|
const { type, data } = event.data;
|
||||||
|
log(`Worker received message: ${type}`);
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "initialize":
|
||||||
|
processes = data.processes;
|
||||||
|
log(`Initialized with ${processes.length} processes`);
|
||||||
|
initializeAnimationStates();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "update":
|
||||||
|
const { timestamp, isPlaying } = data;
|
||||||
|
if (isPlaying) {
|
||||||
|
const delta = lastTimestamp === 0 ? 0.016 : (timestamp - lastTimestamp) / 1000; // Convert to seconds
|
||||||
|
updateAnimations(delta, timestamp);
|
||||||
|
}
|
||||||
|
lastTimestamp = timestamp;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "reset":
|
||||||
|
log("Resetting animations");
|
||||||
|
resetAnimations();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "togglePlay":
|
||||||
|
// If resuming from pause, recalculate the time delta
|
||||||
|
log(`Toggle play: ${data.isPlaying}`);
|
||||||
|
lastTimestamp = data.timestamp;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "setDebug":
|
||||||
|
debugMode = data.enabled;
|
||||||
|
log(`Debug mode: ${debugMode}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize animation states for all processes
|
||||||
|
function initializeAnimationStates() {
|
||||||
|
animationStates = {};
|
||||||
|
|
||||||
|
processes.forEach((process) => {
|
||||||
|
if (!process || !process.id) {
|
||||||
|
log("Invalid process found:", process);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
animationStates[process.id] = {
|
||||||
|
spawnedObjects: {},
|
||||||
|
nextSpawnTime: 0,
|
||||||
|
objectIdCounter: 0,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send initial states back to main thread
|
||||||
|
self.postMessage({
|
||||||
|
type: "statesInitialized",
|
||||||
|
data: { animationStates },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset all animations
|
||||||
|
function resetAnimations() {
|
||||||
|
initializeAnimationStates();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find spawn point in a process
|
||||||
|
function findSpawnPoint(process) {
|
||||||
|
if (!process || !process.paths) {
|
||||||
|
log(`No paths found for process ${process?.id}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const path of process.paths) {
|
||||||
|
if (!path || !path.points) continue;
|
||||||
|
|
||||||
|
for (const point of path.points) {
|
||||||
|
if (!point || !point.actions) continue;
|
||||||
|
|
||||||
|
const spawnAction = point.actions.find(
|
||||||
|
(a) => a && a.isUsed && a.type === "Spawn"
|
||||||
|
);
|
||||||
|
if (spawnAction) {
|
||||||
|
return { point, path };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log(`No spawn points found for process ${process.id}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new spawned object with proper initial position
|
||||||
|
function createSpawnedObject(process, spawnPoint, currentTime, materialType) {
|
||||||
|
// Extract spawn position from the actual spawn point
|
||||||
|
const position = spawnPoint.point.position
|
||||||
|
? [...spawnPoint.point.position]
|
||||||
|
: [0, 0, 0];
|
||||||
|
|
||||||
|
// Get the path position and add it to the spawn point position
|
||||||
|
const pathPosition = spawnPoint.path.pathPosition || [0, 0, 0];
|
||||||
|
const absolutePosition = [
|
||||||
|
position[0] + pathPosition[0],
|
||||||
|
position[1] + pathPosition[1],
|
||||||
|
position[2] + pathPosition[2],
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: `obj-${process.id}-${animationStates[process.id].objectIdCounter}`,
|
||||||
|
position: absolutePosition,
|
||||||
|
state: {
|
||||||
|
currentIndex: 0,
|
||||||
|
progress: 0,
|
||||||
|
isAnimating: true,
|
||||||
|
speed: process.speed || 1,
|
||||||
|
isDelaying: false,
|
||||||
|
delayStartTime: 0,
|
||||||
|
currentDelayDuration: 0,
|
||||||
|
delayComplete: false,
|
||||||
|
currentPathIndex: 0,
|
||||||
|
// Store the spawn point index to start animation from correct path point
|
||||||
|
spawnPointIndex: getPointIndexInProcess(process, spawnPoint.point),
|
||||||
|
},
|
||||||
|
visible: true,
|
||||||
|
materialType: materialType || "Default",
|
||||||
|
spawnTime: currentTime,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the index of a point within the process animation path
|
||||||
|
function getPointIndexInProcess(process, point) {
|
||||||
|
if (!process.paths) return 0;
|
||||||
|
|
||||||
|
let cumulativePoints = 0;
|
||||||
|
for (const path of process.paths) {
|
||||||
|
for (let i = 0; i < (path.points?.length || 0); i++) {
|
||||||
|
if (path.points[i].uuid === point.uuid) {
|
||||||
|
return cumulativePoints + i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cumulativePoints += path.points?.length || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get point data for current animation index
|
||||||
|
function getPointDataForAnimationIndex(process, index) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert process paths to Vector3 format
|
||||||
|
function getProcessPath(process) {
|
||||||
|
if (!process.animationPath) {
|
||||||
|
log(`No animation path for process ${process.id}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return process.animationPath.map((p) => ({ x: p.x, y: p.y, z: p.z })) || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle material swap for an object
|
||||||
|
function handleMaterialSwap(processId, objectId, materialType) {
|
||||||
|
const processState = animationStates[processId];
|
||||||
|
if (!processState || !processState.spawnedObjects[objectId]) return;
|
||||||
|
|
||||||
|
processState.spawnedObjects[objectId].materialType = materialType;
|
||||||
|
|
||||||
|
// Notify main thread about material change
|
||||||
|
self.postMessage({
|
||||||
|
type: "materialChanged",
|
||||||
|
data: {
|
||||||
|
processId,
|
||||||
|
objectId,
|
||||||
|
materialType,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle point actions for an object
|
||||||
|
function handlePointActions(processId, objectId, actions = [], currentTime) {
|
||||||
|
let shouldStopAnimation = false;
|
||||||
|
const processState = animationStates[processId];
|
||||||
|
|
||||||
|
if (!processState || !processState.spawnedObjects[objectId]) return false;
|
||||||
|
|
||||||
|
const objectState = processState.spawnedObjects[objectId];
|
||||||
|
|
||||||
|
actions.forEach((action) => {
|
||||||
|
if (!action || !action.isUsed) return;
|
||||||
|
|
||||||
|
switch (action.type) {
|
||||||
|
case "Delay":
|
||||||
|
if (objectState.state.isDelaying) return;
|
||||||
|
|
||||||
|
const delayDuration =
|
||||||
|
typeof action.delay === "number"
|
||||||
|
? action.delay
|
||||||
|
: parseFloat(action.delay || "0");
|
||||||
|
|
||||||
|
if (delayDuration > 0) {
|
||||||
|
objectState.state.isDelaying = true;
|
||||||
|
objectState.state.delayStartTime = currentTime;
|
||||||
|
objectState.state.currentDelayDuration = delayDuration;
|
||||||
|
objectState.state.delayComplete = false;
|
||||||
|
shouldStopAnimation = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "Despawn":
|
||||||
|
delete processState.spawnedObjects[objectId];
|
||||||
|
shouldStopAnimation = true;
|
||||||
|
|
||||||
|
// Notify main thread about despawn
|
||||||
|
self.postMessage({
|
||||||
|
type: "objectDespawned",
|
||||||
|
data: {
|
||||||
|
processId,
|
||||||
|
objectId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "Swap":
|
||||||
|
if (action.material) {
|
||||||
|
handleMaterialSwap(processId, objectId, action.material);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return shouldStopAnimation;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if point has non-inherit actions
|
||||||
|
function hasNonInheritActions(actions = []) {
|
||||||
|
return actions.some((action) => action && action.isUsed && action.type !== "Inherit");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate vector lerp (linear interpolation)
|
||||||
|
function lerpVectors(v1, v2, alpha) {
|
||||||
|
return {
|
||||||
|
x: v1.x + (v2.x - v1.x) * alpha,
|
||||||
|
y: v1.y + (v2.y - v1.y) * alpha,
|
||||||
|
z: v1.z + (v2.z - v1.z) * alpha,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate vector distance
|
||||||
|
function distanceBetweenVectors(v1, v2) {
|
||||||
|
const dx = v2.x - v1.x;
|
||||||
|
const dy = v2.y - v1.y;
|
||||||
|
const dz = v2.z - v1.z;
|
||||||
|
return Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process spawn logic
|
||||||
|
function processSpawns(currentTime) {
|
||||||
|
processes.forEach((process) => {
|
||||||
|
const processState = animationStates[process.id];
|
||||||
|
if (!processState) return;
|
||||||
|
|
||||||
|
const spawnPointData = findSpawnPoint(process);
|
||||||
|
if (!spawnPointData || !spawnPointData.point.actions) return;
|
||||||
|
|
||||||
|
const spawnAction = spawnPointData.point.actions.find(
|
||||||
|
(a) => a.isUsed && a.type === "Spawn"
|
||||||
|
);
|
||||||
|
if (!spawnAction) return;
|
||||||
|
|
||||||
|
const spawnInterval =
|
||||||
|
typeof spawnAction.spawnInterval === "number"
|
||||||
|
? spawnAction.spawnInterval
|
||||||
|
: parseFloat(spawnAction.spawnInterval || "2"); // Default to 2 seconds if not specified
|
||||||
|
|
||||||
|
if (currentTime >= processState.nextSpawnTime) {
|
||||||
|
const newObject = createSpawnedObject(
|
||||||
|
process,
|
||||||
|
spawnPointData,
|
||||||
|
currentTime,
|
||||||
|
spawnAction.material || "Default"
|
||||||
|
);
|
||||||
|
|
||||||
|
processState.spawnedObjects[newObject.id] = newObject;
|
||||||
|
processState.objectIdCounter++;
|
||||||
|
processState.nextSpawnTime = currentTime + spawnInterval;
|
||||||
|
|
||||||
|
log(`Spawned object ${newObject.id} for process ${process.id}`);
|
||||||
|
|
||||||
|
// Notify main thread about new object
|
||||||
|
self.postMessage({
|
||||||
|
type: "objectSpawned",
|
||||||
|
data: {
|
||||||
|
processId: process.id,
|
||||||
|
object: newObject,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update all animations
|
||||||
|
function updateAnimations(delta, currentTime) {
|
||||||
|
// First handle spawning of new objects
|
||||||
|
processSpawns(currentTime);
|
||||||
|
|
||||||
|
// Then animate existing objects
|
||||||
|
processes.forEach((process) => {
|
||||||
|
const processState = animationStates[process.id];
|
||||||
|
if (!processState) return;
|
||||||
|
|
||||||
|
const path = getProcessPath(process);
|
||||||
|
if (path.length < 2) {
|
||||||
|
log(`Path too short for process ${process.id}, length: ${path.length}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedObjects = {};
|
||||||
|
let hasChanges = false;
|
||||||
|
|
||||||
|
Object.entries(processState.spawnedObjects).forEach(([objectId, obj]) => {
|
||||||
|
if (!obj.visible || !obj.state.isAnimating) return;
|
||||||
|
|
||||||
|
const stateRef = obj.state;
|
||||||
|
|
||||||
|
// Use the spawnPointIndex as starting point if it's the initial movement
|
||||||
|
if (stateRef.currentIndex === 0 && stateRef.progress === 0) {
|
||||||
|
stateRef.currentIndex = stateRef.spawnPointIndex || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
updatedObjects[objectId] = { ...obj, state: { ...stateRef } };
|
||||||
|
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) {
|
||||||
|
// Reached the end of path with no more actions
|
||||||
|
delete processState.spawnedObjects[objectId];
|
||||||
|
log(`Object ${objectId} completed path`);
|
||||||
|
|
||||||
|
// Notify main thread to remove the object
|
||||||
|
self.postMessage({
|
||||||
|
type: "objectCompleted",
|
||||||
|
data: {
|
||||||
|
processId: process.id,
|
||||||
|
objectId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLastPoint) {
|
||||||
|
const currentPos = path[stateRef.currentIndex];
|
||||||
|
const nextPos = path[nextPointIdx];
|
||||||
|
const distance = distanceBetweenVectors(currentPos, nextPos);
|
||||||
|
|
||||||
|
// Ensure we don't divide by zero
|
||||||
|
if (distance > 0) {
|
||||||
|
const movement = stateRef.speed * delta;
|
||||||
|
|
||||||
|
// Update progress based on distance and speed
|
||||||
|
const oldProgress = stateRef.progress;
|
||||||
|
stateRef.progress += movement / distance;
|
||||||
|
|
||||||
|
if (stateRef.progress >= 1) {
|
||||||
|
// Reached next point
|
||||||
|
stateRef.currentIndex = nextPointIdx;
|
||||||
|
stateRef.progress = 0;
|
||||||
|
stateRef.delayComplete = false;
|
||||||
|
obj.position = [nextPos.x, nextPos.y, nextPos.z];
|
||||||
|
} else {
|
||||||
|
// Interpolate position
|
||||||
|
const lerpedPos = lerpVectors(currentPos, nextPos, stateRef.progress);
|
||||||
|
obj.position = [lerpedPos.x, lerpedPos.y, lerpedPos.z];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only send updates when there's meaningful movement
|
||||||
|
if (Math.abs(oldProgress - stateRef.progress) > 0.01) {
|
||||||
|
hasChanges = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Skip to next point if distance is zero
|
||||||
|
stateRef.currentIndex = nextPointIdx;
|
||||||
|
stateRef.progress = 0;
|
||||||
|
obj.position = [nextPos.x, nextPos.y, nextPos.z];
|
||||||
|
hasChanges = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedObjects[objectId] = { ...obj, state: { ...stateRef } };
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update animation state with modified objects
|
||||||
|
if (Object.keys(updatedObjects).length > 0) {
|
||||||
|
processState.spawnedObjects = {
|
||||||
|
...processState.spawnedObjects,
|
||||||
|
...updatedObjects,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only send position updates when there are meaningful changes
|
||||||
|
if (hasChanges) {
|
||||||
|
self.postMessage({
|
||||||
|
type: "positionsUpdated",
|
||||||
|
data: {
|
||||||
|
processId: process.id,
|
||||||
|
objects: updatedObjects,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
|
|
||||||
const animationWorker = () => {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
@@ -5,7 +5,6 @@ import { useLoader, useFrame } from "@react-three/fiber";
|
|||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { GLTF } from "three-stdlib";
|
import { GLTF } from "three-stdlib";
|
||||||
import boxGltb from "../../../assets/gltf-glb/crate_box.glb";
|
import boxGltb from "../../../assets/gltf-glb/crate_box.glb";
|
||||||
import camera from "../../../assets/gltf-glb/camera face 2.gltf";
|
|
||||||
|
|
||||||
interface PointAction {
|
interface PointAction {
|
||||||
uuid: string;
|
uuid: string;
|
||||||
@@ -44,6 +43,8 @@ interface ProcessData {
|
|||||||
animationPath: { x: number; y: number; z: number }[];
|
animationPath: { x: number; y: number; z: number }[];
|
||||||
pointActions: PointAction[][];
|
pointActions: PointAction[][];
|
||||||
speed: number;
|
speed: number;
|
||||||
|
customMaterials?: Record<string, THREE.Material>;
|
||||||
|
renderAs?: "box" | "custom";
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AnimationState {
|
interface AnimationState {
|
||||||
@@ -58,19 +59,35 @@ interface AnimationState {
|
|||||||
currentPathIndex: number;
|
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[] }> = ({
|
const ProcessAnimator: React.FC<{ processes: ProcessData[] }> = ({
|
||||||
processes,
|
processes,
|
||||||
}) => {
|
}) => {
|
||||||
console.log("processes: ", processes);
|
console.log("processes: ", processes);
|
||||||
const gltf = useLoader(GLTFLoader, boxGltb) as GLTF;
|
const gltf = useLoader(GLTFLoader, boxGltb) as GLTF;
|
||||||
const { isPlaying, setIsPlaying } = usePlayButtonStore();
|
const { isPlaying } = usePlayButtonStore();
|
||||||
|
|
||||||
const groupRef = useRef<THREE.Group>(null);
|
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 }),
|
Wood: new THREE.MeshStandardMaterial({ color: 0x8b4513 }),
|
||||||
Box: new THREE.MeshStandardMaterial({
|
Box: new THREE.MeshStandardMaterial({
|
||||||
@@ -88,44 +105,211 @@ const ProcessAnimator: React.FC<{ processes: ProcessData[] }> = ({
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const [currentMaterial, setCurrentMaterial] = useState<THREE.Material>(
|
// Initialize animation states when processes or play state changes
|
||||||
materials.Default
|
useEffect(() => {
|
||||||
);
|
if (!isPlaying) {
|
||||||
|
setAnimationStates({});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const { animationPath, currentProcess } = useMemo(() => {
|
const newStates: Record<string, ProcessAnimationState> = {};
|
||||||
const defaultProcess = {
|
processes.forEach((process) => {
|
||||||
animationPath: [],
|
newStates[process.id] = {
|
||||||
pointActions: [],
|
spawnedObjects: {},
|
||||||
speed: 1,
|
nextSpawnTime: 0,
|
||||||
paths: [],
|
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 {
|
return {
|
||||||
animationPath:
|
ref: React.createRef(),
|
||||||
cp.animationPath?.map((p) => new THREE.Vector3(p.x, p.y, p.z)) || [],
|
state: {
|
||||||
currentProcess: cp,
|
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>({
|
// Handle material swap for an object
|
||||||
currentIndex: 0,
|
const handleMaterialSwap = (
|
||||||
progress: 0,
|
processId: string,
|
||||||
isAnimating: false,
|
objectId: string,
|
||||||
speed: currentProcess.speed,
|
materialType: string
|
||||||
isDelaying: false,
|
) => {
|
||||||
delayStartTime: 0,
|
setAnimationStates((prev) => {
|
||||||
currentDelayDuration: 0,
|
const processState = prev[processId];
|
||||||
delayComplete: false,
|
if (!processState || !processState.spawnedObjects[objectId]) return prev;
|
||||||
currentPathIndex: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
const getPointDataForAnimationIndex = (index: number) => {
|
const process = processes.find((p) => p.id === processId);
|
||||||
if (!processes[0]?.paths) return null;
|
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;
|
let cumulativePoints = 0;
|
||||||
console.log("cumulativePoints: ", cumulativePoints);
|
for (const path of process.paths) {
|
||||||
|
|
||||||
for (const path of processes[0].paths) {
|
|
||||||
const pointCount = path.points?.length || 0;
|
const pointCount = path.points?.length || 0;
|
||||||
|
|
||||||
if (index < cumulativePoints + pointCount) {
|
if (index < cumulativePoints + pointCount) {
|
||||||
@@ -139,209 +323,202 @@ const ProcessAnimator: React.FC<{ processes: ProcessData[] }> = ({
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
// Spawn objects for all processes
|
||||||
if (isPlaying) {
|
useFrame((state) => {
|
||||||
setVisible(true);
|
if (!isPlaying) return;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentTime = state.clock.getElapsedTime();
|
const currentTime = state.clock.getElapsedTime();
|
||||||
const path = animationPath;
|
setAnimationStates((prev) => {
|
||||||
const stateRef = animationStateRef.current;
|
const newStates = { ...prev };
|
||||||
|
|
||||||
// Check if we need to switch paths (specific to your structure)
|
processes.forEach((process) => {
|
||||||
if (stateRef.currentIndex === 3 && stateRef.currentPathIndex === 0) {
|
const processState = newStates[process.id];
|
||||||
setCurrentPathIndex(1);
|
if (!processState) return;
|
||||||
stateRef.currentPathIndex = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the current point data
|
const spawnPoint = findSpawnPoint(process);
|
||||||
const currentPointData = getPointDataForAnimationIndex(
|
if (!spawnPoint || !spawnPoint.actions) return;
|
||||||
stateRef.currentIndex
|
|
||||||
);
|
|
||||||
|
|
||||||
// Execute actions when we first arrive at a point
|
const spawnAction = spawnPoint.actions.find(
|
||||||
|
(a) => a.isUsed && a.type === "Spawn"
|
||||||
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
|
|
||||||
);
|
);
|
||||||
}
|
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) {
|
if (!processes || processes.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!gltf?.scene) {
|
return (
|
||||||
return visible ? (
|
<>
|
||||||
<mesh ref={meshRef} material={currentMaterial}>
|
{Object.entries(animationStates).flatMap(([processId, processState]) =>
|
||||||
<boxGeometry args={[1, 1, 1]} />
|
Object.entries(processState.spawnedObjects)
|
||||||
</mesh>
|
.filter(([_, obj]) => obj.visible)
|
||||||
) : null;
|
.map(([objectId, obj]) => {
|
||||||
}
|
const process = processes.find((p) => p.id === processId);
|
||||||
|
console.log("process: ", process);
|
||||||
|
const renderAs = process?.renderAs || "custom";
|
||||||
|
|
||||||
return visible ? (
|
return renderAs === "box" ? (
|
||||||
<group ref={groupRef}>
|
<mesh
|
||||||
<primitive
|
key={objectId}
|
||||||
object={gltf.scene.clone()}
|
ref={obj.ref as React.RefObject<THREE.Mesh>}
|
||||||
scale={[1, 1, 1]}
|
material={obj.material}
|
||||||
material={currentMaterial}
|
>
|
||||||
/>
|
<boxGeometry args={[1, 1, 1]} />
|
||||||
</group>
|
</mesh>
|
||||||
) : null;
|
) : (
|
||||||
|
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;
|
export default ProcessAnimator;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const ProcessContainer: React.FC = () => {
|
|||||||
<>
|
<>
|
||||||
<ProcessCreator onProcessesCreated={setProcesses} />
|
<ProcessCreator onProcessesCreated={setProcesses} />
|
||||||
{processes.length > 0 && <ProcessAnimator processes={processes} />}
|
{processes.length > 0 && <ProcessAnimator processes={processes} />}
|
||||||
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -422,6 +422,7 @@ export interface PointAction {
|
|||||||
spawnInterval: string | number;
|
spawnInterval: string | number;
|
||||||
isUsed: boolean;
|
isUsed: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PathPoint {
|
export interface PathPoint {
|
||||||
uuid: string;
|
uuid: string;
|
||||||
position: [number, number, number];
|
position: [number, number, number];
|
||||||
@@ -430,12 +431,14 @@ export interface PathPoint {
|
|||||||
targets: Array<{ pathUUID: string }>;
|
targets: Array<{ pathUUID: string }>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SimulationPath {
|
export interface SimulationPath {
|
||||||
modeluuid: string;
|
modeluuid: string;
|
||||||
points: PathPoint[];
|
points: PathPoint[];
|
||||||
pathPosition: [number, number, number];
|
pathPosition: [number, number, number];
|
||||||
speed?: number;
|
speed?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Process {
|
export interface Process {
|
||||||
id: string;
|
id: string;
|
||||||
paths: SimulationPath[];
|
paths: SimulationPath[];
|
||||||
@@ -443,6 +446,7 @@ export interface Process {
|
|||||||
pointActions: PointAction[][];
|
pointActions: PointAction[][];
|
||||||
speed: number;
|
speed: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProcessCreatorProps {
|
interface ProcessCreatorProps {
|
||||||
onProcessesCreated: (processes: Process[]) => void;
|
onProcessesCreated: (processes: Process[]) => void;
|
||||||
}
|
}
|
||||||
@@ -453,83 +457,51 @@ function convertToSimulationPath(
|
|||||||
): SimulationPath {
|
): SimulationPath {
|
||||||
const { modeluuid } = path;
|
const { modeluuid } = path;
|
||||||
|
|
||||||
// Helper function to normalize actions
|
// Simplified normalizeAction function that preserves exact original properties
|
||||||
const normalizeAction = (action: any): PointAction => {
|
const normalizeAction = (action: any): PointAction => {
|
||||||
return { ...action }; // Return exact copy with no modifications
|
return { ...action }; // Return exact copy with no modifications
|
||||||
};
|
};
|
||||||
|
|
||||||
// Extract points from the path
|
|
||||||
let points: PathPoint[];
|
|
||||||
if (path.type === "Conveyor") {
|
if (path.type === "Conveyor") {
|
||||||
points = path.points.map((point) => ({
|
return {
|
||||||
uuid: point.uuid,
|
modeluuid,
|
||||||
position: point.position,
|
points: path.points.map((point) => ({
|
||||||
actions: point.actions.map(normalizeAction), // Preserve exact actions
|
uuid: point.uuid,
|
||||||
connections: {
|
position: point.position,
|
||||||
targets: point.connections.targets.map((target) => ({
|
actions: point.actions.map(normalizeAction), // Preserve exact actions
|
||||||
pathUUID: target.pathUUID,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
points = [
|
|
||||||
{
|
|
||||||
uuid: path.point.uuid,
|
|
||||||
position: path.point.position,
|
|
||||||
actions: Array.isArray(path.point.actions)
|
|
||||||
? path.point.actions.map(normalizeAction)
|
|
||||||
: [normalizeAction(path.point.actions)],
|
|
||||||
connections: {
|
connections: {
|
||||||
targets: path.point.connections.targets.map((target) => ({
|
targets: point.connections.targets.map((target) => ({
|
||||||
pathUUID: target.pathUUID,
|
pathUUID: target.pathUUID,
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
},
|
})),
|
||||||
];
|
pathPosition: path.position,
|
||||||
}
|
speed:
|
||||||
|
typeof path.speed === "string"
|
||||||
// Check if point 1 or point 2 has a spawn action
|
? parseFloat(path.speed) || 1
|
||||||
const hasSpawnInFirstTwoPoints =
|
: path.speed || 1,
|
||||||
points.length >= 2 &&
|
};
|
||||||
(points[0].actions.some(
|
|
||||||
(action) => action.type.toLowerCase() === "spawn"
|
|
||||||
) ||
|
|
||||||
points[1].actions.some(
|
|
||||||
(action) => action.type.toLowerCase() === "spawn"
|
|
||||||
));
|
|
||||||
|
|
||||||
// Swap points 1 and 3 only if:
|
|
||||||
// 1. There are at least three points,
|
|
||||||
// 2. The third point contains a spawn action, and
|
|
||||||
// 3. Neither point 1 nor point 2 has a spawn action
|
|
||||||
if (
|
|
||||||
!hasSpawnInFirstTwoPoints &&
|
|
||||||
points.length >= 3 &&
|
|
||||||
points[2].actions.some((action) => action.type.toLowerCase() === "spawn")
|
|
||||||
) {
|
|
||||||
const temp = points[0];
|
|
||||||
points[0] = points[2];
|
|
||||||
points[2] = temp;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine the speed based on the type of path
|
|
||||||
let speed: number;
|
|
||||||
if (path.type === "Conveyor") {
|
|
||||||
speed =
|
|
||||||
typeof path.speed === "string"
|
|
||||||
? parseFloat(path.speed) || 1
|
|
||||||
: path.speed || 1;
|
|
||||||
} else {
|
} else {
|
||||||
// For VehicleEventsSchema, use a default speed of 1
|
return {
|
||||||
speed = 1;
|
modeluuid,
|
||||||
|
points: [
|
||||||
|
{
|
||||||
|
uuid: path.point.uuid,
|
||||||
|
position: path.point.position,
|
||||||
|
actions: Array.isArray(path.point.actions)
|
||||||
|
? path.point.actions.map(normalizeAction)
|
||||||
|
: [normalizeAction(path.point.actions)],
|
||||||
|
connections: {
|
||||||
|
targets: path.point.connections.targets.map((target) => ({
|
||||||
|
pathUUID: target.pathUUID,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
pathPosition: path.position,
|
||||||
|
speed: path.point.speed || 1,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
|
||||||
modeluuid,
|
|
||||||
points,
|
|
||||||
pathPosition: path.position,
|
|
||||||
speed,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Custom shallow comparison for arrays
|
// Custom shallow comparison for arrays
|
||||||
@@ -556,6 +528,7 @@ function shouldReverseNextPath(
|
|||||||
nextPath: SimulationPath
|
nextPath: SimulationPath
|
||||||
): boolean {
|
): boolean {
|
||||||
if (nextPath.points.length !== 3) return false;
|
if (nextPath.points.length !== 3) return false;
|
||||||
|
|
||||||
const currentLastPoint = currentPath.points[currentPath.points.length - 1];
|
const currentLastPoint = currentPath.points[currentPath.points.length - 1];
|
||||||
const nextFirstPoint = nextPath.points[0];
|
const nextFirstPoint = nextPath.points[0];
|
||||||
const nextLastPoint = nextPath.points[nextPath.points.length - 1];
|
const nextLastPoint = nextPath.points[nextPath.points.length - 1];
|
||||||
@@ -582,25 +555,56 @@ function shouldReverseNextPath(
|
|||||||
return connectsToLast && !connectsToFirst;
|
return connectsToLast && !connectsToFirst;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if a point has a spawn action
|
||||||
|
function hasSpawnAction(point: PathPoint): boolean {
|
||||||
|
return point.actions.some((action) => action.type.toLowerCase() === "spawn");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure spawn point is always at the beginning of the path
|
||||||
|
function ensureSpawnPointIsFirst(path: SimulationPath): SimulationPath {
|
||||||
|
if (path.points.length !== 3) return path;
|
||||||
|
|
||||||
|
// If the third point has spawn action and first doesn't, reverse the array
|
||||||
|
if (hasSpawnAction(path.points[2]) && !hasSpawnAction(path.points[0])) {
|
||||||
|
return {
|
||||||
|
...path,
|
||||||
|
points: [...path.points].reverse(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
// Updated path adjustment function
|
// Updated path adjustment function
|
||||||
function adjustPathPointsOrder(paths: SimulationPath[]): SimulationPath[] {
|
function adjustPathPointsOrder(paths: SimulationPath[]): SimulationPath[] {
|
||||||
if (paths.length < 2) return paths;
|
if (paths.length < 1) return paths;
|
||||||
|
|
||||||
const adjustedPaths = [...paths];
|
const adjustedPaths = [...paths];
|
||||||
|
|
||||||
|
// First ensure all paths have spawn points at the beginning
|
||||||
|
for (let i = 0; i < adjustedPaths.length; i++) {
|
||||||
|
adjustedPaths[i] = ensureSpawnPointIsFirst(adjustedPaths[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then handle connections between paths
|
||||||
for (let i = 0; i < adjustedPaths.length - 1; i++) {
|
for (let i = 0; i < adjustedPaths.length - 1; i++) {
|
||||||
const currentPath = adjustedPaths[i];
|
const currentPath = adjustedPaths[i];
|
||||||
const nextPath = adjustedPaths[i + 1];
|
const nextPath = adjustedPaths[i + 1];
|
||||||
|
|
||||||
if (shouldReverseNextPath(currentPath, nextPath)) {
|
if (shouldReverseNextPath(currentPath, nextPath)) {
|
||||||
const reversedPoints = [
|
const reversedPoints = [
|
||||||
nextPath.points[2],
|
nextPath.points[2],
|
||||||
nextPath.points[1],
|
nextPath.points[1],
|
||||||
nextPath.points[0],
|
nextPath.points[0],
|
||||||
];
|
];
|
||||||
|
|
||||||
adjustedPaths[i + 1] = {
|
adjustedPaths[i + 1] = {
|
||||||
...nextPath,
|
...nextPath,
|
||||||
points: reversedPoints,
|
points: reversedPoints,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return adjustedPaths;
|
return adjustedPaths;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -608,6 +612,7 @@ function adjustPathPointsOrder(paths: SimulationPath[]): SimulationPath[] {
|
|||||||
export function useProcessCreation() {
|
export function useProcessCreation() {
|
||||||
const { scene } = useThree();
|
const { scene } = useThree();
|
||||||
const [processes, setProcesses] = useState<Process[]>([]);
|
const [processes, setProcesses] = useState<Process[]>([]);
|
||||||
|
|
||||||
const hasSpawnAction = useCallback((path: SimulationPath): boolean => {
|
const hasSpawnAction = useCallback((path: SimulationPath): boolean => {
|
||||||
return path.points.some((point) =>
|
return path.points.some((point) =>
|
||||||
point.actions.some((action) => action.type.toLowerCase() === "spawn")
|
point.actions.some((action) => action.type.toLowerCase() === "spawn")
|
||||||
@@ -619,9 +624,11 @@ export function useProcessCreation() {
|
|||||||
if (!paths || paths.length === 0) {
|
if (!paths || paths.length === 0) {
|
||||||
return createEmptyProcess();
|
return createEmptyProcess();
|
||||||
}
|
}
|
||||||
|
|
||||||
const animationPath: THREE.Vector3[] = [];
|
const animationPath: THREE.Vector3[] = [];
|
||||||
const pointActions: PointAction[][] = [];
|
const pointActions: PointAction[][] = [];
|
||||||
const processSpeed = paths[0]?.speed || 1;
|
const processSpeed = paths[0]?.speed || 1;
|
||||||
|
|
||||||
for (const path of paths) {
|
for (const path of paths) {
|
||||||
for (const point of path.points) {
|
for (const point of path.points) {
|
||||||
const obj = scene.getObjectByProperty("uuid", point.uuid);
|
const obj = scene.getObjectByProperty("uuid", point.uuid);
|
||||||
@@ -629,11 +636,13 @@ export function useProcessCreation() {
|
|||||||
console.warn(`Object with UUID ${point.uuid} not found in scene`);
|
console.warn(`Object with UUID ${point.uuid} not found in scene`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const position = obj.getWorldPosition(new THREE.Vector3());
|
const position = obj.getWorldPosition(new THREE.Vector3());
|
||||||
animationPath.push(position.clone());
|
animationPath.push(position.clone());
|
||||||
pointActions.push(point.actions);
|
pointActions.push(point.actions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: `process-${Math.random().toString(36).substring(2, 11)}`,
|
id: `process-${Math.random().toString(36).substring(2, 11)}`,
|
||||||
paths,
|
paths,
|
||||||
@@ -654,8 +663,10 @@ export function useProcessCreation() {
|
|||||||
const connectedPaths: SimulationPath[] = [];
|
const connectedPaths: SimulationPath[] = [];
|
||||||
const queue: SimulationPath[] = [initialPath];
|
const queue: SimulationPath[] = [initialPath];
|
||||||
visited.add(initialPath.modeluuid);
|
visited.add(initialPath.modeluuid);
|
||||||
|
|
||||||
const pathMap = new Map<string, SimulationPath>();
|
const pathMap = new Map<string, SimulationPath>();
|
||||||
allPaths.forEach((path) => pathMap.set(path.modeluuid, path));
|
allPaths.forEach((path) => pathMap.set(path.modeluuid, path));
|
||||||
|
|
||||||
while (queue.length > 0) {
|
while (queue.length > 0) {
|
||||||
const currentPath = queue.shift()!;
|
const currentPath = queue.shift()!;
|
||||||
connectedPaths.push(currentPath);
|
connectedPaths.push(currentPath);
|
||||||
@@ -688,6 +699,7 @@ export function useProcessCreation() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return connectedPaths;
|
return connectedPaths;
|
||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
@@ -696,10 +708,12 @@ export function useProcessCreation() {
|
|||||||
const createProcessesFromPaths = useCallback(
|
const createProcessesFromPaths = useCallback(
|
||||||
(paths: SimulationPath[]): Process[] => {
|
(paths: SimulationPath[]): Process[] => {
|
||||||
if (!paths || paths.length === 0) return [];
|
if (!paths || paths.length === 0) return [];
|
||||||
|
|
||||||
const visited = new Set<string>();
|
const visited = new Set<string>();
|
||||||
const processes: Process[] = [];
|
const processes: Process[] = [];
|
||||||
const pathMap = new Map<string, SimulationPath>();
|
const pathMap = new Map<string, SimulationPath>();
|
||||||
paths.forEach((path) => pathMap.set(path.modeluuid, path));
|
paths.forEach((path) => pathMap.set(path.modeluuid, path));
|
||||||
|
|
||||||
for (const path of paths) {
|
for (const path of paths) {
|
||||||
if (!visited.has(path.modeluuid) && hasSpawnAction(path)) {
|
if (!visited.has(path.modeluuid) && hasSpawnAction(path)) {
|
||||||
const connectedPaths = getAllConnectedPaths(path, paths, visited);
|
const connectedPaths = getAllConnectedPaths(path, paths, visited);
|
||||||
@@ -708,6 +722,7 @@ export function useProcessCreation() {
|
|||||||
processes.push(process);
|
processes.push(process);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return processes;
|
return processes;
|
||||||
},
|
},
|
||||||
[createProcess, getAllConnectedPaths, hasSpawnAction]
|
[createProcess, getAllConnectedPaths, hasSpawnAction]
|
||||||
@@ -736,24 +751,26 @@ const ProcessCreator: React.FC<ProcessCreatorProps> = React.memo(
|
|||||||
);
|
);
|
||||||
}, [simulationPaths]);
|
}, [simulationPaths]);
|
||||||
|
|
||||||
|
// Enhanced dependency tracking that includes action types
|
||||||
const pathsDependency = useMemo(() => {
|
const pathsDependency = useMemo(() => {
|
||||||
if (!convertedPaths) return null;
|
if (!convertedPaths) return null;
|
||||||
return convertedPaths.map((path) => ({
|
return convertedPaths.map((path) => ({
|
||||||
id: path.modeluuid,
|
id: path.modeluuid,
|
||||||
hasSpawn: path.points.some((p: PathPoint) =>
|
// Track all action types for each point
|
||||||
p.actions.some((a: PointAction) => a.type.toLowerCase() === "spawn")
|
actionSignature: path.points
|
||||||
),
|
.map((point, index) =>
|
||||||
|
point.actions.map((action) => `${index}-${action.type}`).join("|")
|
||||||
|
)
|
||||||
|
.join(","),
|
||||||
connections: path.points
|
connections: path.points
|
||||||
.flatMap((p: PathPoint) =>
|
.flatMap((p: PathPoint) =>
|
||||||
p.connections.targets.map((t: { pathUUID: string }) => t.pathUUID)
|
p.connections.targets.map((t: { pathUUID: string }) => t.pathUUID)
|
||||||
)
|
)
|
||||||
.join(","),
|
.join(","),
|
||||||
actions: JSON.stringify(
|
|
||||||
path.points.flatMap((p: PathPoint) => p.actions)
|
|
||||||
), // Serialize all actions
|
|
||||||
}));
|
}));
|
||||||
}, [convertedPaths]);
|
}, [convertedPaths]);
|
||||||
|
|
||||||
|
// Force process recreation when paths change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!convertedPaths || convertedPaths.length === 0) {
|
if (!convertedPaths || convertedPaths.length === 0) {
|
||||||
if (prevProcessesRef.current.length > 0) {
|
if (prevProcessesRef.current.length > 0) {
|
||||||
@@ -762,28 +779,17 @@ const ProcessCreator: React.FC<ProcessCreatorProps> = React.memo(
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (areArraysEqual(prevPathsRef.current, convertedPaths)) {
|
|
||||||
return;
|
// Always regenerate processes if the pathsDependency has changed
|
||||||
}
|
// This ensures action type changes will be detected
|
||||||
prevPathsRef.current = convertedPaths;
|
|
||||||
const newProcesses = createProcessesFromPaths(convertedPaths);
|
const newProcesses = createProcessesFromPaths(convertedPaths);
|
||||||
if (
|
prevPathsRef.current = convertedPaths;
|
||||||
newProcesses.length !== prevProcessesRef.current.length ||
|
|
||||||
!newProcesses.every(
|
// Always update processes when action types change
|
||||||
(proc, i) =>
|
onProcessesCreated(newProcesses);
|
||||||
proc.paths.length === prevProcessesRef.current[i]?.paths.length &&
|
prevProcessesRef.current = newProcesses;
|
||||||
proc.paths.every(
|
|
||||||
(path, j) =>
|
|
||||||
path.modeluuid ===
|
|
||||||
prevProcessesRef.current[i]?.paths[j]?.modeluuid
|
|
||||||
)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
onProcessesCreated(newProcesses);
|
|
||||||
prevProcessesRef.current = newProcesses;
|
|
||||||
}
|
|
||||||
}, [
|
}, [
|
||||||
pathsDependency,
|
pathsDependency, // This now includes action types
|
||||||
onProcessesCreated,
|
onProcessesCreated,
|
||||||
convertedPaths,
|
convertedPaths,
|
||||||
createProcessesFromPaths,
|
createProcessesFromPaths,
|
||||||
|
|||||||
@@ -1,47 +1,50 @@
|
|||||||
import { useState, useEffect, useRef, useMemo } from 'react';
|
import { useState, useEffect, useRef, useMemo } from "react";
|
||||||
import { useSelectedActionSphere, useSelectedPath, useSimulationPaths } from '../../store/store';
|
import {
|
||||||
import * as THREE from 'three';
|
useSelectedActionSphere,
|
||||||
import Behaviour from './behaviour/behaviour';
|
useSelectedPath,
|
||||||
import PathCreation from './path/pathCreation';
|
useSimulationPaths,
|
||||||
import PathConnector from './path/pathConnector';
|
} from "../../store/store";
|
||||||
import useModuleStore from '../../store/useModuleStore';
|
import * as THREE from "three";
|
||||||
import ProcessContainer from './process/processContainer';
|
import Behaviour from "./behaviour/behaviour";
|
||||||
|
import PathCreation from "./path/pathCreation";
|
||||||
|
import PathConnector from "./path/pathConnector";
|
||||||
|
import useModuleStore from "../../store/useModuleStore";
|
||||||
|
import ProcessContainer from "./process/processContainer";
|
||||||
|
|
||||||
function Simulation() {
|
function Simulation() {
|
||||||
const { activeModule } = useModuleStore();
|
const { activeModule } = useModuleStore();
|
||||||
const pathsGroupRef = useRef() as React.MutableRefObject<THREE.Group>;
|
const pathsGroupRef = useRef() as React.MutableRefObject<THREE.Group>;
|
||||||
const { simulationPaths, setSimulationPaths } = useSimulationPaths();
|
const { simulationPaths, setSimulationPaths } = useSimulationPaths();
|
||||||
const [processes, setProcesses] = useState([]);
|
const [processes, setProcesses] = useState([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// console.log('simulationPaths: ', simulationPaths);
|
// console.log('simulationPaths: ', simulationPaths);
|
||||||
}, [simulationPaths]);
|
}, [simulationPaths]);
|
||||||
|
|
||||||
// useEffect(() => {
|
// useEffect(() => {
|
||||||
// if (selectedActionSphere) {
|
// if (selectedActionSphere) {
|
||||||
// console.log('selectedActionSphere: ', selectedActionSphere);
|
// console.log('selectedActionSphere: ', selectedActionSphere);
|
||||||
// }
|
// }
|
||||||
// }, [selectedActionSphere]);
|
// }, [selectedActionSphere]);
|
||||||
|
|
||||||
// useEffect(() => {
|
// useEffect(() => {
|
||||||
// if (selectedPath) {
|
// if (selectedPath) {
|
||||||
// console.log('selectedPath: ', selectedPath);
|
// console.log('selectedPath: ', selectedPath);
|
||||||
// }
|
// }
|
||||||
// }, [selectedPath]);
|
// }, [selectedPath]);
|
||||||
|
|
||||||
|
return (
|
||||||
return (
|
<>
|
||||||
|
<Behaviour />
|
||||||
|
{activeModule === "simulation" && (
|
||||||
<>
|
<>
|
||||||
<Behaviour />
|
<PathCreation pathsGroupRef={pathsGroupRef} />
|
||||||
{activeModule === 'simulation' && (
|
<PathConnector pathsGroupRef={pathsGroupRef} />
|
||||||
<>
|
<ProcessContainer />
|
||||||
<PathCreation pathsGroupRef={pathsGroupRef} />
|
|
||||||
<PathConnector pathsGroupRef={pathsGroupRef} />
|
|
||||||
<ProcessContainer />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Simulation;
|
export default Simulation;
|
||||||
|
|||||||
Reference in New Issue
Block a user