From cf364d707ce328ef76225e72e4404d438f2a84a1 Mon Sep 17 00:00:00 2001 From: SreeNath14 <153710861+SreeNath14@users.noreply.github.com> Date: Thu, 3 Apr 2025 10:28:13 +0530 Subject: [PATCH] "updated animation" --- .../simulation/process/animation.Worker.js | 916 ++++++++++++++++++ .../simulation/process/animationWorker.js | 4 - .../simulation/process/processAnimator.tsx | 631 +++++++----- .../simulation/process/processContainer.tsx | 1 + .../simulation/process/processCreator.tsx | 192 ++-- app/src/modules/simulation/simulation.tsx | 77 +- 6 files changed, 1460 insertions(+), 361 deletions(-) create mode 100644 app/src/modules/simulation/process/animation.Worker.js delete mode 100644 app/src/modules/simulation/process/animationWorker.js diff --git a/app/src/modules/simulation/process/animation.Worker.js b/app/src/modules/simulation/process/animation.Worker.js new file mode 100644 index 0000000..faa6587 --- /dev/null +++ b/app/src/modules/simulation/process/animation.Worker.js @@ -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, + }, + }); + } + } + }); +} diff --git a/app/src/modules/simulation/process/animationWorker.js b/app/src/modules/simulation/process/animationWorker.js deleted file mode 100644 index e9dddd1..0000000 --- a/app/src/modules/simulation/process/animationWorker.js +++ /dev/null @@ -1,4 +0,0 @@ - -const animationWorker = () => { - return; -}; diff --git a/app/src/modules/simulation/process/processAnimator.tsx b/app/src/modules/simulation/process/processAnimator.tsx index d8e35e6..45db8be 100644 --- a/app/src/modules/simulation/process/processAnimator.tsx +++ b/app/src/modules/simulation/process/processAnimator.tsx @@ -5,7 +5,6 @@ import { useLoader, useFrame } from "@react-three/fiber"; import * as THREE from "three"; import { GLTF } from "three-stdlib"; import boxGltb from "../../../assets/gltf-glb/crate_box.glb"; -import camera from "../../../assets/gltf-glb/camera face 2.gltf"; interface PointAction { uuid: string; @@ -44,6 +43,8 @@ interface ProcessData { animationPath: { x: number; y: number; z: number }[]; pointActions: PointAction[][]; speed: number; + customMaterials?: Record; + renderAs?: "box" | "custom"; } interface AnimationState { @@ -58,19 +59,35 @@ interface AnimationState { currentPathIndex: number; } +interface SpawnedObject { + ref: React.RefObject; + state: AnimationState; + visible: boolean; + material: THREE.Material; + spawnTime: number; + currentMaterialType: string; +} + +interface ProcessAnimationState { + spawnedObjects: { [objectId: string]: SpawnedObject }; + nextSpawnTime: number; + objectIdCounter: number; +} + const ProcessAnimator: React.FC<{ processes: ProcessData[] }> = ({ processes, }) => { console.log("processes: ", processes); const gltf = useLoader(GLTFLoader, boxGltb) as GLTF; - const { isPlaying, setIsPlaying } = usePlayButtonStore(); - + const { isPlaying } = usePlayButtonStore(); const groupRef = useRef(null); - const meshRef = useRef(null); - const [visible, setVisible] = useState(false); - const [currentPathIndex, setCurrentPathIndex] = useState(0); - const materials = useMemo( + const [animationStates, setAnimationStates] = useState< + Record + >({}); + + // Base materials + const baseMaterials = useMemo( () => ({ Wood: new THREE.MeshStandardMaterial({ color: 0x8b4513 }), Box: new THREE.MeshStandardMaterial({ @@ -88,44 +105,211 @@ const ProcessAnimator: React.FC<{ processes: ProcessData[] }> = ({ [] ); - const [currentMaterial, setCurrentMaterial] = useState( - materials.Default - ); + // Initialize animation states when processes or play state changes + useEffect(() => { + if (!isPlaying) { + setAnimationStates({}); + return; + } - const { animationPath, currentProcess } = useMemo(() => { - const defaultProcess = { - animationPath: [], - pointActions: [], - speed: 1, - paths: [], + const newStates: Record = {}; + processes.forEach((process) => { + newStates[process.id] = { + spawnedObjects: {}, + nextSpawnTime: 0, + objectIdCounter: 0, + }; + }); + setAnimationStates(newStates); + }, [isPlaying, processes]); + + // Find spawn point in a process + const findSpawnPoint = (process: ProcessData): ProcessPoint | null => { + for (const path of process.paths || []) { + for (const point of path.points || []) { + const spawnAction = point.actions?.find( + (a) => a.isUsed && a.type === "Spawn" + ); + if (spawnAction) { + return point; + } + } + } + return null; + }; + + // Create a new spawned object + const createSpawnedObject = ( + process: ProcessData, + currentTime: number, + materialType: string + ): SpawnedObject => { + const processMaterials = { + ...baseMaterials, + ...(process.customMaterials || {}), }; - const cp = processes?.[0] || defaultProcess; + return { - animationPath: - cp.animationPath?.map((p) => new THREE.Vector3(p.x, p.y, p.z)) || [], - currentProcess: cp, + ref: React.createRef(), + state: { + currentIndex: 0, + progress: 0, + isAnimating: true, + speed: process.speed || 1, + isDelaying: false, + delayStartTime: 0, + currentDelayDuration: 0, + delayComplete: false, + currentPathIndex: 0, + }, + visible: true, + material: + processMaterials[materialType as keyof typeof processMaterials] || + baseMaterials.Default, + currentMaterialType: materialType, + spawnTime: currentTime, }; - }, [processes]); + }; - const animationStateRef = useRef({ - currentIndex: 0, - progress: 0, - isAnimating: false, - speed: currentProcess.speed, - isDelaying: false, - delayStartTime: 0, - currentDelayDuration: 0, - delayComplete: false, - currentPathIndex: 0, - }); + // Handle material swap for an object + const handleMaterialSwap = ( + processId: string, + objectId: string, + materialType: string + ) => { + setAnimationStates((prev) => { + const processState = prev[processId]; + if (!processState || !processState.spawnedObjects[objectId]) return prev; - const getPointDataForAnimationIndex = (index: number) => { - if (!processes[0]?.paths) return null; + const process = processes.find((p) => p.id === processId); + const processMaterials = { + ...baseMaterials, + ...(process?.customMaterials || {}), + }; + + const newMaterial = + processMaterials[materialType as keyof typeof processMaterials] || + baseMaterials.Default; + + return { + ...prev, + [processId]: { + ...processState, + spawnedObjects: { + ...processState.spawnedObjects, + [objectId]: { + ...processState.spawnedObjects[objectId], + material: newMaterial, + currentMaterialType: materialType, + }, + }, + }, + }; + }); + }; + + // Handle point actions for an object + const handlePointActions = ( + processId: string, + objectId: string, + actions: PointAction[] = [], + currentTime: number + ): boolean => { + let shouldStopAnimation = false; + + actions.forEach((action) => { + if (!action.isUsed) return; + + switch (action.type) { + case "Delay": + setAnimationStates((prev) => { + const processState = prev[processId]; + if ( + !processState || + !processState.spawnedObjects[objectId] || + processState.spawnedObjects[objectId].state.isDelaying + ) { + return prev; + } + + const delayDuration = + typeof action.delay === "number" + ? action.delay + : parseFloat(action.delay as string) || 0; + + if (delayDuration > 0) { + return { + ...prev, + [processId]: { + ...processState, + spawnedObjects: { + ...processState.spawnedObjects, + [objectId]: { + ...processState.spawnedObjects[objectId], + state: { + ...processState.spawnedObjects[objectId].state, + isDelaying: true, + delayStartTime: currentTime, + currentDelayDuration: delayDuration, + delayComplete: false, + }, + }, + }, + }, + }; + } + return prev; + }); + shouldStopAnimation = true; + break; + + case "Despawn": + setAnimationStates((prev) => { + const processState = prev[processId]; + if (!processState) return prev; + + const newSpawnedObjects = { ...processState.spawnedObjects }; + delete newSpawnedObjects[objectId]; + + return { + ...prev, + [processId]: { + ...processState, + spawnedObjects: newSpawnedObjects, + }, + }; + }); + shouldStopAnimation = true; + break; + + case "Swap": + if (action.material) { + handleMaterialSwap(processId, objectId, action.material); + } + break; + + default: + break; + } + }); + + return shouldStopAnimation; + }; + + // Check if point has non-inherit actions + const hasNonInheritActions = (actions: PointAction[] = []): boolean => { + return actions.some((action) => action.isUsed && action.type !== "Inherit"); + }; + + // Get point data for current animation index + const getPointDataForAnimationIndex = ( + process: ProcessData, + index: number + ): ProcessPoint | null => { + if (!process.paths) return null; let cumulativePoints = 0; - console.log("cumulativePoints: ", cumulativePoints); - - for (const path of processes[0].paths) { + for (const path of process.paths) { const pointCount = path.points?.length || 0; if (index < cumulativePoints + pointCount) { @@ -139,209 +323,202 @@ const ProcessAnimator: React.FC<{ processes: ProcessData[] }> = ({ return null; }; - useEffect(() => { - if (isPlaying) { - setVisible(true); - animationStateRef.current = { - currentIndex: 0, - progress: 0, - isAnimating: true, - speed: currentProcess.speed, - isDelaying: false, - delayStartTime: 0, - currentDelayDuration: 0, - delayComplete: false, - currentPathIndex: 0, - }; - - const currentRef = gltf?.scene ? groupRef.current : meshRef.current; - if (currentRef && animationPath.length > 0) { - currentRef.position.copy(animationPath[0]); - } - } else { - animationStateRef.current.isAnimating = false; - } - }, [isPlaying, currentProcess, animationPath]); - - const handleMaterialSwap = (materialType: string) => { - setCurrentMaterial( - materials[materialType as keyof typeof materials] || materials.Default - ); - }; - - const hasNonInheritActions = (actions: PointAction[] = []) => { - return actions.some((action) => action.isUsed && action.type !== "Inherit"); - }; - - const handlePointActions = ( - actions: PointAction[] = [], - currentTime: number - ) => { - let shouldStopAnimation = false; - - actions.forEach((action) => { - if (!action.isUsed) return; - - switch (action.type) { - case "Delay": - if ( - !animationStateRef.current.isDelaying && - !animationStateRef.current.delayComplete - ) { - const delayDuration = - typeof action.delay === "number" - ? action.delay - : parseFloat(action.delay as string) || 0; - - if (delayDuration > 0) { - animationStateRef.current.isDelaying = true; - animationStateRef.current.delayStartTime = currentTime; - animationStateRef.current.currentDelayDuration = delayDuration; - shouldStopAnimation = true; - } - } - break; - - case "Despawn": - setVisible(false); - setIsPlaying(false); - animationStateRef.current.isAnimating = false; - shouldStopAnimation = true; - break; - - case "Spawn": - setVisible(true); - break; - - case "Swap": - if (action.material) { - handleMaterialSwap(action.material); - } - break; - - case "Inherit": - // Handle inherit logic if needed - break; - } - }); - - return shouldStopAnimation; - }; - - useFrame((state, delta) => { - const currentRef = gltf?.scene ? groupRef.current : meshRef.current; - if ( - !currentRef || - !animationStateRef.current.isAnimating || - animationPath.length < 2 - ) { - return; - } + // Spawn objects for all processes + useFrame((state) => { + if (!isPlaying) return; const currentTime = state.clock.getElapsedTime(); - const path = animationPath; - const stateRef = animationStateRef.current; + setAnimationStates((prev) => { + const newStates = { ...prev }; - // Check if we need to switch paths (specific to your structure) - if (stateRef.currentIndex === 3 && stateRef.currentPathIndex === 0) { - setCurrentPathIndex(1); - stateRef.currentPathIndex = 1; - } + processes.forEach((process) => { + const processState = newStates[process.id]; + if (!processState) return; - // Get the current point data - const currentPointData = getPointDataForAnimationIndex( - stateRef.currentIndex - ); + const spawnPoint = findSpawnPoint(process); + if (!spawnPoint || !spawnPoint.actions) return; - // Execute actions when we first arrive at a point - - if (stateRef.progress === 0 && currentPointData?.actions) { - console.log( - `Processing actions at point ${stateRef.currentIndex}`, - currentPointData.actions - ); - const shouldStop = handlePointActions( - currentPointData.actions, - currentTime - ); - if (shouldStop) return; - } - - // Handle delays - if (stateRef.isDelaying) { - console.log( - `Delaying... ${currentTime - stateRef.delayStartTime}/${ - stateRef.currentDelayDuration - }` - ); - if ( - currentTime - stateRef.delayStartTime >= - stateRef.currentDelayDuration - ) { - stateRef.isDelaying = false; - stateRef.delayComplete = true; - } else { - return; // Keep waiting - } - } - - const nextPointIdx = stateRef.currentIndex + 1; - const isLastPoint = nextPointIdx >= path.length; - - if (isLastPoint) { - if (currentPointData?.actions) { - const shouldStop = !hasNonInheritActions(currentPointData.actions); - if (shouldStop) { - currentRef.position.copy(path[stateRef.currentIndex]); - setIsPlaying(false); - stateRef.isAnimating = false; - return; - } - } - } - - if (!isLastPoint) { - const nextPoint = path[nextPointIdx]; - const distance = path[stateRef.currentIndex].distanceTo(nextPoint); - const movement = stateRef.speed * delta; - stateRef.progress += movement / distance; - - if (stateRef.progress >= 1) { - stateRef.currentIndex = nextPointIdx; - stateRef.progress = 0; - stateRef.delayComplete = false; - currentRef.position.copy(nextPoint); - } else { - currentRef.position.lerpVectors( - path[stateRef.currentIndex], - nextPoint, - stateRef.progress + const spawnAction = spawnPoint.actions.find( + (a) => a.isUsed && a.type === "Spawn" ); - } - } + if (!spawnAction) return; + + const spawnInterval = + typeof spawnAction.spawnInterval === "number" + ? spawnAction.spawnInterval + : 0; + + if (currentTime >= processState.nextSpawnTime) { + const objectId = `obj-${process.id}-${processState.objectIdCounter}`; + + newStates[process.id] = { + ...processState, + spawnedObjects: { + ...processState.spawnedObjects, + [objectId]: createSpawnedObject( + process, + currentTime, + spawnAction.material || "Default" + ), + }, + objectIdCounter: processState.objectIdCounter + 1, + nextSpawnTime: currentTime + spawnInterval, + }; + } + }); + + return newStates; + }); + }); + + // Animate objects for all processes + useFrame((state, delta) => { + if (!isPlaying) return; + + const currentTime = state.clock.getElapsedTime(); + setAnimationStates((prev) => { + const newStates = { ...prev }; + + processes.forEach((process) => { + const processState = newStates[process.id]; + if (!processState) return; + + const path = + process.animationPath?.map((p) => new THREE.Vector3(p.x, p.y, p.z)) || + []; + if (path.length < 2) return; + + const updatedObjects = { ...processState.spawnedObjects }; + + Object.entries(processState.spawnedObjects).forEach( + ([objectId, obj]) => { + if (!obj.visible || !obj.state.isAnimating) return; + + const currentRef = gltf?.scene ? obj.ref.current : obj.ref.current; + if (!currentRef) return; + + const stateRef = obj.state; + + // Get current point data + const currentPointData = getPointDataForAnimationIndex( + process, + stateRef.currentIndex + ); + + // Execute actions when arriving at a new point + if (stateRef.progress === 0 && currentPointData?.actions) { + const shouldStop = handlePointActions( + process.id, + objectId, + currentPointData.actions, + currentTime + ); + if (shouldStop) return; + } + + // Handle delays + if (stateRef.isDelaying) { + if ( + currentTime - stateRef.delayStartTime >= + stateRef.currentDelayDuration + ) { + stateRef.isDelaying = false; + stateRef.delayComplete = true; + } else { + return; // Keep waiting + } + } + + const nextPointIdx = stateRef.currentIndex + 1; + const isLastPoint = nextPointIdx >= path.length; + + if (isLastPoint) { + if (currentPointData?.actions) { + const shouldStop = !hasNonInheritActions( + currentPointData.actions + ); + if (shouldStop) { + currentRef.position.copy(path[stateRef.currentIndex]); + delete updatedObjects[objectId]; + return; + } + } + } + + if (!isLastPoint) { + const nextPoint = path[nextPointIdx]; + const distance = + path[stateRef.currentIndex].distanceTo(nextPoint); + const movement = stateRef.speed * delta; + stateRef.progress += movement / distance; + + if (stateRef.progress >= 1) { + stateRef.currentIndex = nextPointIdx; + stateRef.progress = 0; + stateRef.delayComplete = false; + currentRef.position.copy(nextPoint); + } else { + currentRef.position.lerpVectors( + path[stateRef.currentIndex], + nextPoint, + stateRef.progress + ); + } + } + + updatedObjects[objectId] = { ...obj, state: { ...stateRef } }; + } + ); + + newStates[process.id] = { + ...processState, + spawnedObjects: updatedObjects, + }; + }); + + return newStates; + }); }); if (!processes || processes.length === 0) { return null; } - if (!gltf?.scene) { - return visible ? ( - - - - ) : null; - } + return ( + <> + {Object.entries(animationStates).flatMap(([processId, processState]) => + Object.entries(processState.spawnedObjects) + .filter(([_, obj]) => obj.visible) + .map(([objectId, obj]) => { + const process = processes.find((p) => p.id === processId); + console.log("process: ", process); + const renderAs = process?.renderAs || "custom"; - return visible ? ( - - - - ) : null; + return renderAs === "box" ? ( + } + material={obj.material} + > + + + ) : ( + gltf?.scene && ( + } + > + + + ) + ); + }) + )} + + ); }; export default ProcessAnimator; diff --git a/app/src/modules/simulation/process/processContainer.tsx b/app/src/modules/simulation/process/processContainer.tsx index 967376c..bf8fa48 100644 --- a/app/src/modules/simulation/process/processContainer.tsx +++ b/app/src/modules/simulation/process/processContainer.tsx @@ -10,6 +10,7 @@ const ProcessContainer: React.FC = () => { <> {processes.length > 0 && } + ); }; diff --git a/app/src/modules/simulation/process/processCreator.tsx b/app/src/modules/simulation/process/processCreator.tsx index e70408b..91bc4c1 100644 --- a/app/src/modules/simulation/process/processCreator.tsx +++ b/app/src/modules/simulation/process/processCreator.tsx @@ -422,6 +422,7 @@ export interface PointAction { spawnInterval: string | number; isUsed: boolean; } + export interface PathPoint { uuid: string; position: [number, number, number]; @@ -430,12 +431,14 @@ export interface PathPoint { targets: Array<{ pathUUID: string }>; }; } + export interface SimulationPath { modeluuid: string; points: PathPoint[]; pathPosition: [number, number, number]; speed?: number; } + export interface Process { id: string; paths: SimulationPath[]; @@ -443,6 +446,7 @@ export interface Process { pointActions: PointAction[][]; speed: number; } + interface ProcessCreatorProps { onProcessesCreated: (processes: Process[]) => void; } @@ -453,83 +457,51 @@ function convertToSimulationPath( ): SimulationPath { const { modeluuid } = path; - // Helper function to normalize actions + // Simplified normalizeAction function that preserves exact original properties const normalizeAction = (action: any): PointAction => { return { ...action }; // Return exact copy with no modifications }; - // Extract points from the path - let points: PathPoint[]; if (path.type === "Conveyor") { - points = path.points.map((point) => ({ - uuid: point.uuid, - position: point.position, - actions: point.actions.map(normalizeAction), // Preserve exact actions - connections: { - targets: point.connections.targets.map((target) => ({ - 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)], + return { + modeluuid, + points: path.points.map((point) => ({ + uuid: point.uuid, + position: point.position, + actions: point.actions.map(normalizeAction), // Preserve exact actions connections: { - targets: path.point.connections.targets.map((target) => ({ + targets: point.connections.targets.map((target) => ({ pathUUID: target.pathUUID, })), }, - }, - ]; - } - - // Check if point 1 or point 2 has a spawn action - const hasSpawnInFirstTwoPoints = - 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; + })), + pathPosition: path.position, + speed: + typeof path.speed === "string" + ? parseFloat(path.speed) || 1 + : path.speed || 1, + }; } else { - // For VehicleEventsSchema, use a default speed of 1 - speed = 1; + return { + 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 @@ -556,6 +528,7 @@ function shouldReverseNextPath( nextPath: SimulationPath ): boolean { if (nextPath.points.length !== 3) return false; + const currentLastPoint = currentPath.points[currentPath.points.length - 1]; const nextFirstPoint = nextPath.points[0]; const nextLastPoint = nextPath.points[nextPath.points.length - 1]; @@ -582,25 +555,56 @@ function shouldReverseNextPath( 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 function adjustPathPointsOrder(paths: SimulationPath[]): SimulationPath[] { - if (paths.length < 2) return paths; + if (paths.length < 1) return 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++) { const currentPath = adjustedPaths[i]; const nextPath = adjustedPaths[i + 1]; + if (shouldReverseNextPath(currentPath, nextPath)) { const reversedPoints = [ nextPath.points[2], nextPath.points[1], nextPath.points[0], ]; + adjustedPaths[i + 1] = { ...nextPath, points: reversedPoints, }; } } + return adjustedPaths; } @@ -608,6 +612,7 @@ function adjustPathPointsOrder(paths: SimulationPath[]): SimulationPath[] { export function useProcessCreation() { const { scene } = useThree(); const [processes, setProcesses] = useState([]); + const hasSpawnAction = useCallback((path: SimulationPath): boolean => { return path.points.some((point) => point.actions.some((action) => action.type.toLowerCase() === "spawn") @@ -619,9 +624,11 @@ export function useProcessCreation() { if (!paths || paths.length === 0) { return createEmptyProcess(); } + const animationPath: THREE.Vector3[] = []; const pointActions: PointAction[][] = []; const processSpeed = paths[0]?.speed || 1; + for (const path of paths) { for (const point of path.points) { 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`); continue; } + const position = obj.getWorldPosition(new THREE.Vector3()); animationPath.push(position.clone()); pointActions.push(point.actions); } } + return { id: `process-${Math.random().toString(36).substring(2, 11)}`, paths, @@ -654,8 +663,10 @@ export function useProcessCreation() { const connectedPaths: SimulationPath[] = []; const queue: SimulationPath[] = [initialPath]; visited.add(initialPath.modeluuid); + const pathMap = new Map(); allPaths.forEach((path) => pathMap.set(path.modeluuid, path)); + while (queue.length > 0) { const currentPath = queue.shift()!; connectedPaths.push(currentPath); @@ -688,6 +699,7 @@ export function useProcessCreation() { } } } + return connectedPaths; }, [] @@ -696,10 +708,12 @@ export function useProcessCreation() { const createProcessesFromPaths = useCallback( (paths: SimulationPath[]): Process[] => { if (!paths || paths.length === 0) return []; + const visited = new Set(); const processes: Process[] = []; const pathMap = new Map(); paths.forEach((path) => pathMap.set(path.modeluuid, path)); + for (const path of paths) { if (!visited.has(path.modeluuid) && hasSpawnAction(path)) { const connectedPaths = getAllConnectedPaths(path, paths, visited); @@ -708,6 +722,7 @@ export function useProcessCreation() { processes.push(process); } } + return processes; }, [createProcess, getAllConnectedPaths, hasSpawnAction] @@ -736,24 +751,26 @@ const ProcessCreator: React.FC = React.memo( ); }, [simulationPaths]); + // Enhanced dependency tracking that includes action types const pathsDependency = useMemo(() => { if (!convertedPaths) return null; return convertedPaths.map((path) => ({ id: path.modeluuid, - hasSpawn: path.points.some((p: PathPoint) => - p.actions.some((a: PointAction) => a.type.toLowerCase() === "spawn") - ), + // Track all action types for each point + actionSignature: path.points + .map((point, index) => + point.actions.map((action) => `${index}-${action.type}`).join("|") + ) + .join(","), connections: path.points .flatMap((p: PathPoint) => p.connections.targets.map((t: { pathUUID: string }) => t.pathUUID) ) .join(","), - actions: JSON.stringify( - path.points.flatMap((p: PathPoint) => p.actions) - ), // Serialize all actions })); }, [convertedPaths]); + // Force process recreation when paths change useEffect(() => { if (!convertedPaths || convertedPaths.length === 0) { if (prevProcessesRef.current.length > 0) { @@ -762,28 +779,17 @@ const ProcessCreator: React.FC = React.memo( } return; } - if (areArraysEqual(prevPathsRef.current, convertedPaths)) { - return; - } - prevPathsRef.current = convertedPaths; + + // Always regenerate processes if the pathsDependency has changed + // This ensures action type changes will be detected const newProcesses = createProcessesFromPaths(convertedPaths); - if ( - newProcesses.length !== prevProcessesRef.current.length || - !newProcesses.every( - (proc, i) => - proc.paths.length === prevProcessesRef.current[i]?.paths.length && - proc.paths.every( - (path, j) => - path.modeluuid === - prevProcessesRef.current[i]?.paths[j]?.modeluuid - ) - ) - ) { - onProcessesCreated(newProcesses); - prevProcessesRef.current = newProcesses; - } + prevPathsRef.current = convertedPaths; + + // Always update processes when action types change + onProcessesCreated(newProcesses); + prevProcessesRef.current = newProcesses; }, [ - pathsDependency, + pathsDependency, // This now includes action types onProcessesCreated, convertedPaths, createProcessesFromPaths, diff --git a/app/src/modules/simulation/simulation.tsx b/app/src/modules/simulation/simulation.tsx index 0212d50..59074a7 100644 --- a/app/src/modules/simulation/simulation.tsx +++ b/app/src/modules/simulation/simulation.tsx @@ -1,47 +1,50 @@ -import { useState, useEffect, useRef, useMemo } from 'react'; -import { useSelectedActionSphere, useSelectedPath, useSimulationPaths } from '../../store/store'; -import * as THREE from 'three'; -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'; +import { useState, useEffect, useRef, useMemo } from "react"; +import { + useSelectedActionSphere, + useSelectedPath, + useSimulationPaths, +} from "../../store/store"; +import * as THREE from "three"; +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() { - const { activeModule } = useModuleStore(); - const pathsGroupRef = useRef() as React.MutableRefObject; - const { simulationPaths, setSimulationPaths } = useSimulationPaths(); - const [processes, setProcesses] = useState([]); + const { activeModule } = useModuleStore(); + const pathsGroupRef = useRef() as React.MutableRefObject; + const { simulationPaths, setSimulationPaths } = useSimulationPaths(); + const [processes, setProcesses] = useState([]); - useEffect(() => { - // console.log('simulationPaths: ', simulationPaths); - }, [simulationPaths]); + useEffect(() => { + // console.log('simulationPaths: ', simulationPaths); + }, [simulationPaths]); - // useEffect(() => { - // if (selectedActionSphere) { - // console.log('selectedActionSphere: ', selectedActionSphere); - // } - // }, [selectedActionSphere]); + // useEffect(() => { + // if (selectedActionSphere) { + // console.log('selectedActionSphere: ', selectedActionSphere); + // } + // }, [selectedActionSphere]); - // useEffect(() => { - // if (selectedPath) { - // console.log('selectedPath: ', selectedPath); - // } - // }, [selectedPath]); + // useEffect(() => { + // if (selectedPath) { + // console.log('selectedPath: ', selectedPath); + // } + // }, [selectedPath]); - - return ( + return ( + <> + + {activeModule === "simulation" && ( <> - - {activeModule === 'simulation' && ( - <> - - - - - )} + + + - ); + )} + + ); } -export default Simulation; \ No newline at end of file +export default Simulation;