917 lines
26 KiB
JavaScript
917 lines
26 KiB
JavaScript
// // 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,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|