// // 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, }, }); } } }); }