From 82a7cd0001b0efd91e7dcc91b73a1f58f0a1f720 Mon Sep 17 00:00:00 2001 From: Poovizhi99 <poovizhi@hexrfactory.com> Date: Tue, 15 Apr 2025 09:11:01 +0530 Subject: [PATCH 01/20] updated paths --- .../modules/simulation/path/pathConnector.tsx | 2562 ++++++++++------- 1 file changed, 1445 insertions(+), 1117 deletions(-) diff --git a/app/src/modules/simulation/path/pathConnector.tsx b/app/src/modules/simulation/path/pathConnector.tsx index c561cbb..f6ee51f 100644 --- a/app/src/modules/simulation/path/pathConnector.tsx +++ b/app/src/modules/simulation/path/pathConnector.tsx @@ -1,1170 +1,1498 @@ -import { useFrame, useThree } from '@react-three/fiber'; -import React, { useEffect, useRef, useState } from 'react'; -import * as THREE from 'three'; -import * as Types from '../../../types/world/worldTypes'; -import { QuadraticBezierLine } from '@react-three/drei'; -import { useDeleteTool, useIsConnecting, useRenderDistance, useSimulationStates, useSocketStore } from '../../../store/store'; -import useModuleStore from '../../../store/useModuleStore'; -import { usePlayButtonStore } from '../../../store/usePlayButtonStore'; -import { setEventApi } from '../../../services/factoryBuilder/assest/floorAsset/setEventsApt'; +import { useFrame, useThree } from "@react-three/fiber"; +import React, { useEffect, useRef, useState } from "react"; +import * as THREE from "three"; +import * as Types from "../../../types/world/worldTypes"; +import { QuadraticBezierLine } from "@react-three/drei"; +import { + useDeleteTool, + useIsConnecting, + useRenderDistance, + useSimulationStates, + useSocketStore, +} from "../../../store/store"; +import useModuleStore from "../../../store/useModuleStore"; +import { usePlayButtonStore } from "../../../store/usePlayButtonStore"; +import { setEventApi } from "../../../services/factoryBuilder/assest/floorAsset/setEventsApt"; -function PathConnector({ pathsGroupRef }: { pathsGroupRef: React.MutableRefObject<THREE.Group> }) { - const { activeModule } = useModuleStore(); - const { gl, raycaster, scene, pointer, camera } = useThree(); - const { deleteTool } = useDeleteTool(); - const { renderDistance } = useRenderDistance(); - const { setIsConnecting } = useIsConnecting(); - const { simulationStates, setSimulationStates } = useSimulationStates(); - const { isPlaying } = usePlayButtonStore(); - const { socket } = useSocketStore(); - const groupRefs = useRef<{ [key: string]: any }>({}); +function PathConnector({ + pathsGroupRef, +}: { + pathsGroupRef: React.MutableRefObject<THREE.Group>; +}) { + const { activeModule } = useModuleStore(); + const { gl, raycaster, scene, pointer, camera } = useThree(); + const { deleteTool } = useDeleteTool(); + const { renderDistance } = useRenderDistance(); + const { setIsConnecting } = useIsConnecting(); + const { simulationStates, setSimulationStates } = useSimulationStates(); + const { isPlaying } = usePlayButtonStore(); + const { socket } = useSocketStore(); + const groupRefs = useRef<{ [key: string]: any }>({}); - const [firstSelected, setFirstSelected] = useState<{ modelUUID: string; sphereUUID: string; position: THREE.Vector3; isCorner: boolean; } | null>(null); - const [currentLine, setCurrentLine] = useState<{ start: THREE.Vector3, end: THREE.Vector3, mid: THREE.Vector3 } | null>(null); - const [helperlineColor, setHelperLineColor] = useState<string>('red'); - const [hoveredLineKey, setHoveredLineKey] = useState<string | null>(null); + const [firstSelected, setFirstSelected] = useState<{ + modelUUID: string; + sphereUUID: string; + position: THREE.Vector3; + isCorner: boolean; + } | null>(null); + const [currentLine, setCurrentLine] = useState<{ + start: THREE.Vector3; + end: THREE.Vector3; + mid: THREE.Vector3; + } | null>(null); + const [helperlineColor, setHelperLineColor] = useState<string>("red"); + const [hoveredLineKey, setHoveredLineKey] = useState<string | null>(null); - const updatePathConnections = (fromModelUUID: string, fromPointUUID: string, toModelUUID: string, toPointUUID: string) => { - const updatedPaths = simulationStates.map(path => { - if (path.type === 'Conveyor') { - // Handle outgoing connections from Conveyor - if (path.modeluuid === fromModelUUID) { - return { - ...path, - points: path.points.map(point => { - if (point.uuid === fromPointUUID) { - const newTarget = { - modelUUID: toModelUUID, - pointUUID: toPointUUID - }; - const existingTargets = point.connections.targets || []; + const updatePathConnections = ( + fromModelUUID: string, + fromPointUUID: string, + toModelUUID: string, + toPointUUID: string + ) => { + const updatedPaths = simulationStates.map((path) => { + if (path.type === "Conveyor") { + // Handle outgoing connections from Conveyor + if (path.modeluuid === fromModelUUID) { + return { + ...path, + points: path.points.map((point) => { + if (point.uuid === fromPointUUID) { + const newTarget = { + modelUUID: toModelUUID, + pointUUID: toPointUUID, + }; + const existingTargets = point.connections.targets || []; - if (!existingTargets.some(target => - target.modelUUID === newTarget.modelUUID && - target.pointUUID === newTarget.pointUUID - )) { - return { - ...point, - connections: { - ...point.connections, - targets: [...existingTargets, newTarget] - } - }; - } - } - return point; - }) - }; + if ( + !existingTargets.some( + (target) => + target.modelUUID === newTarget.modelUUID && + target.pointUUID === newTarget.pointUUID + ) + ) { + return { + ...point, + connections: { + ...point.connections, + targets: [...existingTargets, newTarget], + }, + }; } - // Handle incoming connections to Conveyor - else if (path.modeluuid === toModelUUID) { - return { - ...path, - points: path.points.map(point => { - if (point.uuid === toPointUUID) { - const reverseTarget = { - modelUUID: fromModelUUID, - pointUUID: fromPointUUID - }; - const existingTargets = point.connections.targets || []; + } + return point; + }), + }; + } + // Handle incoming connections to Conveyor + else if (path.modeluuid === toModelUUID) { + return { + ...path, + points: path.points.map((point) => { + if (point.uuid === toPointUUID) { + const reverseTarget = { + modelUUID: fromModelUUID, + pointUUID: fromPointUUID, + }; + const existingTargets = point.connections.targets || []; - if (!existingTargets.some(target => - target.modelUUID === reverseTarget.modelUUID && - target.pointUUID === reverseTarget.pointUUID - )) { - return { - ...point, - connections: { - ...point.connections, - targets: [...existingTargets, reverseTarget] - } - }; - } - } - return point; - }) - }; + if ( + !existingTargets.some( + (target) => + target.modelUUID === reverseTarget.modelUUID && + target.pointUUID === reverseTarget.pointUUID + ) + ) { + return { + ...point, + connections: { + ...point.connections, + targets: [...existingTargets, reverseTarget], + }, + }; } - } - else if (path.type === 'Vehicle') { - // Handle outgoing connections from Vehicle - if (path.modeluuid === fromModelUUID && path.points.uuid === fromPointUUID) { - const newTarget = { - modelUUID: toModelUUID, - pointUUID: toPointUUID - }; - const existingTargets = path.points.connections.targets || []; - - // Check if target is a Conveyor - const toPath = simulationStates.find(p => p.modeluuid === toModelUUID); - if (toPath?.type !== 'Conveyor') { - console.log("Vehicle can only connect to Conveyors"); - return path; - } - - // Check if already has a connection - if (existingTargets.length >= 1) { - console.log("Vehicle can have only one connection"); - return path; - } - - if (!existingTargets.some(target => - target.modelUUID === newTarget.modelUUID && - target.pointUUID === newTarget.pointUUID - )) { - return { - ...path, - points: { - ...path.points, - connections: { - ...path.points.connections, - targets: [...existingTargets, newTarget] - } - } - }; - } - } - // Handle incoming connections to Vehicle - else if (path.modeluuid === toModelUUID && path.points.uuid === toPointUUID) { - const reverseTarget = { - modelUUID: fromModelUUID, - pointUUID: fromPointUUID - }; - const existingTargets = path.points.connections.targets || []; - - // Check if source is a Conveyor - const fromPath = simulationStates.find(p => p.modeluuid === fromModelUUID); - if (fromPath?.type !== 'Conveyor') { - console.log("Vehicle can only connect to Conveyors"); - return path; - } - - // Check if already has a connection - if (existingTargets.length >= 1) { - console.log("Vehicle can have only one connection"); - return path; - } - - if (!existingTargets.some(target => - target.modelUUID === reverseTarget.modelUUID && - target.pointUUID === reverseTarget.pointUUID - )) { - return { - ...path, - points: { - ...path.points, - connections: { - ...path.points.connections, - targets: [...existingTargets, reverseTarget] - } - } - }; - } - } - return path; - } - else if (path.type === 'StaticMachine') { - // Handle outgoing connections from StaticMachine - if (path.modeluuid === fromModelUUID && path.points.uuid === fromPointUUID) { - const newTarget = { - modelUUID: toModelUUID, - pointUUID: toPointUUID - }; - - // Ensure target is an ArmBot - const toPath = simulationStates.find(p => p.modeluuid === toModelUUID); - if (toPath?.type !== 'ArmBot') { - console.log("StaticMachine can only connect to ArmBot"); - return path; - } - - const existingTargets = path.points.connections.targets || []; - - // Allow only one connection - if (existingTargets.length >= 1) { - console.log("StaticMachine can only have one connection"); - return path; - } - - if (!existingTargets.some(target => - target.modelUUID === newTarget.modelUUID && - target.pointUUID === newTarget.pointUUID - )) { - return { - ...path, - points: { - ...path.points, - connections: { - ...path.points.connections, - targets: [...existingTargets, newTarget] - } - } - }; - } - } - - // Handle incoming connections to StaticMachine - else if (path.modeluuid === toModelUUID && path.points.uuid === toPointUUID) { - const reverseTarget = { - modelUUID: fromModelUUID, - pointUUID: fromPointUUID - }; - - const fromPath = simulationStates.find(p => p.modeluuid === fromModelUUID); - if (fromPath?.type !== 'ArmBot') { - console.log("StaticMachine can only be connected from ArmBot"); - return path; - } - - const existingTargets = path.points.connections.targets || []; - - if (existingTargets.length >= 1) { - console.log("StaticMachine can only have one connection"); - return path; - } - - if (!existingTargets.some(target => - target.modelUUID === reverseTarget.modelUUID && - target.pointUUID === reverseTarget.pointUUID - )) { - return { - ...path, - points: { - ...path.points, - connections: { - ...path.points.connections, - targets: [...existingTargets, reverseTarget] - } - } - }; - } - } - return path; - } - else if (path.type === 'ArmBot') { - // Handle outgoing connections from ArmBot - if (path.modeluuid === fromModelUUID && path.points.uuid === fromPointUUID) { - const newTarget = { - modelUUID: toModelUUID, - pointUUID: toPointUUID - }; - - const toPath = simulationStates.find(p => p.modeluuid === toModelUUID); - if (!toPath) return path; - - const existingTargets = path.points.connections.targets || []; - - // Check if connecting to a StaticMachine and already connected to one - const alreadyConnectedToStatic = existingTargets.some(target => { - const targetPath = simulationStates.find(p => p.modeluuid === target.modelUUID); - return targetPath?.type === 'StaticMachine'; - }); - - if (toPath.type === 'StaticMachine') { - if (alreadyConnectedToStatic) { - console.log("ArmBot can only connect to one StaticMachine"); - return path; - } - } - - if (!existingTargets.some(target => - target.modelUUID === newTarget.modelUUID && - target.pointUUID === newTarget.pointUUID - )) { - return { - ...path, - points: { - ...path.points, - connections: { - ...path.points.connections, - targets: [...existingTargets, newTarget] - } - } - }; - } - } - - // Handle incoming connections to ArmBot - else if (path.modeluuid === toModelUUID && path.points.uuid === toPointUUID) { - const reverseTarget = { - modelUUID: fromModelUUID, - pointUUID: fromPointUUID - }; - - const fromPath = simulationStates.find(p => p.modeluuid === fromModelUUID); - if (!fromPath) return path; - - const existingTargets = path.points.connections.targets || []; - - const alreadyConnectedFromStatic = existingTargets.some(target => { - const targetPath = simulationStates.find(p => p.modeluuid === target.modelUUID); - return targetPath?.type === 'StaticMachine'; - }); - - if (fromPath.type === 'StaticMachine') { - if (alreadyConnectedFromStatic) { - console.log("ArmBot can only be connected from one StaticMachine"); - return path; - } - } - - if (!existingTargets.some(target => - target.modelUUID === reverseTarget.modelUUID && - target.pointUUID === reverseTarget.pointUUID - )) { - return { - ...path, - points: { - ...path.points, - connections: { - ...path.points.connections, - targets: [...existingTargets, reverseTarget] - } - } - }; - } - } - return path; - } + } + return point; + }), + }; + } + } else if (path.type === "Vehicle") { + // Handle outgoing connections from Vehicle + if ( + path.modeluuid === fromModelUUID && + path.points.uuid === fromPointUUID + ) { + const newTarget = { + modelUUID: toModelUUID, + pointUUID: toPointUUID, + }; + const existingTargets = path.points.connections.targets || []; + // Check if target is a Conveyor + const toPath = simulationStates.find( + (p) => p.modeluuid === toModelUUID + ); + if (toPath?.type !== "Conveyor") { + console.log("Vehicle can only connect to Conveyors"); return path; - }); + } - setSimulationStates(updatedPaths); + // Check if already has a connection + if (existingTargets.length >= 1) { + console.log("Vehicle can have only one connection"); + return path; + } - const updatedPathDetails = updatedPaths.filter(path => - path.modeluuid === fromModelUUID || path.modeluuid === toModelUUID - ); + if ( + !existingTargets.some( + (target) => + target.modelUUID === newTarget.modelUUID && + target.pointUUID === newTarget.pointUUID + ) + ) { + return { + ...path, + points: { + ...path.points, + connections: { + ...path.points.connections, + targets: [...existingTargets, newTarget], + }, + }, + }; + } + } + // Handle incoming connections to Vehicle + else if ( + path.modeluuid === toModelUUID && + path.points.uuid === toPointUUID + ) { + const reverseTarget = { + modelUUID: fromModelUUID, + pointUUID: fromPointUUID, + }; + const existingTargets = path.points.connections.targets || []; - updateBackend(updatedPathDetails); + // Check if source is a Conveyor + const fromPath = simulationStates.find( + (p) => p.modeluuid === fromModelUUID + ); + if (fromPath?.type !== "Conveyor") { + console.log("Vehicle can only connect to Conveyors"); + return path; + } + + // Check if already has a connection + if (existingTargets.length >= 1) { + console.log("Vehicle can have only one connection"); + return path; + } + + if ( + !existingTargets.some( + (target) => + target.modelUUID === reverseTarget.modelUUID && + target.pointUUID === reverseTarget.pointUUID + ) + ) { + return { + ...path, + points: { + ...path.points, + connections: { + ...path.points.connections, + targets: [...existingTargets, reverseTarget], + }, + }, + }; + } + } + return path; + } else if (path.type === "StaticMachine") { + // Handle outgoing connections from StaticMachine + if ( + path.modeluuid === fromModelUUID && + path.points.uuid === fromPointUUID + ) { + const newTarget = { + modelUUID: toModelUUID, + pointUUID: toPointUUID, + }; + + // Ensure target is an ArmBot + const toPath = simulationStates.find( + (p) => p.modeluuid === toModelUUID + ); + if (toPath?.type !== "ArmBot") { + console.log("StaticMachine can only connect to ArmBot"); + return path; + } + + const existingTargets = path.points.connections.targets || []; + + // Allow only one connection + if (existingTargets.length >= 1) { + console.log("StaticMachine can only have one connection"); + return path; + } + + if ( + !existingTargets.some( + (target) => + target.modelUUID === newTarget.modelUUID && + target.pointUUID === newTarget.pointUUID + ) + ) { + return { + ...path, + points: { + ...path.points, + connections: { + ...path.points.connections, + targets: [...existingTargets, newTarget], + }, + }, + }; + } + } + + // Handle incoming connections to StaticMachine + else if ( + path.modeluuid === toModelUUID && + path.points.uuid === toPointUUID + ) { + const reverseTarget = { + modelUUID: fromModelUUID, + pointUUID: fromPointUUID, + }; + + const fromPath = simulationStates.find( + (p) => p.modeluuid === fromModelUUID + ); + if (fromPath?.type !== "ArmBot") { + console.log("StaticMachine can only be connected from ArmBot"); + return path; + } + + const existingTargets = path.points.connections.targets || []; + + if (existingTargets.length >= 1) { + console.log("StaticMachine can only have one connection"); + return path; + } + + if ( + !existingTargets.some( + (target) => + target.modelUUID === reverseTarget.modelUUID && + target.pointUUID === reverseTarget.pointUUID + ) + ) { + return { + ...path, + points: { + ...path.points, + connections: { + ...path.points.connections, + targets: [...existingTargets, reverseTarget], + }, + }, + }; + } + } + return path; + } else if (path.type === "ArmBot") { + // Handle outgoing connections from ArmBot + if ( + path.modeluuid === fromModelUUID && + path.points.uuid === fromPointUUID + ) { + const newTarget = { + modelUUID: toModelUUID, + pointUUID: toPointUUID, + }; + + const toPath = simulationStates.find( + (p) => p.modeluuid === toModelUUID + ); + if (!toPath) return path; + + const existingTargets = path.points.connections.targets || []; + + // Check if connecting to a StaticMachine and already connected to one + const alreadyConnectedToStatic = existingTargets.some((target) => { + const targetPath = simulationStates.find( + (p) => p.modeluuid === target.modelUUID + ); + return targetPath?.type === "StaticMachine"; + }); + + if (toPath.type === "StaticMachine") { + if (alreadyConnectedToStatic) { + console.log("ArmBot can only connect to one StaticMachine"); + return path; + } + } + + if ( + !existingTargets.some( + (target) => + target.modelUUID === newTarget.modelUUID && + target.pointUUID === newTarget.pointUUID + ) + ) { + return { + ...path, + points: { + ...path.points, + connections: { + ...path.points.connections, + targets: [...existingTargets, newTarget], + }, + }, + }; + } + } + + // Handle incoming connections to ArmBot + else if ( + path.modeluuid === toModelUUID && + path.points.uuid === toPointUUID + ) { + const reverseTarget = { + modelUUID: fromModelUUID, + pointUUID: fromPointUUID, + }; + + const fromPath = simulationStates.find( + (p) => p.modeluuid === fromModelUUID + ); + if (!fromPath) return path; + + const existingTargets = path.points.connections.targets || []; + + const alreadyConnectedFromStatic = existingTargets.some((target) => { + const targetPath = simulationStates.find( + (p) => p.modeluuid === target.modelUUID + ); + return targetPath?.type === "StaticMachine"; + }); + + if (fromPath.type === "StaticMachine") { + if (alreadyConnectedFromStatic) { + console.log( + "ArmBot can only be connected from one StaticMachine" + ); + return path; + } + } + + if ( + !existingTargets.some( + (target) => + target.modelUUID === reverseTarget.modelUUID && + target.pointUUID === reverseTarget.pointUUID + ) + ) { + return { + ...path, + points: { + ...path.points, + connections: { + ...path.points.connections, + targets: [...existingTargets, reverseTarget], + }, + }, + }; + } + } + return path; + } + + return path; + }); + + setSimulationStates(updatedPaths); + + const updatedPathDetails = updatedPaths.filter( + (path) => + path.modeluuid === fromModelUUID || path.modeluuid === toModelUUID + ); + + updateBackend(updatedPathDetails); + }; + + const updateBackend = async ( + updatedPaths: ( + | Types.ConveyorEventsSchema + | Types.VehicleEventsSchema + | Types.StaticMachineEventsSchema + | Types.ArmBotEventsSchema + )[] + ) => { + if (updatedPaths.length === 0) return; + const email = localStorage.getItem("email"); + const organization = email ? email.split("@")[1].split(".")[0] : ""; + + updatedPaths.forEach(async (updatedPath) => { + if (updatedPath.type === "Conveyor") { + // await setEventApi( + // organization, + // updatedPath.modeluuid, + // { type: "Conveyor", points: updatedPath.points, speed: updatedPath.speed } + // ); + + const data = { + organization: organization, + modeluuid: updatedPath.modeluuid, + eventData: { + type: "Conveyor", + points: updatedPath.points, + speed: updatedPath.speed, + }, + }; + + socket.emit("v2:model-asset:updateEventData", data); + } else if (updatedPath.type === "Vehicle") { + // await setEventApi( + // organization, + // updatedPath.modeluuid, + // { type: "Vehicle", points: updatedPath.points } + // ); + + const data = { + organization: organization, + modeluuid: updatedPath.modeluuid, + eventData: { type: "Vehicle", points: updatedPath.points }, + }; + + socket.emit("v2:model-asset:updateEventData", data); + } else if (updatedPath.type === "StaticMachine") { + // await setEventApi( + // organization, + // updatedPath.modeluuid, + // { type: "StaticMachine", points: updatedPath.points } + // ); + + const data = { + organization: organization, + modeluuid: updatedPath.modeluuid, + eventData: { type: "StaticMachine", points: updatedPath.points }, + }; + + socket.emit("v2:model-asset:updateEventData", data); + } else if (updatedPath.type === "ArmBot") { + // await setEventApi( + // organization, + // updatedPath.modeluuid, + // { type: "ArmBot", points: updatedPath.points } + // ); + + const data = { + organization: organization, + modeluuid: updatedPath.modeluuid, + eventData: { type: "ArmBot", points: updatedPath.points }, + }; + + socket.emit("v2:model-asset:updateEventData", data); + } + }); + }; + + const handleAddConnection = ( + fromModelUUID: string, + fromUUID: string, + toModelUUID: string, + toUUID: string + ) => { + updatePathConnections(fromModelUUID, fromUUID, toModelUUID, toUUID); + setFirstSelected(null); + setCurrentLine(null); + setIsConnecting(false); + }; + + useEffect(() => { + const canvasElement = gl.domElement; + let drag = false; + let MouseDown = false; + + const onMouseDown = () => { + MouseDown = true; + drag = false; }; - const updateBackend = async (updatedPaths: (Types.ConveyorEventsSchema | Types.VehicleEventsSchema | Types.StaticMachineEventsSchema | Types.ArmBotEventsSchema)[]) => { - if (updatedPaths.length === 0) return; - const email = localStorage.getItem("email"); - const organization = email ? email.split("@")[1].split(".")[0] : ""; + const onMouseUp = () => { + MouseDown = false; + }; - updatedPaths.forEach(async (updatedPath) => { - if (updatedPath.type === 'Conveyor') { + const onMouseMove = () => { + if (MouseDown) { + drag = true; + } + }; - // await setEventApi( - // organization, - // updatedPath.modeluuid, - // { type: "Conveyor", points: updatedPath.points, speed: updatedPath.speed } - // ); + const onContextMenu = (evt: MouseEvent) => { + evt.preventDefault(); + if (drag || evt.button === 0) return; - const data = { - organization: organization, - modeluuid: updatedPath.modeluuid, - eventData: { type: "Conveyor", points: updatedPath.points, speed: updatedPath.speed } - } + raycaster.setFromCamera(pointer, camera); + const intersects = raycaster.intersectObjects( + pathsGroupRef.current.children, + true + ); - socket.emit('v2:model-asset:updateEventData', data); + if (intersects.length > 0) { + const intersected = intersects[0].object; - } else if (updatedPath.type === 'Vehicle') { + if (intersected.name.includes("events-sphere")) { + const modelUUID = intersected.userData.path.modeluuid; + const sphereUUID = intersected.uuid; + const worldPosition = new THREE.Vector3(); + intersected.getWorldPosition(worldPosition); - // await setEventApi( - // organization, - // updatedPath.modeluuid, - // { type: "Vehicle", points: updatedPath.points } - // ); + let isStartOrEnd = false; - const data = { - organization: organization, - modeluuid: updatedPath.modeluuid, - eventData: { type: "Vehicle", points: updatedPath.points } - } + if ( + intersected.userData.path.points && + intersected.userData.path.points.length > 1 + ) { + isStartOrEnd = + intersected.userData.path.points.length > 0 && + (sphereUUID === intersected.userData.path.points[0].uuid || + sphereUUID === + intersected.userData.path.points[ + intersected.userData.path.points.length - 1 + ].uuid); + } else if (intersected.userData.path.points) { + isStartOrEnd = sphereUUID === intersected.userData.path.points.uuid; + } - socket.emit('v2:model-asset:updateEventData', data); - - } else if (updatedPath.type === 'StaticMachine') { - - // await setEventApi( - // organization, - // updatedPath.modeluuid, - // { type: "StaticMachine", points: updatedPath.points } - // ); - - const data = { - organization: organization, - modeluuid: updatedPath.modeluuid, - eventData: { type: "StaticMachine", points: updatedPath.points } - } - - socket.emit('v2:model-asset:updateEventData', data); - - } else if (updatedPath.type === 'ArmBot') { - - // await setEventApi( - // organization, - // updatedPath.modeluuid, - // { type: "ArmBot", points: updatedPath.points } - // ); - - const data = { - organization: organization, - modeluuid: updatedPath.modeluuid, - eventData: { type: "ArmBot", points: updatedPath.points } - } - - socket.emit('v2:model-asset:updateEventData', data); + if (modelUUID) { + const firstPath = simulationStates.find( + (p) => p.modeluuid === firstSelected?.modelUUID + ); + const secondPath = simulationStates.find( + (p) => p.modeluuid === modelUUID + ); + // Prevent vehicle-to-vehicle connections + if ( + firstPath && + secondPath && + firstPath.type === "Vehicle" && + secondPath.type === "Vehicle" + ) { + console.log("Cannot connect two vehicle paths together"); + return; } - }) - } + // Prevent conveyor middle point to conveyor connections + if ( + firstPath && + secondPath && + firstPath.type === "Conveyor" && + secondPath.type === "Conveyor" && + (!firstSelected?.isCorner || !isStartOrEnd) + ) { + console.log( + "Conveyor connections must be between start/end points" + ); + return; + } - const handleAddConnection = (fromModelUUID: string, fromUUID: string, toModelUUID: string, toUUID: string) => { - updatePathConnections(fromModelUUID, fromUUID, toModelUUID, toUUID); + // Check if this specific connection already exists + const isDuplicateConnection = firstSelected + ? simulationStates.some((path) => { + if (path.modeluuid === firstSelected.modelUUID) { + if (path.type === "Conveyor") { + const point = path.points.find( + (p) => p.uuid === firstSelected.sphereUUID + ); + return point?.connections.targets.some( + (t) => + t.modelUUID === modelUUID && + t.pointUUID === sphereUUID + ); + } else if (path.type === "Vehicle") { + return path.points.connections.targets.some( + (t) => + t.modelUUID === modelUUID && + t.pointUUID === sphereUUID + ); + } + } + return false; + }) + : false; + + if (isDuplicateConnection) { + console.log("These points are already connected. Ignoring."); + return; + } + + // For Vehicles, check if they're already connected to anything + if (intersected.userData.path.type === "Vehicle") { + const vehicleConnections = + intersected.userData.path.points.connections.targets.length; + if (vehicleConnections >= 1) { + console.log("Vehicle can only have one connection"); + return; + } + } + + // For non-Vehicle paths, check if already connected + if (intersected.userData.path.type !== "Vehicle") { + const isAlreadyConnected = simulationStates.some((path) => { + if (path.type === "Conveyor") { + return path.points.some( + (point) => + point.uuid === sphereUUID && + point.connections.targets.length > 0 + ); + } + return false; + }); + + if (isAlreadyConnected) { + console.log("Conveyor point is already connected. Ignoring."); + return; + } + } + + if (firstSelected) { + // Check if trying to connect Vehicle to non-Conveyor + if ( + (firstPath?.type === "Vehicle" && + secondPath?.type !== "Conveyor") || + (secondPath?.type === "Vehicle" && + firstPath?.type !== "Conveyor") + ) { + console.log("Vehicle can only connect to Conveyors"); + return; + } + + // Prevent same-path connections + if (firstSelected.modelUUID === modelUUID) { + console.log("Cannot connect spheres on the same path."); + return; + } + + // Check if StaticMachine is involved in the connection + if ( + (firstPath?.type === "StaticMachine" && + secondPath?.type !== "ArmBot") || + (secondPath?.type === "StaticMachine" && + firstPath?.type !== "ArmBot") + ) { + console.log("StaticMachine can only connect to ArmBot"); + return; + } + + // Check if StaticMachine already has a connection + if (firstPath?.type === "StaticMachine") { + const staticConnections = + firstPath.points.connections.targets.length; + if (staticConnections >= 1) { + console.log("StaticMachine can only have one connection"); + return; + } + } + if (secondPath?.type === "StaticMachine") { + const staticConnections = + secondPath.points.connections.targets.length; + if (staticConnections >= 1) { + console.log("StaticMachine can only have one connection"); + return; + } + } + + // Check if ArmBot is involved + if ( + (firstPath?.type === "ArmBot" && + secondPath?.type === "StaticMachine") || + (secondPath?.type === "ArmBot" && + firstPath?.type === "StaticMachine") + ) { + const armBotPath = + firstPath?.type === "ArmBot" ? firstPath : secondPath; + const staticPath = + firstPath?.type === "StaticMachine" ? firstPath : secondPath; + + const armBotConnections = + armBotPath.points.connections.targets || []; + const alreadyConnectedToStatic = armBotConnections.some( + (target) => { + const targetPath = simulationStates.find( + (p) => p.modeluuid === target.modelUUID + ); + return targetPath?.type === "StaticMachine"; + } + ); + + if (alreadyConnectedToStatic) { + console.log("ArmBot can only connect to one StaticMachine"); + return; + } + + const staticConnections = + staticPath.points.connections.targets.length; + if (staticConnections >= 1) { + console.log("StaticMachine can only have one connection"); + return; + } + } + + // Prevent ArmBot ↔ ArmBot + if ( + firstPath?.type === "ArmBot" && + secondPath?.type === "ArmBot" + ) { + console.log("Cannot connect two ArmBots together"); + return; + } + + // If one is ArmBot, ensure the other is StaticMachine or Conveyor + if ( + firstPath?.type === "ArmBot" || + secondPath?.type === "ArmBot" + ) { + const otherType = + firstPath?.type === "ArmBot" + ? secondPath?.type + : firstPath?.type; + if (otherType !== "StaticMachine" && otherType !== "Conveyor") { + console.log( + "ArmBot can only connect to Conveyors or one StaticMachine" + ); + return; + } + } + + // At least one must be start/end point + if (!firstSelected.isCorner && !isStartOrEnd) { + console.log( + "At least one of the selected spheres must be a start or end point." + ); + return; + } + + // All checks passed - make the connection + handleAddConnection( + firstSelected.modelUUID, + firstSelected.sphereUUID, + modelUUID, + sphereUUID + ); + } else { + // First selection - just store it + setFirstSelected({ + modelUUID, + sphereUUID, + position: worldPosition, + isCorner: isStartOrEnd, + }); + setIsConnecting(true); + } + } + } + } else { + // Clicked outside - cancel connection setFirstSelected(null); setCurrentLine(null); setIsConnecting(false); + } }; - useEffect(() => { - const canvasElement = gl.domElement; - let drag = false; - let MouseDown = false; + if (activeModule === "simulation" && !deleteTool) { + canvasElement.addEventListener("mousedown", onMouseDown); + canvasElement.addEventListener("mouseup", onMouseUp); + canvasElement.addEventListener("mousemove", onMouseMove); + canvasElement.addEventListener("contextmenu", onContextMenu); + } else { + setFirstSelected(null); + setCurrentLine(null); + setIsConnecting(false); + } - const onMouseDown = () => { - MouseDown = true; - drag = false; - }; + return () => { + canvasElement.removeEventListener("mousedown", onMouseDown); + canvasElement.removeEventListener("mouseup", onMouseUp); + canvasElement.removeEventListener("mousemove", onMouseMove); + canvasElement.removeEventListener("contextmenu", onContextMenu); + }; + }, [camera, scene, raycaster, firstSelected, simulationStates, deleteTool]); - const onMouseUp = () => { - MouseDown = false; - }; - - const onMouseMove = () => { - if (MouseDown) { - drag = true; - } - }; - - const onContextMenu = (evt: MouseEvent) => { - evt.preventDefault(); - if (drag || evt.button === 0) return; - - raycaster.setFromCamera(pointer, camera); - const intersects = raycaster.intersectObjects(pathsGroupRef.current.children, true); - - if (intersects.length > 0) { - const intersected = intersects[0].object; - - if (intersected.name.includes("events-sphere")) { - const modelUUID = intersected.userData.path.modeluuid; - const sphereUUID = intersected.uuid; - const worldPosition = new THREE.Vector3(); - intersected.getWorldPosition(worldPosition); - - let isStartOrEnd = false; - - if (intersected.userData.path.points && intersected.userData.path.points.length > 1) { - isStartOrEnd = intersected.userData.path.points.length > 0 && ( - sphereUUID === intersected.userData.path.points[0].uuid || - sphereUUID === intersected.userData.path.points[intersected.userData.path.points.length - 1].uuid - ); - } else if (intersected.userData.path.points) { - isStartOrEnd = sphereUUID === intersected.userData.path.points.uuid; - } - - if (modelUUID) { - const firstPath = simulationStates.find(p => p.modeluuid === firstSelected?.modelUUID); - const secondPath = simulationStates.find(p => p.modeluuid === modelUUID); - - // Prevent vehicle-to-vehicle connections - if (firstPath && secondPath && firstPath.type === 'Vehicle' && secondPath.type === 'Vehicle') { - console.log("Cannot connect two vehicle paths together"); - return; - } - - // Prevent conveyor middle point to conveyor connections - if (firstPath && secondPath && - firstPath.type === 'Conveyor' && - secondPath.type === 'Conveyor' && - (!firstSelected?.isCorner || !isStartOrEnd)) { - console.log("Conveyor connections must be between start/end points"); - return; - } - - // Check if this specific connection already exists - const isDuplicateConnection = firstSelected - ? simulationStates.some(path => { - if (path.modeluuid === firstSelected.modelUUID) { - if (path.type === 'Conveyor') { - const point = path.points.find(p => p.uuid === firstSelected.sphereUUID); - return point?.connections.targets.some(t => - t.modelUUID === modelUUID && t.pointUUID === sphereUUID - ); - } else if (path.type === 'Vehicle') { - return path.points.connections.targets.some(t => - t.modelUUID === modelUUID && t.pointUUID === sphereUUID - ); - } - } - return false; - }) - : false; - - if (isDuplicateConnection) { - console.log("These points are already connected. Ignoring."); - return; - } - - // For Vehicles, check if they're already connected to anything - if (intersected.userData.path.type === 'Vehicle') { - const vehicleConnections = intersected.userData.path.points.connections.targets.length; - if (vehicleConnections >= 1) { - console.log("Vehicle can only have one connection"); - return; - } - } - - // For non-Vehicle paths, check if already connected - if (intersected.userData.path.type !== 'Vehicle') { - const isAlreadyConnected = simulationStates.some(path => { - if (path.type === 'Conveyor') { - return path.points.some(point => - point.uuid === sphereUUID && - point.connections.targets.length > 0 - ); - } - return false; - }); - - if (isAlreadyConnected) { - console.log("Conveyor point is already connected. Ignoring."); - return; - } - } - - if (firstSelected) { - // Check if trying to connect Vehicle to non-Conveyor - if ((firstPath?.type === 'Vehicle' && secondPath?.type !== 'Conveyor') || - (secondPath?.type === 'Vehicle' && firstPath?.type !== 'Conveyor')) { - console.log("Vehicle can only connect to Conveyors"); - return; - } - - // Prevent same-path connections - if (firstSelected.modelUUID === modelUUID) { - console.log("Cannot connect spheres on the same path."); - return; - } - - // Check if StaticMachine is involved in the connection - if ((firstPath?.type === 'StaticMachine' && secondPath?.type !== 'ArmBot') || - (secondPath?.type === 'StaticMachine' && firstPath?.type !== 'ArmBot')) { - console.log("StaticMachine can only connect to ArmBot"); - return; - } - - // Check if StaticMachine already has a connection - if (firstPath?.type === 'StaticMachine') { - const staticConnections = firstPath.points.connections.targets.length; - if (staticConnections >= 1) { - console.log("StaticMachine can only have one connection"); - return; - } - } - if (secondPath?.type === 'StaticMachine') { - const staticConnections = secondPath.points.connections.targets.length; - if (staticConnections >= 1) { - console.log("StaticMachine can only have one connection"); - return; - } - } - - // Check if ArmBot is involved - if ((firstPath?.type === 'ArmBot' && secondPath?.type === 'StaticMachine') || - (secondPath?.type === 'ArmBot' && firstPath?.type === 'StaticMachine')) { - - const armBotPath = firstPath?.type === 'ArmBot' ? firstPath : secondPath; - const staticPath = firstPath?.type === 'StaticMachine' ? firstPath : secondPath; - - const armBotConnections = armBotPath.points.connections.targets || []; - const alreadyConnectedToStatic = armBotConnections.some(target => { - const targetPath = simulationStates.find(p => p.modeluuid === target.modelUUID); - return targetPath?.type === 'StaticMachine'; - }); - - if (alreadyConnectedToStatic) { - console.log("ArmBot can only connect to one StaticMachine"); - return; - } - - const staticConnections = staticPath.points.connections.targets.length; - if (staticConnections >= 1) { - console.log("StaticMachine can only have one connection"); - return; - } - } - - // Prevent ArmBot ↔ ArmBot - if (firstPath?.type === 'ArmBot' && secondPath?.type === 'ArmBot') { - console.log("Cannot connect two ArmBots together"); - return; - } - - // If one is ArmBot, ensure the other is StaticMachine or Conveyor - if (firstPath?.type === 'ArmBot' || secondPath?.type === 'ArmBot') { - const otherType = firstPath?.type === 'ArmBot' ? secondPath?.type : firstPath?.type; - if (otherType !== 'StaticMachine' && otherType !== 'Conveyor') { - console.log("ArmBot can only connect to Conveyors or one StaticMachine"); - return; - } - } - - // At least one must be start/end point - if (!firstSelected.isCorner && !isStartOrEnd) { - console.log("At least one of the selected spheres must be a start or end point."); - return; - } - - // All checks passed - make the connection - handleAddConnection(firstSelected.modelUUID, firstSelected.sphereUUID, modelUUID, sphereUUID); - } else { - // First selection - just store it - setFirstSelected({ modelUUID, sphereUUID, position: worldPosition, isCorner: isStartOrEnd }); - setIsConnecting(true); - } - } - } - } else { - // Clicked outside - cancel connection - setFirstSelected(null); - setCurrentLine(null); - setIsConnecting(false); - } - }; - - if (activeModule === 'simulation' && !deleteTool) { - canvasElement.addEventListener("mousedown", onMouseDown); - canvasElement.addEventListener("mouseup", onMouseUp); - canvasElement.addEventListener("mousemove", onMouseMove); - canvasElement.addEventListener("contextmenu", onContextMenu); - } else { - setFirstSelected(null); - setCurrentLine(null); - setIsConnecting(false); - } - - return () => { - canvasElement.removeEventListener("mousedown", onMouseDown); - canvasElement.removeEventListener("mouseup", onMouseUp); - canvasElement.removeEventListener("mousemove", onMouseMove); - canvasElement.removeEventListener("contextmenu", onContextMenu); - }; - }, [camera, scene, raycaster, firstSelected, simulationStates, deleteTool]); - - useFrame(() => { - Object.values(groupRefs.current).forEach((group) => { - if (group) { - const distance = new THREE.Vector3(...group.position.toArray()).distanceTo(camera.position); - group.visible = ((distance <= renderDistance) && !isPlaying); - } - }); + useFrame(() => { + Object.values(groupRefs.current).forEach((group) => { + if (group) { + const distance = new THREE.Vector3( + ...group.position.toArray() + ).distanceTo(camera.position); + group.visible = distance <= renderDistance && !isPlaying; + } }); + }); - useFrame(() => { - if (firstSelected) { - raycaster.setFromCamera(pointer, camera); - const intersects = raycaster.intersectObjects(scene.children, true).filter((intersect) => - !intersect.object.name.includes("Roof") && - !intersect.object.name.includes("agv-collider") && - !intersect.object.name.includes("MeasurementReference") && - !intersect.object.userData.isPathObject && - !(intersect.object.type === "GridHelper") - ); - - let point: THREE.Vector3 | null = null; - let snappedSphere: { sphereUUID: string, position: THREE.Vector3, modelUUID: string, isCorner: boolean } | null = null; - let isInvalidConnection = false; - - if (intersects.length > 0) { - point = intersects[0].point; - if (point.y < 0.05) { - point = new THREE.Vector3(point.x, 0.05, point.z); - } - } - - const sphereIntersects = raycaster.intersectObjects(pathsGroupRef.current.children, true).filter((obj) => obj.object.name.includes("events-sphere")); - - if (sphereIntersects.length > 0) { - const sphere = sphereIntersects[0].object; - const sphereUUID = sphere.uuid; - const spherePosition = new THREE.Vector3(); - sphere.getWorldPosition(spherePosition); - const pathData = sphere.userData.path; - const modelUUID = pathData.modeluuid; - - const firstPath = simulationStates.find(p => p.modeluuid === firstSelected.modelUUID); - const secondPath = simulationStates.find(p => p.modeluuid === modelUUID); - const isVehicleToVehicle = firstPath?.type === 'Vehicle' && secondPath?.type === 'Vehicle'; - - // Inside the useFrame hook, where we check for snapped spheres: - const isConnectable = ( - pathData.type === 'Vehicle' || - pathData.type === 'ArmBot' || - (pathData.points.length > 0 && ( - sphereUUID === pathData.points[0].uuid || - sphereUUID === pathData.points[pathData.points.length - 1].uuid || - (pathData.type === 'Conveyor' && firstPath?.type === 'ArmBot') // Allow ArmBot to connect to middle points - )) - ) && - !isVehicleToVehicle && - !( - firstPath?.type === 'Conveyor' && - pathData.type === 'Conveyor' && - !firstSelected.isCorner - ); - - // Check for duplicate connection (regardless of path type) - const isDuplicateConnection = simulationStates.some(path => { - if (path.modeluuid === firstSelected.modelUUID) { - if (path.type === 'Conveyor') { - const point = path.points.find(p => p.uuid === firstSelected.sphereUUID); - return point?.connections.targets.some(t => - t.modelUUID === modelUUID && t.pointUUID === sphereUUID - ); - } else if (path.type === 'Vehicle') { - return path.points.connections.targets.some(t => - t.modelUUID === modelUUID && t.pointUUID === sphereUUID - ); - } - } - return false; - }); - - // For non-Vehicle paths, check if already connected - const isNonVehicleAlreadyConnected = pathData.type !== 'Vehicle' && - simulationStates.some(path => { - if (path.type === 'Conveyor') { - return path.points.some(point => - point.uuid === sphereUUID && - point.connections.targets.length > 0 - ); - } - return false; - }); - - // Check vehicle connection rules - const isVehicleAtMaxConnections = pathData.type === 'Vehicle' && - pathData.points.connections.targets.length >= 1; - const isVehicleConnectingToNonConveyor = - (firstPath?.type === 'Vehicle' && secondPath?.type !== 'Conveyor') || - (secondPath?.type === 'Vehicle' && firstPath?.type !== 'Conveyor'); - - // Check if StaticMachine is connecting to non-ArmBot - const isStaticMachineToNonArmBot = - (firstPath?.type === 'StaticMachine' && secondPath?.type !== 'ArmBot') || - (secondPath?.type === 'StaticMachine' && firstPath?.type !== 'ArmBot'); - - // Check if StaticMachine already has a connection - const isStaticMachineAtMaxConnections = - (firstPath?.type === 'StaticMachine' && firstPath.points.connections.targets.length >= 1) || - (secondPath?.type === 'StaticMachine' && secondPath.points.connections.targets.length >= 1); - - // Check if ArmBot is connecting to StaticMachine - const isArmBotToStaticMachine = - (firstPath?.type === 'ArmBot' && secondPath?.type === 'StaticMachine') || - (secondPath?.type === 'ArmBot' && firstPath?.type === 'StaticMachine'); - - // Prevent multiple StaticMachine connections to ArmBot - let isArmBotAlreadyConnectedToStatic = false; - if (isArmBotToStaticMachine) { - const armBotPath = firstPath?.type === 'ArmBot' ? firstPath : secondPath; - isArmBotAlreadyConnectedToStatic = armBotPath.points.connections.targets.some(target => { - const targetPath = simulationStates.find(p => p.modeluuid === target.modelUUID); - return targetPath?.type === 'StaticMachine'; - }); - } - - // Prevent ArmBot to ArmBot - const isArmBotToArmBot = firstPath?.type === 'ArmBot' && secondPath?.type === 'ArmBot'; - - // If ArmBot is involved, other must be Conveyor or StaticMachine - const isArmBotToInvalidType = (firstPath?.type === 'ArmBot' || secondPath?.type === 'ArmBot') && - !(firstPath?.type === 'Conveyor' || firstPath?.type === 'StaticMachine' || - secondPath?.type === 'Conveyor' || secondPath?.type === 'StaticMachine'); - - if ( - !isDuplicateConnection && - !isVehicleToVehicle && - !isNonVehicleAlreadyConnected && - !isVehicleAtMaxConnections && - !isVehicleConnectingToNonConveyor && - !isStaticMachineToNonArmBot && - !isStaticMachineAtMaxConnections && - !isArmBotToArmBot && - !isArmBotToInvalidType && - !isArmBotAlreadyConnectedToStatic && - firstSelected.sphereUUID !== sphereUUID && - firstSelected.modelUUID !== modelUUID && - (firstSelected.isCorner || isConnectable) && - !(firstPath?.type === 'Conveyor' && - pathData.type === 'Conveyor' && - !(firstSelected.isCorner && isConnectable)) - ) { - snappedSphere = { - sphereUUID, - position: spherePosition, - modelUUID, - isCorner: isConnectable - }; - } else { - isInvalidConnection = true; - } - - } - - if (snappedSphere) { - point = snappedSphere.position; - } - - if (point) { - const distance = firstSelected.position.distanceTo(point); - const heightFactor = Math.max(0.5, distance * 0.2); - const midPoint = new THREE.Vector3( - (firstSelected.position.x + point.x) / 2, - Math.max(firstSelected.position.y, point.y) + heightFactor, - (firstSelected.position.z + point.z) / 2 - ); - - setCurrentLine({ - start: firstSelected.position, - end: point, - mid: midPoint, - }); - - if (sphereIntersects.length > 0) { - setHelperLineColor(isInvalidConnection ? 'red' : '#6cf542'); - } else { - setHelperLineColor('yellow'); - } - } else { - setCurrentLine(null); - setIsConnecting(false); - } - } else { - setCurrentLine(null); - setIsConnecting(false); - } - }); - - const removeConnections = (connection1: { model: string; point: string }, connection2: { model: string; point: string }) => { - const updatedStates = simulationStates.map(state => { - // Handle Conveyor (which has multiple points) - if (state.type === 'Conveyor') { - const updatedConveyor: Types.ConveyorEventsSchema = { - ...state, - points: state.points.map(point => { - // Check if this point is either connection1 or connection2 - if ((state.modeluuid === connection1.model && point.uuid === connection1.point) || - (state.modeluuid === connection2.model && point.uuid === connection2.point)) { - - return { - ...point, - connections: { - ...point.connections, - targets: point.connections.targets.filter(target => { - // Remove the target that matches the other connection - return !( - (target.modelUUID === connection1.model && target.pointUUID === connection1.point) || - (target.modelUUID === connection2.model && target.pointUUID === connection2.point) - ); - }) - } - }; - } - return point; - }) - }; - return updatedConveyor; - } - // Handle Vehicle - else if (state.type === 'Vehicle') { - if ((state.modeluuid === connection1.model && state.points.uuid === connection1.point) || - (state.modeluuid === connection2.model && state.points.uuid === connection2.point)) { - - const updatedVehicle: Types.VehicleEventsSchema = { - ...state, - points: { - ...state.points, - connections: { - ...state.points.connections, - targets: state.points.connections.targets.filter(target => { - return !( - (target.modelUUID === connection1.model && target.pointUUID === connection1.point) || - (target.modelUUID === connection2.model && target.pointUUID === connection2.point) - ); - }) - }, - // Ensure all required Vehicle point properties are included - speed: state.points.speed, - actions: state.points.actions - } - }; - return updatedVehicle; - } - } - // Handle StaticMachine - else if (state.type === 'StaticMachine') { - if ((state.modeluuid === connection1.model && state.points.uuid === connection1.point) || - (state.modeluuid === connection2.model && state.points.uuid === connection2.point)) { - - const updatedStaticMachine: Types.StaticMachineEventsSchema = { - ...state, - points: { - ...state.points, - connections: { - ...state.points.connections, - targets: state.points.connections.targets.filter(target => { - return !( - (target.modelUUID === connection1.model && target.pointUUID === connection1.point) || - (target.modelUUID === connection2.model && target.pointUUID === connection2.point) - ); - }) - }, - // Ensure all required StaticMachine point properties are included - actions: state.points.actions, - triggers: state.points.triggers - } - }; - return updatedStaticMachine; - } - } - // Handle ArmBot - else if (state.type === 'ArmBot') { - if ((state.modeluuid === connection1.model && state.points.uuid === connection1.point) || - (state.modeluuid === connection2.model && state.points.uuid === connection2.point)) { - - const updatedArmBot: Types.ArmBotEventsSchema = { - ...state, - points: { - ...state.points, - connections: { - ...state.points.connections, - targets: state.points.connections.targets.filter(target => { - return !( - (target.modelUUID === connection1.model && target.pointUUID === connection1.point) || - (target.modelUUID === connection2.model && target.pointUUID === connection2.point) - ); - }) - }, - actions: { - ...state.points.actions, - processes: state.points.actions.processes?.filter(process => { - return !( - process.startPoint === connection1.point || - process.endPoint === connection1.point || - process.startPoint === connection2.point || - process.endPoint === connection2.point - ); - }) || [] - }, - triggers: state.points.triggers - } - }; - return updatedArmBot; - } - } - return state; - }); - - const updatedPaths = updatedStates.filter(state => - state.modeluuid === connection1.model || state.modeluuid === connection2.model + useFrame(() => { + if (firstSelected) { + raycaster.setFromCamera(pointer, camera); + const intersects = raycaster + .intersectObjects(scene.children, true) + .filter( + (intersect) => + !intersect.object.name.includes("Roof") && + !intersect.object.name.includes("agv-collider") && + !intersect.object.name.includes("MeasurementReference") && + !intersect.object.userData.isPathObject && + !(intersect.object.type === "GridHelper") ); - updateBackend(updatedPaths); + let point: THREE.Vector3 | null = null; + let snappedSphere: { + sphereUUID: string; + position: THREE.Vector3; + modelUUID: string; + isCorner: boolean; + } | null = null; + let isInvalidConnection = false; - setSimulationStates(updatedStates); - }; + if (intersects.length > 0) { + point = intersects[0].point; + if (point.y < 0.05) { + point = new THREE.Vector3(point.x, 0.05, point.z); + } + } + const sphereIntersects = raycaster + .intersectObjects(pathsGroupRef.current.children, true) + .filter((obj) => obj.object.name.includes("events-sphere")); - return ( - <group name='simulationConnectionGroup' visible={!isPlaying}> - {simulationStates.flatMap(path => { - if (path.type === 'Conveyor') { - return path.points.flatMap(point => - point.connections.targets.map((target, index) => { - const targetPath = simulationStates.find(p => p.modeluuid === target.modelUUID); - if (targetPath?.type !== 'Conveyor' && targetPath?.type !== 'ArmBot') return null; + if (sphereIntersects.length > 0) { + const sphere = sphereIntersects[0].object; + const sphereUUID = sphere.uuid; + const spherePosition = new THREE.Vector3(); + sphere.getWorldPosition(spherePosition); + const pathData = sphere.userData.path; + const modelUUID = pathData.modeluuid; - const fromSphere = pathsGroupRef.current?.getObjectByProperty('uuid', point.uuid); - const toSphere = pathsGroupRef.current?.getObjectByProperty('uuid', target.pointUUID); + const firstPath = simulationStates.find( + (p) => p.modeluuid === firstSelected.modelUUID + ); + const secondPath = simulationStates.find( + (p) => p.modeluuid === modelUUID + ); + const isVehicleToVehicle = + firstPath?.type === "Vehicle" && secondPath?.type === "Vehicle"; - if (fromSphere && toSphere) { - const fromWorldPosition = new THREE.Vector3(); - const toWorldPosition = new THREE.Vector3(); - fromSphere.getWorldPosition(fromWorldPosition); - toSphere.getWorldPosition(toWorldPosition); + // Inside the useFrame hook, where we check for snapped spheres: + const isConnectable = + (pathData.type === "Vehicle" || + pathData.type === "ArmBot" || + (pathData.points.length > 0 && + (sphereUUID === pathData.points[0].uuid || + sphereUUID === + pathData.points[pathData.points.length - 1].uuid || + (pathData.type === "Conveyor" && + firstPath?.type === "ArmBot")))) && // Allow ArmBot to connect to middle points + !isVehicleToVehicle && + !( + firstPath?.type === "Conveyor" && + pathData.type === "Conveyor" && + !firstSelected.isCorner + ); - const distance = fromWorldPosition.distanceTo(toWorldPosition); - const heightFactor = Math.max(0.5, distance * 0.2); - const midPoint = new THREE.Vector3((fromWorldPosition.x + toWorldPosition.x) / 2, Math.max(fromWorldPosition.y, toWorldPosition.y) + heightFactor, (fromWorldPosition.z + toWorldPosition.z) / 2); + // Check for duplicate connection (regardless of path type) + const isDuplicateConnection = simulationStates.some((path) => { + if (path.modeluuid === firstSelected.modelUUID) { + if (path.type === "Conveyor") { + const point = path.points.find( + (p) => p.uuid === firstSelected.sphereUUID + ); + return point?.connections.targets.some( + (t) => t.modelUUID === modelUUID && t.pointUUID === sphereUUID + ); + } else if (path.type === "Vehicle") { + return path.points.connections.targets.some( + (t) => t.modelUUID === modelUUID && t.pointUUID === sphereUUID + ); + } + } + return false; + }); - return ( - <QuadraticBezierLine - key={`${point.uuid}-${target.pointUUID}-${index}`} - ref={(el) => (groupRefs.current[`${point.uuid}-${target.pointUUID}-${index}`] = el!)} - start={fromWorldPosition.toArray()} - end={toWorldPosition.toArray()} - mid={midPoint.toArray()} - color={ - deleteTool && hoveredLineKey === `${point.uuid}-${target.pointUUID}-${index}` - ? 'red' - : targetPath?.type === 'ArmBot' - ? '#42a5f5' - : 'white' - } - lineWidth={4} - dashed={(deleteTool && hoveredLineKey === `${point.uuid}-${target.pointUUID}-${index}`) ? false : true} - dashSize={0.75} - dashScale={20} - onPointerOver={() => setHoveredLineKey(`${point.uuid}-${target.pointUUID}-${index}`)} - onPointerOut={() => setHoveredLineKey(null)} - onClick={() => { - if (deleteTool) { + // For non-Vehicle paths, check if already connected + const isNonVehicleAlreadyConnected = + pathData.type !== "Vehicle" && + simulationStates.some((path) => { + if (path.type === "Conveyor") { + return path.points.some( + (point) => + point.uuid === sphereUUID && + point.connections.targets.length > 0 + ); + } + return false; + }); - const connection1 = { model: path.modeluuid, point: point.uuid } - const connection2 = { model: target.modelUUID, point: target.pointUUID } + // Check vehicle connection rules + const isVehicleAtMaxConnections = + pathData.type === "Vehicle" && + pathData.points.connections.targets.length >= 1; + const isVehicleConnectingToNonConveyor = + (firstPath?.type === "Vehicle" && secondPath?.type !== "Conveyor") || + (secondPath?.type === "Vehicle" && firstPath?.type !== "Conveyor"); - removeConnections(connection1, connection2) + // Check if StaticMachine is connecting to non-ArmBot + const isStaticMachineToNonArmBot = + (firstPath?.type === "StaticMachine" && + secondPath?.type !== "ArmBot") || + (secondPath?.type === "StaticMachine" && + firstPath?.type !== "ArmBot"); - } - }} - userData={target} - /> - ); - } - return null; - }) + // Check if StaticMachine already has a connection + const isStaticMachineAtMaxConnections = + (firstPath?.type === "StaticMachine" && + firstPath.points.connections.targets.length >= 1) || + (secondPath?.type === "StaticMachine" && + secondPath.points.connections.targets.length >= 1); + + // Check if ArmBot is connecting to StaticMachine + const isArmBotToStaticMachine = + (firstPath?.type === "ArmBot" && + secondPath?.type === "StaticMachine") || + (secondPath?.type === "ArmBot" && + firstPath?.type === "StaticMachine"); + + // Prevent multiple StaticMachine connections to ArmBot + let isArmBotAlreadyConnectedToStatic = false; + if (isArmBotToStaticMachine) { + const armBotPath = + firstPath?.type === "ArmBot" ? firstPath : secondPath; + isArmBotAlreadyConnectedToStatic = + armBotPath.points.connections.targets.some((target) => { + const targetPath = simulationStates.find( + (p) => p.modeluuid === target.modelUUID + ); + return targetPath?.type === "StaticMachine"; + }); + } + + // Prevent ArmBot to ArmBot + const isArmBotToArmBot = + firstPath?.type === "ArmBot" && secondPath?.type === "ArmBot"; + + // If ArmBot is involved, other must be Conveyor or StaticMachine + const isArmBotToInvalidType = + (firstPath?.type === "ArmBot" || secondPath?.type === "ArmBot") && + !( + firstPath?.type === "Conveyor" || + firstPath?.type === "StaticMachine" || + secondPath?.type === "Conveyor" || + secondPath?.type === "StaticMachine" + ); + + if ( + !isDuplicateConnection && + !isVehicleToVehicle && + !isNonVehicleAlreadyConnected && + !isVehicleAtMaxConnections && + !isVehicleConnectingToNonConveyor && + !isStaticMachineToNonArmBot && + !isStaticMachineAtMaxConnections && + !isArmBotToArmBot && + !isArmBotToInvalidType && + !isArmBotAlreadyConnectedToStatic && + firstSelected.sphereUUID !== sphereUUID && + firstSelected.modelUUID !== modelUUID && + (firstSelected.isCorner || isConnectable) && + !( + firstPath?.type === "Conveyor" && + pathData.type === "Conveyor" && + !(firstSelected.isCorner && isConnectable) + ) + ) { + snappedSphere = { + sphereUUID, + position: spherePosition, + modelUUID, + isCorner: isConnectable, + }; + } else { + isInvalidConnection = true; + } + } + + if (snappedSphere) { + point = snappedSphere.position; + } + + if (point) { + const distance = firstSelected.position.distanceTo(point); + const heightFactor = Math.max(0.5, distance * 0.2); + const midPoint = new THREE.Vector3( + (firstSelected.position.x + point.x) / 2, + Math.max(firstSelected.position.y, point.y) + heightFactor, + (firstSelected.position.z + point.z) / 2 + ); + + setCurrentLine({ + start: firstSelected.position, + end: point, + mid: midPoint, + }); + + if (sphereIntersects.length > 0) { + setHelperLineColor(isInvalidConnection ? "red" : "#6cf542"); + } else { + setHelperLineColor("yellow"); + } + } else { + setCurrentLine(null); + setIsConnecting(false); + } + } else { + setCurrentLine(null); + setIsConnecting(false); + } + }); + + const removeConnections = ( + connection1: { model: string; point: string }, + connection2: { model: string; point: string } + ) => { + const updatedStates = simulationStates.map((state) => { + // Handle Conveyor (which has multiple points) + if (state.type === "Conveyor") { + const updatedConveyor: Types.ConveyorEventsSchema = { + ...state, + points: state.points.map((point) => { + // Check if this point is either connection1 or connection2 + if ( + (state.modeluuid === connection1.model && + point.uuid === connection1.point) || + (state.modeluuid === connection2.model && + point.uuid === connection2.point) + ) { + return { + ...point, + connections: { + ...point.connections, + targets: point.connections.targets.filter((target) => { + // Remove the target that matches the other connection + return !( + (target.modelUUID === connection1.model && + target.pointUUID === connection1.point) || + (target.modelUUID === connection2.model && + target.pointUUID === connection2.point) ); - } + }), + }, + }; + } + return point; + }), + }; + return updatedConveyor; + } + // Handle Vehicle + else if (state.type === "Vehicle") { + if ( + (state.modeluuid === connection1.model && + state.points.uuid === connection1.point) || + (state.modeluuid === connection2.model && + state.points.uuid === connection2.point) + ) { + const updatedVehicle: Types.VehicleEventsSchema = { + ...state, + points: { + ...state.points, + connections: { + ...state.points.connections, + targets: state.points.connections.targets.filter((target) => { + return !( + (target.modelUUID === connection1.model && + target.pointUUID === connection1.point) || + (target.modelUUID === connection2.model && + target.pointUUID === connection2.point) + ); + }), + }, + // Ensure all required Vehicle point properties are included + speed: state.points.speed, + actions: state.points.actions, + }, + }; + return updatedVehicle; + } + } + // Handle StaticMachine + else if (state.type === "StaticMachine") { + if ( + (state.modeluuid === connection1.model && + state.points.uuid === connection1.point) || + (state.modeluuid === connection2.model && + state.points.uuid === connection2.point) + ) { + const updatedStaticMachine: Types.StaticMachineEventsSchema = { + ...state, + points: { + ...state.points, + connections: { + ...state.points.connections, + targets: state.points.connections.targets.filter((target) => { + return !( + (target.modelUUID === connection1.model && + target.pointUUID === connection1.point) || + (target.modelUUID === connection2.model && + target.pointUUID === connection2.point) + ); + }), + }, + // Ensure all required StaticMachine point properties are included + actions: state.points.actions, + triggers: state.points.triggers, + }, + }; + return updatedStaticMachine; + } + } + // Handle ArmBot + else if (state.type === "ArmBot") { + if ( + (state.modeluuid === connection1.model && + state.points.uuid === connection1.point) || + (state.modeluuid === connection2.model && + state.points.uuid === connection2.point) + ) { + const updatedArmBot: Types.ArmBotEventsSchema = { + ...state, + points: { + ...state.points, + connections: { + ...state.points.connections, + targets: state.points.connections.targets.filter((target) => { + return !( + (target.modelUUID === connection1.model && + target.pointUUID === connection1.point) || + (target.modelUUID === connection2.model && + target.pointUUID === connection2.point) + ); + }), + }, + actions: { + ...state.points.actions, + processes: + state.points.actions.processes?.filter((process) => { + return !( + process.startPoint === connection1.point || + process.endPoint === connection1.point || + process.startPoint === connection2.point || + process.endPoint === connection2.point + ); + }) || [], + }, + triggers: state.points.triggers, + }, + }; + return updatedArmBot; + } + } + return state; + }); - if (path.type === 'Vehicle') { - return path.points.connections.targets.map((target, index) => { - const fromSphere = pathsGroupRef.current?.getObjectByProperty('uuid', path.points.uuid); - const toSphere = pathsGroupRef.current?.getObjectByProperty('uuid', target.pointUUID); - - if (fromSphere && toSphere) { - const fromWorldPosition = new THREE.Vector3(); - const toWorldPosition = new THREE.Vector3(); - fromSphere.getWorldPosition(fromWorldPosition); - toSphere.getWorldPosition(toWorldPosition); - - const distance = fromWorldPosition.distanceTo(toWorldPosition); - const heightFactor = Math.max(0.5, distance * 0.2); - const midPoint = new THREE.Vector3((fromWorldPosition.x + toWorldPosition.x) / 2, Math.max(fromWorldPosition.y, toWorldPosition.y) + heightFactor, (fromWorldPosition.z + toWorldPosition.z) / 2); - - return ( - <QuadraticBezierLine - key={`${path.points.uuid}-${target.pointUUID}-${index}`} - ref={(el) => (groupRefs.current[`${path.points.uuid}-${target.pointUUID}-${index}`] = el!)} - start={fromWorldPosition.toArray()} - end={toWorldPosition.toArray()} - mid={midPoint.toArray()} - color={ - deleteTool && hoveredLineKey === `${path.points.uuid}-${target.pointUUID}-${index}` - ? 'red' - : 'orange' - } - lineWidth={4} - dashed={(deleteTool && hoveredLineKey === `${path.points.uuid}-${target.pointUUID}-${index}`) ? false : true} - dashSize={0.75} - dashScale={20} - onPointerOver={() => setHoveredLineKey(`${path.points.uuid}-${target.pointUUID}-${index}`)} - onPointerOut={() => setHoveredLineKey(null)} - onClick={() => { - if (deleteTool) { - - const connection1 = { model: path.modeluuid, point: path.points.uuid } - const connection2 = { model: target.modelUUID, point: target.pointUUID } - - removeConnections(connection1, connection2) - } - }} - userData={target} - /> - ); - } - return null; - }); - } - - if (path.type === 'StaticMachine') { - return path.points.connections.targets.map((target, index) => { - const targetPath = simulationStates.find(p => p.modeluuid === target.modelUUID); - if (targetPath?.type !== 'ArmBot') return null; - - const fromSphere = pathsGroupRef.current?.getObjectByProperty('uuid', path.points.uuid); - const toSphere = pathsGroupRef.current?.getObjectByProperty('uuid', target.pointUUID); - - if (fromSphere && toSphere) { - const fromWorldPosition = new THREE.Vector3(); - const toWorldPosition = new THREE.Vector3(); - fromSphere.getWorldPosition(fromWorldPosition); - toSphere.getWorldPosition(toWorldPosition); - - const distance = fromWorldPosition.distanceTo(toWorldPosition); - const heightFactor = Math.max(0.5, distance * 0.2); - const midPoint = new THREE.Vector3((fromWorldPosition.x + toWorldPosition.x) / 2, Math.max(fromWorldPosition.y, toWorldPosition.y) + heightFactor, (fromWorldPosition.z + toWorldPosition.z) / 2); - - return ( - <QuadraticBezierLine - key={`${path.points.uuid}-${target.pointUUID}-${index}`} - ref={(el) => (groupRefs.current[`${path.points.uuid}-${target.pointUUID}-${index}`] = el!)} - start={fromWorldPosition.toArray()} - end={toWorldPosition.toArray()} - mid={midPoint.toArray()} - color={ - deleteTool && hoveredLineKey === `${path.points.uuid}-${target.pointUUID}-${index}` - ? 'red' - : '#42a5f5' - } - lineWidth={4} - dashed={(deleteTool && hoveredLineKey === `${path.points.uuid}-${target.pointUUID}-${index}`) ? false : true} - dashSize={0.75} - dashScale={20} - onPointerOver={() => setHoveredLineKey(`${path.points.uuid}-${target.pointUUID}-${index}`)} - onPointerOut={() => setHoveredLineKey(null)} - onClick={() => { - if (deleteTool) { - - const connection1 = { model: path.modeluuid, point: path.points.uuid } - const connection2 = { model: target.modelUUID, point: target.pointUUID } - - removeConnections(connection1, connection2) - - } - }} - userData={target} - /> - ); - } - return null; - }); - } - - return []; - })} - - {currentLine && ( - <QuadraticBezierLine - start={currentLine.start.toArray()} - end={currentLine.end.toArray()} - mid={currentLine.mid.toArray()} - color={helperlineColor} - lineWidth={4} - dashed - dashSize={1} - dashScale={20} - /> - )} - </group> + const updatedPaths = updatedStates.filter( + (state) => + state.modeluuid === connection1.model || + state.modeluuid === connection2.model ); + console.log("updatedPaths: ", updatedPaths); + updateBackend(updatedPaths); + + setSimulationStates(updatedStates); + }; + + return ( + <group name="simulationConnectionGroup" visible={!isPlaying}> + {simulationStates.flatMap((path) => { + if (path.type === "Conveyor") { + return path.points.flatMap((point) => + point.connections.targets.map((target, index) => { + const targetPath = simulationStates.find( + (p) => p.modeluuid === target.modelUUID + ); + if ( + targetPath?.type !== "Conveyor" && + targetPath?.type !== "ArmBot" + ) + return null; + + const fromSphere = pathsGroupRef.current?.getObjectByProperty( + "uuid", + point.uuid + ); + const toSphere = pathsGroupRef.current?.getObjectByProperty( + "uuid", + target.pointUUID + ); + + if (fromSphere && toSphere) { + const fromWorldPosition = new THREE.Vector3(); + const toWorldPosition = new THREE.Vector3(); + fromSphere.getWorldPosition(fromWorldPosition); + toSphere.getWorldPosition(toWorldPosition); + + const distance = fromWorldPosition.distanceTo(toWorldPosition); + const heightFactor = Math.max(0.5, distance * 0.2); + const midPoint = new THREE.Vector3( + (fromWorldPosition.x + toWorldPosition.x) / 2, + Math.max(fromWorldPosition.y, toWorldPosition.y) + + heightFactor, + (fromWorldPosition.z + toWorldPosition.z) / 2 + ); + + return ( + <QuadraticBezierLine + key={`${point.uuid}-${target.pointUUID}-${index}`} + ref={(el) => + (groupRefs.current[ + `${point.uuid}-${target.pointUUID}-${index}` + ] = el!) + } + start={fromWorldPosition.toArray()} + end={toWorldPosition.toArray()} + mid={midPoint.toArray()} + color={ + deleteTool && + hoveredLineKey === + `${point.uuid}-${target.pointUUID}-${index}` + ? "red" + : targetPath?.type === "ArmBot" + ? "#42a5f5" + : "white" + } + lineWidth={4} + dashed={ + deleteTool && + hoveredLineKey === + `${point.uuid}-${target.pointUUID}-${index}` + ? false + : true + } + dashSize={0.75} + dashScale={20} + onPointerOver={() => + setHoveredLineKey( + `${point.uuid}-${target.pointUUID}-${index}` + ) + } + onPointerOut={() => setHoveredLineKey(null)} + onClick={() => { + if (deleteTool) { + const connection1 = { + model: path.modeluuid, + point: point.uuid, + }; + const connection2 = { + model: target.modelUUID, + point: target.pointUUID, + }; + + removeConnections(connection1, connection2); + } + }} + userData={target} + /> + ); + } + return null; + }) + ); + } + + if (path.type === "Vehicle") { + return path.points.connections.targets.map((target, index) => { + const fromSphere = pathsGroupRef.current?.getObjectByProperty( + "uuid", + path.points.uuid + ); + const toSphere = pathsGroupRef.current?.getObjectByProperty( + "uuid", + target.pointUUID + ); + + if (fromSphere && toSphere) { + const fromWorldPosition = new THREE.Vector3(); + const toWorldPosition = new THREE.Vector3(); + fromSphere.getWorldPosition(fromWorldPosition); + toSphere.getWorldPosition(toWorldPosition); + + const distance = fromWorldPosition.distanceTo(toWorldPosition); + const heightFactor = Math.max(0.5, distance * 0.2); + const midPoint = new THREE.Vector3( + (fromWorldPosition.x + toWorldPosition.x) / 2, + Math.max(fromWorldPosition.y, toWorldPosition.y) + heightFactor, + (fromWorldPosition.z + toWorldPosition.z) / 2 + ); + + return ( + <QuadraticBezierLine + key={`${path.points.uuid}-${target.pointUUID}-${index}`} + ref={(el) => + (groupRefs.current[ + `${path.points.uuid}-${target.pointUUID}-${index}` + ] = el!) + } + start={fromWorldPosition.toArray()} + end={toWorldPosition.toArray()} + mid={midPoint.toArray()} + color={ + deleteTool && + hoveredLineKey === + `${path.points.uuid}-${target.pointUUID}-${index}` + ? "red" + : "orange" + } + lineWidth={4} + dashed={ + deleteTool && + hoveredLineKey === + `${path.points.uuid}-${target.pointUUID}-${index}` + ? false + : true + } + dashSize={0.75} + dashScale={20} + onPointerOver={() => + setHoveredLineKey( + `${path.points.uuid}-${target.pointUUID}-${index}` + ) + } + onPointerOut={() => setHoveredLineKey(null)} + onClick={() => { + if (deleteTool) { + const connection1 = { + model: path.modeluuid, + point: path.points.uuid, + }; + const connection2 = { + model: target.modelUUID, + point: target.pointUUID, + }; + + removeConnections(connection1, connection2); + } + }} + userData={target} + /> + ); + } + return null; + }); + } + + if (path.type === "StaticMachine") { + return path.points.connections.targets.map((target, index) => { + const targetPath = simulationStates.find( + (p) => p.modeluuid === target.modelUUID + ); + if (targetPath?.type !== "ArmBot") return null; + + const fromSphere = pathsGroupRef.current?.getObjectByProperty( + "uuid", + path.points.uuid + ); + const toSphere = pathsGroupRef.current?.getObjectByProperty( + "uuid", + target.pointUUID + ); + + if (fromSphere && toSphere) { + const fromWorldPosition = new THREE.Vector3(); + const toWorldPosition = new THREE.Vector3(); + fromSphere.getWorldPosition(fromWorldPosition); + toSphere.getWorldPosition(toWorldPosition); + + const distance = fromWorldPosition.distanceTo(toWorldPosition); + const heightFactor = Math.max(0.5, distance * 0.2); + const midPoint = new THREE.Vector3( + (fromWorldPosition.x + toWorldPosition.x) / 2, + Math.max(fromWorldPosition.y, toWorldPosition.y) + heightFactor, + (fromWorldPosition.z + toWorldPosition.z) / 2 + ); + + return ( + <QuadraticBezierLine + key={`${path.points.uuid}-${target.pointUUID}-${index}`} + ref={(el) => + (groupRefs.current[ + `${path.points.uuid}-${target.pointUUID}-${index}` + ] = el!) + } + start={fromWorldPosition.toArray()} + end={toWorldPosition.toArray()} + mid={midPoint.toArray()} + color={ + deleteTool && + hoveredLineKey === + `${path.points.uuid}-${target.pointUUID}-${index}` + ? "red" + : "#42a5f5" + } + lineWidth={4} + dashed={ + deleteTool && + hoveredLineKey === + `${path.points.uuid}-${target.pointUUID}-${index}` + ? false + : true + } + dashSize={0.75} + dashScale={20} + onPointerOver={() => + setHoveredLineKey( + `${path.points.uuid}-${target.pointUUID}-${index}` + ) + } + onPointerOut={() => setHoveredLineKey(null)} + onClick={() => { + if (deleteTool) { + const connection1 = { + model: path.modeluuid, + point: path.points.uuid, + }; + const connection2 = { + model: target.modelUUID, + point: target.pointUUID, + }; + + removeConnections(connection1, connection2); + } + }} + userData={target} + /> + ); + } + return null; + }); + } + + return []; + })} + + {currentLine && ( + <QuadraticBezierLine + start={currentLine.start.toArray()} + end={currentLine.end.toArray()} + mid={currentLine.mid.toArray()} + color={helperlineColor} + lineWidth={4} + dashed + dashSize={1} + dashScale={20} + /> + )} + </group> + ); } -export default PathConnector; \ No newline at end of file +export default PathConnector; From 344650730725600b882ee4fa33885dfe3fcfa0fa Mon Sep 17 00:00:00 2001 From: Poovizhi99 <poovizhi@hexrfactory.com> Date: Tue, 15 Apr 2025 10:05:53 +0530 Subject: [PATCH 02/20] removed targets based on condition for armbot --- .../controls/selection/selectionControls.tsx | 1300 +++++++++-------- 1 file changed, 712 insertions(+), 588 deletions(-) diff --git a/app/src/modules/scene/controls/selection/selectionControls.tsx b/app/src/modules/scene/controls/selection/selectionControls.tsx index d1eed1e..80a7518 100644 --- a/app/src/modules/scene/controls/selection/selectionControls.tsx +++ b/app/src/modules/scene/controls/selection/selectionControls.tsx @@ -3,7 +3,13 @@ import { useEffect, useMemo, useRef, useState } from "react"; import { SelectionBox } from "three/examples/jsm/interactive/SelectionBox"; import { SelectionHelper } from "./selectionHelper"; import { useFrame, useThree } from "@react-three/fiber"; -import { useFloorItems, useSelectedAssets, useSimulationStates, useSocketStore, useToggleView, } from "../../../../store/store"; +import { + useFloorItems, + useSelectedAssets, + useSimulationStates, + useSocketStore, + useToggleView, +} from "../../../../store/store"; import BoundingBox from "./boundingBoxHelper"; import { toast } from "react-toastify"; // import { deleteFloorItem } from '../../../../services/factoryBuilder/assest/floorAsset/deleteFloorItemApi'; @@ -17,622 +23,740 @@ import RotateControls from "./rotateControls"; import useModuleStore from "../../../../store/useModuleStore"; const SelectionControls: React.FC = () => { - const { camera, controls, gl, scene, pointer } = useThree(); - const itemsGroupRef = useRef<THREE.Group | undefined>(undefined); - const selectionGroup = useRef() as Types.RefGroup; - const { toggleView } = useToggleView(); - const { simulationStates, setSimulationStates } = useSimulationStates(); - const { selectedAssets, setSelectedAssets } = useSelectedAssets(); - const [movedObjects, setMovedObjects] = useState<THREE.Object3D[]>([]); - const [rotatedObjects, setRotatedObjects] = useState<THREE.Object3D[]>([]); - const [copiedObjects, setCopiedObjects] = useState<THREE.Object3D[]>([]); - const [pastedObjects, setpastedObjects] = useState<THREE.Object3D[]>([]); - const [duplicatedObjects, setDuplicatedObjects] = useState<THREE.Object3D[]>([]); - const boundingBoxRef = useRef<THREE.Mesh>(); - const { floorItems, setFloorItems } = useFloorItems(); - const { activeModule } = useModuleStore(); - const { socket } = useSocketStore(); - const selectionBox = useMemo(() => new SelectionBox(camera, scene), [camera, scene]); + const { camera, controls, gl, scene, pointer } = useThree(); + const itemsGroupRef = useRef<THREE.Group | undefined>(undefined); + const selectionGroup = useRef() as Types.RefGroup; + const { toggleView } = useToggleView(); + const { simulationStates, setSimulationStates } = useSimulationStates(); + const { selectedAssets, setSelectedAssets } = useSelectedAssets(); + const [movedObjects, setMovedObjects] = useState<THREE.Object3D[]>([]); + const [rotatedObjects, setRotatedObjects] = useState<THREE.Object3D[]>([]); + const [copiedObjects, setCopiedObjects] = useState<THREE.Object3D[]>([]); + const [pastedObjects, setpastedObjects] = useState<THREE.Object3D[]>([]); + const [duplicatedObjects, setDuplicatedObjects] = useState<THREE.Object3D[]>( + [] + ); + const boundingBoxRef = useRef<THREE.Mesh>(); + const { floorItems, setFloorItems } = useFloorItems(); + const { activeModule } = useModuleStore(); + const { socket } = useSocketStore(); + const selectionBox = useMemo( + () => new SelectionBox(camera, scene), + [camera, scene] + ); - useEffect(() => { - if (!camera || !scene || toggleView) return; + useEffect(() => { + if (!camera || !scene || toggleView) return; - const canvasElement = gl.domElement; - canvasElement.tabIndex = 0; + const canvasElement = gl.domElement; + canvasElement.tabIndex = 0; - const itemsGroup: any = scene.getObjectByName("itemsGroup"); - itemsGroupRef.current = itemsGroup; + const itemsGroup: any = scene.getObjectByName("itemsGroup"); + itemsGroupRef.current = itemsGroup; - let isSelecting = false; - let isRightClick = false; - let rightClickMoved = false; - let isCtrlSelecting = false; + let isSelecting = false; + let isRightClick = false; + let rightClickMoved = false; + let isCtrlSelecting = false; - const helper = new SelectionHelper(gl); + const helper = new SelectionHelper(gl); - if (!itemsGroup) { - toast.warn("itemsGroup not found in the scene."); - return; + if (!itemsGroup) { + toast.warn("itemsGroup not found in the scene."); + return; + } + + const onPointerDown = (event: PointerEvent) => { + if (event.button === 2) { + isRightClick = true; + rightClickMoved = false; + } else if (event.button === 0) { + isSelecting = false; + isCtrlSelecting = event.ctrlKey; + if (event.ctrlKey && duplicatedObjects.length === 0) { + if (controls) (controls as any).enabled = false; + selectionBox.startPoint.set(pointer.x, pointer.y, 0); } + } + }; - const onPointerDown = (event: PointerEvent) => { - if (event.button === 2) { - isRightClick = true; - rightClickMoved = false; - } else if (event.button === 0) { - isSelecting = false; - isCtrlSelecting = event.ctrlKey; - if (event.ctrlKey && duplicatedObjects.length === 0) { - if (controls) (controls as any).enabled = false; - selectionBox.startPoint.set(pointer.x, pointer.y, 0); - } - } - }; + const onPointerMove = (event: PointerEvent) => { + if (isRightClick) { + rightClickMoved = true; + } + isSelecting = true; + if ( + helper.isDown && + event.ctrlKey && + duplicatedObjects.length === 0 && + isCtrlSelecting + ) { + selectionBox.endPoint.set(pointer.x, pointer.y, 0); + } + }; - const onPointerMove = (event: PointerEvent) => { - if (isRightClick) { - rightClickMoved = true; - } - isSelecting = true; - if (helper.isDown && event.ctrlKey && duplicatedObjects.length === 0 && isCtrlSelecting) { - selectionBox.endPoint.set(pointer.x, pointer.y, 0); - } - }; - - const onPointerUp = (event: PointerEvent) => { - if (event.button === 2) { - isRightClick = false; - if (!rightClickMoved) { - clearSelection(); - } - return; - } - - if (isSelecting && isCtrlSelecting) { - isCtrlSelecting = false; - isSelecting = false; - if (event.ctrlKey && duplicatedObjects.length === 0) { - selectAssets(); - } - } else if (!isSelecting && selectedAssets.length > 0 && ((pastedObjects.length === 0 && duplicatedObjects.length === 0 && movedObjects.length === 0 && rotatedObjects.length === 0) || event.button !== 0)) { - clearSelection(); - helper.enabled = true; - isCtrlSelecting = false; - } - }; - - const onKeyDown = (event: KeyboardEvent) => { - if (movedObjects.length > 0 || rotatedObjects.length > 0) return; - if (event.key.toLowerCase() === "escape") { - event.preventDefault(); - clearSelection(); - } - if (event.key.toLowerCase() === "delete") { - event.preventDefault(); - deleteSelection(); - } - }; - - const onContextMenu = (event: MouseEvent) => { - event.preventDefault(); - if (!rightClickMoved) { - clearSelection(); - } - }; - - if (!toggleView && activeModule === "builder") { - helper.enabled = true; - if (duplicatedObjects.length === 0 && pastedObjects.length === 0) { - canvasElement.addEventListener("pointerdown", onPointerDown); - canvasElement.addEventListener("pointermove", onPointerMove); - canvasElement.addEventListener("pointerup", onPointerUp); - } else { - helper.enabled = false; - helper.dispose(); - } - canvasElement.addEventListener("contextmenu", onContextMenu); - canvasElement.addEventListener("keydown", onKeyDown); - } else { - helper.enabled = false; - helper.dispose(); + const onPointerUp = (event: PointerEvent) => { + if (event.button === 2) { + isRightClick = false; + if (!rightClickMoved) { + clearSelection(); } + return; + } - return () => { - canvasElement.removeEventListener("pointerdown", onPointerDown); - canvasElement.removeEventListener("pointermove", onPointerMove); - canvasElement.removeEventListener("contextmenu", onContextMenu); - canvasElement.removeEventListener("pointerup", onPointerUp); - canvasElement.removeEventListener("keydown", onKeyDown); - helper.enabled = false; - helper.dispose(); - }; - }, [camera, controls, scene, toggleView, selectedAssets, copiedObjects, pastedObjects, duplicatedObjects, movedObjects, socket, floorItems, rotatedObjects, activeModule,]); - - useEffect(() => { - if (activeModule !== "builder") { - clearSelection(); + if (isSelecting && isCtrlSelecting) { + isCtrlSelecting = false; + isSelecting = false; + if (event.ctrlKey && duplicatedObjects.length === 0) { + selectAssets(); } - }, [activeModule]); + } else if ( + !isSelecting && + selectedAssets.length > 0 && + ((pastedObjects.length === 0 && + duplicatedObjects.length === 0 && + movedObjects.length === 0 && + rotatedObjects.length === 0) || + event.button !== 0) + ) { + clearSelection(); + helper.enabled = true; + isCtrlSelecting = false; + } + }; - useFrame(() => { - if (pastedObjects.length === 0 && duplicatedObjects.length === 0 && movedObjects.length === 0 && rotatedObjects.length === 0) { - selectionGroup.current.position.set(0, 0, 0); + const onKeyDown = (event: KeyboardEvent) => { + if (movedObjects.length > 0 || rotatedObjects.length > 0) return; + if (event.key.toLowerCase() === "escape") { + event.preventDefault(); + clearSelection(); + } + if (event.key.toLowerCase() === "delete") { + event.preventDefault(); + deleteSelection(); + } + }; + + const onContextMenu = (event: MouseEvent) => { + event.preventDefault(); + if (!rightClickMoved) { + clearSelection(); + } + }; + + if (!toggleView && activeModule === "builder") { + helper.enabled = true; + if (duplicatedObjects.length === 0 && pastedObjects.length === 0) { + canvasElement.addEventListener("pointerdown", onPointerDown); + canvasElement.addEventListener("pointermove", onPointerMove); + canvasElement.addEventListener("pointerup", onPointerUp); + } else { + helper.enabled = false; + helper.dispose(); + } + canvasElement.addEventListener("contextmenu", onContextMenu); + canvasElement.addEventListener("keydown", onKeyDown); + } else { + helper.enabled = false; + helper.dispose(); + } + + return () => { + canvasElement.removeEventListener("pointerdown", onPointerDown); + canvasElement.removeEventListener("pointermove", onPointerMove); + canvasElement.removeEventListener("contextmenu", onContextMenu); + canvasElement.removeEventListener("pointerup", onPointerUp); + canvasElement.removeEventListener("keydown", onKeyDown); + helper.enabled = false; + helper.dispose(); + }; + }, [ + camera, + controls, + scene, + toggleView, + selectedAssets, + copiedObjects, + pastedObjects, + duplicatedObjects, + movedObjects, + socket, + floorItems, + rotatedObjects, + activeModule, + ]); + + useEffect(() => { + if (activeModule !== "builder") { + clearSelection(); + } + }, [activeModule]); + + useFrame(() => { + if ( + pastedObjects.length === 0 && + duplicatedObjects.length === 0 && + movedObjects.length === 0 && + rotatedObjects.length === 0 + ) { + selectionGroup.current.position.set(0, 0, 0); + } + }); + + const selectAssets = () => { + selectionBox.endPoint.set(pointer.x, pointer.y, 0); + if (controls) (controls as any).enabled = true; + + let selectedObjects = selectionBox.select(); + let Objects = new Set<THREE.Object3D>(); + + selectedObjects.map((object) => { + let currentObject: THREE.Object3D | null = object; + while (currentObject) { + if (currentObject.userData.modelId) { + Objects.add(currentObject); + break; } + currentObject = currentObject.parent || null; + } }); - const selectAssets = () => { - selectionBox.endPoint.set(pointer.x, pointer.y, 0); - if (controls) (controls as any).enabled = true; + if (Objects.size === 0) { + clearSelection(); + return; + } - let selectedObjects = selectionBox.select(); - let Objects = new Set<THREE.Object3D>(); + const updatedSelections = new Set(selectedAssets); + Objects.forEach((obj) => { + updatedSelections.has(obj) + ? updatedSelections.delete(obj) + : updatedSelections.add(obj); + }); - selectedObjects.map((object) => { - let currentObject: THREE.Object3D | null = object; - while (currentObject) { - if (currentObject.userData.modelId) { - Objects.add(currentObject); - break; - } - currentObject = currentObject.parent || null; - } + const selected = Array.from(updatedSelections); + + setSelectedAssets(selected); + }; + + const clearSelection = () => { + selectionGroup.current.children = []; + selectionGroup.current.position.set(0, 0, 0); + selectionGroup.current.rotation.set(0, 0, 0); + setpastedObjects([]); + setDuplicatedObjects([]); + setSelectedAssets([]); + }; + const updateBackend = async ( + updatedPaths: ( + | SimulationTypes.ConveyorEventsSchema + | SimulationTypes.VehicleEventsSchema + | SimulationTypes.StaticMachineEventsSchema + | SimulationTypes.ArmBotEventsSchema + )[] + ) => { + if (updatedPaths.length === 0) return; + const email = localStorage.getItem("email"); + const organization = email ? email.split("@")[1].split(".")[0] : ""; + + updatedPaths.forEach(async (updatedPath) => { + if (updatedPath.type === "Conveyor") { + // await setEventApi( + // organization, + // updatedPath.modeluuid, + // { type: "Conveyor", points: updatedPath.points, speed: updatedPath.speed } + // ); + + const data = { + organization: organization, + modeluuid: updatedPath.modeluuid, + eventData: { + type: "Conveyor", + points: updatedPath.points, + speed: updatedPath.speed, + }, + }; + + socket.emit("v2:model-asset:updateEventData", data); + } else if (updatedPath.type === "Vehicle") { + // await setEventApi( + // organization, + // updatedPath.modeluuid, + // { type: "Vehicle", points: updatedPath.points } + // ); + + const data = { + organization: organization, + modeluuid: updatedPath.modeluuid, + eventData: { type: "Vehicle", points: updatedPath.points }, + }; + + socket.emit("v2:model-asset:updateEventData", data); + } else if (updatedPath.type === "StaticMachine") { + // await setEventApi( + // organization, + // updatedPath.modeluuid, + // { type: "StaticMachine", points: updatedPath.points } + // ); + + const data = { + organization: organization, + modeluuid: updatedPath.modeluuid, + eventData: { type: "StaticMachine", points: updatedPath.points }, + }; + + socket.emit("v2:model-asset:updateEventData", data); + } else if (updatedPath.type === "ArmBot") { + // await setEventApi( + // organization, + // updatedPath.modeluuid, + // { type: "ArmBot", points: updatedPath.points } + // ); + + const data = { + organization: organization, + modeluuid: updatedPath.modeluuid, + eventData: { type: "ArmBot", points: updatedPath.points }, + }; + + socket.emit("v2:model-asset:updateEventData", data); + } + }); + }; + + // const removeConnection = (modelUUID: any) => { + // + // const removedPath = simulationStates?.flatMap((state) => { + // let shouldInclude = false; + + // if (state.type === "Conveyor") { + // state.points.forEach((point: any) => { + // const sourceMatch = + // point.connections?.source?.modelUUID === modelUUID; + // const targetMatch = point.connections?.targets?.some( + // (target: any) => target.modelUUID === modelUUID + // ); + + // if (sourceMatch || targetMatch) shouldInclude = true; + // }); + // } + + // if (state.type === "Vehicle") { + // const targetMatch = state.points.connections?.targets?.some( + // (target: any) => target.modelUUID === modelUUID + // ); + + // if (targetMatch) shouldInclude = true; + // } + + // if (state.type === "StaticMachine") { + // const targetMatch = state.points.connections?.targets?.some( + // (target: any) => target.modelUUID === modelUUID + // ); + + // if (targetMatch) shouldInclude = true; + // } + + // if (state.type === "ArmBot") { + // const sourceMatch = + // state.points.connections?.source?.modelUUID === modelUUID; + // const targetMatch = state.points.connections?.targets?.some( + // (target: any) => target.modelUUID === modelUUID + // ); + + // const processMatch = + // state.points.actions?.processes?.some( + // (process: any) => + // process.startPoint === modelUUID || process.endPoint === modelUUID + // ) ?? false; + + // if (sourceMatch || targetMatch || processMatch) shouldInclude = true; + // } + + // return shouldInclude ? [state] : []; + // }); + // updateBackend(removedPath); + // + // return removedPath; + // // updateBackend(updatedPaths); + + // // setSimulationStates(updatedStates); + // }; + // const removeConnection = (modelUUIDs: any[]) => { + // + // const removedPath = simulationStates?.flatMap((state) => { + // let shouldInclude = false; + + // if (state.type === "Conveyor") { + // state.points.forEach((point: any) => { + // const sourceMatch = modelUUIDs.includes( + // point.connections?.source?.modelUUID + // ); + // const targetMatch = point.connections?.targets?.some((target: any) => + // modelUUIDs.includes(target.modelUUID) + // ); + + // if (sourceMatch || targetMatch) shouldInclude = true; + // }); + // } + + // if (state.type === "Vehicle") { + // const targetMatch = state.points.connections?.targets?.some( + // (target: any) => modelUUIDs.includes(target.modelUUID) + // ); + + // if (targetMatch) shouldInclude = true; + // } + + // if (state.type === "StaticMachine") { + // const targetMatch = state.points.connections?.targets?.some( + // (target: any) => modelUUIDs.includes(target.modelUUID) + // ); + + // if (targetMatch) shouldInclude = true; + // } + + // if (state.type === "ArmBot") { + // const sourceMatch = modelUUIDs.includes( + // state.points.connections?.source?.modelUUID + // ); + // const targetMatch = state.points.connections?.targets?.some( + // (target: any) => modelUUIDs.includes(target.modelUUID) + // ); + + // const processMatch = + // state.points.actions?.processes?.some( + // (process: any) => + // modelUUIDs.includes(process.startPoint) || + // modelUUIDs.includes(process.endPoint) + // ) ?? false; + + // if (sourceMatch || targetMatch || processMatch) shouldInclude = true; + // } + + // return shouldInclude ? [state] : []; + // }); + // updateBackend(removedPath); + // + // return removedPath; + // }; + + const removeConnection = (modelUUIDs: any[]) => { + const removedPath = simulationStates?.flatMap((state: any) => { + let shouldInclude = false; + + // Conveyor type + if (state.type === "Conveyor") { + state.points.forEach((point: any) => { + const sourceMatch = modelUUIDs.includes( + point.connections?.source?.modelUUID + ); + const targetMatch = point.connections?.targets?.some((target: any) => + modelUUIDs.includes(target.modelUUID) + ); + + if (sourceMatch) { + point.connections.source = {}; + shouldInclude = true; + } + + if (targetMatch) { + point.connections.targets = []; + shouldInclude = true; + } }); + } + // Vehicle & StaticMachine types + if (state.type === "Vehicle") { + const targets = state.points?.connections?.targets || []; + const targetMatch = targets.some((target: any) => + modelUUIDs.includes(target.modelUUID) + ); - if (Objects.size === 0) { - clearSelection(); - return; + if (targetMatch) { + state.points.connections.targets = []; + shouldInclude = true; + } + } + if (state.type === "StaticMachine") { + const targets = state.points?.connections?.targets || []; + const targetMatch = targets.some((target: any) => + modelUUIDs.includes(target.modelUUID) + ); + + if (targetMatch) { + state.points.connections.targets = []; + shouldInclude = true; + } + } + + // ArmBot type + if (state.type === "ArmBot") { + const sourceMatch = modelUUIDs.includes( + state.points.connections?.source?.modelUUID + ); + console.log("model", modelUUIDs); + console.log("state.points.connections: ", state.points); + + const targetMatch = state.points.connections?.targets?.some( + (target: any) => modelUUIDs.includes(target.modelUUID) + ); + // state.points.actions.processes = state.points.actions.processes.filter( + // (process: any) => + // console.log( + // !modelUUIDs.includes(process.startPoint), + // !modelUUIDs.includes(process.endPoint), + // modelUUIDs, + // process.startPoint, + // process.endPoint + // ) + // ); + + // shouldInclude = true; + + // const processMatches = state.points.actions?.processes?.some( + // (process: any) => + // console.log( + // "process: ", + // process, + // modelUUIDs, + // process.startPoint, + // process.endPoint + // ) + // // modelUUIDs.includes(process.startPoint) || + // // modelUUIDs.includes(process.endPoint) + // ); + // const processMatch = state.points.actions?.processes?.some( + // (process: any) => + // modelUUIDs.includes(String(process.startPoint)) || + // modelUUIDs.includes(String(process.endPoint)) + // ); + + // console.log("processMatch: ", processMatch); + if (sourceMatch) { + state.points.connections.source = {}; + shouldInclude = true; } - const updatedSelections = new Set(selectedAssets); - Objects.forEach((obj) => { updatedSelections.has(obj) ? updatedSelections.delete(obj) : updatedSelections.add(obj); }); - - const selected = Array.from(updatedSelections); - - setSelectedAssets(selected); - }; - - const clearSelection = () => { - selectionGroup.current.children = []; - selectionGroup.current.position.set(0, 0, 0); - selectionGroup.current.rotation.set(0, 0, 0); - setpastedObjects([]); - setDuplicatedObjects([]); - setSelectedAssets([]); - }; - const updateBackend = async (updatedPaths: (SimulationTypes.ConveyorEventsSchema | SimulationTypes.VehicleEventsSchema | SimulationTypes.StaticMachineEventsSchema | SimulationTypes.ArmBotEventsSchema)[]) => { - if (updatedPaths.length === 0) return; - const email = localStorage.getItem("email"); - const organization = email ? email.split("@")[1].split(".")[0] : ""; - - updatedPaths.forEach(async (updatedPath) => { - - if (updatedPath.type === "Conveyor") { - // await setEventApi( - // organization, - // updatedPath.modeluuid, - // { type: "Conveyor", points: updatedPath.points, speed: updatedPath.speed } - // ); - - const data = { - organization: organization, - modeluuid: updatedPath.modeluuid, - eventData: { - type: "Conveyor", - points: updatedPath.points, - speed: updatedPath.speed, - }, - }; - - socket.emit("v2:model-asset:updateEventData", data); - - } else if (updatedPath.type === "Vehicle") { - - // await setEventApi( - // organization, - // updatedPath.modeluuid, - // { type: "Vehicle", points: updatedPath.points } - // ); - - const data = { - organization: organization, - modeluuid: updatedPath.modeluuid, - eventData: { type: "Vehicle", points: updatedPath.points }, - }; - - socket.emit("v2:model-asset:updateEventData", data); - - } else if (updatedPath.type === "StaticMachine") { - - // await setEventApi( - // organization, - // updatedPath.modeluuid, - // { type: "StaticMachine", points: updatedPath.points } - // ); - - const data = { - organization: organization, - modeluuid: updatedPath.modeluuid, - eventData: { type: "StaticMachine", points: updatedPath.points }, - }; - - socket.emit("v2:model-asset:updateEventData", data); - - } else if (updatedPath.type === "ArmBot") { - - // await setEventApi( - // organization, - // updatedPath.modeluuid, - // { type: "ArmBot", points: updatedPath.points } - // ); - - const data = { - organization: organization, - modeluuid: updatedPath.modeluuid, - eventData: { type: "ArmBot", points: updatedPath.points }, - }; - - socket.emit("v2:model-asset:updateEventData", data); - } - - }); - }; - - // const removeConnection = (modelUUID: any) => { - // - // const removedPath = simulationStates?.flatMap((state) => { - // let shouldInclude = false; - - // if (state.type === "Conveyor") { - // state.points.forEach((point: any) => { - // const sourceMatch = - // point.connections?.source?.modelUUID === modelUUID; - // const targetMatch = point.connections?.targets?.some( - // (target: any) => target.modelUUID === modelUUID - // ); - - // if (sourceMatch || targetMatch) shouldInclude = true; - // }); - // } - - // if (state.type === "Vehicle") { - // const targetMatch = state.points.connections?.targets?.some( - // (target: any) => target.modelUUID === modelUUID - // ); - - // if (targetMatch) shouldInclude = true; - // } - - // if (state.type === "StaticMachine") { - // const targetMatch = state.points.connections?.targets?.some( - // (target: any) => target.modelUUID === modelUUID - // ); - - // if (targetMatch) shouldInclude = true; - // } - - // if (state.type === "ArmBot") { - // const sourceMatch = - // state.points.connections?.source?.modelUUID === modelUUID; - // const targetMatch = state.points.connections?.targets?.some( - // (target: any) => target.modelUUID === modelUUID - // ); - - // const processMatch = - // state.points.actions?.processes?.some( - // (process: any) => - // process.startPoint === modelUUID || process.endPoint === modelUUID - // ) ?? false; - - // if (sourceMatch || targetMatch || processMatch) shouldInclude = true; - // } - - // return shouldInclude ? [state] : []; - // }); - // updateBackend(removedPath); - // - // return removedPath; - // // updateBackend(updatedPaths); - - // // setSimulationStates(updatedStates); - // }; - // const removeConnection = (modelUUIDs: any[]) => { - // - // const removedPath = simulationStates?.flatMap((state) => { - // let shouldInclude = false; - - // if (state.type === "Conveyor") { - // state.points.forEach((point: any) => { - // const sourceMatch = modelUUIDs.includes( - // point.connections?.source?.modelUUID - // ); - // const targetMatch = point.connections?.targets?.some((target: any) => - // modelUUIDs.includes(target.modelUUID) - // ); - - // if (sourceMatch || targetMatch) shouldInclude = true; - // }); - // } - - // if (state.type === "Vehicle") { - // const targetMatch = state.points.connections?.targets?.some( - // (target: any) => modelUUIDs.includes(target.modelUUID) - // ); - - // if (targetMatch) shouldInclude = true; - // } - - // if (state.type === "StaticMachine") { - // const targetMatch = state.points.connections?.targets?.some( - // (target: any) => modelUUIDs.includes(target.modelUUID) - // ); - - // if (targetMatch) shouldInclude = true; - // } - - // if (state.type === "ArmBot") { - // const sourceMatch = modelUUIDs.includes( - // state.points.connections?.source?.modelUUID - // ); - // const targetMatch = state.points.connections?.targets?.some( - // (target: any) => modelUUIDs.includes(target.modelUUID) - // ); - - // const processMatch = - // state.points.actions?.processes?.some( - // (process: any) => - // modelUUIDs.includes(process.startPoint) || - // modelUUIDs.includes(process.endPoint) - // ) ?? false; - - // if (sourceMatch || targetMatch || processMatch) shouldInclude = true; - // } - - // return shouldInclude ? [state] : []; - // }); - // updateBackend(removedPath); - // - // return removedPath; - // }; - - const removeConnection = (modelUUIDs: any[]) => { - const removedPath = simulationStates?.flatMap((state: any) => { - let shouldInclude = false; - - // Conveyor type - if (state.type === "Conveyor") { - state.points.forEach((point: any) => { - const sourceMatch = modelUUIDs.includes(point.connections?.source?.modelUUID); - const targetMatch = point.connections?.targets?.some((target: any) => modelUUIDs.includes(target.modelUUID)); - - if (sourceMatch) { - point.connections.source = {}; - shouldInclude = true; - } - - if (targetMatch) { - point.connections.targets = []; - shouldInclude = true; - } - }); - } - - // Vehicle & StaticMachine types - if (state.type === "Vehicle") { - const targets = state.points?.connections?.targets || []; - const targetMatch = targets.some((target: any) => modelUUIDs.includes(target.modelUUID)); - - if (targetMatch) { - state.points.connections.targets = []; - shouldInclude = true; - } - } - if (state.type === "StaticMachine") { - const targets = state.points?.connections?.targets || []; - const targetMatch = targets.some((target: any) => modelUUIDs.includes(target.modelUUID)); - - if (targetMatch) { - state.points.connections.targets = []; - shouldInclude = true; - } - } - - // ArmBot type - if (state.type === "ArmBot") { - const sourceMatch = modelUUIDs.includes(state.points.connections?.source?.modelUUID); - - const targetMatch = state.points.connections?.targets?.some( - (target: any) => modelUUIDs.includes(target.modelUUID) - ); - // state.points.actions.processes = state.points.actions.processes.filter( - // (process: any) => - // console.log( - // !modelUUIDs.includes(process.startPoint), - // !modelUUIDs.includes(process.endPoint), - // modelUUIDs, - // process.startPoint, - // process.endPoint - // ) - // ); - - // shouldInclude = true; - - // const processMatches = state.points.actions?.processes?.some( - // (process: any) => - // console.log( - // "process: ", - // process, - // modelUUIDs, - // process.startPoint, - // process.endPoint - // ) - // // modelUUIDs.includes(process.startPoint) || - // // modelUUIDs.includes(process.endPoint) - // ); - // const processMatch = state.points.actions?.processes?.some( - // (process: any) => - // modelUUIDs.includes(String(process.startPoint)) || - // modelUUIDs.includes(String(process.endPoint)) - // ); - - // console.log("processMatch: ", processMatch); - if (sourceMatch) { - state.points.connections.source = {}; - shouldInclude = true; - } - - if (targetMatch) { - state.points.connections.targets = - state.points.connections.targets.filter((target: any) => !modelUUIDs.includes(target.modelUUID)); - shouldInclude = true; - } - - // console.log("processMatch: ", processMatch); - // if (processMatch) { - // state.points.actions.processes = - // state.points.actions.processes.filter((process: any) => { - // const shouldRemove = - // modelUUIDs.includes(process.startPoint) || - // modelUUIDs.includes(process.endPoint); - - // console.log("shouldRemove: ", shouldRemove); - // return !shouldRemove; - // }); - // shouldInclude = true; - // } - } - - return shouldInclude ? [state] : []; - }); - - updateBackend(removedPath); - return removedPath; - }; - - const deleteSelection = () => { - if (selectedAssets.length > 0 && duplicatedObjects.length === 0) { - const email = localStorage.getItem("email"); - const organization = email!.split("@")[1].split(".")[0]; - - const storedItems = JSON.parse(localStorage.getItem("FloorItems") || "[]"); - const selectedUUIDs = selectedAssets.map((mesh: THREE.Object3D) => mesh.uuid); - - const updatedStoredItems = storedItems.filter((item: { modeluuid: string }) => !selectedUUIDs.includes(item.modeluuid)); - localStorage.setItem("FloorItems", JSON.stringify(updatedStoredItems)); - - selectedAssets.forEach((selectedMesh: THREE.Object3D) => { - //REST - - // const response = await deleteFloorItem(organization, selectedMesh.uuid, selectedMesh.userData.name); - - //SOCKET - - const data = { - organization: organization, - modeluuid: selectedMesh.uuid, - modelname: selectedMesh.userData.name, - socketId: socket.id, - }; - - socket.emit("v2:model-asset:delete", data); - - selectedMesh.traverse((child: THREE.Object3D) => { - if (child instanceof THREE.Mesh) { - if (child.geometry) child.geometry.dispose(); - if (Array.isArray(child.material)) { - child.material.forEach((material) => { - if (material.map) material.map.dispose(); - material.dispose(); - }); - } else if (child.material) { - if (child.material.map) child.material.map.dispose(); - child.material.dispose(); - } - } - }); - - setSimulationStates((prevEvents: (| SimulationTypes.ConveyorEventsSchema | SimulationTypes.VehicleEventsSchema | SimulationTypes.StaticMachineEventsSchema | SimulationTypes.ArmBotEventsSchema)[]) => { - const updatedEvents = (prevEvents || []).filter( - (event) => event.modeluuid !== selectedMesh.uuid - ); - return updatedEvents; - } - ); - - itemsGroupRef.current?.remove(selectedMesh); - }); - - const allUUIDs = selectedAssets.map((val: any) => val.uuid); - removeConnection(allUUIDs); - - // const removedPath = simulationStates?.flatMap((path: any) => { - // let shouldInclude = false; - - // if (Array.isArray(path.points)) { - // path.points.forEach((point: any) => { - // const sourceMatch = - // point.connections?.source?.modelUUID === selectedAssets[0].uuid; - // const targetMatch = point.connections?.targets?.some( - // (target: any) => target.modelUUID === selectedAssets[0].uuid - // ); - - // if (sourceMatch) { - // point.connections.source = {}; - // shouldInclude = true; - // } - - // if (targetMatch) { - // point.connections.targets = []; - // shouldInclude = true; - // } - // }); - // } else { - // const sourceMatch = - // path.connections?.source?.modelUUID === selectedAssets[0].uuid; - // const targetMatch = path.connections?.targets?.some( - // (target: any) => target.modelUUID === selectedAssets[0].uuid - // ); - - // if (sourceMatch) { - // path.connections.source = {}; - // shouldInclude = true; - // } - - // if (targetMatch) { - // path.connections.targets = []; - // shouldInclude = true; - // } - // } - - // return shouldInclude ? [path] : []; - // }); - // updateBackend(removedPath); - - const updatedItems = floorItems.filter( - (item: { modeluuid: string }) => !selectedUUIDs.includes(item.modeluuid) + if (targetMatch) { + state.points.connections.targets = + state.points.connections.targets.filter( + (target: any) => !modelUUIDs.includes(target.modelUUID) ); - setFloorItems(updatedItems); + shouldInclude = true; } - toast.success("Selected models removed!"); - clearSelection(); - }; - return ( - <> - <group name="SelectionGroup"> - <group ref={selectionGroup} name="selectionAssetGroup"> - <BoundingBox boundingBoxRef={boundingBoxRef} /> - </group> - </group> + // console.log("processMatch: ", processMatch); + // if (processMatch) { + // state.points.actions.processes = + // state.points.actions.processes.filter((process: any) => { + // const shouldRemove = + // modelUUIDs.includes(process.startPoint) || + // modelUUIDs.includes(process.endPoint); + // console.log("shouldRemove: ", shouldRemove); + // return !shouldRemove; + // }); + // shouldInclude = true; + // } + } - <MoveControls movedObjects={movedObjects} setMovedObjects={setMovedObjects} itemsGroupRef={itemsGroupRef} copiedObjects={copiedObjects} setCopiedObjects={setCopiedObjects} pastedObjects={pastedObjects} setpastedObjects={setpastedObjects} duplicatedObjects={duplicatedObjects} setDuplicatedObjects={setDuplicatedObjects} rotatedObjects={rotatedObjects} setRotatedObjects={setRotatedObjects} selectionGroup={selectionGroup} boundingBoxRef={boundingBoxRef} /> + return shouldInclude ? [state] : []; + }); - <RotateControls rotatedObjects={rotatedObjects} setRotatedObjects={setRotatedObjects} movedObjects={movedObjects} setMovedObjects={setMovedObjects} itemsGroupRef={itemsGroupRef} copiedObjects={copiedObjects} setCopiedObjects={setCopiedObjects} pastedObjects={pastedObjects} setpastedObjects={setpastedObjects} duplicatedObjects={duplicatedObjects} setDuplicatedObjects={setDuplicatedObjects} selectionGroup={selectionGroup} boundingBoxRef={boundingBoxRef} /> + updateBackend(removedPath); + return removedPath; + }; - <DuplicationControls itemsGroupRef={itemsGroupRef} duplicatedObjects={duplicatedObjects} setDuplicatedObjects={setDuplicatedObjects} setpastedObjects={setpastedObjects} selectionGroup={selectionGroup} movedObjects={movedObjects} setMovedObjects={setMovedObjects} rotatedObjects={rotatedObjects} setRotatedObjects={setRotatedObjects} boundingBoxRef={boundingBoxRef} /> + const deleteSelection = () => { + if (selectedAssets.length > 0 && duplicatedObjects.length === 0) { + const email = localStorage.getItem("email"); + const organization = email!.split("@")[1].split(".")[0]; - <CopyPasteControls itemsGroupRef={itemsGroupRef} copiedObjects={copiedObjects} setCopiedObjects={setCopiedObjects} pastedObjects={pastedObjects} setpastedObjects={setpastedObjects} selectionGroup={selectionGroup} setDuplicatedObjects={setDuplicatedObjects} movedObjects={movedObjects} setMovedObjects={setMovedObjects} rotatedObjects={rotatedObjects} setRotatedObjects={setRotatedObjects} boundingBoxRef={boundingBoxRef} /> - </> - ); + const storedItems = JSON.parse( + localStorage.getItem("FloorItems") || "[]" + ); + const selectedUUIDs = selectedAssets.map( + (mesh: THREE.Object3D) => mesh.uuid + ); + + const updatedStoredItems = storedItems.filter( + (item: { modeluuid: string }) => !selectedUUIDs.includes(item.modeluuid) + ); + localStorage.setItem("FloorItems", JSON.stringify(updatedStoredItems)); + + selectedAssets.forEach((selectedMesh: THREE.Object3D) => { + //REST + + // const response = await deleteFloorItem(organization, selectedMesh.uuid, selectedMesh.userData.name); + + //SOCKET + + const data = { + organization: organization, + modeluuid: selectedMesh.uuid, + modelname: selectedMesh.userData.name, + socketId: socket.id, + }; + + socket.emit("v2:model-asset:delete", data); + + selectedMesh.traverse((child: THREE.Object3D) => { + if (child instanceof THREE.Mesh) { + if (child.geometry) child.geometry.dispose(); + if (Array.isArray(child.material)) { + child.material.forEach((material) => { + if (material.map) material.map.dispose(); + material.dispose(); + }); + } else if (child.material) { + if (child.material.map) child.material.map.dispose(); + child.material.dispose(); + } + } + }); + + setSimulationStates( + ( + prevEvents: ( + | SimulationTypes.ConveyorEventsSchema + | SimulationTypes.VehicleEventsSchema + | SimulationTypes.StaticMachineEventsSchema + | SimulationTypes.ArmBotEventsSchema + )[] + ) => { + const updatedEvents = (prevEvents || []).filter( + (event) => event.modeluuid !== selectedMesh.uuid + ); + return updatedEvents; + } + ); + + itemsGroupRef.current?.remove(selectedMesh); + }); + + const allUUIDs = selectedAssets.map((val: any) => val.uuid); + removeConnection(allUUIDs); + + // const removedPath = simulationStates?.flatMap((path: any) => { + // let shouldInclude = false; + + // if (Array.isArray(path.points)) { + // path.points.forEach((point: any) => { + // const sourceMatch = + // point.connections?.source?.modelUUID === selectedAssets[0].uuid; + // const targetMatch = point.connections?.targets?.some( + // (target: any) => target.modelUUID === selectedAssets[0].uuid + // ); + + // if (sourceMatch) { + // point.connections.source = {}; + // shouldInclude = true; + // } + + // if (targetMatch) { + // point.connections.targets = []; + // shouldInclude = true; + // } + // }); + // } else { + // const sourceMatch = + // path.connections?.source?.modelUUID === selectedAssets[0].uuid; + // const targetMatch = path.connections?.targets?.some( + // (target: any) => target.modelUUID === selectedAssets[0].uuid + // ); + + // if (sourceMatch) { + // path.connections.source = {}; + // shouldInclude = true; + // } + + // if (targetMatch) { + // path.connections.targets = []; + // shouldInclude = true; + // } + // } + + // return shouldInclude ? [path] : []; + // }); + // updateBackend(removedPath); + + const updatedItems = floorItems.filter( + (item: { modeluuid: string }) => !selectedUUIDs.includes(item.modeluuid) + ); + setFloorItems(updatedItems); + } + toast.success("Selected models removed!"); + clearSelection(); + }; + + return ( + <> + <group name="SelectionGroup"> + <group ref={selectionGroup} name="selectionAssetGroup"> + <BoundingBox boundingBoxRef={boundingBoxRef} /> + </group> + </group> + + <MoveControls + movedObjects={movedObjects} + setMovedObjects={setMovedObjects} + itemsGroupRef={itemsGroupRef} + copiedObjects={copiedObjects} + setCopiedObjects={setCopiedObjects} + pastedObjects={pastedObjects} + setpastedObjects={setpastedObjects} + duplicatedObjects={duplicatedObjects} + setDuplicatedObjects={setDuplicatedObjects} + rotatedObjects={rotatedObjects} + setRotatedObjects={setRotatedObjects} + selectionGroup={selectionGroup} + boundingBoxRef={boundingBoxRef} + /> + + <RotateControls + rotatedObjects={rotatedObjects} + setRotatedObjects={setRotatedObjects} + movedObjects={movedObjects} + setMovedObjects={setMovedObjects} + itemsGroupRef={itemsGroupRef} + copiedObjects={copiedObjects} + setCopiedObjects={setCopiedObjects} + pastedObjects={pastedObjects} + setpastedObjects={setpastedObjects} + duplicatedObjects={duplicatedObjects} + setDuplicatedObjects={setDuplicatedObjects} + selectionGroup={selectionGroup} + boundingBoxRef={boundingBoxRef} + /> + + <DuplicationControls + itemsGroupRef={itemsGroupRef} + duplicatedObjects={duplicatedObjects} + setDuplicatedObjects={setDuplicatedObjects} + setpastedObjects={setpastedObjects} + selectionGroup={selectionGroup} + movedObjects={movedObjects} + setMovedObjects={setMovedObjects} + rotatedObjects={rotatedObjects} + setRotatedObjects={setRotatedObjects} + boundingBoxRef={boundingBoxRef} + /> + + <CopyPasteControls + itemsGroupRef={itemsGroupRef} + copiedObjects={copiedObjects} + setCopiedObjects={setCopiedObjects} + pastedObjects={pastedObjects} + setpastedObjects={setpastedObjects} + selectionGroup={selectionGroup} + setDuplicatedObjects={setDuplicatedObjects} + movedObjects={movedObjects} + setMovedObjects={setMovedObjects} + rotatedObjects={rotatedObjects} + setRotatedObjects={setRotatedObjects} + boundingBoxRef={boundingBoxRef} + /> + </> + ); }; export default SelectionControls; From c2a29fc8930da44087e4a2532a8537b6f90c72e1 Mon Sep 17 00:00:00 2001 From: Poovizhi99 <poovizhi@hexrfactory.com> Date: Tue, 15 Apr 2025 15:37:11 +0530 Subject: [PATCH 03/20] integrated path while deleting the asset --- .../controls/selection/selectionControls.tsx | 391 +++++------------- 1 file changed, 113 insertions(+), 278 deletions(-) diff --git a/app/src/modules/scene/controls/selection/selectionControls.tsx b/app/src/modules/scene/controls/selection/selectionControls.tsx index 80a7518..8d7c4cc 100644 --- a/app/src/modules/scene/controls/selection/selectionControls.tsx +++ b/app/src/modules/scene/controls/selection/selectionControls.tsx @@ -323,247 +323,124 @@ const SelectionControls: React.FC = () => { }); }; - // const removeConnection = (modelUUID: any) => { - // - // const removedPath = simulationStates?.flatMap((state) => { - // let shouldInclude = false; - - // if (state.type === "Conveyor") { - // state.points.forEach((point: any) => { - // const sourceMatch = - // point.connections?.source?.modelUUID === modelUUID; - // const targetMatch = point.connections?.targets?.some( - // (target: any) => target.modelUUID === modelUUID - // ); - - // if (sourceMatch || targetMatch) shouldInclude = true; - // }); - // } - - // if (state.type === "Vehicle") { - // const targetMatch = state.points.connections?.targets?.some( - // (target: any) => target.modelUUID === modelUUID - // ); - - // if (targetMatch) shouldInclude = true; - // } - - // if (state.type === "StaticMachine") { - // const targetMatch = state.points.connections?.targets?.some( - // (target: any) => target.modelUUID === modelUUID - // ); - - // if (targetMatch) shouldInclude = true; - // } - - // if (state.type === "ArmBot") { - // const sourceMatch = - // state.points.connections?.source?.modelUUID === modelUUID; - // const targetMatch = state.points.connections?.targets?.some( - // (target: any) => target.modelUUID === modelUUID - // ); - - // const processMatch = - // state.points.actions?.processes?.some( - // (process: any) => - // process.startPoint === modelUUID || process.endPoint === modelUUID - // ) ?? false; - - // if (sourceMatch || targetMatch || processMatch) shouldInclude = true; - // } - - // return shouldInclude ? [state] : []; - // }); - // updateBackend(removedPath); - // - // return removedPath; - // // updateBackend(updatedPaths); - - // // setSimulationStates(updatedStates); - // }; - // const removeConnection = (modelUUIDs: any[]) => { - // - // const removedPath = simulationStates?.flatMap((state) => { - // let shouldInclude = false; - - // if (state.type === "Conveyor") { - // state.points.forEach((point: any) => { - // const sourceMatch = modelUUIDs.includes( - // point.connections?.source?.modelUUID - // ); - // const targetMatch = point.connections?.targets?.some((target: any) => - // modelUUIDs.includes(target.modelUUID) - // ); - - // if (sourceMatch || targetMatch) shouldInclude = true; - // }); - // } - - // if (state.type === "Vehicle") { - // const targetMatch = state.points.connections?.targets?.some( - // (target: any) => modelUUIDs.includes(target.modelUUID) - // ); - - // if (targetMatch) shouldInclude = true; - // } - - // if (state.type === "StaticMachine") { - // const targetMatch = state.points.connections?.targets?.some( - // (target: any) => modelUUIDs.includes(target.modelUUID) - // ); - - // if (targetMatch) shouldInclude = true; - // } - - // if (state.type === "ArmBot") { - // const sourceMatch = modelUUIDs.includes( - // state.points.connections?.source?.modelUUID - // ); - // const targetMatch = state.points.connections?.targets?.some( - // (target: any) => modelUUIDs.includes(target.modelUUID) - // ); - - // const processMatch = - // state.points.actions?.processes?.some( - // (process: any) => - // modelUUIDs.includes(process.startPoint) || - // modelUUIDs.includes(process.endPoint) - // ) ?? false; - - // if (sourceMatch || targetMatch || processMatch) shouldInclude = true; - // } - - // return shouldInclude ? [state] : []; - // }); - // updateBackend(removedPath); - // - // return removedPath; - // }; - - const removeConnection = (modelUUIDs: any[]) => { - const removedPath = simulationStates?.flatMap((state: any) => { - let shouldInclude = false; - - // Conveyor type + const removeConnections = (deletedModelUUIDs: string[]) => { + const updatedStates = simulationStates.map((state) => { + // Handle Conveyor if (state.type === "Conveyor") { - state.points.forEach((point: any) => { - const sourceMatch = modelUUIDs.includes( - point.connections?.source?.modelUUID - ); - const targetMatch = point.connections?.targets?.some((target: any) => - modelUUIDs.includes(target.modelUUID) - ); - - if (sourceMatch) { - point.connections.source = {}; - shouldInclude = true; - } - - if (targetMatch) { - point.connections.targets = []; - shouldInclude = true; - } - }); - } - // Vehicle & StaticMachine types - if (state.type === "Vehicle") { - const targets = state.points?.connections?.targets || []; - const targetMatch = targets.some((target: any) => - modelUUIDs.includes(target.modelUUID) - ); - - if (targetMatch) { - state.points.connections.targets = []; - shouldInclude = true; - } - } - if (state.type === "StaticMachine") { - const targets = state.points?.connections?.targets || []; - const targetMatch = targets.some((target: any) => - modelUUIDs.includes(target.modelUUID) - ); - - if (targetMatch) { - state.points.connections.targets = []; - shouldInclude = true; - } + const updatedConveyor: SimulationTypes.ConveyorEventsSchema = { + ...state, + points: state.points.map((point) => { + return { + ...point, + connections: { + ...point.connections, + targets: point.connections.targets.filter( + (target) => !deletedModelUUIDs.includes(target.modelUUID) + ), + }, + }; + }), + }; + return updatedConveyor; } - // ArmBot type - if (state.type === "ArmBot") { - const sourceMatch = modelUUIDs.includes( - state.points.connections?.source?.modelUUID - ); - console.log("model", modelUUIDs); - console.log("state.points.connections: ", state.points); - - const targetMatch = state.points.connections?.targets?.some( - (target: any) => modelUUIDs.includes(target.modelUUID) - ); - // state.points.actions.processes = state.points.actions.processes.filter( - // (process: any) => - // console.log( - // !modelUUIDs.includes(process.startPoint), - // !modelUUIDs.includes(process.endPoint), - // modelUUIDs, - // process.startPoint, - // process.endPoint - // ) - // ); - - // shouldInclude = true; - - // const processMatches = state.points.actions?.processes?.some( - // (process: any) => - // console.log( - // "process: ", - // process, - // modelUUIDs, - // process.startPoint, - // process.endPoint - // ) - // // modelUUIDs.includes(process.startPoint) || - // // modelUUIDs.includes(process.endPoint) - // ); - // const processMatch = state.points.actions?.processes?.some( - // (process: any) => - // modelUUIDs.includes(String(process.startPoint)) || - // modelUUIDs.includes(String(process.endPoint)) - // ); - - // console.log("processMatch: ", processMatch); - if (sourceMatch) { - state.points.connections.source = {}; - shouldInclude = true; - } - - if (targetMatch) { - state.points.connections.targets = - state.points.connections.targets.filter( - (target: any) => !modelUUIDs.includes(target.modelUUID) - ); - shouldInclude = true; - } - - // console.log("processMatch: ", processMatch); - // if (processMatch) { - // state.points.actions.processes = - // state.points.actions.processes.filter((process: any) => { - // const shouldRemove = - // modelUUIDs.includes(process.startPoint) || - // modelUUIDs.includes(process.endPoint); - // console.log("shouldRemove: ", shouldRemove); - // return !shouldRemove; - // }); - // shouldInclude = true; - // } + // Handle Vehicle + else if (state.type === "Vehicle") { + const updatedVehicle: SimulationTypes.VehicleEventsSchema = { + ...state, + points: { + ...state.points, + connections: { + ...state.points.connections, + targets: state.points.connections.targets.filter( + (target) => !deletedModelUUIDs.includes(target.modelUUID) + ), + }, + }, + }; + return updatedVehicle; } - return shouldInclude ? [state] : []; + // Handle StaticMachine + else if (state.type === "StaticMachine") { + const updatedStaticMachine: SimulationTypes.StaticMachineEventsSchema = + { + ...state, + points: { + ...state.points, + connections: { + ...state.points.connections, + targets: state.points.connections.targets.filter( + (target) => !deletedModelUUIDs.includes(target.modelUUID) + ), + }, + }, + }; + return updatedStaticMachine; + } + + // Handle ArmBot + else if (state.type === "ArmBot") { + const updatedArmBot: SimulationTypes.ArmBotEventsSchema = { + ...state, + points: { + ...state.points, + connections: { + ...state.points.connections, + + targets: state.points.connections.targets.filter( + (target: any) => !deletedModelUUIDs.includes(target.modelUUID) + ), + }, + actions: { + ...state.points.actions, + processes: (state.points.actions.processes = + state.points.actions.processes?.filter((process) => { + const matchedStates = simulationStates.filter((s) => + deletedModelUUIDs.includes(s.modeluuid) + ); + + if (matchedStates.length > 0) { + if (matchedStates[0]?.type === "StaticMachine") { + const trigPoints = matchedStates[0]?.points; + + return !( + process.triggerId === trigPoints?.triggers?.uuid + ); + } else if (matchedStates[0]?.type === "Conveyor") { + const trigPoints = matchedStates[0]?.points; + + if (Array.isArray(trigPoints)) { + const nonEmptyTriggers = trigPoints.filter( + (point) => + point && point.triggers && point.triggers.length > 0 + ); + + const allTriggerUUIDs = nonEmptyTriggers + .flatMap((point) => point.triggers) + .map((trigger) => trigger.uuid); + + return !allTriggerUUIDs.includes(process.triggerId); + } + } + } + return true; + })), + }, + }, + }; + return updatedArmBot; + } + + return state; }); - updateBackend(removedPath); - return removedPath; + const filteredStates = updatedStates.filter( + (state) => !deletedModelUUIDs.includes(state.modeluuid) + ); + + updateBackend(filteredStates); + setSimulationStates(filteredStates); }; const deleteSelection = () => { @@ -633,51 +510,9 @@ const SelectionControls: React.FC = () => { itemsGroupRef.current?.remove(selectedMesh); }); + console.log("selectedAssets: ", selectedAssets); const allUUIDs = selectedAssets.map((val: any) => val.uuid); - removeConnection(allUUIDs); - - // const removedPath = simulationStates?.flatMap((path: any) => { - // let shouldInclude = false; - - // if (Array.isArray(path.points)) { - // path.points.forEach((point: any) => { - // const sourceMatch = - // point.connections?.source?.modelUUID === selectedAssets[0].uuid; - // const targetMatch = point.connections?.targets?.some( - // (target: any) => target.modelUUID === selectedAssets[0].uuid - // ); - - // if (sourceMatch) { - // point.connections.source = {}; - // shouldInclude = true; - // } - - // if (targetMatch) { - // point.connections.targets = []; - // shouldInclude = true; - // } - // }); - // } else { - // const sourceMatch = - // path.connections?.source?.modelUUID === selectedAssets[0].uuid; - // const targetMatch = path.connections?.targets?.some( - // (target: any) => target.modelUUID === selectedAssets[0].uuid - // ); - - // if (sourceMatch) { - // path.connections.source = {}; - // shouldInclude = true; - // } - - // if (targetMatch) { - // path.connections.targets = []; - // shouldInclude = true; - // } - // } - - // return shouldInclude ? [path] : []; - // }); - // updateBackend(removedPath); + removeConnections(allUUIDs); const updatedItems = floorItems.filter( (item: { modeluuid: string }) => !selectedUUIDs.includes(item.modeluuid) From f62d231a79e6709fedd6ee94034ac13c53091866 Mon Sep 17 00:00:00 2001 From: Poovizhi99 <poovizhi@hexrfactory.com> Date: Tue, 15 Apr 2025 15:39:05 +0530 Subject: [PATCH 04/20] integrated the removeConnections in path connector --- .../modules/simulation/path/pathConnector.tsx | 158 +++++++++++++++--- 1 file changed, 139 insertions(+), 19 deletions(-) diff --git a/app/src/modules/simulation/path/pathConnector.tsx b/app/src/modules/simulation/path/pathConnector.tsx index 1e93bd5..fe3bb1b 100644 --- a/app/src/modules/simulation/path/pathConnector.tsx +++ b/app/src/modules/simulation/path/pathConnector.tsx @@ -1133,26 +1133,27 @@ function PathConnector({ (state.modeluuid === connection2.model && state.points.uuid === connection2.point) ) { - const updatedStaticMachine: SimulationTypes.StaticMachineEventsSchema = { - ...state, - points: { - ...state.points, - connections: { - ...state.points.connections, - targets: state.points.connections.targets.filter((target) => { - return !( - (target.modelUUID === connection1.model && - target.pointUUID === connection1.point) || - (target.modelUUID === connection2.model && - target.pointUUID === connection2.point) - ); - }), + const updatedStaticMachine: SimulationTypes.StaticMachineEventsSchema = + { + ...state, + points: { + ...state.points, + connections: { + ...state.points.connections, + targets: state.points.connections.targets.filter((target) => { + return !( + (target.modelUUID === connection1.model && + target.pointUUID === connection1.point) || + (target.modelUUID === connection2.model && + target.pointUUID === connection2.point) + ); + }), + }, + // Ensure all required StaticMachine point properties are included + actions: state.points.actions, + triggers: state.points.triggers, }, - // Ensure all required StaticMachine point properties are included - actions: state.points.actions, - triggers: state.points.triggers, - }, - }; + }; return updatedStaticMachine; } } @@ -1211,6 +1212,125 @@ function PathConnector({ setSimulationStates(updatedStates); }; + const removeConnection = (deletedModelUUIDs: string[]) => { + const updatedStates = simulationStates.map((state) => { + // Handle Conveyor + if (state.type === "Conveyor") { + const updatedConveyor: SimulationTypes.ConveyorEventsSchema = { + ...state, + points: state.points.map((point) => { + return { + ...point, + connections: { + ...point.connections, + targets: point.connections.targets.filter( + (target) => !deletedModelUUIDs.includes(target.modelUUID) + ), + }, + }; + }), + }; + return updatedConveyor; + } + + // Handle Vehicle + else if (state.type === "Vehicle") { + const updatedVehicle: SimulationTypes.VehicleEventsSchema = { + ...state, + points: { + ...state.points, + connections: { + ...state.points.connections, + targets: state.points.connections.targets.filter( + (target) => !deletedModelUUIDs.includes(target.modelUUID) + ), + }, + }, + }; + return updatedVehicle; + } + + // Handle StaticMachine + else if (state.type === "StaticMachine") { + const updatedStaticMachine: SimulationTypes.StaticMachineEventsSchema = + { + ...state, + points: { + ...state.points, + connections: { + ...state.points.connections, + targets: state.points.connections.targets.filter( + (target) => !deletedModelUUIDs.includes(target.modelUUID) + ), + }, + }, + }; + return updatedStaticMachine; + } + + // Handle ArmBot + else if (state.type === "ArmBot") { + const updatedArmBot: SimulationTypes.ArmBotEventsSchema = { + ...state, + points: { + ...state.points, + connections: { + ...state.points.connections, + + targets: state.points.connections.targets.filter( + (target: any) => !deletedModelUUIDs.includes(target.modelUUID) + ), + }, + actions: { + ...state.points.actions, + processes: (state.points.actions.processes = + state.points.actions.processes?.filter((process) => { + const matchedStates = simulationStates.filter((s) => + deletedModelUUIDs.includes(s.modeluuid) + ); + + if (matchedStates.length > 0) { + if (matchedStates[0]?.type === "StaticMachine") { + const trigPoints = matchedStates[0]?.points; + + return !( + process.triggerId === trigPoints?.triggers?.uuid + ); + } else if (matchedStates[0]?.type === "Conveyor") { + const trigPoints = matchedStates[0]?.points; + + if (Array.isArray(trigPoints)) { + const nonEmptyTriggers = trigPoints.filter( + (point) => + point && point.triggers && point.triggers.length > 0 + ); + + const allTriggerUUIDs = nonEmptyTriggers + .flatMap((point) => point.triggers) + .map((trigger) => trigger.uuid); + + return !allTriggerUUIDs.includes(process.triggerId); + } + } + } + return true; + })), + }, + }, + }; + return updatedArmBot; + } + + return state; + }); + + const filteredStates = updatedStates.filter( + (state) => !deletedModelUUIDs.includes(state.modeluuid) + ); + + updateBackend(filteredStates); + setSimulationStates(filteredStates); + }; return ( <group name="simulationConnectionGroup" visible={!isPlaying}> From db9c9fb8b5f7b36a806d4c1f17a358dcf7baaea2 Mon Sep 17 00:00:00 2001 From: Gomathi9520 <gomathi@hexrfactory.com> Date: Tue, 15 Apr 2025 18:28:37 +0530 Subject: [PATCH 05/20] duplicate zone rename bug resolved --- .../properties/ZoneProperties.tsx | 12 +++-- app/src/components/ui/inputs/RenameInput.tsx | 50 +++++++++++++------ app/src/components/ui/list/List.tsx | 42 +++++++++++++--- .../modules/builder/agv/navMeshCreator.tsx | 2 +- app/src/modules/visualization/DisplayZone.tsx | 4 +- .../floating/DroppedFloatingWidgets.tsx | 26 +++++----- app/src/styles/components/input.scss | 15 +++--- 7 files changed, 101 insertions(+), 50 deletions(-) diff --git a/app/src/components/layout/sidebarRight/properties/ZoneProperties.tsx b/app/src/components/layout/sidebarRight/properties/ZoneProperties.tsx index c36b656..ab1430c 100644 --- a/app/src/components/layout/sidebarRight/properties/ZoneProperties.tsx +++ b/app/src/components/layout/sidebarRight/properties/ZoneProperties.tsx @@ -62,19 +62,25 @@ const ZoneProperties: React.FC = () => { : zone ) ); - }else{ + } else { // console.log(response?.message); } } function handleVectorChange(key: "zoneViewPortTarget" | "zoneViewPortPosition", newValue: [number, number, number]) { setSelectedZone((prev) => ({ ...prev, [key]: newValue })); } - + const checkZoneNameDuplicate = (name: string) => { + return zones.some( + (zone: any) => + zone.zoneName.trim().toLowerCase() === name.trim().toLowerCase() && + zone.zoneId !== selectedZone.zoneId + ); + }; return ( <div className="zone-properties-container"> <div className="header"> - <RenameInput value={selectedZone.zoneName} onRename={handleZoneNameChange} /> + <RenameInput value={selectedZone.zoneName} onRename={handleZoneNameChange} checkDuplicate={checkZoneNameDuplicate} /> <div className="button" onClick={handleEditView}> {Edit ? "Cancel" : "Edit"} </div> diff --git a/app/src/components/ui/inputs/RenameInput.tsx b/app/src/components/ui/inputs/RenameInput.tsx index d197dd4..593e1f1 100644 --- a/app/src/components/ui/inputs/RenameInput.tsx +++ b/app/src/components/ui/inputs/RenameInput.tsx @@ -1,26 +1,42 @@ import React, { useState, useRef, useEffect } from "react"; +// interface RenameInputProps { +// value: string; +// onRename?: (newText: string) => void; +// } + interface RenameInputProps { value: string; onRename?: (newText: string) => void; + checkDuplicate?: (name: string) => boolean; } -const RenameInput: React.FC<RenameInputProps> = ({ value, onRename }) => { +const RenameInput: React.FC<RenameInputProps> = ({ value, onRename, checkDuplicate }) => { const [isEditing, setIsEditing] = useState(false); const [text, setText] = useState(value); + const [isDuplicate, setIsDuplicate] = useState(false); const inputRef = useRef<HTMLInputElement | null>(null); + useEffect(() => { - setText(value); // Ensure state updates when parent value changes + setText(value); }, [value]); + useEffect(() => { + if (checkDuplicate) { + setIsDuplicate(checkDuplicate(text)); + } + }, [text, checkDuplicate]); + const handleDoubleClick = () => { setIsEditing(true); - setTimeout(() => inputRef.current?.focus(), 0); // Focus the input after rendering + setTimeout(() => inputRef.current?.focus(), 0); }; const handleBlur = () => { + + if(isDuplicate) return setIsEditing(false); - if (onRename) { + if (onRename && !isDuplicate) { onRename(text); } }; @@ -30,7 +46,7 @@ const RenameInput: React.FC<RenameInputProps> = ({ value, onRename }) => { }; const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { - if (e.key === "Enter") { + if (e.key === "Enter" && !isDuplicate) { setIsEditing(false); if (onRename) { onRename(text); @@ -41,15 +57,18 @@ const RenameInput: React.FC<RenameInputProps> = ({ value, onRename }) => { return ( <> {isEditing ? ( - <input - ref={inputRef} - type="text" - value={text} - onChange={handleChange} - onBlur={handleBlur} - onKeyDown={handleKeyDown} - className="rename-input" - /> + <> + <input + ref={inputRef} + type="text" + value={text} + onChange={handleChange} + onBlur={handleBlur} + onKeyDown={handleKeyDown} + className={`rename-input ${isDuplicate ? "input-error" : ""}`} + /> + {/* {isDuplicate && <div className="error-msg">Name already exists!</div>} */} + </> ) : ( <span onDoubleClick={handleDoubleClick} className="input-value"> {text} @@ -58,5 +77,4 @@ const RenameInput: React.FC<RenameInputProps> = ({ value, onRename }) => { </> ); }; - -export default RenameInput; +export default RenameInput diff --git a/app/src/components/ui/list/List.tsx b/app/src/components/ui/list/List.tsx index 06bc129..49e86f4 100644 --- a/app/src/components/ui/list/List.tsx +++ b/app/src/components/ui/list/List.tsx @@ -13,7 +13,7 @@ import { RmoveIcon, } from "../../icons/ExportCommonIcons"; import { useThree } from "@react-three/fiber"; -import { useFloorItems, useZoneAssetId } from "../../../store/store"; +import { useFloorItems, useZoneAssetId, useZones } from "../../../store/store"; import { zoneCameraUpdate } from "../../../services/realTimeVisulization/zoneData/zoneCameraUpdation"; import { setFloorItemApi } from "../../../services/factoryBuilder/assest/floorAsset/setFloorItemApi"; @@ -40,7 +40,7 @@ const List: React.FC<ListProps> = ({ items = [], remove }) => { const { activeModule, setActiveModule } = useModuleStore(); const { selectedZone, setSelectedZone } = useSelectedZoneStore(); const { zoneAssetId, setZoneAssetId } = useZoneAssetId(); - + const { zones, setZones } = useZones(); const { setSubModule } = useSubModuleStore(); const [expandedZones, setExpandedZones] = useState<Record<string, boolean>>( {} @@ -100,19 +100,33 @@ const List: React.FC<ListProps> = ({ items = [], remove }) => { function handleAssetClick(asset: Asset) { setZoneAssetId(asset) } + async function handleZoneNameChange(newName: string) { - //zone apiiiiii const email = localStorage.getItem("email") || ""; const organization = email?.split("@")[1]?.split(".")[0]; - let zonesdata = { + + const isDuplicate = zones.some( + (zone: any) => + zone.zoneName.trim().toLowerCase() === newName.trim().toLowerCase() && + zone.zoneId !== selectedZone.zoneId + ); + + if (isDuplicate) { + alert("Zone name already exists. Please choose a different name."); + return; // DO NOT update state + } + + const zonesdata = { zoneId: selectedZone.zoneId, - zoneName: newName + zoneName: newName, }; - let response = await zoneCameraUpdate(zonesdata, organization); + + const response = await zoneCameraUpdate(zonesdata, organization); if (response.message === "updated successfully") { setSelectedZone((prev) => ({ ...prev, zoneName: newName })); } } + async function handleZoneAssetName(newName: string) { const email = localStorage.getItem("email") || ""; const organization = email?.split("@")[1]?.split(".")[0]; @@ -128,10 +142,17 @@ const List: React.FC<ListProps> = ({ items = [], remove }) => { ) ); } - + console.log('newName: ', newName); } + const checkZoneNameDuplicate = (name: string) => { + return zones.some( + (zone: any) => + zone.zoneName.trim().toLowerCase() === name.trim().toLowerCase() && + zone.zoneId !== selectedZone.zoneId + ); + }; return ( <> @@ -146,7 +167,12 @@ const List: React.FC<ListProps> = ({ items = [], remove }) => { className="value" onClick={() => handleSelectZone(item.id)} > - <RenameInput value={item.name} onRename={handleZoneNameChange} /> + <RenameInput + value={item.name} + onRename={handleZoneNameChange} + checkDuplicate={checkZoneNameDuplicate} + /> + </div> </div> <div className="options-container"> diff --git a/app/src/modules/builder/agv/navMeshCreator.tsx b/app/src/modules/builder/agv/navMeshCreator.tsx index cdbca45..c0f8808 100644 --- a/app/src/modules/builder/agv/navMeshCreator.tsx +++ b/app/src/modules/builder/agv/navMeshCreator.tsx @@ -19,7 +19,7 @@ function NavMeshCreator({ lines }: NavMeshCreatorProps) { <NavMeshDetails lines={lines} setNavMesh={setNavMesh} groupRef={groupRef} /> <group ref={groupRef} visible={false} name="Meshes"> - <mesh rotation-x={CONSTANTS.planeConfig.rotation} position={CONSTANTS.planeConfig.position3D} name="Plane" receiveShadow> + <mesh rotation-x={CONSTANTS.planeConfig.rotation} position={CONSTANTS.planeConfig.position3D} receiveShadow> <planeGeometry args={[300, 300]} /> <meshBasicMaterial color={CONSTANTS.planeConfig.color} /> </mesh> diff --git a/app/src/modules/visualization/DisplayZone.tsx b/app/src/modules/visualization/DisplayZone.tsx index dfe68f6..e2f3b2f 100644 --- a/app/src/modules/visualization/DisplayZone.tsx +++ b/app/src/modules/visualization/DisplayZone.tsx @@ -104,8 +104,8 @@ const DisplayZone: React.FC<DisplayZoneProps> = ({ setShowLeftArrow(isOverflowing && canScrollLeft); setShowRightArrow(isOverflowing && canScrollRight); - console.log('canScrollRight: ', canScrollRight); - console.log('isOverflowing: ', isOverflowing); + // console.log('canScrollRight: ', canScrollRight); + // console.log('isOverflowing: ', isOverflowing); } }, []); diff --git a/app/src/modules/visualization/widgets/floating/DroppedFloatingWidgets.tsx b/app/src/modules/visualization/widgets/floating/DroppedFloatingWidgets.tsx index 62c1de7..6d12ad8 100644 --- a/app/src/modules/visualization/widgets/floating/DroppedFloatingWidgets.tsx +++ b/app/src/modules/visualization/widgets/floating/DroppedFloatingWidgets.tsx @@ -520,37 +520,37 @@ const DroppedObjects: React.FC = () => { onPointerUp={handlePointerUp} className="floating-wrapper" > - {zone.objects.map((obj, index) => { + {zone?.objects?.map((obj, index) => { const topPosition = - typeof obj.position.top === "number" - ? `calc(${obj.position.top}px + ${ - isPlaying && selectedZone.activeSides.includes("top") + typeof obj?.position?.top === "number" + ? `calc(${obj?.position?.top}px + ${ + isPlaying && selectedZone?.activeSides?.includes("top") ? `${heightMultiplier - 55}px` : "0px" })` : "auto"; const leftPosition = - typeof obj.position.left === "number" - ? `calc(${obj.position.left}px + ${ - isPlaying && selectedZone.activeSides.includes("left") + typeof obj?.position?.left === "number" + ? `calc(${obj?.position?.left}px + ${ + isPlaying && selectedZone?.activeSides?.includes("left") ? `${widthMultiplier - 150}px` : "0px" })` : "auto"; const rightPosition = - typeof obj.position.right === "number" - ? `calc(${obj.position.right}px + ${ - isPlaying && selectedZone.activeSides.includes("right") + typeof obj?.position?.right === "number" + ? `calc(${obj?.position?.right}px + ${ + isPlaying && selectedZone?.activeSides?.includes("right") ? `${widthMultiplier - 150}px` : "0px" })` : "auto"; const bottomPosition = - typeof obj.position.bottom === "number" - ? `calc(${obj.position.bottom}px + ${ - isPlaying && selectedZone.activeSides.includes("bottom") + typeof obj?.position?.bottom === "number" + ? `calc(${obj?.position?.bottom}px + ${ + isPlaying && selectedZone?.activeSides?.includes("bottom") ? `${heightMultiplier - 55}px` : "0px" })` diff --git a/app/src/styles/components/input.scss b/app/src/styles/components/input.scss index 0c89cc5..883f6c2 100644 --- a/app/src/styles/components/input.scss +++ b/app/src/styles/components/input.scss @@ -56,6 +56,12 @@ input { padding: 0 8px; } +.input-error { + border: 1px solid #f65648 !important; + outline: none !important; + color: #f65648; +} + .toggle-header-container { @include flex-center; padding: 6px 12px; @@ -344,7 +350,6 @@ input { padding: 10px; } - .loading { position: absolute; bottom: 0; @@ -364,9 +369,7 @@ input { left: -50%; height: 100%; width: 50%; - background: linear-gradient(to right, - var(--accent-color), - transparent); + background: linear-gradient(to right, var(--accent-color), transparent); animation: loadingAnimation 1.2s linear infinite; border-radius: 4px; } @@ -381,8 +384,6 @@ input { } } - - .dropdown-item { display: block; padding: 5px 10px; @@ -710,4 +711,4 @@ input { .multi-email-invite-input.active { border: 1px solid var(--accent-color); } -} \ No newline at end of file +} From c7147773c56633176dace562eea88f36e2dbd722 Mon Sep 17 00:00:00 2001 From: SreeNath14 <153710861+SreeNath14@users.noreply.github.com> Date: Tue, 15 Apr 2025 18:34:38 +0530 Subject: [PATCH 06/20] "updated arm logic" --- .../mechanics/ArmBotMechanics.tsx | 1 + app/src/modules/builder/agv/pathNavigator.tsx | 116 +- .../simulation/process/processAnimator.tsx | 52 +- .../simulation/process/processCreator.tsx | 508 +------ app/src/modules/simulation/process/types.ts | 16 +- .../process/useProcessAnimations.tsx | 1196 +++++++++-------- app/src/modules/simulation/simulation.tsx | 2 + 7 files changed, 822 insertions(+), 1069 deletions(-) diff --git a/app/src/components/layout/sidebarRight/mechanics/ArmBotMechanics.tsx b/app/src/components/layout/sidebarRight/mechanics/ArmBotMechanics.tsx index e157d07..8b05525 100644 --- a/app/src/components/layout/sidebarRight/mechanics/ArmBotMechanics.tsx +++ b/app/src/components/layout/sidebarRight/mechanics/ArmBotMechanics.tsx @@ -150,6 +150,7 @@ const ArmBotMechanics: React.FC = () => { modeluuid: updatedPath.modeluuid, eventData: { type: "ArmBot", points: updatedPath.points } } + console.log('data: ', data); socket.emit('v2:model-asset:updateEventData', data); } diff --git a/app/src/modules/builder/agv/pathNavigator.tsx b/app/src/modules/builder/agv/pathNavigator.tsx index 9a6fae7..a81925f 100644 --- a/app/src/modules/builder/agv/pathNavigator.tsx +++ b/app/src/modules/builder/agv/pathNavigator.tsx @@ -3,7 +3,10 @@ import * as THREE from "three"; import { useFrame, useThree } from "@react-three/fiber"; import { NavMeshQuery } from "@recast-navigation/core"; import { Line } from "@react-three/drei"; -import { useAnimationPlaySpeed, usePlayButtonStore } from "../../../store/usePlayButtonStore"; +import { + useAnimationPlaySpeed, + usePlayButtonStore, +} from "../../../store/usePlayButtonStore"; import { usePlayAgv } from "../../../store/store"; interface PathNavigatorProps { @@ -11,7 +14,7 @@ interface PathNavigatorProps { pathPoints: any; id: string; speed: number; - globalSpeed: number, + globalSpeed: number; bufferTime: number; hitCount: number; processes: any[]; @@ -40,11 +43,21 @@ export default function PathNavigator({ }: PathNavigatorProps) { const [currentPhase, setCurrentPhase] = useState<Phase>("initial"); const [path, setPath] = useState<[number, number, number][]>([]); - const [toPickupPath, setToPickupPath] = useState<[number, number, number][]>([]); - const [pickupDropPath, setPickupDropPath] = useState<[number, number, number][]>([]); - const [dropPickupPath, setDropPickupPath] = useState<[number, number, number][]>([]); - const [initialPosition, setInitialPosition] = useState<THREE.Vector3 | null>(null); - const [initialRotation, setInitialRotation] = useState<THREE.Euler | null>(null); + const [toPickupPath, setToPickupPath] = useState<[number, number, number][]>( + [] + ); + const [pickupDropPath, setPickupDropPath] = useState< + [number, number, number][] + >([]); + const [dropPickupPath, setDropPickupPath] = useState< + [number, number, number][] + >([]); + const [initialPosition, setInitialPosition] = useState<THREE.Vector3 | null>( + null + ); + const [initialRotation, setInitialRotation] = useState<THREE.Euler | null>( + null + ); const [boxVisible, setBoxVisible] = useState(false); const distancesRef = useRef<number[]>([]); @@ -61,11 +74,14 @@ export default function PathNavigator({ const boxRef = useRef<THREE.Mesh | null>(null); - const baseMaterials = useMemo(() => ({ - Box: new THREE.MeshStandardMaterial({ color: 0x8b4513 }), - Crate: new THREE.MeshStandardMaterial({ color: 0x00ff00 }), - Default: new THREE.MeshStandardMaterial({ color: 0xcccccc }) - }), []); + const baseMaterials = useMemo( + () => ({ + Box: new THREE.MeshStandardMaterial({ color: 0x8b4513 }), + Crate: new THREE.MeshStandardMaterial({ color: 0x00ff00 }), + Default: new THREE.MeshStandardMaterial({ color: 0xcccccc }), + }), + [] + ); useEffect(() => { const object = scene.getObjectByProperty("uuid", id); @@ -135,7 +151,11 @@ export default function PathNavigator({ const pickupToDropPath = computePath(pickup, drop); const dropToPickupPath = computePath(drop, pickup); - if (toPickupPath.length && pickupToDropPath.length && dropToPickupPath.length) { + if ( + toPickupPath.length && + pickupToDropPath.length && + dropToPickupPath.length + ) { setPickupDropPath(pickupToDropPath); setDropPickupPath(dropToPickupPath); setToPickupPath(toPickupPath); @@ -163,7 +183,10 @@ export default function PathNavigator({ }, [path]); function logAgvStatus(id: string, status: string) { - // console.log(`AGV ${id}: ${status}`); + // console.log( + // `AGV ${id}: ${status}` + + // ); } function findProcessByTargetModelUUID(processes: any, targetModelUUID: any) { @@ -223,7 +246,9 @@ export default function PathNavigator({ }, [processes, MaterialRef, boxVisible, scene, id, baseMaterials]); useFrame((_, delta) => { - const currentAgv = (agvRef.current || []).find((agv: AGVData) => agv.vehicleId === id); + const currentAgv = (agvRef.current || []).find( + (agv: AGVData) => agv.vehicleId === id + ); if (!scene || !id || !isPlaying) return; @@ -243,6 +268,7 @@ export default function PathNavigator({ const isAgvReady = () => { if (!agvRef.current || agvRef.current.length === 0) return false; if (!currentAgv) return false; + return currentAgv.isActive && hitCount >= currentAgv.maxHitCount; }; @@ -266,12 +292,19 @@ export default function PathNavigator({ } if (isPlaying && currentPhase === "initial" && !hasReachedPickup.current) { - const reached = moveAlongPath(object, path, distancesRef.current, speed, delta, progressRef); + const reached = moveAlongPath( + object, + path, + distancesRef.current, + speed, + delta, + progressRef + ); if (reached) { hasReachedPickup.current = true; if (currentAgv) { - currentAgv.status = 'picking'; + currentAgv.status = "picking"; } logAgvStatus(id, "Reached pickup point, Waiting for material"); } @@ -287,20 +320,28 @@ export default function PathNavigator({ progressRef.current = 0; logAgvStatus(id, "Started from pickup point, heading to drop point"); if (currentAgv) { - currentAgv.status = 'toDrop'; + currentAgv.status = "toDrop"; } - }, 0) + }, 0); return; } if (isPlaying && currentPhase === "toDrop") { - const reached = moveAlongPath(object, path, distancesRef.current, speed, delta, progressRef); + const reached = moveAlongPath( + object, + path, + distancesRef.current, + speed, + delta, + progressRef + ); if (reached && !isWaiting.current) { isWaiting.current = true; logAgvStatus(id, "Reached drop point"); if (currentAgv) { - currentAgv.status = 'droping'; + currentAgv.status = "droping"; + currentAgv.hitCount = currentAgv.hitCount--; } timeoutRef.current = setTimeout(() => { setPath([...dropPickupPath]); @@ -309,16 +350,26 @@ export default function PathNavigator({ isWaiting.current = false; setBoxVisible(false); if (currentAgv) { - currentAgv.status = 'toPickup'; + currentAgv.status = "toPickup"; } - logAgvStatus(id, "Started from droping point, heading to pickup point"); + logAgvStatus( + id, + "Started from droping point, heading to pickup point" + ); }, bufferTime * 1000); } return; } if (isPlaying && currentPhase === "toPickup") { - const reached = moveAlongPath(object, path, distancesRef.current, speed, delta, progressRef); + const reached = moveAlongPath( + object, + path, + distancesRef.current, + speed, + delta, + progressRef + ); if (reached) { if (currentAgv) { @@ -326,14 +377,21 @@ export default function PathNavigator({ } setCurrentPhase("initial"); if (currentAgv) { - currentAgv.status = 'picking'; + currentAgv.status = "picking"; } logAgvStatus(id, "Reached pickup point again, cycle complete"); } return; } - moveAlongPath(object, path, distancesRef.current, speed, delta, progressRef); + moveAlongPath( + object, + path, + distancesRef.current, + speed, + delta, + progressRef + ); }); function moveAlongPath( @@ -379,7 +437,11 @@ export default function PathNavigator({ const targetRotationY = Math.atan2(targetDirection.x, targetDirection.z); const rotationSpeed = Math.min(5 * delta, 1); - object.rotation.y = THREE.MathUtils.lerp(object.rotation.y, targetRotationY, rotationSpeed); + object.rotation.y = THREE.MathUtils.lerp( + object.rotation.y, + targetRotationY, + rotationSpeed + ); } return false; diff --git a/app/src/modules/simulation/process/processAnimator.tsx b/app/src/modules/simulation/process/processAnimator.tsx index c1063c3..65bc4c5 100644 --- a/app/src/modules/simulation/process/processAnimator.tsx +++ b/app/src/modules/simulation/process/processAnimator.tsx @@ -8,8 +8,7 @@ import crate from "../../../assets/gltf-glb/crate_box.glb"; import { useProcessAnimation } from "./useProcessAnimations"; import ProcessObject from "./processObject"; import { ProcessData } from "./types"; -import { useSimulationStates } from "../../../store/store"; -import { retrieveGLTF } from "../../../utils/indexDB/idbUtils"; + interface ProcessContainerProps { processes: ProcessData[]; @@ -44,19 +43,23 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ checkAndCountTriggers, } = useProcessAnimation(processes, setProcesses, agvRef); - const baseMaterials = useMemo(() => ({ - Box: new THREE.MeshStandardMaterial({ color: 0x8b4513 }), - Crate: new THREE.MeshStandardMaterial({ color: 0x00ff00 }), - Default: new THREE.MeshStandardMaterial(), - }), []); + const baseMaterials = useMemo( + () => ({ + Box: new THREE.MeshStandardMaterial({ color: 0x8b4513 }), + Crate: new THREE.MeshStandardMaterial({ color: 0x00ff00 }), + Default: new THREE.MeshStandardMaterial(), + }), + [] + ); useEffect(() => { // Update material references for all spawned objects Object.entries(animationStates).forEach(([processId, processState]) => { Object.keys(processState.spawnedObjects).forEach((objectId) => { - const entry = { processId, objectId, }; + const entry = { processId, objectId }; - const materialType = processState.spawnedObjects[objectId]?.currentMaterialType; + const materialType = + processState.spawnedObjects[objectId]?.currentMaterialType; if (!materialType) { return; @@ -65,13 +68,17 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ const matRefArray = MaterialRef.current; // Find existing material group - const existing = matRefArray.find((entryGroup: { material: string; objects: any[] }) => - entryGroup.material === materialType + const existing = matRefArray.find( + (entryGroup: { material: string; objects: any[] }) => + entryGroup.material === materialType ); if (existing) { // Check if this processId + objectId already exists - const alreadyExists = existing.objects.some((o: any) => o.processId === entry.processId && o.objectId === entry.objectId); + const alreadyExists = existing.objects.some( + (o: any) => + o.processId === entry.processId && o.objectId === entry.objectId + ); if (!alreadyExists) { existing.objects.push(entry); @@ -91,7 +98,8 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ useFrame(() => { // Spawn logic frame - const currentTime = clockRef.current.getElapsedTime() - elapsedBeforePauseRef.current; + const currentTime = + clockRef.current.getElapsedTime() - elapsedBeforePauseRef.current; setAnimationStates((prev) => { const newStates = { ...prev }; @@ -119,7 +127,10 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ : parseFloat(spawnAction.spawnInterval as string) || 0; // Check if this is a zero interval spawn and we already spawned an object - if (spawnInterval === 0 && processState.hasSpawnedZeroIntervalObject === true) { + if ( + spawnInterval === 0 && + processState.hasSpawnedZeroIntervalObject === true + ) { return; // Don't spawn more objects for zero interval } @@ -324,10 +335,13 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ if (isLastPoint) { const isAgvPicking = agvRef.current.some( - (agv: any) => agv.processId === process.id && agv.status === "picking" + (agv: any) => + agv.processId === process.id && agv.status === "picking" ); - const shouldHide = !currentPointData?.actions || !hasNonInheritActions(currentPointData.actions); + const shouldHide = + !currentPointData?.actions || + !hasNonInheritActions(currentPointData.actions); if (shouldHide) { if (isAgvPicking) { @@ -358,7 +372,8 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ if (tempStackedObjectsRef.current[objectId]) { const isAgvPicking = agvRef.current.some( - (agv: any) => agv.processId === process.id && agv.status === "picking" + (agv: any) => + agv.processId === process.id && agv.status === "picking" ); if (isAgvPicking) { @@ -377,7 +392,8 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ if (!isLastPoint) { const nextPoint = path[nextPointIdx]; - const distance = path[stateRef.currentIndex].distanceTo(nextPoint); + const distance = + path[stateRef.currentIndex].distanceTo(nextPoint); const effectiveSpeed = stateRef.speed * speedRef.current; const movement = effectiveSpeed * delta; diff --git a/app/src/modules/simulation/process/processCreator.tsx b/app/src/modules/simulation/process/processCreator.tsx index f7f9974..c46791f 100644 --- a/app/src/modules/simulation/process/processCreator.tsx +++ b/app/src/modules/simulation/process/processCreator.tsx @@ -1,450 +1,3 @@ -// import React, { -// useEffect, -// useMemo, -// useState, -// useCallback, -// useRef, -// } from "react"; -// import { useSimulationStates } from "../../../store/store"; -// import * as THREE from "three"; -// import { useThree } from "@react-three/fiber"; -// import { -// ConveyorEventsSchema, -// VehicleEventsSchema, -// } from "../../../types/world/worldTypes"; - -// // Type definitions -// export interface PointAction { -// uuid: string; -// name: string; -// type: string; -// material: string; -// delay: number | string; -// spawnInterval: string | number; -// isUsed: boolean; -// } - -// export interface PointTrigger { -// uuid: string; -// bufferTime: number; -// name: string; -// type: string; -// isUsed: boolean; -// } - -// export interface PathPoint { -// uuid: string; -// position: [number, number, number]; -// actions: PointAction[]; -// triggers: PointTrigger[]; -// connections: { -// targets: Array<{ modelUUID: string }>; -// }; -// } - -// export interface SimulationPath { -// type: string; -// modeluuid: string; -// points: PathPoint[]; -// pathPosition: [number, number, number]; -// speed?: number; -// } - -// export interface Process { -// id: string; -// paths: SimulationPath[]; -// animationPath: THREE.Vector3[]; -// pointActions: PointAction[][]; -// pointTriggers: PointTrigger[][]; -// speed: number; -// isActive: boolean; -// } - -// interface ProcessCreatorProps { -// onProcessesCreated: (processes: Process[]) => void; -// } - -// // Convert event schemas to SimulationPath -// function convertToSimulationPath( -// path: ConveyorEventsSchema | VehicleEventsSchema -// ): SimulationPath { -// const { modeluuid } = path; - -// // Normalized action handler -// const normalizeAction = (action: any): PointAction => { -// return { ...action }; // Return exact copy with no modifications -// }; - -// // Normalized trigger handler -// const normalizeTrigger = (trigger: any): PointTrigger => { -// return { ...trigger }; // Return exact copy with no modifications -// }; - -// if (path.type === "Conveyor") { -// return { -// type: path.type, -// modeluuid, -// points: path.points.map((point) => ({ -// uuid: point.uuid, -// position: point.position, -// actions: Array.isArray(point.actions) -// ? point.actions.map(normalizeAction) -// : point.actions -// ? [normalizeAction(point.actions)] -// : [], -// triggers: Array.isArray(point.triggers) -// ? point.triggers.map(normalizeTrigger) -// : point.triggers -// ? [normalizeTrigger(point.triggers)] -// : [], -// connections: { -// targets: point.connections.targets.map((target) => ({ -// modelUUID: target.modelUUID, -// })), -// }, -// })), -// pathPosition: path.position, -// speed: -// typeof path.speed === "string" -// ? parseFloat(path.speed) || 1 -// : path.speed || 1, -// }; -// } else { -// // For vehicle paths, handle the case where triggers might not exist -// return { -// type: path.type, -// modeluuid, -// points: [ -// { -// uuid: path.points.uuid, -// position: path.points.position, -// actions: Array.isArray(path.points.actions) -// ? path.points.actions.map(normalizeAction) -// : path.points.actions -// ? [normalizeAction(path.points.actions)] -// : [], -// // For vehicle paths, since triggers might not exist in the schema, -// // we always define default to an empty array -// triggers: [], -// connections: { -// targets: path.points.connections.targets.map((target) => ({ -// modelUUID: target.modelUUID, -// })), -// }, -// }, -// ], -// pathPosition: path.position, -// speed: path.points.speed || 1, -// }; -// } -// } - -// // Helper function to create an empty process -// const createEmptyProcess = (): Process => ({ -// id: `process-${Math.random().toString(36).substring(2, 11)}`, -// paths: [], -// animationPath: [], -// pointActions: [], -// pointTriggers: [], // Added point triggers array -// speed: 1, -// isActive: false, -// }); - -// // Enhanced connection checking function -// function shouldReverseNextPath( -// currentPath: SimulationPath, -// 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]; - -// // Check if current last connects to next last (requires reversal) -// const connectsToLast = currentLastPoint.connections.targets.some( -// (target) => -// target.modelUUID === nextPath.modeluuid && -// nextLastPoint.connections.targets.some( -// (t) => t.modelUUID === currentPath.modeluuid -// ) -// ); - -// // Check if current last connects to next first (no reversal needed) -// const connectsToFirst = currentLastPoint.connections.targets.some( -// (target) => -// target.modelUUID === nextPath.modeluuid && -// nextFirstPoint.connections.targets.some( -// (t) => t.modelUUID === currentPath.modeluuid -// ) -// ); - -// // Only reverse if connected to last point and not to first point -// 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 < 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; -// } - -// // Main hook for process creation -// export function useProcessCreation() { -// const { scene } = useThree(); -// const [processes, setProcesses] = useState<Process[]>([]); - -// const hasSpawnAction = useCallback((path: SimulationPath): boolean => { -// if (path.type !== "Conveyor") return false; -// return path.points.some((point) => -// point.actions.some((action) => action.type.toLowerCase() === "spawn") -// ); -// }, []); - -// const createProcess = useCallback( -// (paths: SimulationPath[]): Process => { -// if (!paths || paths.length === 0) { -// return createEmptyProcess(); -// } - -// const animationPath: THREE.Vector3[] = []; -// const pointActions: PointAction[][] = []; -// const pointTriggers: PointTrigger[][] = []; // Added point triggers collection -// const processSpeed = paths[0]?.speed || 1; - -// for (const path of paths) { -// for (const point of path.points) { -// if (path.type === "Conveyor") { -// const obj = scene.getObjectByProperty("uuid", point.uuid); -// if (!obj) { -// 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); -// pointTriggers.push(point.triggers); // Collect triggers for each point -// } -// } -// } - -// return { -// id: `process-${Math.random().toString(36).substring(2, 11)}`, -// paths, -// animationPath, -// pointActions, -// pointTriggers, -// speed: processSpeed, -// isActive: false, -// }; -// }, -// [scene] -// ); - -// const getAllConnectedPaths = useCallback( -// ( -// initialPath: SimulationPath, -// allPaths: SimulationPath[], -// visited: Set<string> = new Set() -// ): SimulationPath[] => { -// const connectedPaths: SimulationPath[] = []; -// const queue: SimulationPath[] = [initialPath]; -// visited.add(initialPath.modeluuid); - -// const pathMap = new Map<string, SimulationPath>(); -// allPaths.forEach((path) => pathMap.set(path.modeluuid, path)); - -// while (queue.length > 0) { -// const currentPath = queue.shift()!; -// connectedPaths.push(currentPath); - -// // Process outgoing connections -// for (const point of currentPath.points) { -// for (const target of point.connections.targets) { -// if (!visited.has(target.modelUUID)) { -// const targetPath = pathMap.get(target.modelUUID); -// if (targetPath) { -// visited.add(target.modelUUID); -// queue.push(targetPath); -// } -// } -// } -// } - -// // Process incoming connections -// for (const [uuid, path] of pathMap) { -// if (!visited.has(uuid)) { -// const hasConnectionToCurrent = path.points.some((point) => -// point.connections.targets.some( -// (t) => t.modelUUID === currentPath.modeluuid -// ) -// ); -// if (hasConnectionToCurrent) { -// visited.add(uuid); -// queue.push(path); -// } -// } -// } -// } - -// return connectedPaths; -// }, -// [] -// ); - -// const createProcessesFromPaths = useCallback( -// (paths: SimulationPath[]): Process[] => { -// if (!paths || paths.length === 0) return []; - -// const visited = new Set<string>(); -// const processes: Process[] = []; -// const pathMap = new Map<string, SimulationPath>(); -// 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); -// const adjustedPaths = adjustPathPointsOrder(connectedPaths); -// const process = createProcess(adjustedPaths); -// processes.push(process); -// } -// } - -// return processes; -// }, -// [createProcess, getAllConnectedPaths, hasSpawnAction] -// ); - -// return { -// processes, -// createProcessesFromPaths, -// setProcesses, -// }; -// } - -// const ProcessCreator: React.FC<ProcessCreatorProps> = React.memo( -// ({ onProcessesCreated }) => { -// const { simulationStates } = useSimulationStates(); -// const { createProcessesFromPaths } = useProcessCreation(); -// const prevPathsRef = useRef<SimulationPath[]>([]); -// const prevProcessesRef = useRef<Process[]>([]); - -// const convertedPaths = useMemo((): SimulationPath[] => { -// if (!simulationStates) return []; -// return simulationStates.map((path) => -// convertToSimulationPath( -// path as ConveyorEventsSchema | VehicleEventsSchema -// ) -// ); -// }, [simulationStates]); - -// // Enhanced dependency tracking that includes action and trigger types -// const pathsDependency = useMemo(() => { -// if (!convertedPaths) return null; -// return convertedPaths.map((path) => ({ -// id: path.modeluuid, -// // Track all action types for each point -// actionSignature: path.points -// .map((point, index) => -// point.actions.map((action) => `${index}-${action.type}`).join("|") -// ) -// .join(","), -// // Track all trigger types for each point -// triggerSignature: path.points -// .map((point, index) => -// point.triggers -// .map((trigger) => `${index}-${trigger.type}`) -// .join("|") -// ) -// .join(","), -// connections: path.points -// .flatMap((p: PathPoint) => -// p.connections.targets.map((t: { modelUUID: string }) => t.modelUUID) -// ) -// .join(","), -// isActive: false, -// })); -// }, [convertedPaths]); - -// // Force process recreation when paths change -// useEffect(() => { -// if (!convertedPaths || convertedPaths.length === 0) { -// if (prevProcessesRef.current.length > 0) { -// onProcessesCreated([]); -// prevProcessesRef.current = []; -// } -// return; -// } - -// // Always regenerate processes if the pathsDependency has changed -// // This ensures action and trigger type changes will be detected -// const newProcesses = createProcessesFromPaths(convertedPaths); -// prevPathsRef.current = convertedPaths; - -// // Always update processes when action or trigger types change -// onProcessesCreated(newProcesses); -// prevProcessesRef.current = newProcesses; -// }, [ -// pathsDependency, // This now includes action and trigger types -// onProcessesCreated, -// convertedPaths, -// createProcessesFromPaths, -// ]); - -// return null; -// } -// ); - -// export default ProcessCreator; - import React, { useEffect, useMemo, @@ -456,6 +9,7 @@ import { useSimulationStates } from "../../../store/store"; import * as THREE from "three"; import { useThree } from "@react-three/fiber"; import { + ArmBotEventsSchema, ConveyorEventsSchema, VehicleEventsSchema, } from "../../../types/simulation"; @@ -480,13 +34,14 @@ export interface PointTrigger { isUsed: boolean; } +// Update the connections type in your interfaces export interface PathPoint { uuid: string; position: [number, number, number]; actions: PointAction[]; triggers: PointTrigger[]; connections: { - targets: Array<{ modelUUID: string }>; + targets: Array<{ modelUUID: string; pointUUID?: string }>; }; } @@ -498,6 +53,14 @@ export interface SimulationPath { speed?: number; isActive: boolean; } +export interface ArmBot { + type: string; + modeluuid: string; + points: PathPoint[]; + pathPosition: [number, number, number]; + speed?: number; + isActive: boolean; +} export interface Process { id: string; @@ -515,7 +78,7 @@ interface ProcessCreatorProps { // Convert event schemas to SimulationPath function convertToSimulationPath( - path: ConveyorEventsSchema | VehicleEventsSchema + path: ConveyorEventsSchema | VehicleEventsSchema | ArmBotEventsSchema ): SimulationPath { const { modeluuid } = path; @@ -539,13 +102,13 @@ function convertToSimulationPath( actions: Array.isArray(point.actions) ? point.actions.map(normalizeAction) : point.actions - ? [normalizeAction(point.actions)] - : [], + ? [normalizeAction(point.actions)] + : [], triggers: Array.isArray(point.triggers) ? point.triggers.map(normalizeTrigger) : point.triggers - ? [normalizeTrigger(point.triggers)] - : [], + ? [normalizeTrigger(point.triggers)] + : [], connections: { targets: point.connections.targets.map((target) => ({ modelUUID: target.modelUUID, @@ -559,6 +122,36 @@ function convertToSimulationPath( : path.speed || 1, isActive: false, // Added missing property }; + } else if (path.type === "ArmBot") { + return { + type: path.type, + modeluuid, + points: [ + { + uuid: path.points.uuid, + position: path.points.position, + actions: Array.isArray(path.points.actions) + ? path.points.actions.map(normalizeAction) + : path.points.actions + ? [normalizeAction(path.points.actions)] + : [], + triggers: Array.isArray(path.points.triggers) + ? path.points.triggers.map(normalizeTrigger) + : path.points.triggers + ? [normalizeTrigger(path.points.triggers)] + : [], + connections: { + targets: path.points.connections.targets.map((target) => ({ + modelUUID: target.modelUUID, + pointUUID: target.pointUUID, // Include if available + })), + }, + }, + ], + pathPosition: path.position, + speed: path.points.actions?.speed || 1, + isActive: false, + }; } else { // For vehicle paths, handle the case where triggers might not exist return { @@ -571,8 +164,8 @@ function convertToSimulationPath( actions: Array.isArray(path.points.actions) ? path.points.actions.map(normalizeAction) : path.points.actions - ? [normalizeAction(path.points.actions)] - : [], + ? [normalizeAction(path.points.actions)] + : [], triggers: [], connections: { targets: path.points.connections.targets.map((target) => ({ @@ -831,7 +424,10 @@ const ProcessCreator: React.FC<ProcessCreatorProps> = React.memo( if (!simulationStates) return []; return simulationStates.map((path) => convertToSimulationPath( - path as ConveyorEventsSchema | VehicleEventsSchema + path as + | ConveyorEventsSchema + | VehicleEventsSchema + | ArmBotEventsSchema ) ); }, [simulationStates]); diff --git a/app/src/modules/simulation/process/types.ts b/app/src/modules/simulation/process/types.ts index 6f935fc..246e780 100644 --- a/app/src/modules/simulation/process/types.ts +++ b/app/src/modules/simulation/process/types.ts @@ -21,15 +21,15 @@ export interface PointAction { } export interface ProcessPoint { - uuid: string; + uuid: string; position: number[]; - rotation: number[]; - actions: PointAction[]; + rotation: number[]; + actions: PointAction[]; connections: { - source: { modelUUID: string; pointUUID: string }; - targets: { modelUUID: string; pointUUID: string }[]; + source: { modelUUID: string; pointUUID: string }; + targets: { modelUUID: string; pointUUID: string }[]; }; - triggers?: Trigger[]; + triggers?: Trigger[]; } export interface ProcessPath { modeluuid: string; @@ -38,8 +38,8 @@ export interface ProcessPath { pathPosition: number[]; pathRotation: number[]; speed: number; - type: "Conveyor" | "Vehicle"; - isActive: boolean + type: "Conveyor" | "Vehicle" | "ArmBot"; + isActive: boolean; } export interface ProcessData { diff --git a/app/src/modules/simulation/process/useProcessAnimations.tsx b/app/src/modules/simulation/process/useProcessAnimations.tsx index 032d013..232f17c 100644 --- a/app/src/modules/simulation/process/useProcessAnimations.tsx +++ b/app/src/modules/simulation/process/useProcessAnimations.tsx @@ -1,609 +1,685 @@ import { useCallback, useEffect, useRef, useState } from "react"; import * as THREE from "three"; import { - ProcessData, - ProcessAnimationState, - SpawnedObject, - AnimationState, - ProcessPoint, - PointAction, - Trigger, + ProcessData, + ProcessAnimationState, + SpawnedObject, + AnimationState, + ProcessPoint, + PointAction, + Trigger, } from "./types"; import { - useAnimationPlaySpeed, - usePauseButtonStore, - usePlayButtonStore, - useResetButtonStore, + useAnimationPlaySpeed, + usePauseButtonStore, + usePlayButtonStore, + useResetButtonStore, } from "../../../store/usePlayButtonStore"; import { usePlayAgv } from "../../../store/store"; +interface ArmBotProcess { + triggerId: string; + startPoint: string; + endPoint: string; +} + // Enhanced ProcessAnimationState with trigger tracking interface EnhancedProcessAnimationState extends ProcessAnimationState { - triggerCounts: Record<string, number>; - triggerLogs: Array<{ - timestamp: number; - pointId: string; - objectId: string; - triggerId: string; - }>; + triggerCounts: Record<string, number>; + triggerLogs: Array<{ + timestamp: number; + pointId: string; + objectId: string; + triggerId: string; + }>; } interface ProcessContainerProps { - processes: ProcessData[]; - setProcesses: React.Dispatch<React.SetStateAction<any[]>>; - agvRef: any; + processes: ProcessData[]; + setProcesses: React.Dispatch<React.SetStateAction<any[]>>; + agvRef: any; } interface PlayAgvState { - playAgv: Record<string, any>; - setPlayAgv: (data: any) => void; + playAgv: Record<string, any>; + setPlayAgv: (data: any) => void; } export const useProcessAnimation = ( - processes: ProcessData[], - setProcesses: React.Dispatch<React.SetStateAction<any[]>>, - agvRef: any + processes: ProcessData[], + setProcesses: React.Dispatch<React.SetStateAction<any[]>>, + agvRef: any ) => { - // State and refs initialization - const { isPlaying, setIsPlaying } = usePlayButtonStore(); - const { isPaused, setIsPaused } = usePauseButtonStore(); - const { isReset, setReset } = useResetButtonStore(); - const debugRef = useRef<boolean>(false); - const clockRef = useRef<THREE.Clock>(new THREE.Clock()); - const pauseTimeRef = useRef<number>(0); - const elapsedBeforePauseRef = useRef<number>(0); - const animationStatesRef = useRef<Record<string, EnhancedProcessAnimationState>>({}); - const { speed } = useAnimationPlaySpeed(); - const prevIsPlaying = useRef<boolean | null>(null); - const [internalResetFlag, setInternalResetFlag] = useState(false); - const [animationStates, setAnimationStates] = useState<Record<string, EnhancedProcessAnimationState>>({}); - const speedRef = useRef<number>(speed); - const { PlayAgv, setPlayAgv } = usePlayAgv(); + // State and refs initialization + const { isPlaying, setIsPlaying } = usePlayButtonStore(); + const { isPaused, setIsPaused } = usePauseButtonStore(); + const { isReset, setReset } = useResetButtonStore(); + const debugRef = useRef<boolean>(false); + const clockRef = useRef<THREE.Clock>(new THREE.Clock()); + const pauseTimeRef = useRef<number>(0); + const elapsedBeforePauseRef = useRef<number>(0); + const animationStatesRef = useRef< + Record<string, EnhancedProcessAnimationState> + >({}); + const { speed } = useAnimationPlaySpeed(); + const prevIsPlaying = useRef<boolean | null>(null); + const [internalResetFlag, setInternalResetFlag] = useState(false); + const [animationStates, setAnimationStates] = useState< + Record<string, EnhancedProcessAnimationState> + >({}); + const speedRef = useRef<number>(speed); + const armBotRef = useRef<any[]>([]); + - // Effect hooks - useEffect(() => { - speedRef.current = speed; - }, [speed]); + // Effect hooks + useEffect(() => { + speedRef.current = speed; + }, [speed]); - useEffect(() => { - if (prevIsPlaying.current !== null || !isPlaying) { - setAnimationStates({}); - } - prevIsPlaying.current = isPlaying; - }, [isPlaying]); + useEffect(() => { + if (prevIsPlaying.current !== null || !isPlaying) { + setAnimationStates({}); + } + prevIsPlaying.current = isPlaying; + }, [isPlaying]); - useEffect(() => { - animationStatesRef.current = animationStates; - }, [animationStates]); + useEffect(() => { + animationStatesRef.current = animationStates; + }, [animationStates]); - // Reset handler - useEffect(() => { - if (isReset) { - setInternalResetFlag(true); - setIsPlaying(false); - setIsPaused(false); - setAnimationStates({}); - animationStatesRef.current = {}; - clockRef.current = new THREE.Clock(); - elapsedBeforePauseRef.current = 0; - pauseTimeRef.current = 0; - setReset(false); - setTimeout(() => { - setInternalResetFlag(false); - setIsPlaying(true); - }, 0); - } - }, [isReset, setReset, setIsPlaying, setIsPaused]); + // Reset handler + useEffect(() => { + if (isReset) { + setInternalResetFlag(true); + setIsPlaying(false); + setIsPaused(false); + setAnimationStates({}); + animationStatesRef.current = {}; + clockRef.current = new THREE.Clock(); + elapsedBeforePauseRef.current = 0; + pauseTimeRef.current = 0; + setReset(false); + setTimeout(() => { + setInternalResetFlag(false); + setIsPlaying(true); + }, 0); + } + }, [isReset, setReset, setIsPlaying, setIsPaused]); - // Pause handler - useEffect(() => { - if (isPaused) { - pauseTimeRef.current = clockRef.current.getElapsedTime(); - } else if (pauseTimeRef.current > 0) { - const pausedDuration = clockRef.current.getElapsedTime() - pauseTimeRef.current; - elapsedBeforePauseRef.current += pausedDuration; - } - }, [isPaused]); + // Pause handler + useEffect(() => { + if (isPaused) { + pauseTimeRef.current = clockRef.current.getElapsedTime(); + } else if (pauseTimeRef.current > 0) { + const pausedDuration = + clockRef.current.getElapsedTime() - pauseTimeRef.current; + elapsedBeforePauseRef.current += pausedDuration; + } + }, [isPaused]); - // Initialize animation states with trigger tracking - useEffect(() => { - if (isPlaying && !internalResetFlag) { - const newStates: Record<string, EnhancedProcessAnimationState> = {}; + // Initialize animation states with trigger tracking + useEffect(() => { + if (isPlaying && !internalResetFlag) { + const newStates: Record<string, EnhancedProcessAnimationState> = {}; - processes.forEach((process) => { - const triggerCounts: Record<string, number> = {}; + processes.forEach((process) => { + const triggerCounts: Record<string, number> = {}; - // Initialize trigger counts for all On-Hit triggers - process.paths?.forEach((path) => { - path.points?.forEach((point) => { - point.triggers?.forEach((trigger: Trigger) => { - if (trigger.type === "On-Hit" && trigger.isUsed) { - triggerCounts[`${point.uuid}-${trigger.uuid}`] = 0; - } - }); - }); - }); - - newStates[process.id] = { - spawnedObjects: {}, - nextSpawnTime: 0, - objectIdCounter: 0, - isProcessDelaying: false, - processDelayStartTime: 0, - processDelayDuration: 0, - triggerCounts, - triggerLogs: [], - }; + // Initialize trigger counts for all On-Hit triggers + process.paths?.forEach((path) => { + path.points?.forEach((point) => { + point.triggers?.forEach((trigger: Trigger) => { + if (trigger.type === "On-Hit" && trigger.isUsed) { + triggerCounts[`${point.uuid}-${trigger.uuid}`] = 0; + } }); + }); + }); - setAnimationStates(newStates); - animationStatesRef.current = newStates; - clockRef.current.start(); - } - }, [isPlaying, processes, internalResetFlag]); + newStates[process.id] = { + spawnedObjects: {}, + nextSpawnTime: 0, + objectIdCounter: 0, + isProcessDelaying: false, + processDelayStartTime: 0, + processDelayDuration: 0, + triggerCounts, + triggerLogs: [], + }; + }); - useEffect(() => { - if (isPlaying && !internalResetFlag) { - const newStates: Record<string, EnhancedProcessAnimationState> = {}; + setAnimationStates(newStates); + animationStatesRef.current = newStates; + clockRef.current.start(); + } + }, [isPlaying, processes, internalResetFlag]); - // Initialize AGVs for each process first - processes.forEach((process) => { - // Find all vehicle paths for this process - const vehiclePaths = process.paths?.filter( - (path) => path.type === "Vehicle" - ) || []; + useEffect(() => { + if (isPlaying && !internalResetFlag) { + const newStates: Record<string, EnhancedProcessAnimationState> = {}; - // Initialize AGVs for each vehicle path - vehiclePaths.forEach((vehiclePath) => { - if (vehiclePath.points?.length > 0) { - const vehiclePoint = vehiclePath.points[0]; - const action = vehiclePoint.actions?.[0]; - const maxHitCount = action?.hitCount; + // Initialize AGVs for each process first + processes.forEach((process) => { + // Find all vehicle paths for this process + const vehiclePaths = + process.paths?.filter((path) => path.type === "Vehicle") || []; - const vehicleId = vehiclePath.modeluuid; - const processId = process.id; + // Initialize AGVs for each vehicle path + vehiclePaths.forEach((vehiclePath) => { + if (vehiclePath.points?.length > 0) { + const vehiclePoint = vehiclePath.points[0]; + const action = vehiclePoint.actions?.[0]; + const maxHitCount = action?.hitCount; - // Check if this AGV already exists - const existingAgv = agvRef.current.find( - (v: any) => v.vehicleId === vehicleId && v.processId === processId - ); + const vehicleId = vehiclePath.modeluuid; + const processId = process.id; - if (!existingAgv) { - // Initialize the AGV in a stationed state - agvRef.current.push({ - processId, - vehicleId, - maxHitCount: maxHitCount || 0, - isActive: false, - hitCount: 0, - status: 'stationed', - lastUpdated: 0 - }); - } - } - }); - - // Then initialize trigger counts as before - const triggerCounts: Record<string, number> = {}; - process.paths?.forEach((path) => { - path.points?.forEach((point) => { - point.triggers?.forEach((trigger: Trigger) => { - if (trigger.type === "On-Hit" && trigger.isUsed) { - triggerCounts[`${point.uuid}-${trigger.uuid}`] = 0; - } - }); - }); - }); - - newStates[process.id] = { - spawnedObjects: {}, - nextSpawnTime: 0, - objectIdCounter: 0, - isProcessDelaying: false, - processDelayStartTime: 0, - processDelayDuration: 0, - triggerCounts, - triggerLogs: [], - }; - }); - - setAnimationStates(newStates); - animationStatesRef.current = newStates; - clockRef.current.start(); - } - }, [isPlaying, processes, internalResetFlag]); - - // Helper functions - 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; - }; - - const findAnimationPathPoint = ( - process: ProcessData, - spawnPoint: ProcessPoint - ): THREE.Vector3 => { - if (process.animationPath && process.animationPath.length > 0) { - let pointIndex = 0; - for (const path of process.paths || []) { - for (let i = 0; i < (path.points?.length || 0); i++) { - const point = path.points?.[i]; - if (point && point.uuid === spawnPoint.uuid) { - if (process.animationPath[pointIndex]) { - const p = process.animationPath[pointIndex]; - return new THREE.Vector3(p.x, p.y, p.z); - } - } - pointIndex++; - } - } - } - return new THREE.Vector3( - spawnPoint.position[0], - spawnPoint.position[1], - spawnPoint.position[2] - ); - }; - - // Optimized object creation - const createSpawnedObject = useCallback( - ( - process: ProcessData, - currentTime: number, - materialType: string, - spawnPoint: ProcessPoint, - baseMaterials: Record<string, THREE.Material> - ): SpawnedObject => { - const processMaterials = { - ...baseMaterials, - ...(process.customMaterials || {}), - }; - - const spawnPosition = findAnimationPathPoint(process, spawnPoint); - const material = - processMaterials[materialType as keyof typeof processMaterials] || - baseMaterials.Default; - - return { - ref: { current: null }, - state: { - currentIndex: 0, - progress: 0, - isAnimating: true, - speed: process.speed || 1, - isDelaying: false, - delayStartTime: 0, - currentDelayDuration: 0, - delayComplete: false, - currentPathIndex: 0, - }, - visible: true, - material: material, - currentMaterialType: materialType, - spawnTime: currentTime, - position: spawnPosition, - }; - }, - [] - ); - - // Material handling - const handleMaterialSwap = useCallback( - ( - processId: string, - objectId: string, - materialType: string, - processes: ProcessData[], - baseMaterials: Record<string, THREE.Material> - ) => { - setAnimationStates((prev) => { - const processState = prev[processId]; - if (!processState || !processState.spawnedObjects[objectId]) - return prev; - - const process = processes.find((p) => p.id === processId); - if (!process) return prev; - - const processMaterials = { - ...baseMaterials, - ...(process.customMaterials || {}), - }; - - const newMaterial = - processMaterials[materialType as keyof typeof processMaterials]; - if (!newMaterial) return prev; - - return { - ...prev, - [processId]: { - ...processState, - spawnedObjects: { - ...processState.spawnedObjects, - [objectId]: { - ...processState.spawnedObjects[objectId], - material: newMaterial, - currentMaterialType: materialType, - }, - }, - }, - }; - }); - }, - [] - ); - - // Point action handler with trigger counting - const handlePointActions = useCallback( - ( - processId: string, - objectId: string, - actions: PointAction[] = [], - currentTime: number, - processes: ProcessData[], - baseMaterials: Record<string, THREE.Material> - ): 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.isProcessDelaying) return prev; - - const delayDuration = - typeof action.delay === "number" - ? action.delay - : parseFloat(action.delay as string) || 0; - - if (delayDuration > 0) { - return { - ...prev, - [processId]: { - ...processState, - isProcessDelaying: true, - processDelayStartTime: currentTime, - processDelayDuration: delayDuration, - spawnedObjects: { - ...processState.spawnedObjects, - [objectId]: { - ...processState.spawnedObjects[objectId], - state: { - ...processState.spawnedObjects[objectId].state, - isAnimating: false, - 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, - processes, - baseMaterials - ); - } - break; - - default: - break; - } - }); - - return shouldStopAnimation; - }, - [handleMaterialSwap] - ); - - // Trigger counting system - const checkAndCountTriggers = useCallback( - ( - processId: string, - objectId: string, - currentPointIndex: number, - processes: ProcessData[], - currentTime: number - ) => { - setAnimationStates((prev) => { - const processState = prev[processId]; - if (!processState) return prev; - - const process = processes.find((p) => p.id === processId); - if (!process) return prev; - - const point = getPointDataForAnimationIndex(process, currentPointIndex); - if (!point?.triggers) return prev; - - const onHitTriggers = point.triggers.filter( - (t: Trigger) => t.type === "On-Hit" && t.isUsed - ); - if (onHitTriggers.length === 0) return prev; - - let newTriggerCounts = { ...processState.triggerCounts }; - const newTriggerLogs = [...processState.triggerLogs]; - let shouldLog = false; - - // Find all vehicle paths for this process - const vehiclePaths = process.paths.filter( - (path) => path.type === "Vehicle" - ); - - // Check if any vehicle is active for this process - const activeVehicles = vehiclePaths.filter(path => { - const vehicleId = path.modeluuid; - const vehicleEntry = agvRef.current.find( - (v: any) => v.vehicleId === vehicleId && v.processId === processId - ); - return vehicleEntry?.isActive; - }); - - // Only count triggers if no vehicles are active for this process - if (activeVehicles.length === 0) { - onHitTriggers.forEach((trigger: Trigger) => { - const triggerKey = `${point.uuid}-${trigger.uuid}`; - newTriggerCounts[triggerKey] = (newTriggerCounts[triggerKey] || 0) + 1; - shouldLog = true; - newTriggerLogs.push({ - timestamp: currentTime, - pointId: point.uuid, - objectId, - triggerId: trigger.uuid, - }); - }); - } - - let processTotalHits = Object.values(newTriggerCounts).reduce((a, b) => a + b, 0); - - if (shouldLog) { - vehiclePaths.forEach((vehiclePath) => { - if (vehiclePath.points?.length > 0) { - const vehiclePoint = vehiclePath.points[0]; - const action = vehiclePoint.actions?.[0]; - const maxHitCount = action?.hitCount; - - if (maxHitCount !== undefined) { - const vehicleId = vehiclePath.modeluuid; - let vehicleEntry = agvRef.current.find((v: any) => v.vehicleId === vehicleId && v.processId === processId); - - if (!vehicleEntry) { - vehicleEntry = { - processId, - vehicleId, - maxHitCount: maxHitCount, - isActive: false, - hitCount: 0, - status: 'stationed' - }; - agvRef.current.push(vehicleEntry); - } - - // if (!vehicleEntry.isActive && vehicleEntry.status === 'picking') { - if (!vehicleEntry.isActive) { - vehicleEntry.hitCount = processTotalHits; - vehicleEntry.lastUpdated = currentTime; - - if (vehicleEntry.hitCount >= vehicleEntry.maxHitCount) { - vehicleEntry.isActive = true; - newTriggerCounts = {}; - processTotalHits = 0; - } - } - } - } - }); - } - - return { - ...prev, - [processId]: { - ...processState, - triggerCounts: newTriggerCounts, - triggerLogs: newTriggerLogs, - totalHits: processTotalHits, - }, - }; - }); - }, - [] - ); - - // Utility functions - const hasNonInheritActions = useCallback( - (actions: PointAction[] = []): boolean => { - return actions.some( - (action) => action.isUsed && action.type !== "Inherit" + // Check if this AGV already exists + const existingAgv = agvRef.current.find( + (v: any) => v.vehicleId === vehicleId && v.processId === processId ); - }, - [] + + if (!existingAgv) { + // Initialize the AGV in a stationed state + agvRef.current.push({ + processId, + vehicleId, + maxHitCount: maxHitCount || 0, + isActive: false, + hitCount: 0, + status: "stationed", + lastUpdated: 0, + }); + } + } + }); + + // Then initialize trigger counts as before + const triggerCounts: Record<string, number> = {}; + process.paths?.forEach((path) => { + path.points?.forEach((point) => { + point.triggers?.forEach((trigger: Trigger) => { + if (trigger.type === "On-Hit" && trigger.isUsed) { + triggerCounts[`${point.uuid}-${trigger.uuid}`] = 0; + } + }); + }); + }); + + newStates[process.id] = { + spawnedObjects: {}, + nextSpawnTime: 0, + objectIdCounter: 0, + isProcessDelaying: false, + processDelayStartTime: 0, + processDelayDuration: 0, + triggerCounts, + triggerLogs: [], + }; + }); + + setAnimationStates(newStates); + animationStatesRef.current = newStates; + clockRef.current.start(); + } + }, [isPlaying, processes, internalResetFlag]); + + // Helper functions + 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; + }; + + const findAnimationPathPoint = ( + process: ProcessData, + spawnPoint: ProcessPoint + ): THREE.Vector3 => { + if (process.animationPath && process.animationPath.length > 0) { + let pointIndex = 0; + for (const path of process.paths || []) { + for (let i = 0; i < (path.points?.length || 0); i++) { + const point = path.points?.[i]; + if (point && point.uuid === spawnPoint.uuid) { + if (process.animationPath[pointIndex]) { + const p = process.animationPath[pointIndex]; + return new THREE.Vector3(p.x, p.y, p.z); + } + } + pointIndex++; + } + } + } + return new THREE.Vector3( + spawnPoint.position[0], + spawnPoint.position[1], + spawnPoint.position[2] ); + }; - const getPointDataForAnimationIndex = useCallback( - (process: ProcessData, index: number): ProcessPoint | null => { - if (!process.paths) return null; + // Optimized object creation + const createSpawnedObject = useCallback( + ( + process: ProcessData, + currentTime: number, + materialType: string, + spawnPoint: ProcessPoint, + baseMaterials: Record<string, THREE.Material> + ): SpawnedObject => { + const processMaterials = { + ...baseMaterials, + ...(process.customMaterials || {}), + }; - let cumulativePoints = 0; - for (const path of process.paths) { - const pointCount = path.points?.length || 0; + const spawnPosition = findAnimationPathPoint(process, spawnPoint); + const material = + processMaterials[materialType as keyof typeof processMaterials] || + baseMaterials.Default; - if (index < cumulativePoints + pointCount) { - const pointIndex = index - cumulativePoints; - return path.points?.[pointIndex] || null; + return { + ref: { current: null }, + state: { + currentIndex: 0, + progress: 0, + isAnimating: true, + speed: process.speed || 1, + isDelaying: false, + delayStartTime: 0, + currentDelayDuration: 0, + delayComplete: false, + currentPathIndex: 0, + }, + visible: true, + material: material, + currentMaterialType: materialType, + spawnTime: currentTime, + position: spawnPosition, + }; + }, + [] + ); + + // Material handling + const handleMaterialSwap = useCallback( + ( + processId: string, + objectId: string, + materialType: string, + processes: ProcessData[], + baseMaterials: Record<string, THREE.Material> + ) => { + setAnimationStates((prev) => { + const processState = prev[processId]; + if (!processState || !processState.spawnedObjects[objectId]) + return prev; + + const process = processes.find((p) => p.id === processId); + if (!process) return prev; + + const processMaterials = { + ...baseMaterials, + ...(process.customMaterials || {}), + }; + + const newMaterial = + processMaterials[materialType as keyof typeof processMaterials]; + if (!newMaterial) return prev; + + return { + ...prev, + [processId]: { + ...processState, + spawnedObjects: { + ...processState.spawnedObjects, + [objectId]: { + ...processState.spawnedObjects[objectId], + material: newMaterial, + currentMaterialType: materialType, + }, + }, + }, + }; + }); + }, + [] + ); + + // Point action handler with trigger counting + const handlePointActions = useCallback( + ( + processId: string, + objectId: string, + actions: PointAction[] = [], + currentTime: number, + processes: ProcessData[], + baseMaterials: Record<string, THREE.Material> + ): 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.isProcessDelaying) return prev; + + const delayDuration = + typeof action.delay === "number" + ? action.delay + : parseFloat(action.delay as string) || 0; + + if (delayDuration > 0) { + return { + ...prev, + [processId]: { + ...processState, + isProcessDelaying: true, + processDelayStartTime: currentTime, + processDelayDuration: delayDuration, + spawnedObjects: { + ...processState.spawnedObjects, + [objectId]: { + ...processState.spawnedObjects[objectId], + state: { + ...processState.spawnedObjects[objectId].state, + isAnimating: false, + 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, + processes, + baseMaterials + ); + } + break; + + default: + break; + } + }); + + return shouldStopAnimation; + }, + [handleMaterialSwap] + ); + + // Trigger counting system + const checkAndCountTriggers = useCallback( + ( + processId: string, + objectId: string, + currentPointIndex: number, + processes: ProcessData[], + currentTime: number + ) => { + setAnimationStates((prev) => { + const processState = prev[processId]; + if (!processState) return prev; + + const process = processes.find((p) => p.id === processId); + if (!process) return prev; + + const point = getPointDataForAnimationIndex(process, currentPointIndex); + if (!point?.triggers) return prev; + + const onHitTriggers = point.triggers.filter( + (t: Trigger) => t.type === "On-Hit" && t.isUsed + ); + + if (onHitTriggers.length === 0) return prev; + + let newTriggerCounts = { ...processState.triggerCounts }; + const newTriggerLogs = [...processState.triggerLogs]; + let shouldLog = false; + + // Find all vehicle paths for this process + const vehiclePaths = process.paths.filter( + (path) => path.type === "Vehicle" + ); + + // Find all ArmBot paths for this process + const armBotPaths = process.paths.filter( + (path) => path.type === "ArmBot" + ); + + // Check if any vehicle is active for this process + const activeVehicles = vehiclePaths.filter((path) => { + const vehicleId = path.modeluuid; + const vehicleEntry = agvRef.current.find( + (v: any) => v.vehicleId === vehicleId && v.processId === processId + ); + return vehicleEntry?.isActive; + }); + + // Check if any ArmBot is active for this process + const activeArmBots = armBotPaths.filter((path) => { + const armBotId = path.modeluuid; + const armBotEntry = armBotRef.current.find( + (a: any) => a.armBotId === armBotId && a.processId === processId + ); + return armBotEntry?.isActive; + }); + + // Only count triggers if no vehicles and no ArmBots are active for this process + if (activeVehicles.length === 0 && activeArmBots.length === 0) { + onHitTriggers.forEach((trigger: Trigger) => { + const triggerKey = `${point.uuid}-${trigger.uuid}`; + newTriggerCounts[triggerKey] = + (newTriggerCounts[triggerKey] || 0) + 1; + shouldLog = true; + newTriggerLogs.push({ + timestamp: currentTime, + pointId: point.uuid, + objectId, + triggerId: trigger.uuid, + }); + }); + } + + let processTotalHits = Object.values(newTriggerCounts).reduce( + (a, b) => a + b, + 0 + ); + + // Handle logic for vehicles and ArmBots when a trigger is hit + if (shouldLog) { + // Handle vehicle logic (existing code) + vehiclePaths.forEach((vehiclePath) => { + if (vehiclePath.points?.length > 0) { + const vehiclePoint = vehiclePath.points[0]; + const action = vehiclePoint.actions?.[0]; + const maxHitCount = action?.hitCount; + + if (maxHitCount !== undefined) { + const vehicleId = vehiclePath.modeluuid; + let vehicleEntry = agvRef.current.find( + (v: any) => + v.vehicleId === vehicleId && v.processId === processId + ); + + if (!vehicleEntry) { + vehicleEntry = { + processId, + vehicleId, + maxHitCount: maxHitCount, + isActive: false, + hitCount: 0, + status: "stationed", + }; + agvRef.current.push(vehicleEntry); } - cumulativePoints += pointCount; + if (!vehicleEntry.isActive) { + vehicleEntry.hitCount++; + vehicleEntry.lastUpdated = currentTime; + + if (vehicleEntry.hitCount >= vehicleEntry.maxHitCount) { + vehicleEntry.isActive = true; + newTriggerCounts = {}; + processTotalHits = 0; + } + } + } } + }); - return null; - }, - [] - ); + // Handle ArmBot logic (new code) + armBotPaths.forEach((armBotPath) => { + if (armBotPath.points?.length > 0) { + const armBotPoint = armBotPath.points[0]; + const action = armBotPoint.actions?.[0]; + const maxHitCount = action?.hitCount; - const getTriggerCounts = useCallback((processId: string) => { - return animationStatesRef.current[processId]?.triggerCounts || {}; - }, []); + if (maxHitCount !== undefined) { + const armBotId = armBotPath.modeluuid; + let armBotEntry = armBotRef.current.find( + (a: any) => + a.armBotId === armBotId && a.processId === processId + ); - const getTriggerLogs = useCallback((processId: string) => { - return animationStatesRef.current[processId]?.triggerLogs || []; - }, []); + if (!armBotEntry) { + armBotEntry = { + processId, + armBotId, + maxHitCount: maxHitCount, + isActive: false, + hitCount: 0, + status: "stationed", + }; + armBotRef.current.push(armBotEntry); + console.log('armBotEntry: ', armBotEntry); + } - return { - animationStates, - setAnimationStates, - clockRef, - elapsedBeforePauseRef, - speedRef, - debugRef, - findSpawnPoint, - createSpawnedObject, - handlePointActions, - hasNonInheritActions, - getPointDataForAnimationIndex, - checkAndCountTriggers, - getTriggerCounts, - getTriggerLogs, - processes, - }; + if (!armBotEntry.isActive) { + armBotEntry.hitCount++; + armBotEntry.lastUpdated = currentTime; + + if (armBotEntry.hitCount >= armBotEntry.maxHitCount) { + armBotEntry.isActive = true; + // Reset trigger counts when ArmBot activates, similar to vehicle + newTriggerCounts = {}; + processTotalHits = 0; + } + } + } + } + }); + } + + return { + ...prev, + [processId]: { + ...processState, + triggerCounts: newTriggerCounts, + triggerLogs: newTriggerLogs, + totalHits: processTotalHits, + }, + }; + }); + }, + [] + ); + + // Utility functions + const hasNonInheritActions = useCallback( + (actions: PointAction[] = []): boolean => { + return actions.some( + (action) => action.isUsed && action.type !== "Inherit" + ); + }, + [] + ); + + const getPointDataForAnimationIndex = useCallback( + (process: ProcessData, index: number): ProcessPoint | null => { + 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; + }, + [] + ); + + const getTriggerCounts = useCallback((processId: string) => { + return animationStatesRef.current[processId]?.triggerCounts || {}; + }, []); + + const getTriggerLogs = useCallback((processId: string) => { + return animationStatesRef.current[processId]?.triggerLogs || []; + }, []); + + return { + animationStates, + setAnimationStates, + clockRef, + elapsedBeforePauseRef, + speedRef, + debugRef, + findSpawnPoint, + createSpawnedObject, + handlePointActions, + hasNonInheritActions, + getPointDataForAnimationIndex, + checkAndCountTriggers, + getTriggerCounts, + getTriggerLogs, + processes, + }; }; diff --git a/app/src/modules/simulation/simulation.tsx b/app/src/modules/simulation/simulation.tsx index b1f961e..fb789ec 100644 --- a/app/src/modules/simulation/simulation.tsx +++ b/app/src/modules/simulation/simulation.tsx @@ -11,8 +11,10 @@ function Simulation() { const { activeModule } = useModuleStore(); const pathsGroupRef = useRef() as React.MutableRefObject<THREE.Group>; const [processes, setProcesses] = useState<any[]>([]); + const agvRef = useRef([]); const MaterialRef = useRef([]); + const { simulationStates } = useSimulationStates(); return ( <> From a26e0dacd08d743bcfd55a9e2169e45bb0eed921 Mon Sep 17 00:00:00 2001 From: Jerald-Golden-B <jerald@hexrfactory.com> Date: Wed, 16 Apr 2025 10:03:01 +0530 Subject: [PATCH 07/20] Implement code changes to enhance functionality and improve performance --- .../controls/selection/selectionControls.tsx | 978 +++--- .../modules/simulation/path/pathConnector.tsx | 2784 ++++++++--------- 2 files changed, 1646 insertions(+), 2116 deletions(-) diff --git a/app/src/modules/scene/controls/selection/selectionControls.tsx b/app/src/modules/scene/controls/selection/selectionControls.tsx index aa2bca4..ece6924 100644 --- a/app/src/modules/scene/controls/selection/selectionControls.tsx +++ b/app/src/modules/scene/controls/selection/selectionControls.tsx @@ -3,13 +3,7 @@ import { useEffect, useMemo, useRef, useState } from "react"; import { SelectionBox } from "three/examples/jsm/interactive/SelectionBox"; import { SelectionHelper } from "./selectionHelper"; import { useFrame, useThree } from "@react-three/fiber"; -import { - useFloorItems, - useSelectedAssets, - useSimulationStates, - useSocketStore, - useToggleView, -} from "../../../../store/store"; +import { useFloorItems, useSelectedAssets, useSimulationStates, useSocketStore, useToggleView, } from "../../../../store/store"; import BoundingBox from "./boundingBoxHelper"; import { toast } from "react-toastify"; // import { deleteFloorItem } from '../../../../services/factoryBuilder/assest/floorAsset/deleteFloorItemApi'; @@ -23,574 +17,446 @@ import RotateControls from "./rotateControls"; import useModuleStore from "../../../../store/useModuleStore"; const SelectionControls: React.FC = () => { - const { camera, controls, gl, scene, pointer } = useThree(); - const itemsGroupRef = useRef<THREE.Group | undefined>(undefined); - const selectionGroup = useRef() as Types.RefGroup; - const { toggleView } = useToggleView(); - const { simulationStates, setSimulationStates } = useSimulationStates(); - const { selectedAssets, setSelectedAssets } = useSelectedAssets(); - const [movedObjects, setMovedObjects] = useState<THREE.Object3D[]>([]); - const [rotatedObjects, setRotatedObjects] = useState<THREE.Object3D[]>([]); - const [copiedObjects, setCopiedObjects] = useState<THREE.Object3D[]>([]); - const [pastedObjects, setpastedObjects] = useState<THREE.Object3D[]>([]); - const [duplicatedObjects, setDuplicatedObjects] = useState<THREE.Object3D[]>( - [] - ); - const boundingBoxRef = useRef<THREE.Mesh>(); - const { floorItems, setFloorItems } = useFloorItems(); - const { activeModule } = useModuleStore(); - const { socket } = useSocketStore(); - const selectionBox = useMemo( - () => new SelectionBox(camera, scene), - [camera, scene] - ); + const { camera, controls, gl, scene, pointer } = useThree(); + const itemsGroupRef = useRef<THREE.Group | undefined>(undefined); + const selectionGroup = useRef() as Types.RefGroup; + const { toggleView } = useToggleView(); + const { simulationStates, setSimulationStates } = useSimulationStates(); + const { selectedAssets, setSelectedAssets } = useSelectedAssets(); + const [movedObjects, setMovedObjects] = useState<THREE.Object3D[]>([]); + const [rotatedObjects, setRotatedObjects] = useState<THREE.Object3D[]>([]); + const [copiedObjects, setCopiedObjects] = useState<THREE.Object3D[]>([]); + const [pastedObjects, setpastedObjects] = useState<THREE.Object3D[]>([]); + const [duplicatedObjects, setDuplicatedObjects] = useState<THREE.Object3D[]>([]); + const boundingBoxRef = useRef<THREE.Mesh>(); + const { floorItems, setFloorItems } = useFloorItems(); + const { activeModule } = useModuleStore(); + const { socket } = useSocketStore(); + const selectionBox = useMemo(() => new SelectionBox(camera, scene), [camera, scene]); - useEffect(() => { - if (!camera || !scene || toggleView) return; + useEffect(() => { + if (!camera || !scene || toggleView) return; - const canvasElement = gl.domElement; - canvasElement.tabIndex = 0; + const canvasElement = gl.domElement; + canvasElement.tabIndex = 0; - const itemsGroup: any = scene.getObjectByName("itemsGroup"); - itemsGroupRef.current = itemsGroup; + const itemsGroup: any = scene.getObjectByName("itemsGroup"); + itemsGroupRef.current = itemsGroup; - let isSelecting = false; - let isRightClick = false; - let rightClickMoved = false; - let isCtrlSelecting = false; + let isSelecting = false; + let isRightClick = false; + let rightClickMoved = false; + let isCtrlSelecting = false; - const helper = new SelectionHelper(gl); + const helper = new SelectionHelper(gl); - if (!itemsGroup) { - toast.warn("itemsGroup not found in the scene."); - return; - } - - const onPointerDown = (event: PointerEvent) => { - if (event.button === 2) { - isRightClick = true; - rightClickMoved = false; - } else if (event.button === 0) { - isSelecting = false; - isCtrlSelecting = event.ctrlKey; - if (event.ctrlKey && duplicatedObjects.length === 0) { - if (controls) (controls as any).enabled = false; - selectionBox.startPoint.set(pointer.x, pointer.y, 0); + if (!itemsGroup) { + toast.warn("itemsGroup not found in the scene."); + return; } - } - }; - const onPointerMove = (event: PointerEvent) => { - if (isRightClick) { - rightClickMoved = true; - } - isSelecting = true; - if ( - helper.isDown && - event.ctrlKey && - duplicatedObjects.length === 0 && - isCtrlSelecting - ) { - selectionBox.endPoint.set(pointer.x, pointer.y, 0); - } - }; - - const onPointerUp = (event: PointerEvent) => { - if (event.button === 2) { - isRightClick = false; - if (!rightClickMoved) { - clearSelection(); - } - return; - } - - if (isSelecting && isCtrlSelecting) { - isCtrlSelecting = false; - isSelecting = false; - if (event.ctrlKey && duplicatedObjects.length === 0) { - selectAssets(); - } - } else if ( - !isSelecting && - selectedAssets.length > 0 && - ((pastedObjects.length === 0 && - duplicatedObjects.length === 0 && - movedObjects.length === 0 && - rotatedObjects.length === 0) || - event.button !== 0) - ) { - clearSelection(); - helper.enabled = true; - isCtrlSelecting = false; - } - }; - - const onKeyDown = (event: KeyboardEvent) => { - if (movedObjects.length > 0 || rotatedObjects.length > 0) return; - if (event.key.toLowerCase() === "escape") { - event.preventDefault(); - clearSelection(); - } - if (event.key.toLowerCase() === "delete") { - event.preventDefault(); - deleteSelection(); - } - }; - - const onContextMenu = (event: MouseEvent) => { - event.preventDefault(); - if (!rightClickMoved) { - clearSelection(); - } - }; - - if (!toggleView && activeModule === "builder") { - helper.enabled = true; - if (duplicatedObjects.length === 0 && pastedObjects.length === 0) { - canvasElement.addEventListener("pointerdown", onPointerDown); - canvasElement.addEventListener("pointermove", onPointerMove); - canvasElement.addEventListener("pointerup", onPointerUp); - } else { - helper.enabled = false; - helper.dispose(); - } - canvasElement.addEventListener("contextmenu", onContextMenu); - canvasElement.addEventListener("keydown", onKeyDown); - } else { - helper.enabled = false; - helper.dispose(); - } - - return () => { - canvasElement.removeEventListener("pointerdown", onPointerDown); - canvasElement.removeEventListener("pointermove", onPointerMove); - canvasElement.removeEventListener("contextmenu", onContextMenu); - canvasElement.removeEventListener("pointerup", onPointerUp); - canvasElement.removeEventListener("keydown", onKeyDown); - helper.enabled = false; - helper.dispose(); - }; - }, [ - camera, - controls, - scene, - toggleView, - selectedAssets, - copiedObjects, - pastedObjects, - duplicatedObjects, - movedObjects, - socket, - floorItems, - rotatedObjects, - activeModule, - ]); - - useEffect(() => { - if (activeModule !== "builder") { - clearSelection(); - } - }, [activeModule]); - - useFrame(() => { - if ( - pastedObjects.length === 0 && - duplicatedObjects.length === 0 && - movedObjects.length === 0 && - rotatedObjects.length === 0 - ) { - selectionGroup.current.position.set(0, 0, 0); - } - }); - - const selectAssets = () => { - selectionBox.endPoint.set(pointer.x, pointer.y, 0); - if (controls) (controls as any).enabled = true; - - let selectedObjects = selectionBox.select(); - let Objects = new Set<THREE.Object3D>(); - - selectedObjects.map((object) => { - let currentObject: THREE.Object3D | null = object; - while (currentObject) { - if (currentObject.userData.modelId) { - Objects.add(currentObject); - break; - } - currentObject = currentObject.parent || null; - } - }); - - if (Objects.size === 0) { - clearSelection(); - return; - } - - const updatedSelections = new Set(selectedAssets); - Objects.forEach((obj) => { - updatedSelections.has(obj) - ? updatedSelections.delete(obj) - : updatedSelections.add(obj); - }); - - const selected = Array.from(updatedSelections); - - setSelectedAssets(selected); - }; - - const clearSelection = () => { - selectionGroup.current.children = []; - selectionGroup.current.position.set(0, 0, 0); - selectionGroup.current.rotation.set(0, 0, 0); - setpastedObjects([]); - setDuplicatedObjects([]); - setSelectedAssets([]); - }; - const updateBackend = async ( - updatedPaths: ( - | SimulationTypes.ConveyorEventsSchema - | SimulationTypes.VehicleEventsSchema - | SimulationTypes.StaticMachineEventsSchema - | SimulationTypes.ArmBotEventsSchema - )[] - ) => { - if (updatedPaths.length === 0) return; - const email = localStorage.getItem("email"); - const organization = email ? email.split("@")[1].split(".")[0] : ""; - - updatedPaths.forEach(async (updatedPath) => { - if (updatedPath.type === "Conveyor") { - // await setEventApi( - // organization, - // updatedPath.modeluuid, - // { type: "Conveyor", points: updatedPath.points, speed: updatedPath.speed } - // ); - - const data = { - organization: organization, - modeluuid: updatedPath.modeluuid, - eventData: { - type: "Conveyor", - points: updatedPath.points, - speed: updatedPath.speed, - }, - }; - - socket.emit("v2:model-asset:updateEventData", data); - } else if (updatedPath.type === "Vehicle") { - // await setEventApi( - // organization, - // updatedPath.modeluuid, - // { type: "Vehicle", points: updatedPath.points } - // ); - - const data = { - organization: organization, - modeluuid: updatedPath.modeluuid, - eventData: { type: "Vehicle", points: updatedPath.points }, - }; - - socket.emit("v2:model-asset:updateEventData", data); - } else if (updatedPath.type === "StaticMachine") { - // await setEventApi( - // organization, - // updatedPath.modeluuid, - // { type: "StaticMachine", points: updatedPath.points } - // ); - - const data = { - organization: organization, - modeluuid: updatedPath.modeluuid, - eventData: { type: "StaticMachine", points: updatedPath.points }, - }; - - socket.emit("v2:model-asset:updateEventData", data); - } else if (updatedPath.type === "ArmBot") { - // await setEventApi( - // organization, - // updatedPath.modeluuid, - // { type: "ArmBot", points: updatedPath.points } - // ); - - const data = { - organization: organization, - modeluuid: updatedPath.modeluuid, - eventData: { type: "ArmBot", points: updatedPath.points }, - }; - - socket.emit("v2:model-asset:updateEventData", data); - } - }); - }; - - const removeConnections = (deletedModelUUIDs: string[]) => { - const updatedStates = simulationStates.map((state) => { - // Handle Conveyor - if (state.type === "Conveyor") { - const updatedConveyor: SimulationTypes.ConveyorEventsSchema = { - ...state, - points: state.points.map((point) => { - return { - ...point, - connections: { - ...point.connections, - targets: point.connections.targets.filter( - (target) => !deletedModelUUIDs.includes(target.modelUUID) - ), - }, - }; - }), - }; - return updatedConveyor; - } - - // Handle Vehicle - else if (state.type === "Vehicle") { - const updatedVehicle: SimulationTypes.VehicleEventsSchema = { - ...state, - points: { - ...state.points, - connections: { - ...state.points.connections, - targets: state.points.connections.targets.filter( - (target) => !deletedModelUUIDs.includes(target.modelUUID) - ), - }, - }, - }; - return updatedVehicle; - } - - // Handle StaticMachine - else if (state.type === "StaticMachine") { - const updatedStaticMachine: SimulationTypes.StaticMachineEventsSchema = - { - ...state, - points: { - ...state.points, - connections: { - ...state.points.connections, - targets: state.points.connections.targets.filter( - (target) => !deletedModelUUIDs.includes(target.modelUUID) - ), - }, - }, - }; - return updatedStaticMachine; - } - - // Handle ArmBot - else if (state.type === "ArmBot") { - const updatedArmBot: SimulationTypes.ArmBotEventsSchema = { - ...state, - points: { - ...state.points, - connections: { - ...state.points.connections, - - targets: state.points.connections.targets.filter( - (target: any) => !deletedModelUUIDs.includes(target.modelUUID) - ), - }, - actions: { - ...state.points.actions, - processes: (state.points.actions.processes = - state.points.actions.processes?.filter((process) => { - const matchedStates = simulationStates.filter((s) => - deletedModelUUIDs.includes(s.modeluuid) - ); - - if (matchedStates.length > 0) { - if (matchedStates[0]?.type === "StaticMachine") { - const trigPoints = matchedStates[0]?.points; - - return !( - process.triggerId === trigPoints?.triggers?.uuid - ); - } else if (matchedStates[0]?.type === "Conveyor") { - const trigPoints = matchedStates[0]?.points; - - if (Array.isArray(trigPoints)) { - const nonEmptyTriggers = trigPoints.filter( - (point) => - point && point.triggers && point.triggers.length > 0 - ); - - const allTriggerUUIDs = nonEmptyTriggers - .flatMap((point) => point.triggers) - .map((trigger) => trigger.uuid); - - return !allTriggerUUIDs.includes(process.triggerId); - } - } - } - return true; - })), - }, - }, - }; - return updatedArmBot; - } - - return state; - }); - - const filteredStates = updatedStates.filter( - (state) => !deletedModelUUIDs.includes(state.modeluuid) - ); - - updateBackend(filteredStates); - setSimulationStates(filteredStates); - }; - - const deleteSelection = () => { - if (selectedAssets.length > 0 && duplicatedObjects.length === 0) { - const email = localStorage.getItem("email"); - const organization = email!.split("@")[1].split(".")[0]; - - const storedItems = JSON.parse( - localStorage.getItem("FloorItems") || "[]" - ); - const selectedUUIDs = selectedAssets.map( - (mesh: THREE.Object3D) => mesh.uuid - ); - - const updatedStoredItems = storedItems.filter( - (item: { modeluuid: string }) => !selectedUUIDs.includes(item.modeluuid) - ); - localStorage.setItem("FloorItems", JSON.stringify(updatedStoredItems)); - - selectedAssets.forEach((selectedMesh: THREE.Object3D) => { - //REST - - // const response = await deleteFloorItem(organization, selectedMesh.uuid, selectedMesh.userData.name); - - //SOCKET - - const data = { - organization: organization, - modeluuid: selectedMesh.uuid, - modelname: selectedMesh.userData.name, - socketId: socket.id, - }; - - socket.emit("v2:model-asset:delete", data); - - selectedMesh.traverse((child: THREE.Object3D) => { - if (child instanceof THREE.Mesh) { - if (child.geometry) child.geometry.dispose(); - if (Array.isArray(child.material)) { - child.material.forEach((material) => { - if (material.map) material.map.dispose(); - material.dispose(); - }); - } else if (child.material) { - if (child.material.map) child.material.map.dispose(); - child.material.dispose(); + const onPointerDown = (event: PointerEvent) => { + if (event.button === 2) { + isRightClick = true; + rightClickMoved = false; + } else if (event.button === 0) { + isSelecting = false; + isCtrlSelecting = event.ctrlKey; + if (event.ctrlKey && duplicatedObjects.length === 0) { + if (controls) (controls as any).enabled = false; + selectionBox.startPoint.set(pointer.x, pointer.y, 0); + } + } + }; + + const onPointerMove = (event: PointerEvent) => { + if (isRightClick) { + rightClickMoved = true; + } + isSelecting = true; + if (helper.isDown && event.ctrlKey && duplicatedObjects.length === 0 && isCtrlSelecting) { + selectionBox.endPoint.set(pointer.x, pointer.y, 0); + } + }; + + const onPointerUp = (event: PointerEvent) => { + if (event.button === 2) { + isRightClick = false; + if (!rightClickMoved) { + clearSelection(); + } + return; + } + + if (isSelecting && isCtrlSelecting) { + isCtrlSelecting = false; + isSelecting = false; + if (event.ctrlKey && duplicatedObjects.length === 0) { + selectAssets(); + } + } else if (!isSelecting && selectedAssets.length > 0 && ((pastedObjects.length === 0 && duplicatedObjects.length === 0 && movedObjects.length === 0 && rotatedObjects.length === 0) || event.button !== 0)) { + clearSelection(); + helper.enabled = true; + isCtrlSelecting = false; + } + }; + + const onKeyDown = (event: KeyboardEvent) => { + if (movedObjects.length > 0 || rotatedObjects.length > 0) return; + if (event.key.toLowerCase() === "escape") { + event.preventDefault(); + clearSelection(); + } + if (event.key.toLowerCase() === "delete") { + event.preventDefault(); + deleteSelection(); + } + }; + + const onContextMenu = (event: MouseEvent) => { + event.preventDefault(); + if (!rightClickMoved) { + clearSelection(); + } + }; + + if (!toggleView && activeModule === "builder") { + helper.enabled = true; + if (duplicatedObjects.length === 0 && pastedObjects.length === 0) { + canvasElement.addEventListener("pointerdown", onPointerDown); + canvasElement.addEventListener("pointermove", onPointerMove); + canvasElement.addEventListener("pointerup", onPointerUp); + } else { + helper.enabled = false; + helper.dispose(); + } + canvasElement.addEventListener("contextmenu", onContextMenu); + canvasElement.addEventListener("keydown", onKeyDown); + } else { + helper.enabled = false; + helper.dispose(); + } + + return () => { + canvasElement.removeEventListener("pointerdown", onPointerDown); + canvasElement.removeEventListener("pointermove", onPointerMove); + canvasElement.removeEventListener("contextmenu", onContextMenu); + canvasElement.removeEventListener("pointerup", onPointerUp); + canvasElement.removeEventListener("keydown", onKeyDown); + helper.enabled = false; + helper.dispose(); + }; + }, [camera, controls, scene, toggleView, selectedAssets, copiedObjects, pastedObjects, duplicatedObjects, movedObjects, socket, floorItems, rotatedObjects, activeModule,]); + + useEffect(() => { + if (activeModule !== "builder") { + clearSelection(); + } + }, [activeModule]); + + useFrame(() => { + if (pastedObjects.length === 0 && duplicatedObjects.length === 0 && movedObjects.length === 0 && rotatedObjects.length === 0) { + selectionGroup.current.position.set(0, 0, 0); + } + }); + + const selectAssets = () => { + selectionBox.endPoint.set(pointer.x, pointer.y, 0); + if (controls) (controls as any).enabled = true; + + let selectedObjects = selectionBox.select(); + let Objects = new Set<THREE.Object3D>(); + + selectedObjects.map((object) => { + let currentObject: THREE.Object3D | null = object; + while (currentObject) { + if (currentObject.userData.modelId) { + Objects.add(currentObject); + break; + } + currentObject = currentObject.parent || null; } - } }); - setSimulationStates( - ( - prevEvents: ( - | SimulationTypes.ConveyorEventsSchema - | SimulationTypes.VehicleEventsSchema - | SimulationTypes.StaticMachineEventsSchema - | SimulationTypes.ArmBotEventsSchema - )[] - ) => { - const updatedEvents = (prevEvents || []).filter( - (event) => event.modeluuid !== selectedMesh.uuid - ); - return updatedEvents; - } - ); + if (Objects.size === 0) { + clearSelection(); + return; + } - itemsGroupRef.current?.remove(selectedMesh); - }); + const updatedSelections = new Set(selectedAssets); + Objects.forEach((obj) => { updatedSelections.has(obj) ? updatedSelections.delete(obj) : updatedSelections.add(obj); }); - const allUUIDs = selectedAssets.map((val: any) => val.uuid); - removeConnections(allUUIDs); + const selected = Array.from(updatedSelections); - const updatedItems = floorItems.filter( - (item: { modeluuid: string }) => !selectedUUIDs.includes(item.modeluuid) - ); - setFloorItems(updatedItems); - } - toast.success("Selected models removed!"); - clearSelection(); - }; + setSelectedAssets(selected); + }; - return ( - <> - <group name="SelectionGroup"> - <group ref={selectionGroup} name="selectionAssetGroup"> - <BoundingBox boundingBoxRef={boundingBoxRef} /> - </group> - </group> + const clearSelection = () => { + selectionGroup.current.children = []; + selectionGroup.current.position.set(0, 0, 0); + selectionGroup.current.rotation.set(0, 0, 0); + setpastedObjects([]); + setDuplicatedObjects([]); + setSelectedAssets([]); + }; + const updateBackend = async (updatedPaths: (SimulationTypes.ConveyorEventsSchema | SimulationTypes.VehicleEventsSchema | SimulationTypes.StaticMachineEventsSchema | SimulationTypes.ArmBotEventsSchema)[]) => { + if (updatedPaths.length === 0) return; + const email = localStorage.getItem("email"); + const organization = email ? email.split("@")[1].split(".")[0] : ""; - <MoveControls - movedObjects={movedObjects} - setMovedObjects={setMovedObjects} - itemsGroupRef={itemsGroupRef} - copiedObjects={copiedObjects} - setCopiedObjects={setCopiedObjects} - pastedObjects={pastedObjects} - setpastedObjects={setpastedObjects} - duplicatedObjects={duplicatedObjects} - setDuplicatedObjects={setDuplicatedObjects} - rotatedObjects={rotatedObjects} - setRotatedObjects={setRotatedObjects} - selectionGroup={selectionGroup} - boundingBoxRef={boundingBoxRef} - /> + updatedPaths.forEach(async (updatedPath) => { + if (updatedPath.type === "Conveyor") { + // await setEventApi( + // organization, + // updatedPath.modeluuid, + // { type: "Conveyor", points: updatedPath.points, speed: updatedPath.speed } + // ); - <RotateControls - rotatedObjects={rotatedObjects} - setRotatedObjects={setRotatedObjects} - movedObjects={movedObjects} - setMovedObjects={setMovedObjects} - itemsGroupRef={itemsGroupRef} - copiedObjects={copiedObjects} - setCopiedObjects={setCopiedObjects} - pastedObjects={pastedObjects} - setpastedObjects={setpastedObjects} - duplicatedObjects={duplicatedObjects} - setDuplicatedObjects={setDuplicatedObjects} - selectionGroup={selectionGroup} - boundingBoxRef={boundingBoxRef} - /> + const data = { + organization: organization, + modeluuid: updatedPath.modeluuid, + eventData: { + type: "Conveyor", + points: updatedPath.points, + speed: updatedPath.speed, + }, + }; - <DuplicationControls - itemsGroupRef={itemsGroupRef} - duplicatedObjects={duplicatedObjects} - setDuplicatedObjects={setDuplicatedObjects} - setpastedObjects={setpastedObjects} - selectionGroup={selectionGroup} - movedObjects={movedObjects} - setMovedObjects={setMovedObjects} - rotatedObjects={rotatedObjects} - setRotatedObjects={setRotatedObjects} - boundingBoxRef={boundingBoxRef} - /> + socket.emit("v2:model-asset:updateEventData", data); + } else if (updatedPath.type === "Vehicle") { + // await setEventApi( + // organization, + // updatedPath.modeluuid, + // { type: "Vehicle", points: updatedPath.points } + // ); - <CopyPasteControls - itemsGroupRef={itemsGroupRef} - copiedObjects={copiedObjects} - setCopiedObjects={setCopiedObjects} - pastedObjects={pastedObjects} - setpastedObjects={setpastedObjects} - selectionGroup={selectionGroup} - setDuplicatedObjects={setDuplicatedObjects} - movedObjects={movedObjects} - setMovedObjects={setMovedObjects} - rotatedObjects={rotatedObjects} - setRotatedObjects={setRotatedObjects} - boundingBoxRef={boundingBoxRef} - /> - </> - ); + const data = { + organization: organization, + modeluuid: updatedPath.modeluuid, + eventData: { type: "Vehicle", points: updatedPath.points }, + }; + + socket.emit("v2:model-asset:updateEventData", data); + } else if (updatedPath.type === "StaticMachine") { + // await setEventApi( + // organization, + // updatedPath.modeluuid, + // { type: "StaticMachine", points: updatedPath.points } + // ); + + const data = { + organization: organization, + modeluuid: updatedPath.modeluuid, + eventData: { type: "StaticMachine", points: updatedPath.points }, + }; + + socket.emit("v2:model-asset:updateEventData", data); + } else if (updatedPath.type === "ArmBot") { + // await setEventApi( + // organization, + // updatedPath.modeluuid, + // { type: "ArmBot", points: updatedPath.points } + // ); + + const data = { + organization: organization, + modeluuid: updatedPath.modeluuid, + eventData: { type: "ArmBot", points: updatedPath.points }, + }; + + socket.emit("v2:model-asset:updateEventData", data); + } + }); + }; + + const removeConnections = (deletedModelUUIDs: string[]) => { + const updatedStates = simulationStates.map((state) => { + // Handle Conveyor + if (state.type === "Conveyor") { + const updatedConveyor: SimulationTypes.ConveyorEventsSchema = { + ...state, + points: state.points.map((point) => { + return { + ...point, + connections: { + ...point.connections, + targets: point.connections.targets.filter( + (target) => !deletedModelUUIDs.includes(target.modelUUID) + ), + }, + }; + }), + }; + return updatedConveyor; + } + + // Handle Vehicle + else if (state.type === "Vehicle") { + const updatedVehicle: SimulationTypes.VehicleEventsSchema = { + ...state, + points: { + ...state.points, + connections: { + ...state.points.connections, + targets: state.points.connections.targets.filter( + (target) => !deletedModelUUIDs.includes(target.modelUUID) + ), + }, + }, + }; + return updatedVehicle; + } + + // Handle StaticMachine + else if (state.type === "StaticMachine") { + const updatedStaticMachine: SimulationTypes.StaticMachineEventsSchema = + { + ...state, + points: { + ...state.points, + connections: { + ...state.points.connections, + targets: state.points.connections.targets.filter( + (target) => !deletedModelUUIDs.includes(target.modelUUID) + ), + }, + }, + }; + return updatedStaticMachine; + } + + // Handle ArmBot + else if (state.type === "ArmBot") { + const updatedArmBot: SimulationTypes.ArmBotEventsSchema = { + ...state, + points: { + ...state.points, + connections: { + ...state.points.connections, + + targets: state.points.connections.targets.filter( + (target: any) => !deletedModelUUIDs.includes(target.modelUUID) + ), + }, + actions: { + ...state.points.actions, + processes: (state.points.actions.processes = + state.points.actions.processes?.filter((process) => { + const matchedStates = simulationStates.filter((s) => deletedModelUUIDs.includes(s.modeluuid)); + + if (matchedStates.length > 0) { + if (matchedStates[0]?.type === "StaticMachine") { + const trigPoints = matchedStates[0]?.points; + + return !( + process.triggerId === trigPoints?.triggers?.uuid + ); + } else if (matchedStates[0]?.type === "Conveyor") { + const trigPoints = matchedStates[0]?.points; + + if (Array.isArray(trigPoints)) { + const nonEmptyTriggers = trigPoints.filter((point) => point && point.triggers && point.triggers.length > 0); + + const allTriggerUUIDs = nonEmptyTriggers.flatMap((point) => point.triggers).map((trigger) => trigger.uuid); + + return !allTriggerUUIDs.includes(process.triggerId); + } + } + } + return true; + })), + }, + }, + }; + return updatedArmBot; + } + + return state; + }); + + const filteredStates = updatedStates.filter((state) => !deletedModelUUIDs.includes(state.modeluuid)); + + updateBackend(filteredStates); + setSimulationStates(filteredStates); + }; + + const deleteSelection = () => { + if (selectedAssets.length > 0 && duplicatedObjects.length === 0) { + const email = localStorage.getItem("email"); + const organization = email!.split("@")[1].split(".")[0]; + + const storedItems = JSON.parse(localStorage.getItem("FloorItems") || "[]"); + const selectedUUIDs = selectedAssets.map((mesh: THREE.Object3D) => mesh.uuid); + + const updatedStoredItems = storedItems.filter((item: { modeluuid: string }) => !selectedUUIDs.includes(item.modeluuid)); + localStorage.setItem("FloorItems", JSON.stringify(updatedStoredItems)); + + selectedAssets.forEach((selectedMesh: THREE.Object3D) => { + //REST + + // const response = await deleteFloorItem(organization, selectedMesh.uuid, selectedMesh.userData.name); + + //SOCKET + + const data = { + organization: organization, + modeluuid: selectedMesh.uuid, + modelname: selectedMesh.userData.name, + socketId: socket.id, + }; + + socket.emit("v2:model-asset:delete", data); + + selectedMesh.traverse((child: THREE.Object3D) => { + if (child instanceof THREE.Mesh) { + if (child.geometry) child.geometry.dispose(); + if (Array.isArray(child.material)) { + child.material.forEach((material) => { + if (material.map) material.map.dispose(); + material.dispose(); + }); + } else if (child.material) { + if (child.material.map) child.material.map.dispose(); + child.material.dispose(); + } + } + }); + + setSimulationStates((prevEvents: (| SimulationTypes.ConveyorEventsSchema | SimulationTypes.VehicleEventsSchema | SimulationTypes.StaticMachineEventsSchema | SimulationTypes.ArmBotEventsSchema)[]) => { + const updatedEvents = (prevEvents || []).filter((event) => event.modeluuid !== selectedMesh.uuid); + return updatedEvents; + }); + + itemsGroupRef.current?.remove(selectedMesh); + }); + + const allUUIDs = selectedAssets.map((val: any) => val.uuid); + removeConnections(allUUIDs); + + const updatedItems = floorItems.filter((item: { modeluuid: string }) => !selectedUUIDs.includes(item.modeluuid)); + setFloorItems(updatedItems); + } + toast.success("Selected models removed!"); + clearSelection(); + }; + + return ( + <> + <group name="SelectionGroup"> + <group ref={selectionGroup} name="selectionAssetGroup"> + <BoundingBox boundingBoxRef={boundingBoxRef} /> + </group> + </group> + + <MoveControls movedObjects={movedObjects} setMovedObjects={setMovedObjects} itemsGroupRef={itemsGroupRef} copiedObjects={copiedObjects} setCopiedObjects={setCopiedObjects} pastedObjects={pastedObjects} setpastedObjects={setpastedObjects} duplicatedObjects={duplicatedObjects} setDuplicatedObjects={setDuplicatedObjects} rotatedObjects={rotatedObjects} setRotatedObjects={setRotatedObjects} selectionGroup={selectionGroup} boundingBoxRef={boundingBoxRef} /> + + <RotateControls rotatedObjects={rotatedObjects} setRotatedObjects={setRotatedObjects} movedObjects={movedObjects} setMovedObjects={setMovedObjects} itemsGroupRef={itemsGroupRef} copiedObjects={copiedObjects} setCopiedObjects={setCopiedObjects} pastedObjects={pastedObjects} setpastedObjects={setpastedObjects} duplicatedObjects={duplicatedObjects} setDuplicatedObjects={setDuplicatedObjects} selectionGroup={selectionGroup} boundingBoxRef={boundingBoxRef} /> + + <DuplicationControls itemsGroupRef={itemsGroupRef} duplicatedObjects={duplicatedObjects} setDuplicatedObjects={setDuplicatedObjects} setpastedObjects={setpastedObjects} selectionGroup={selectionGroup} movedObjects={movedObjects} setMovedObjects={setMovedObjects} rotatedObjects={rotatedObjects} setRotatedObjects={setRotatedObjects} boundingBoxRef={boundingBoxRef} /> + + <CopyPasteControls itemsGroupRef={itemsGroupRef} copiedObjects={copiedObjects} setCopiedObjects={setCopiedObjects} pastedObjects={pastedObjects} setpastedObjects={setpastedObjects} selectionGroup={selectionGroup} setDuplicatedObjects={setDuplicatedObjects} movedObjects={movedObjects} setMovedObjects={setMovedObjects} rotatedObjects={rotatedObjects} setRotatedObjects={setRotatedObjects} boundingBoxRef={boundingBoxRef} /> + </> + ); }; export default SelectionControls; diff --git a/app/src/modules/simulation/path/pathConnector.tsx b/app/src/modules/simulation/path/pathConnector.tsx index 9a49f39..4ca12a2 100644 --- a/app/src/modules/simulation/path/pathConnector.tsx +++ b/app/src/modules/simulation/path/pathConnector.tsx @@ -4,1616 +4,1280 @@ import * as THREE from "three"; import * as Types from "../../../types/world/worldTypes"; import * as SimulationTypes from "../../../types/simulationTypes"; import { QuadraticBezierLine } from "@react-three/drei"; -import { - useDeleteTool, - useIsConnecting, - useRenderDistance, - useSimulationStates, - useSocketStore, -} from "../../../store/store"; +import { useDeleteTool, useIsConnecting, useRenderDistance, useSimulationStates, useSocketStore, } from "../../../store/store"; import useModuleStore from "../../../store/useModuleStore"; import { usePlayButtonStore } from "../../../store/usePlayButtonStore"; import { setEventApi } from "../../../services/factoryBuilder/assest/floorAsset/setEventsApt"; -function PathConnector({ - pathsGroupRef, -}: { - pathsGroupRef: React.MutableRefObject<THREE.Group>; -}) { - const { activeModule } = useModuleStore(); - const { gl, raycaster, scene, pointer, camera } = useThree(); - const { deleteTool } = useDeleteTool(); - const { renderDistance } = useRenderDistance(); - const { setIsConnecting } = useIsConnecting(); - const { simulationStates, setSimulationStates } = useSimulationStates(); - const { isPlaying } = usePlayButtonStore(); - const { socket } = useSocketStore(); - const groupRefs = useRef<{ [key: string]: any }>({}); +function PathConnector({ pathsGroupRef, }: { pathsGroupRef: React.MutableRefObject<THREE.Group>; }) { + const { activeModule } = useModuleStore(); + const { gl, raycaster, scene, pointer, camera } = useThree(); + const { deleteTool } = useDeleteTool(); + const { renderDistance } = useRenderDistance(); + const { setIsConnecting } = useIsConnecting(); + const { simulationStates, setSimulationStates } = useSimulationStates(); + const { isPlaying } = usePlayButtonStore(); + const { socket } = useSocketStore(); + const groupRefs = useRef<{ [key: string]: any }>({}); - const [firstSelected, setFirstSelected] = useState<{ - modelUUID: string; - sphereUUID: string; - position: THREE.Vector3; - isCorner: boolean; - } | null>(null); - const [currentLine, setCurrentLine] = useState<{ - start: THREE.Vector3; - end: THREE.Vector3; - mid: THREE.Vector3; - } | null>(null); - const [helperlineColor, setHelperLineColor] = useState<string>("red"); - const [hoveredLineKey, setHoveredLineKey] = useState<string | null>(null); + const [firstSelected, setFirstSelected] = useState<{ modelUUID: string; sphereUUID: string; position: THREE.Vector3; isCorner: boolean; } | null>(null); + const [currentLine, setCurrentLine] = useState<{ start: THREE.Vector3; end: THREE.Vector3; mid: THREE.Vector3; } | null>(null); + const [helperlineColor, setHelperLineColor] = useState<string>("red"); + const [hoveredLineKey, setHoveredLineKey] = useState<string | null>(null); - const updatePathConnections = ( - fromModelUUID: string, - fromPointUUID: string, - toModelUUID: string, - toPointUUID: string - ) => { - const updatedPaths = simulationStates.map((path) => { - if (path.type === "Conveyor") { - // Handle outgoing connections from Conveyor - if (path.modeluuid === fromModelUUID) { - return { - ...path, - points: path.points.map((point) => { - if (point.uuid === fromPointUUID) { - const newTarget = { - modelUUID: toModelUUID, - pointUUID: toPointUUID, - }; - const existingTargets = point.connections.targets || []; + const updatePathConnections = (fromModelUUID: string, fromPointUUID: string, toModelUUID: string, toPointUUID: string) => { + const updatedPaths = simulationStates.map((path) => { + if (path.type === "Conveyor") { + // Handle outgoing connections from Conveyor + if (path.modeluuid === fromModelUUID) { + return { + ...path, + points: path.points.map((point) => { + if (point.uuid === fromPointUUID) { + const newTarget = { + modelUUID: toModelUUID, + pointUUID: toPointUUID, + }; + const existingTargets = point.connections.targets || []; - if ( - !existingTargets.some( - (target) => - target.modelUUID === newTarget.modelUUID && - target.pointUUID === newTarget.pointUUID - ) - ) { - return { - ...point, - connections: { - ...point.connections, - targets: [...existingTargets, newTarget], - }, - }; + if (!existingTargets.some((target) => target.modelUUID === newTarget.modelUUID && target.pointUUID === newTarget.pointUUID)) { + return { + ...point, + connections: { + ...point.connections, + targets: [...existingTargets, newTarget], + }, + }; + } + } + return point; + }), + }; } - } - return point; - }), - }; - } - // Handle incoming connections to Conveyor - else if (path.modeluuid === toModelUUID) { - return { - ...path, - points: path.points.map((point) => { - if (point.uuid === toPointUUID) { - const reverseTarget = { - modelUUID: fromModelUUID, - pointUUID: fromPointUUID, - }; - const existingTargets = point.connections.targets || []; + // Handle incoming connections to Conveyor + else if (path.modeluuid === toModelUUID) { + return { + ...path, + points: path.points.map((point) => { + if (point.uuid === toPointUUID) { + const reverseTarget = { + modelUUID: fromModelUUID, + pointUUID: fromPointUUID, + }; + const existingTargets = point.connections.targets || []; - if ( - !existingTargets.some( - (target) => - target.modelUUID === reverseTarget.modelUUID && - target.pointUUID === reverseTarget.pointUUID - ) - ) { - return { - ...point, - connections: { - ...point.connections, - targets: [...existingTargets, reverseTarget], - }, - }; + if (!existingTargets.some((target) => target.modelUUID === reverseTarget.modelUUID && target.pointUUID === reverseTarget.pointUUID)) { + return { + ...point, + connections: { + ...point.connections, + targets: [...existingTargets, reverseTarget], + }, + }; + } + } + return point; + }), + }; } - } - return point; - }), - }; - } - } else if (path.type === "Vehicle") { - // Handle outgoing connections from Vehicle - if ( - path.modeluuid === fromModelUUID && - path.points.uuid === fromPointUUID - ) { - const newTarget = { - modelUUID: toModelUUID, - pointUUID: toPointUUID, - }; - const existingTargets = path.points.connections.targets || []; + } else if (path.type === "Vehicle") { + // Handle outgoing connections from Vehicle + if (path.modeluuid === fromModelUUID && path.points.uuid === fromPointUUID) { + const newTarget = { + modelUUID: toModelUUID, + pointUUID: toPointUUID, + }; + const existingTargets = path.points.connections.targets || []; - // Check if target is a Conveyor - const toPath = simulationStates.find( - (p) => p.modeluuid === toModelUUID - ); - if (toPath?.type !== "Conveyor") { - console.log("Vehicle can only connect to Conveyors"); - return path; - } - - // Check if already has a connection - if (existingTargets.length >= 1) { - console.log("Vehicle can have only one connection"); - return path; - } - - if ( - !existingTargets.some( - (target) => - target.modelUUID === newTarget.modelUUID && - target.pointUUID === newTarget.pointUUID - ) - ) { - return { - ...path, - points: { - ...path.points, - connections: { - ...path.points.connections, - targets: [...existingTargets, newTarget], - }, - }, - }; - } - } - // Handle incoming connections to Vehicle - else if ( - path.modeluuid === toModelUUID && - path.points.uuid === toPointUUID - ) { - const reverseTarget = { - modelUUID: fromModelUUID, - pointUUID: fromPointUUID, - }; - const existingTargets = path.points.connections.targets || []; - - // Check if source is a Conveyor - const fromPath = simulationStates.find( - (p) => p.modeluuid === fromModelUUID - ); - if (fromPath?.type !== "Conveyor") { - console.log("Vehicle can only connect to Conveyors"); - return path; - } - - // Check if already has a connection - if (existingTargets.length >= 1) { - console.log("Vehicle can have only one connection"); - return path; - } - - if ( - !existingTargets.some( - (target) => - target.modelUUID === reverseTarget.modelUUID && - target.pointUUID === reverseTarget.pointUUID - ) - ) { - return { - ...path, - points: { - ...path.points, - connections: { - ...path.points.connections, - targets: [...existingTargets, reverseTarget], - }, - }, - }; - } - } - return path; - } else if (path.type === "StaticMachine") { - // Handle outgoing connections from StaticMachine - if ( - path.modeluuid === fromModelUUID && - path.points.uuid === fromPointUUID - ) { - const newTarget = { - modelUUID: toModelUUID, - pointUUID: toPointUUID, - }; - - // Ensure target is an ArmBot - const toPath = simulationStates.find( - (p) => p.modeluuid === toModelUUID - ); - if (toPath?.type !== "ArmBot") { - console.log("StaticMachine can only connect to ArmBot"); - return path; - } - - const existingTargets = path.points.connections.targets || []; - - // Allow only one connection - if (existingTargets.length >= 1) { - console.log("StaticMachine can only have one connection"); - return path; - } - - if ( - !existingTargets.some( - (target) => - target.modelUUID === newTarget.modelUUID && - target.pointUUID === newTarget.pointUUID - ) - ) { - return { - ...path, - points: { - ...path.points, - connections: { - ...path.points.connections, - targets: [...existingTargets, newTarget], - }, - }, - }; - } - } - - // Handle incoming connections to StaticMachine - else if ( - path.modeluuid === toModelUUID && - path.points.uuid === toPointUUID - ) { - const reverseTarget = { - modelUUID: fromModelUUID, - pointUUID: fromPointUUID, - }; - - const fromPath = simulationStates.find( - (p) => p.modeluuid === fromModelUUID - ); - if (fromPath?.type !== "ArmBot") { - console.log("StaticMachine can only be connected from ArmBot"); - return path; - } - - const existingTargets = path.points.connections.targets || []; - - if (existingTargets.length >= 1) { - console.log("StaticMachine can only have one connection"); - return path; - } - - if ( - !existingTargets.some( - (target) => - target.modelUUID === reverseTarget.modelUUID && - target.pointUUID === reverseTarget.pointUUID - ) - ) { - return { - ...path, - points: { - ...path.points, - connections: { - ...path.points.connections, - targets: [...existingTargets, reverseTarget], - }, - }, - }; - } - } - return path; - } else if (path.type === "ArmBot") { - // Handle outgoing connections from ArmBot - if ( - path.modeluuid === fromModelUUID && - path.points.uuid === fromPointUUID - ) { - const newTarget = { - modelUUID: toModelUUID, - pointUUID: toPointUUID, - }; - - const toPath = simulationStates.find( - (p) => p.modeluuid === toModelUUID - ); - if (!toPath) return path; - - const existingTargets = path.points.connections.targets || []; - - // Check if connecting to a StaticMachine and already connected to one - const alreadyConnectedToStatic = existingTargets.some((target) => { - const targetPath = simulationStates.find( - (p) => p.modeluuid === target.modelUUID - ); - return targetPath?.type === "StaticMachine"; - }); - - if (toPath.type === "StaticMachine") { - if (alreadyConnectedToStatic) { - console.log("ArmBot can only connect to one StaticMachine"); - return path; - } - } - - if ( - !existingTargets.some( - (target) => - target.modelUUID === newTarget.modelUUID && - target.pointUUID === newTarget.pointUUID - ) - ) { - return { - ...path, - points: { - ...path.points, - connections: { - ...path.points.connections, - targets: [...existingTargets, newTarget], - }, - }, - }; - } - } - - // Handle incoming connections to ArmBot - else if ( - path.modeluuid === toModelUUID && - path.points.uuid === toPointUUID - ) { - const reverseTarget = { - modelUUID: fromModelUUID, - pointUUID: fromPointUUID, - }; - - const fromPath = simulationStates.find( - (p) => p.modeluuid === fromModelUUID - ); - if (!fromPath) return path; - - const existingTargets = path.points.connections.targets || []; - - const alreadyConnectedFromStatic = existingTargets.some((target) => { - const targetPath = simulationStates.find( - (p) => p.modeluuid === target.modelUUID - ); - return targetPath?.type === "StaticMachine"; - }); - - if (fromPath.type === "StaticMachine") { - if (alreadyConnectedFromStatic) { - console.log( - "ArmBot can only be connected from one StaticMachine" - ); - return path; - } - } - - if ( - !existingTargets.some( - (target) => - target.modelUUID === reverseTarget.modelUUID && - target.pointUUID === reverseTarget.pointUUID - ) - ) { - return { - ...path, - points: { - ...path.points, - connections: { - ...path.points.connections, - targets: [...existingTargets, reverseTarget], - }, - }, - }; - } - } - return path; - } - - return path; - }); - - setSimulationStates(updatedPaths); - - const updatedPathDetails = updatedPaths.filter( - (path) => - path.modeluuid === fromModelUUID || path.modeluuid === toModelUUID - ); - - updateBackend(updatedPathDetails); - }; - - const updateBackend = async ( - updatedPaths: ( - | SimulationTypes.ConveyorEventsSchema - | SimulationTypes.VehicleEventsSchema - | SimulationTypes.StaticMachineEventsSchema - | SimulationTypes.ArmBotEventsSchema - )[] - ) => { - if (updatedPaths.length === 0) return; - const email = localStorage.getItem("email"); - const organization = email ? email.split("@")[1].split(".")[0] : ""; - - updatedPaths.forEach(async (updatedPath) => { - if (updatedPath.type === "Conveyor") { - // await setEventApi( - // organization, - // updatedPath.modeluuid, - // { type: "Conveyor", points: updatedPath.points, speed: updatedPath.speed } - // ); - - const data = { - organization: organization, - modeluuid: updatedPath.modeluuid, - eventData: { - type: "Conveyor", - points: updatedPath.points, - speed: updatedPath.speed, - }, - }; - - socket.emit("v2:model-asset:updateEventData", data); - } else if (updatedPath.type === "Vehicle") { - // await setEventApi( - // organization, - // updatedPath.modeluuid, - // { type: "Vehicle", points: updatedPath.points } - // ); - - const data = { - organization: organization, - modeluuid: updatedPath.modeluuid, - eventData: { type: "Vehicle", points: updatedPath.points }, - }; - - socket.emit("v2:model-asset:updateEventData", data); - } else if (updatedPath.type === "StaticMachine") { - // await setEventApi( - // organization, - // updatedPath.modeluuid, - // { type: "StaticMachine", points: updatedPath.points } - // ); - - const data = { - organization: organization, - modeluuid: updatedPath.modeluuid, - eventData: { type: "StaticMachine", points: updatedPath.points }, - }; - - socket.emit("v2:model-asset:updateEventData", data); - } else if (updatedPath.type === "ArmBot") { - // await setEventApi( - // organization, - // updatedPath.modeluuid, - // { type: "ArmBot", points: updatedPath.points } - // ); - - const data = { - organization: organization, - modeluuid: updatedPath.modeluuid, - eventData: { type: "ArmBot", points: updatedPath.points }, - }; - - socket.emit("v2:model-asset:updateEventData", data); - } - }); - }; - - const handleAddConnection = ( - fromModelUUID: string, - fromUUID: string, - toModelUUID: string, - toUUID: string - ) => { - updatePathConnections(fromModelUUID, fromUUID, toModelUUID, toUUID); - setFirstSelected(null); - setCurrentLine(null); - setIsConnecting(false); - }; - - useEffect(() => { - const canvasElement = gl.domElement; - let drag = false; - let MouseDown = false; - - const onMouseDown = () => { - MouseDown = true; - drag = false; - }; - - const onMouseUp = () => { - MouseDown = false; - }; - - const onMouseMove = () => { - if (MouseDown) { - drag = true; - } - }; - - const onContextMenu = (evt: MouseEvent) => { - evt.preventDefault(); - if (drag || evt.button === 0) return; - - raycaster.setFromCamera(pointer, camera); - const intersects = raycaster.intersectObjects( - pathsGroupRef.current.children, - true - ); - - if (intersects.length > 0) { - const intersected = intersects[0].object; - - if (intersected.name.includes("events-sphere")) { - const modelUUID = intersected.userData.path.modeluuid; - const sphereUUID = intersected.uuid; - const worldPosition = new THREE.Vector3(); - intersected.getWorldPosition(worldPosition); - - let isStartOrEnd = false; - - if ( - intersected.userData.path.points && - intersected.userData.path.points.length > 1 - ) { - isStartOrEnd = - intersected.userData.path.points.length > 0 && - (sphereUUID === intersected.userData.path.points[0].uuid || - sphereUUID === - intersected.userData.path.points[ - intersected.userData.path.points.length - 1 - ].uuid); - } else if (intersected.userData.path.points) { - isStartOrEnd = sphereUUID === intersected.userData.path.points.uuid; - } - - if (modelUUID) { - const firstPath = simulationStates.find( - (p) => p.modeluuid === firstSelected?.modelUUID - ); - const secondPath = simulationStates.find( - (p) => p.modeluuid === modelUUID - ); - - // Prevent vehicle-to-vehicle connections - if ( - firstPath && - secondPath && - firstPath.type === "Vehicle" && - secondPath.type === "Vehicle" - ) { - console.log("Cannot connect two vehicle paths together"); - return; - } - - // Prevent conveyor middle point to conveyor connections - if ( - firstPath && - secondPath && - firstPath.type === "Conveyor" && - secondPath.type === "Conveyor" && - (!firstSelected?.isCorner || !isStartOrEnd) - ) { - console.log( - "Conveyor connections must be between start/end points" - ); - return; - } - - // Check if this specific connection already exists - const isDuplicateConnection = firstSelected - ? simulationStates.some((path) => { - if (path.modeluuid === firstSelected.modelUUID) { - if (path.type === "Conveyor") { - const point = path.points.find( - (p) => p.uuid === firstSelected.sphereUUID - ); - return point?.connections.targets.some( - (t) => - t.modelUUID === modelUUID && - t.pointUUID === sphereUUID - ); - } else if (path.type === "Vehicle") { - return path.points.connections.targets.some( - (t) => - t.modelUUID === modelUUID && - t.pointUUID === sphereUUID - ); + // Check if target is a Conveyor + const toPath = simulationStates.find((p) => p.modeluuid === toModelUUID); + if (toPath?.type !== "Conveyor") { + console.log("Vehicle can only connect to Conveyors"); + return path; } - } - return false; - }) - : false; - if (isDuplicateConnection) { - console.log("These points are already connected. Ignoring."); - return; + // Check if already has a connection + if (existingTargets.length >= 1) { + console.log("Vehicle can have only one connection"); + return path; + } + + if (!existingTargets.some((target) => target.modelUUID === newTarget.modelUUID && target.pointUUID === newTarget.pointUUID)) { + return { + ...path, + points: { + ...path.points, + connections: { + ...path.points.connections, + targets: [...existingTargets, newTarget], + }, + }, + }; + } + } + // Handle incoming connections to Vehicle + else if (path.modeluuid === toModelUUID && path.points.uuid === toPointUUID) { + const reverseTarget = { + modelUUID: fromModelUUID, + pointUUID: fromPointUUID, + }; + const existingTargets = path.points.connections.targets || []; + + // Check if source is a Conveyor + const fromPath = simulationStates.find((p) => p.modeluuid === fromModelUUID); + if (fromPath?.type !== "Conveyor") { + console.log("Vehicle can only connect to Conveyors"); + return path; + } + + // Check if already has a connection + if (existingTargets.length >= 1) { + console.log("Vehicle can have only one connection"); + return path; + } + + if (!existingTargets.some((target) => target.modelUUID === reverseTarget.modelUUID && target.pointUUID === reverseTarget.pointUUID)) { + return { + ...path, + points: { + ...path.points, + connections: { + ...path.points.connections, + targets: [...existingTargets, reverseTarget], + }, + }, + }; + } + } + return path; + } else if (path.type === "StaticMachine") { + // Handle outgoing connections from StaticMachine + if (path.modeluuid === fromModelUUID && path.points.uuid === fromPointUUID) { + const newTarget = { + modelUUID: toModelUUID, + pointUUID: toPointUUID, + }; + + // Ensure target is an ArmBot + const toPath = simulationStates.find((p) => p.modeluuid === toModelUUID); + if (toPath?.type !== "ArmBot") { + console.log("StaticMachine can only connect to ArmBot"); + return path; + } + + const existingTargets = path.points.connections.targets || []; + + // Allow only one connection + if (existingTargets.length >= 1) { + console.log("StaticMachine can only have one connection"); + return path; + } + + if (!existingTargets.some((target) => target.modelUUID === newTarget.modelUUID && target.pointUUID === newTarget.pointUUID)) { + return { + ...path, + points: { + ...path.points, + connections: { + ...path.points.connections, + targets: [...existingTargets, newTarget], + }, + }, + }; + } + } + + // Handle incoming connections to StaticMachine + else if (path.modeluuid === toModelUUID && path.points.uuid === toPointUUID) { + const reverseTarget = { + modelUUID: fromModelUUID, + pointUUID: fromPointUUID, + }; + + const fromPath = simulationStates.find((p) => p.modeluuid === fromModelUUID); + if (fromPath?.type !== "ArmBot") { + console.log("StaticMachine can only be connected from ArmBot"); + return path; + } + + const existingTargets = path.points.connections.targets || []; + + if (existingTargets.length >= 1) { + console.log("StaticMachine can only have one connection"); + return path; + } + + if (!existingTargets.some((target) => target.modelUUID === reverseTarget.modelUUID && target.pointUUID === reverseTarget.pointUUID)) { + return { + ...path, + points: { + ...path.points, + connections: { + ...path.points.connections, + targets: [...existingTargets, reverseTarget], + }, + }, + }; + } + } + return path; + } else if (path.type === "ArmBot") { + // Handle outgoing connections from ArmBot + if (path.modeluuid === fromModelUUID && path.points.uuid === fromPointUUID) { + const newTarget = { + modelUUID: toModelUUID, + pointUUID: toPointUUID, + }; + + const toPath = simulationStates.find((p) => p.modeluuid === toModelUUID); + if (!toPath) return path; + + const existingTargets = path.points.connections.targets || []; + + // Check if connecting to a StaticMachine and already connected to one + const alreadyConnectedToStatic = existingTargets.some((target) => { + const targetPath = simulationStates.find((p) => p.modeluuid === target.modelUUID); + return targetPath?.type === "StaticMachine"; + }); + + if (toPath.type === "StaticMachine") { + if (alreadyConnectedToStatic) { + console.log("ArmBot can only connect to one StaticMachine"); + return path; + } + } + + if (!existingTargets.some((target) => target.modelUUID === newTarget.modelUUID && target.pointUUID === newTarget.pointUUID)) { + return { + ...path, + points: { + ...path.points, + connections: { + ...path.points.connections, + targets: [...existingTargets, newTarget], + }, + }, + }; + } + } + + // Handle incoming connections to ArmBot + else if (path.modeluuid === toModelUUID && path.points.uuid === toPointUUID) { + const reverseTarget = { + modelUUID: fromModelUUID, + pointUUID: fromPointUUID, + }; + + const fromPath = simulationStates.find((p) => p.modeluuid === fromModelUUID); + if (!fromPath) return path; + + const existingTargets = path.points.connections.targets || []; + + const alreadyConnectedFromStatic = existingTargets.some((target) => { + const targetPath = simulationStates.find((p) => p.modeluuid === target.modelUUID); + return targetPath?.type === "StaticMachine"; + }); + + if (fromPath.type === "StaticMachine") { + if (alreadyConnectedFromStatic) { + console.log( + "ArmBot can only be connected from one StaticMachine" + ); + return path; + } + } + + if (!existingTargets.some((target) => target.modelUUID === reverseTarget.modelUUID && target.pointUUID === reverseTarget.pointUUID)) { + return { + ...path, + points: { + ...path.points, + connections: { + ...path.points.connections, + targets: [...existingTargets, reverseTarget], + }, + }, + }; + } + } + return path; } - // For Vehicles, check if they're already connected to anything - if (intersected.userData.path.type === "Vehicle") { - const vehicleConnections = - intersected.userData.path.points.connections.targets.length; - if (vehicleConnections >= 1) { - console.log("Vehicle can only have one connection"); - return; - } + return path; + }); + + setSimulationStates(updatedPaths); + + const updatedPathDetails = updatedPaths.filter((path) => path.modeluuid === fromModelUUID || path.modeluuid === toModelUUID); + + updateBackend(updatedPathDetails); + }; + + const updateBackend = async (updatedPaths: (| SimulationTypes.ConveyorEventsSchema | SimulationTypes.VehicleEventsSchema | SimulationTypes.StaticMachineEventsSchema | SimulationTypes.ArmBotEventsSchema)[]) => { + if (updatedPaths.length === 0) return; + const email = localStorage.getItem("email"); + const organization = email ? email.split("@")[1].split(".")[0] : ""; + + updatedPaths.forEach(async (updatedPath) => { + if (updatedPath.type === "Conveyor") { + // await setEventApi( + // organization, + // updatedPath.modeluuid, + // { type: "Conveyor", points: updatedPath.points, speed: updatedPath.speed } + // ); + + const data = { + organization: organization, + modeluuid: updatedPath.modeluuid, + eventData: { + type: "Conveyor", + points: updatedPath.points, + speed: updatedPath.speed, + }, + }; + + socket.emit("v2:model-asset:updateEventData", data); + } else if (updatedPath.type === "Vehicle") { + // await setEventApi( + // organization, + // updatedPath.modeluuid, + // { type: "Vehicle", points: updatedPath.points } + // ); + + const data = { + organization: organization, + modeluuid: updatedPath.modeluuid, + eventData: { type: "Vehicle", points: updatedPath.points }, + }; + + socket.emit("v2:model-asset:updateEventData", data); + } else if (updatedPath.type === "StaticMachine") { + // await setEventApi( + // organization, + // updatedPath.modeluuid, + // { type: "StaticMachine", points: updatedPath.points } + // ); + + const data = { + organization: organization, + modeluuid: updatedPath.modeluuid, + eventData: { type: "StaticMachine", points: updatedPath.points }, + }; + + socket.emit("v2:model-asset:updateEventData", data); + } else if (updatedPath.type === "ArmBot") { + // await setEventApi( + // organization, + // updatedPath.modeluuid, + // { type: "ArmBot", points: updatedPath.points } + // ); + + const data = { + organization: organization, + modeluuid: updatedPath.modeluuid, + eventData: { type: "ArmBot", points: updatedPath.points }, + }; + + socket.emit("v2:model-asset:updateEventData", data); } + }); + }; - // For non-Vehicle paths, check if already connected - if (intersected.userData.path.type !== "Vehicle") { - const isAlreadyConnected = simulationStates.some((path) => { - if (path.type === "Conveyor") { - return path.points.some( - (point) => - point.uuid === sphereUUID && - point.connections.targets.length > 0 - ); - } - return false; - }); - - if (isAlreadyConnected) { - console.log("Conveyor point is already connected. Ignoring."); - return; - } - } - - if (firstSelected) { - // Check if trying to connect Vehicle to non-Conveyor - if ( - (firstPath?.type === "Vehicle" && - secondPath?.type !== "Conveyor") || - (secondPath?.type === "Vehicle" && - firstPath?.type !== "Conveyor") - ) { - console.log("Vehicle can only connect to Conveyors"); - return; - } - - // Prevent same-path connections - if (firstSelected.modelUUID === modelUUID) { - console.log("Cannot connect spheres on the same path."); - return; - } - - // Check if StaticMachine is involved in the connection - if ( - (firstPath?.type === "StaticMachine" && - secondPath?.type !== "ArmBot") || - (secondPath?.type === "StaticMachine" && - firstPath?.type !== "ArmBot") - ) { - console.log("StaticMachine can only connect to ArmBot"); - return; - } - - // Check if StaticMachine already has a connection - if (firstPath?.type === "StaticMachine") { - const staticConnections = - firstPath.points.connections.targets.length; - if (staticConnections >= 1) { - console.log("StaticMachine can only have one connection"); - return; - } - } - if (secondPath?.type === "StaticMachine") { - const staticConnections = - secondPath.points.connections.targets.length; - if (staticConnections >= 1) { - console.log("StaticMachine can only have one connection"); - return; - } - } - - // Check if ArmBot is involved - if ( - (firstPath?.type === "ArmBot" && - secondPath?.type === "StaticMachine") || - (secondPath?.type === "ArmBot" && - firstPath?.type === "StaticMachine") - ) { - const armBotPath = - firstPath?.type === "ArmBot" ? firstPath : secondPath; - const staticPath = - firstPath?.type === "StaticMachine" ? firstPath : secondPath; - - const armBotConnections = - armBotPath.points.connections.targets || []; - const alreadyConnectedToStatic = armBotConnections.some( - (target) => { - const targetPath = simulationStates.find( - (p) => p.modeluuid === target.modelUUID - ); - return targetPath?.type === "StaticMachine"; - } - ); - - if (alreadyConnectedToStatic) { - console.log("ArmBot can only connect to one StaticMachine"); - return; - } - - const staticConnections = - staticPath.points.connections.targets.length; - if (staticConnections >= 1) { - console.log("StaticMachine can only have one connection"); - return; - } - } - - // Prevent ArmBot ↔ ArmBot - if ( - firstPath?.type === "ArmBot" && - secondPath?.type === "ArmBot" - ) { - console.log("Cannot connect two ArmBots together"); - return; - } - - // If one is ArmBot, ensure the other is StaticMachine or Conveyor - if ( - firstPath?.type === "ArmBot" || - secondPath?.type === "ArmBot" - ) { - const otherType = - firstPath?.type === "ArmBot" - ? secondPath?.type - : firstPath?.type; - if (otherType !== "StaticMachine" && otherType !== "Conveyor") { - console.log( - "ArmBot can only connect to Conveyors or one StaticMachine" - ); - return; - } - } - - // At least one must be start/end point - if (!firstSelected.isCorner && !isStartOrEnd) { - console.log( - "At least one of the selected spheres must be a start or end point." - ); - return; - } - - // All checks passed - make the connection - handleAddConnection( - firstSelected.modelUUID, - firstSelected.sphereUUID, - modelUUID, - sphereUUID - ); - } else { - // First selection - just store it - setFirstSelected({ - modelUUID, - sphereUUID, - position: worldPosition, - isCorner: isStartOrEnd, - }); - setIsConnecting(true); - } - } - } - } else { - // Clicked outside - cancel connection + const handleAddConnection = (fromModelUUID: string, fromUUID: string, toModelUUID: string, toUUID: string) => { + updatePathConnections(fromModelUUID, fromUUID, toModelUUID, toUUID); setFirstSelected(null); setCurrentLine(null); setIsConnecting(false); - } }; - if (activeModule === "simulation" && !deleteTool) { - canvasElement.addEventListener("mousedown", onMouseDown); - canvasElement.addEventListener("mouseup", onMouseUp); - canvasElement.addEventListener("mousemove", onMouseMove); - canvasElement.addEventListener("contextmenu", onContextMenu); - } else { - setFirstSelected(null); - setCurrentLine(null); - setIsConnecting(false); - } + useEffect(() => { + const canvasElement = gl.domElement; + let drag = false; + let MouseDown = false; - return () => { - canvasElement.removeEventListener("mousedown", onMouseDown); - canvasElement.removeEventListener("mouseup", onMouseUp); - canvasElement.removeEventListener("mousemove", onMouseMove); - canvasElement.removeEventListener("contextmenu", onContextMenu); - }; - }, [camera, scene, raycaster, firstSelected, simulationStates, deleteTool]); - - useFrame(() => { - Object.values(groupRefs.current).forEach((group) => { - if (group) { - const distance = new THREE.Vector3( - ...group.position.toArray() - ).distanceTo(camera.position); - group.visible = distance <= renderDistance && !isPlaying; - } - }); - }); - - useFrame(() => { - if (firstSelected) { - raycaster.setFromCamera(pointer, camera); - const intersects = raycaster - .intersectObjects(scene.children, true) - .filter( - (intersect) => - !intersect.object.name.includes("Roof") && - !intersect.object.name.includes("agv-collider") && - !intersect.object.name.includes("MeasurementReference") && - !intersect.object.userData.isPathObject && - !(intersect.object.type === "GridHelper") - ); - - let point: THREE.Vector3 | null = null; - let snappedSphere: { - sphereUUID: string; - position: THREE.Vector3; - modelUUID: string; - isCorner: boolean; - } | null = null; - let isInvalidConnection = false; - - if (intersects.length > 0) { - point = intersects[0].point; - if (point.y < 0.05) { - point = new THREE.Vector3(point.x, 0.05, point.z); - } - } - - const sphereIntersects = raycaster - .intersectObjects(pathsGroupRef.current.children, true) - .filter((obj) => obj.object.name.includes("events-sphere")); - - if (sphereIntersects.length > 0) { - const sphere = sphereIntersects[0].object; - const sphereUUID = sphere.uuid; - const spherePosition = new THREE.Vector3(); - sphere.getWorldPosition(spherePosition); - const pathData = sphere.userData.path; - const modelUUID = pathData.modeluuid; - - const firstPath = simulationStates.find( - (p) => p.modeluuid === firstSelected.modelUUID - ); - const secondPath = simulationStates.find( - (p) => p.modeluuid === modelUUID - ); - const isVehicleToVehicle = - firstPath?.type === "Vehicle" && secondPath?.type === "Vehicle"; - - // Inside the useFrame hook, where we check for snapped spheres: - const isConnectable = - (pathData.type === "Vehicle" || - pathData.type === "ArmBot" || - (pathData.points.length > 0 && - (sphereUUID === pathData.points[0].uuid || - sphereUUID === - pathData.points[pathData.points.length - 1].uuid || - (pathData.type === "Conveyor" && - firstPath?.type === "ArmBot")))) && // Allow ArmBot to connect to middle points - !isVehicleToVehicle && - !( - firstPath?.type === "Conveyor" && - pathData.type === "Conveyor" && - !firstSelected.isCorner - ); - - // Check for duplicate connection (regardless of path type) - const isDuplicateConnection = simulationStates.some((path) => { - if (path.modeluuid === firstSelected.modelUUID) { - if (path.type === "Conveyor") { - const point = path.points.find( - (p) => p.uuid === firstSelected.sphereUUID - ); - return point?.connections.targets.some( - (t) => t.modelUUID === modelUUID && t.pointUUID === sphereUUID - ); - } else if (path.type === "Vehicle") { - return path.points.connections.targets.some( - (t) => t.modelUUID === modelUUID && t.pointUUID === sphereUUID - ); - } - } - return false; - }); - - // For non-Vehicle paths, check if already connected - const isNonVehicleAlreadyConnected = - pathData.type !== "Vehicle" && - simulationStates.some((path) => { - if (path.type === "Conveyor") { - return path.points.some( - (point) => - point.uuid === sphereUUID && - point.connections.targets.length > 0 - ); - } - return false; - }); - - // Check vehicle connection rules - const isVehicleAtMaxConnections = - pathData.type === "Vehicle" && - pathData.points.connections.targets.length >= 1; - const isVehicleConnectingToNonConveyor = - (firstPath?.type === "Vehicle" && secondPath?.type !== "Conveyor") || - (secondPath?.type === "Vehicle" && firstPath?.type !== "Conveyor"); - - // Check if StaticMachine is connecting to non-ArmBot - const isStaticMachineToNonArmBot = - (firstPath?.type === "StaticMachine" && - secondPath?.type !== "ArmBot") || - (secondPath?.type === "StaticMachine" && - firstPath?.type !== "ArmBot"); - - // Check if StaticMachine already has a connection - const isStaticMachineAtMaxConnections = - (firstPath?.type === "StaticMachine" && - firstPath.points.connections.targets.length >= 1) || - (secondPath?.type === "StaticMachine" && - secondPath.points.connections.targets.length >= 1); - - // Check if ArmBot is connecting to StaticMachine - const isArmBotToStaticMachine = - (firstPath?.type === "ArmBot" && - secondPath?.type === "StaticMachine") || - (secondPath?.type === "ArmBot" && - firstPath?.type === "StaticMachine"); - - // Prevent multiple StaticMachine connections to ArmBot - let isArmBotAlreadyConnectedToStatic = false; - if (isArmBotToStaticMachine) { - const armBotPath = - firstPath?.type === "ArmBot" ? firstPath : secondPath; - isArmBotAlreadyConnectedToStatic = - armBotPath.points.connections.targets.some((target) => { - const targetPath = simulationStates.find( - (p) => p.modeluuid === target.modelUUID - ); - return targetPath?.type === "StaticMachine"; - }); - } - - // Prevent ArmBot to ArmBot - const isArmBotToArmBot = - firstPath?.type === "ArmBot" && secondPath?.type === "ArmBot"; - - // If ArmBot is involved, other must be Conveyor or StaticMachine - const isArmBotToInvalidType = - (firstPath?.type === "ArmBot" || secondPath?.type === "ArmBot") && - !( - firstPath?.type === "Conveyor" || - firstPath?.type === "StaticMachine" || - secondPath?.type === "Conveyor" || - secondPath?.type === "StaticMachine" - ); - - if ( - !isDuplicateConnection && - !isVehicleToVehicle && - !isNonVehicleAlreadyConnected && - !isVehicleAtMaxConnections && - !isVehicleConnectingToNonConveyor && - !isStaticMachineToNonArmBot && - !isStaticMachineAtMaxConnections && - !isArmBotToArmBot && - !isArmBotToInvalidType && - !isArmBotAlreadyConnectedToStatic && - firstSelected.sphereUUID !== sphereUUID && - firstSelected.modelUUID !== modelUUID && - (firstSelected.isCorner || isConnectable) && - !( - firstPath?.type === "Conveyor" && - pathData.type === "Conveyor" && - !(firstSelected.isCorner && isConnectable) - ) - ) { - snappedSphere = { - sphereUUID, - position: spherePosition, - modelUUID, - isCorner: isConnectable, - }; - } else { - isInvalidConnection = true; - } - } - - if (snappedSphere) { - point = snappedSphere.position; - } - - if (point) { - const distance = firstSelected.position.distanceTo(point); - const heightFactor = Math.max(0.5, distance * 0.2); - const midPoint = new THREE.Vector3( - (firstSelected.position.x + point.x) / 2, - Math.max(firstSelected.position.y, point.y) + heightFactor, - (firstSelected.position.z + point.z) / 2 - ); - - setCurrentLine({ - start: firstSelected.position, - end: point, - mid: midPoint, - }); - - if (sphereIntersects.length > 0) { - setHelperLineColor(isInvalidConnection ? "red" : "#6cf542"); - } else { - setHelperLineColor("yellow"); - } - } else { - setCurrentLine(null); - setIsConnecting(false); - } - } else { - setCurrentLine(null); - setIsConnecting(false); - } - }); - - const removeConnections = ( - connection1: { model: string; point: string }, - connection2: { model: string; point: string } - ) => { - const updatedStates = simulationStates.map((state) => { - // Handle Conveyor (which has multiple points) - if (state.type === "Conveyor") { - const updatedConveyor: SimulationTypes.ConveyorEventsSchema = { - ...state, - points: state.points.map((point) => { - // Check if this point is either connection1 or connection2 - if ( - (state.modeluuid === connection1.model && - point.uuid === connection1.point) || - (state.modeluuid === connection2.model && - point.uuid === connection2.point) - ) { - return { - ...point, - connections: { - ...point.connections, - targets: point.connections.targets.filter((target) => { - // Remove the target that matches the other connection - return !( - (target.modelUUID === connection1.model && - target.pointUUID === connection1.point) || - (target.modelUUID === connection2.model && - target.pointUUID === connection2.point) - ); - }), - }, - }; - } - return point; - }), + const onMouseDown = () => { + MouseDown = true; + drag = false; }; - return updatedConveyor; - } - // Handle Vehicle - else if (state.type === "Vehicle") { - if ( - (state.modeluuid === connection1.model && - state.points.uuid === connection1.point) || - (state.modeluuid === connection2.model && - state.points.uuid === connection2.point) - ) { - const updatedVehicle: SimulationTypes.VehicleEventsSchema = { - ...state, - points: { - ...state.points, - connections: { - ...state.points.connections, - targets: state.points.connections.targets.filter((target) => { - return !( - (target.modelUUID === connection1.model && - target.pointUUID === connection1.point) || - (target.modelUUID === connection2.model && - target.pointUUID === connection2.point) - ); - }), - }, - // Ensure all required Vehicle point properties are included - speed: state.points.speed, - actions: state.points.actions, - }, - }; - return updatedVehicle; - } - } - // Handle StaticMachine - else if (state.type === "StaticMachine") { - if ( - (state.modeluuid === connection1.model && - state.points.uuid === connection1.point) || - (state.modeluuid === connection2.model && - state.points.uuid === connection2.point) - ) { - const updatedStaticMachine: SimulationTypes.StaticMachineEventsSchema = - { - ...state, - points: { - ...state.points, - connections: { - ...state.points.connections, - targets: state.points.connections.targets.filter((target) => { - return !( - (target.modelUUID === connection1.model && - target.pointUUID === connection1.point) || - (target.modelUUID === connection2.model && - target.pointUUID === connection2.point) - ); - }), - }, - // Ensure all required StaticMachine point properties are included - actions: state.points.actions, - triggers: state.points.triggers, - }, - }; - return updatedStaticMachine; - } - } - // Handle ArmBot - else if (state.type === "ArmBot") { - if ( - (state.modeluuid === connection1.model && - state.points.uuid === connection1.point) || - (state.modeluuid === connection2.model && - state.points.uuid === connection2.point) - ) { - const updatedArmBot: SimulationTypes.ArmBotEventsSchema = { - ...state, - points: { - ...state.points, - connections: { - ...state.points.connections, - targets: state.points.connections.targets.filter((target) => { - return !( - (target.modelUUID === connection1.model && - target.pointUUID === connection1.point) || - (target.modelUUID === connection2.model && - target.pointUUID === connection2.point) - ); - }), - }, - actions: { - ...state.points.actions, - processes: - state.points.actions.processes?.filter((process) => { - return !( - process.startPoint === connection1.point || - process.endPoint === connection1.point || - process.startPoint === connection2.point || - process.endPoint === connection2.point - ); - }) || [], - }, - triggers: state.points.triggers, - }, - }; - return updatedArmBot; - } - } - return state; - }); - const updatedPaths = updatedStates.filter( - (state) => - state.modeluuid === connection1.model || - state.modeluuid === connection2.model - ); - - console.log("updatedPaths: ", updatedPaths); - updateBackend(updatedPaths); - - setSimulationStates(updatedStates); - }; - const removeConnection = (deletedModelUUIDs: string[]) => { - const updatedStates = simulationStates.map((state) => { - // Handle Conveyor - if (state.type === "Conveyor") { - const updatedConveyor: SimulationTypes.ConveyorEventsSchema = { - ...state, - points: state.points.map((point) => { - return { - ...point, - connections: { - ...point.connections, - targets: point.connections.targets.filter( - (target) => !deletedModelUUIDs.includes(target.modelUUID) - ), - }, - }; - }), + const onMouseUp = () => { + MouseDown = false; }; - return updatedConveyor; - } - // Handle Vehicle - else if (state.type === "Vehicle") { - const updatedVehicle: SimulationTypes.VehicleEventsSchema = { - ...state, - points: { - ...state.points, - connections: { - ...state.points.connections, - targets: state.points.connections.targets.filter( - (target) => !deletedModelUUIDs.includes(target.modelUUID) - ), - }, - }, + const onMouseMove = () => { + if (MouseDown) { + drag = true; + } }; - return updatedVehicle; - } - // Handle StaticMachine - else if (state.type === "StaticMachine") { - const updatedStaticMachine: SimulationTypes.StaticMachineEventsSchema = - { - ...state, - points: { - ...state.points, - connections: { - ...state.points.connections, - targets: state.points.connections.targets.filter( - (target) => !deletedModelUUIDs.includes(target.modelUUID) - ), - }, - }, - }; - return updatedStaticMachine; - } + const onContextMenu = (evt: MouseEvent) => { + evt.preventDefault(); + if (drag || evt.button === 0) return; - // Handle ArmBot - else if (state.type === "ArmBot") { - const updatedArmBot: SimulationTypes.ArmBotEventsSchema = { - ...state, - points: { - ...state.points, - connections: { - ...state.points.connections, + raycaster.setFromCamera(pointer, camera); + const intersects = raycaster.intersectObjects( + pathsGroupRef.current.children, + true + ); - targets: state.points.connections.targets.filter( - (target: any) => !deletedModelUUIDs.includes(target.modelUUID) - ), - }, - actions: { - ...state.points.actions, - processes: (state.points.actions.processes = - state.points.actions.processes?.filter((process) => { - const matchedStates = simulationStates.filter((s) => - deletedModelUUIDs.includes(s.modeluuid) - ); + if (intersects.length > 0) { + const intersected = intersects[0].object; - if (matchedStates.length > 0) { - if (matchedStates[0]?.type === "StaticMachine") { - const trigPoints = matchedStates[0]?.points; + if (intersected.name.includes("events-sphere")) { + const modelUUID = intersected.userData.path.modeluuid; + const sphereUUID = intersected.uuid; + const worldPosition = new THREE.Vector3(); + intersected.getWorldPosition(worldPosition); - return !( - process.triggerId === trigPoints?.triggers?.uuid - ); - } else if (matchedStates[0]?.type === "Conveyor") { - const trigPoints = matchedStates[0]?.points; + let isStartOrEnd = false; - if (Array.isArray(trigPoints)) { - const nonEmptyTriggers = trigPoints.filter( - (point) => - point && point.triggers && point.triggers.length > 0 - ); - - const allTriggerUUIDs = nonEmptyTriggers - .flatMap((point) => point.triggers) - .map((trigger) => trigger.uuid); - - return !allTriggerUUIDs.includes(process.triggerId); - } + if (intersected.userData.path.points && intersected.userData.path.points.length > 1) { + isStartOrEnd = intersected.userData.path.points.length > 0 + && (sphereUUID === intersected.userData.path.points[0].uuid || sphereUUID === intersected.userData.path.points[intersected.userData.path.points.length - 1].uuid); + } else if (intersected.userData.path.points) { + isStartOrEnd = sphereUUID === intersected.userData.path.points.uuid; } - } - return true; - })), - }, - }, - }; - return updatedArmBot; - } - return state; + if (modelUUID) { + const firstPath = simulationStates.find((p) => p.modeluuid === firstSelected?.modelUUID); + const secondPath = simulationStates.find((p) => p.modeluuid === modelUUID); + + // Prevent vehicle-to-vehicle connections + if (firstPath && secondPath && firstPath.type === "Vehicle" && secondPath.type === "Vehicle") { + console.log("Cannot connect two vehicle paths together"); + return; + } + + // Prevent conveyor middle point to conveyor connections + if (firstPath && secondPath && firstPath.type === "Conveyor" && secondPath.type === "Conveyor" && (!firstSelected?.isCorner || !isStartOrEnd)) { + console.log("Conveyor connections must be between start/end points"); + return; + } + + // Check if this specific connection already exists + const isDuplicateConnection = firstSelected + ? simulationStates.some((path) => { + if (path.modeluuid === firstSelected.modelUUID) { + if (path.type === "Conveyor") { + const point = path.points.find( + (p) => p.uuid === firstSelected.sphereUUID + ); + return point?.connections.targets.some( + (t) => + t.modelUUID === modelUUID && + t.pointUUID === sphereUUID + ); + } else if (path.type === "Vehicle") { + return path.points.connections.targets.some( + (t) => + t.modelUUID === modelUUID && + t.pointUUID === sphereUUID + ); + } + } + return false; + }) + : false; + + if (isDuplicateConnection) { + console.log("These points are already connected. Ignoring."); + return; + } + + // For Vehicles, check if they're already connected to anything + if (intersected.userData.path.type === "Vehicle") { + const vehicleConnections = + intersected.userData.path.points.connections.targets.length; + if (vehicleConnections >= 1) { + console.log("Vehicle can only have one connection"); + return; + } + } + + // For non-Vehicle paths, check if already connected + if (intersected.userData.path.type !== "Vehicle") { + const isAlreadyConnected = simulationStates.some((path) => { + if (path.type === "Conveyor") { + return path.points.some( + (point) => + point.uuid === sphereUUID && + point.connections.targets.length > 0 + ); + } + return false; + }); + + if (isAlreadyConnected) { + console.log("Conveyor point is already connected. Ignoring."); + return; + } + } + + if (firstSelected) { + // Check if trying to connect Vehicle to non-Conveyor + if ((firstPath?.type === "Vehicle" && secondPath?.type !== "Conveyor") || (secondPath?.type === "Vehicle" && firstPath?.type !== "Conveyor")) { + console.log("Vehicle can only connect to Conveyors"); + return; + } + + // Prevent same-path connections + if (firstSelected.modelUUID === modelUUID) { + console.log("Cannot connect spheres on the same path."); + return; + } + + // Check if StaticMachine is involved in the connection + if ((firstPath?.type === "StaticMachine" && secondPath?.type !== "ArmBot") || (secondPath?.type === "StaticMachine" && firstPath?.type !== "ArmBot")) { + console.log("StaticMachine can only connect to ArmBot"); + return; + } + + // Check if StaticMachine already has a connection + if (firstPath?.type === "StaticMachine") { + const staticConnections = firstPath.points.connections.targets.length; + if (staticConnections >= 1) { + console.log("StaticMachine can only have one connection"); + return; + } + } + if (secondPath?.type === "StaticMachine") { + const staticConnections = secondPath.points.connections.targets.length; + if (staticConnections >= 1) { + console.log("StaticMachine can only have one connection"); + return; + } + } + + // Check if ArmBot is involved + if ((firstPath?.type === "ArmBot" && secondPath?.type === "StaticMachine") || (secondPath?.type === "ArmBot" && firstPath?.type === "StaticMachine")) { + const armBotPath = firstPath?.type === "ArmBot" ? firstPath : secondPath; + const staticPath = firstPath?.type === "StaticMachine" ? firstPath : secondPath; + + const armBotConnections = armBotPath.points.connections.targets || []; + const alreadyConnectedToStatic = armBotConnections.some( + (target) => { + const targetPath = simulationStates.find((p) => p.modeluuid === target.modelUUID); + return targetPath?.type === "StaticMachine"; + } + ); + + if (alreadyConnectedToStatic) { + console.log("ArmBot can only connect to one StaticMachine"); + return; + } + + const staticConnections = staticPath.points.connections.targets.length; + if (staticConnections >= 1) { + console.log("StaticMachine can only have one connection"); + return; + } + } + + // Prevent ArmBot ↔ ArmBot + if (firstPath?.type === "ArmBot" && secondPath?.type === "ArmBot") { + console.log("Cannot connect two ArmBots together"); + return; + } + + // If one is ArmBot, ensure the other is StaticMachine or Conveyor + if (firstPath?.type === "ArmBot" || secondPath?.type === "ArmBot") { + const otherType = firstPath?.type === "ArmBot" ? secondPath?.type : firstPath?.type; + if (otherType !== "StaticMachine" && otherType !== "Conveyor") { + console.log("ArmBot can only connect to Conveyors or one StaticMachine"); + return; + } + } + + // At least one must be start/end point + if (!firstSelected.isCorner && !isStartOrEnd) { + console.log("At least one of the selected spheres must be a start or end point."); + return; + } + + // All checks passed - make the connection + handleAddConnection(firstSelected.modelUUID, firstSelected.sphereUUID, modelUUID, sphereUUID); + } else { + // First selection - just store it + setFirstSelected({ modelUUID, sphereUUID, position: worldPosition, isCorner: isStartOrEnd }); + setIsConnecting(true); + } + } + } + } else { + // Clicked outside - cancel connection + setFirstSelected(null); + setCurrentLine(null); + setIsConnecting(false); + } + }; + + if (activeModule === "simulation" && !deleteTool) { + canvasElement.addEventListener("mousedown", onMouseDown); + canvasElement.addEventListener("mouseup", onMouseUp); + canvasElement.addEventListener("mousemove", onMouseMove); + canvasElement.addEventListener("contextmenu", onContextMenu); + } else { + setFirstSelected(null); + setCurrentLine(null); + setIsConnecting(false); + } + + return () => { + canvasElement.removeEventListener("mousedown", onMouseDown); + canvasElement.removeEventListener("mouseup", onMouseUp); + canvasElement.removeEventListener("mousemove", onMouseMove); + canvasElement.removeEventListener("contextmenu", onContextMenu); + }; + }, [camera, scene, raycaster, firstSelected, simulationStates, deleteTool]); + + useFrame(() => { + Object.values(groupRefs.current).forEach((group) => { + if (group) { + const distance = new THREE.Vector3(...group.position.toArray()).distanceTo(camera.position); + group.visible = distance <= renderDistance && !isPlaying; + } + }); }); - const filteredStates = updatedStates.filter( - (state) => !deletedModelUUIDs.includes(state.modeluuid) - ); + useFrame(() => { + if (firstSelected) { + raycaster.setFromCamera(pointer, camera); + const intersects = raycaster.intersectObjects(scene.children, true).filter( + (intersect) => + !intersect.object.name.includes("Roof") && + !intersect.object.name.includes("agv-collider") && + !intersect.object.name.includes("MeasurementReference") && + !intersect.object.userData.isPathObject && + !(intersect.object.type === "GridHelper") + ); - updateBackend(filteredStates); - setSimulationStates(filteredStates); - }; + let point: THREE.Vector3 | null = null; + let snappedSphere: { sphereUUID: string; position: THREE.Vector3; modelUUID: string; isCorner: boolean; } | null = null; + let isInvalidConnection = false; - return ( - <group name="simulationConnectionGroup" visible={!isPlaying}> - {simulationStates.flatMap((path) => { - if (path.type === "Conveyor") { - return path.points.flatMap((point) => - point.connections.targets.map((target, index) => { - const targetPath = simulationStates.find( - (p) => p.modeluuid === target.modelUUID - ); - if ( - targetPath?.type !== "Conveyor" && - targetPath?.type !== "ArmBot" - ) - return null; + if (intersects.length > 0) { + point = intersects[0].point; + if (point.y < 0.05) { + point = new THREE.Vector3(point.x, 0.05, point.z); + } + } - const fromSphere = pathsGroupRef.current?.getObjectByProperty( - "uuid", - point.uuid - ); - const toSphere = pathsGroupRef.current?.getObjectByProperty( - "uuid", - target.pointUUID - ); + const sphereIntersects = raycaster.intersectObjects(pathsGroupRef.current.children, true).filter((obj) => obj.object.name.includes("events-sphere")); - if (fromSphere && toSphere) { - const fromWorldPosition = new THREE.Vector3(); - const toWorldPosition = new THREE.Vector3(); - fromSphere.getWorldPosition(fromWorldPosition); - toSphere.getWorldPosition(toWorldPosition); + if (sphereIntersects.length > 0) { + const sphere = sphereIntersects[0].object; + const sphereUUID = sphere.uuid; + const spherePosition = new THREE.Vector3(); + sphere.getWorldPosition(spherePosition); + const pathData = sphere.userData.path; + const modelUUID = pathData.modeluuid; - const distance = fromWorldPosition.distanceTo(toWorldPosition); + const firstPath = simulationStates.find((p) => p.modeluuid === firstSelected.modelUUID); + const secondPath = simulationStates.find((p) => p.modeluuid === modelUUID); + const isVehicleToVehicle = firstPath?.type === "Vehicle" && secondPath?.type === "Vehicle"; + + // Inside the useFrame hook, where we check for snapped spheres: + const isConnectable = ( + pathData.type === 'Vehicle' || + pathData.type === 'ArmBot' || + (pathData.points.length > 0 && ( + sphereUUID === pathData.points[0].uuid || + sphereUUID === pathData.points[pathData.points.length - 1].uuid || + (pathData.type === 'Conveyor' && firstPath?.type === 'ArmBot') // Allow ArmBot to connect to middle points + )) + ) && + !isVehicleToVehicle && + !( + firstPath?.type === 'Conveyor' && + pathData.type === 'Conveyor' && + !firstSelected.isCorner + ); + + // Check for duplicate connection (regardless of path type) + const isDuplicateConnection = simulationStates.some(path => { + if (path.modeluuid === firstSelected.modelUUID) { + if (path.type === 'Conveyor') { + const point = path.points.find(p => p.uuid === firstSelected.sphereUUID); + return point?.connections.targets.some(t => + t.modelUUID === modelUUID && t.pointUUID === sphereUUID + ); + } else if (path.type === 'Vehicle') { + return path.points.connections.targets.some(t => + t.modelUUID === modelUUID && t.pointUUID === sphereUUID + ); + } + } + return false; + }); + + // For non-Vehicle paths, check if already connected + const isNonVehicleAlreadyConnected = pathData.type !== 'Vehicle' && + simulationStates.some(path => { + if (path.type === 'Conveyor') { + return path.points.some(point => + point.uuid === sphereUUID && + point.connections.targets.length > 0 + ); + } + return false; + }); + + // Check vehicle connection rules + const isVehicleAtMaxConnections = pathData.type === 'Vehicle' && + pathData.points.connections.targets.length >= 1; + const isVehicleConnectingToNonConveyor = + (firstPath?.type === 'Vehicle' && secondPath?.type !== 'Conveyor') || + (secondPath?.type === 'Vehicle' && firstPath?.type !== 'Conveyor'); + + // Check if StaticMachine is connecting to non-ArmBot + const isStaticMachineToNonArmBot = + (firstPath?.type === 'StaticMachine' && secondPath?.type !== 'ArmBot') || + (secondPath?.type === 'StaticMachine' && firstPath?.type !== 'ArmBot'); + + // Check if StaticMachine already has a connection + const isStaticMachineAtMaxConnections = + (firstPath?.type === 'StaticMachine' && firstPath.points.connections.targets.length >= 1) || + (secondPath?.type === 'StaticMachine' && secondPath.points.connections.targets.length >= 1); + + // Check if ArmBot is connecting to StaticMachine + const isArmBotToStaticMachine = + (firstPath?.type === 'ArmBot' && secondPath?.type === 'StaticMachine') || + (secondPath?.type === 'ArmBot' && firstPath?.type === 'StaticMachine'); + + // Prevent multiple StaticMachine connections to ArmBot + let isArmBotAlreadyConnectedToStatic = false; + if (isArmBotToStaticMachine) { + const armBotPath = firstPath?.type === 'ArmBot' ? firstPath : secondPath; + isArmBotAlreadyConnectedToStatic = armBotPath.points.connections.targets.some(target => { + const targetPath = simulationStates.find(p => p.modeluuid === target.modelUUID); + return targetPath?.type === 'StaticMachine'; + }); + } + + // Prevent ArmBot to ArmBot + const isArmBotToArmBot = firstPath?.type === 'ArmBot' && secondPath?.type === 'ArmBot'; + + // If ArmBot is involved, other must be Conveyor or StaticMachine + const isArmBotToInvalidType = (firstPath?.type === 'ArmBot' || secondPath?.type === 'ArmBot') && + !(firstPath?.type === 'Conveyor' || firstPath?.type === 'StaticMachine' || + secondPath?.type === 'Conveyor' || secondPath?.type === 'StaticMachine'); + + if ( + !isDuplicateConnection && + !isVehicleToVehicle && + !isNonVehicleAlreadyConnected && + !isVehicleAtMaxConnections && + !isVehicleConnectingToNonConveyor && + !isStaticMachineToNonArmBot && + !isStaticMachineAtMaxConnections && + !isArmBotToArmBot && + !isArmBotToInvalidType && + !isArmBotAlreadyConnectedToStatic && + firstSelected.sphereUUID !== sphereUUID && + firstSelected.modelUUID !== modelUUID && + (firstSelected.isCorner || isConnectable) && + !(firstPath?.type === 'Conveyor' && + pathData.type === 'Conveyor' && + !(firstSelected.isCorner && isConnectable)) + ) { + snappedSphere = { + sphereUUID, + position: spherePosition, + modelUUID, + isCorner: isConnectable + }; + } else { + isInvalidConnection = true; + } + } + + if (snappedSphere) { + point = snappedSphere.position; + } + + if (point) { + const distance = firstSelected.position.distanceTo(point); const heightFactor = Math.max(0.5, distance * 0.2); const midPoint = new THREE.Vector3( - (fromWorldPosition.x + toWorldPosition.x) / 2, - Math.max(fromWorldPosition.y, toWorldPosition.y) + - heightFactor, - (fromWorldPosition.z + toWorldPosition.z) / 2 + (firstSelected.position.x + point.x) / 2, + Math.max(firstSelected.position.y, point.y) + heightFactor, + (firstSelected.position.z + point.z) / 2 ); - return ( - <QuadraticBezierLine - key={`${point.uuid}-${target.pointUUID}-${index}`} - ref={(el) => - (groupRefs.current[ - `${point.uuid}-${target.pointUUID}-${index}` - ] = el!) - } - start={fromWorldPosition.toArray()} - end={toWorldPosition.toArray()} - mid={midPoint.toArray()} - color={ - deleteTool && - hoveredLineKey === - `${point.uuid}-${target.pointUUID}-${index}` - ? "red" - : targetPath?.type === "ArmBot" - ? "#42a5f5" - : "white" - } + setCurrentLine({ + start: firstSelected.position, + end: point, + mid: midPoint, + }); + + if (sphereIntersects.length > 0) { + setHelperLineColor(isInvalidConnection ? "red" : "#6cf542"); + } else { + setHelperLineColor("yellow"); + } + } else { + setCurrentLine(null); + setIsConnecting(false); + } + } else { + setCurrentLine(null); + setIsConnecting(false); + } + }); + + const removeConnection = (connection1: { model: string; point: string }, connection2: { model: string; point: string }) => { + const updatedStates = simulationStates.map((state) => { + + // Handle Conveyor (which has multiple points) + if (state.type === "Conveyor") { + const updatedConveyor: SimulationTypes.ConveyorEventsSchema = { + ...state, + points: state.points.map((point) => { + // Check if this point is either connection1 or connection2 + if ((state.modeluuid === connection1.model && point.uuid === connection1.point) || (state.modeluuid === connection2.model && point.uuid === connection2.point)) { + return { + ...point, + connections: { + ...point.connections, + targets: point.connections.targets.filter((target) => { + // Remove the target that matches the other connection + return !( + (target.modelUUID === connection1.model && + target.pointUUID === connection1.point) || + (target.modelUUID === connection2.model && + target.pointUUID === connection2.point) + ); + }), + }, + }; + } + return point; + }), + }; + return updatedConveyor; + } + + // Handle Vehicle + else if (state.type === "Vehicle") { + if ((state.modeluuid === connection1.model && state.points.uuid === connection1.point) || (state.modeluuid === connection2.model && state.points.uuid === connection2.point)) { + const updatedVehicle: SimulationTypes.VehicleEventsSchema = { + ...state, + points: { + ...state.points, + connections: { + ...state.points.connections, + targets: state.points.connections.targets.filter((target) => { + return !( + (target.modelUUID === connection1.model && + target.pointUUID === connection1.point) || + (target.modelUUID === connection2.model && + target.pointUUID === connection2.point) + ); + }), + }, + // Ensure all required Vehicle point properties are included + speed: state.points.speed, + actions: state.points.actions, + }, + }; + return updatedVehicle; + } + } + + // Handle StaticMachine + else if (state.type === "StaticMachine") { + if ((state.modeluuid === connection1.model && state.points.uuid === connection1.point) || (state.modeluuid === connection2.model && state.points.uuid === connection2.point)) { + const updatedStaticMachine: SimulationTypes.StaticMachineEventsSchema = + { + ...state, + points: { + ...state.points, + connections: { + ...state.points.connections, + targets: state.points.connections.targets.filter((target) => { + return !( + (target.modelUUID === connection1.model && + target.pointUUID === connection1.point) || + (target.modelUUID === connection2.model && + target.pointUUID === connection2.point) + ); + }), + }, + // Ensure all required StaticMachine point properties are included + actions: state.points.actions, + triggers: state.points.triggers, + }, + }; + return updatedStaticMachine; + } + } + + // Handle ArmBot + else if (state.type === "ArmBot") { + if ((state.modeluuid === connection1.model && state.points.uuid === connection1.point) || (state.modeluuid === connection2.model && state.points.uuid === connection2.point)) { + const updatedArmBot: SimulationTypes.ArmBotEventsSchema = { + ...state, + points: { + ...state.points, + connections: { + ...state.points.connections, + targets: state.points.connections.targets.filter((target) => { + return !( + (target.modelUUID === connection1.model && + target.pointUUID === connection1.point) || + (target.modelUUID === connection2.model && + target.pointUUID === connection2.point) + ); + }), + }, + actions: { + ...state.points.actions, + processes: + state.points.actions.processes?.filter((process) => { + return !( + process.startPoint === connection1.point || + process.endPoint === connection1.point || + process.startPoint === connection2.point || + process.endPoint === connection2.point + ); + }) || [], + }, + triggers: state.points.triggers, + }, + }; + return updatedArmBot; + } + } + return state; + }); + + const updatedPaths = updatedStates.filter( + (state) => + state.modeluuid === connection1.model || + state.modeluuid === connection2.model + ); + + console.log("updatedPaths: ", updatedPaths); + updateBackend(updatedPaths); + + setSimulationStates(updatedStates); + }; + + const removeConnections = (deletedModelUUIDs: string[]) => { + const updatedStates = simulationStates.map((state) => { + + // Handle Conveyor + if (state.type === "Conveyor") { + const updatedConveyor: SimulationTypes.ConveyorEventsSchema = { + ...state, + points: state.points.map((point) => { + return { + ...point, + connections: { + ...point.connections, + targets: point.connections.targets.filter( + (target) => !deletedModelUUIDs.includes(target.modelUUID) + ), + }, + }; + }), + }; + return updatedConveyor; + } + + // Handle Vehicle + else if (state.type === "Vehicle") { + const updatedVehicle: SimulationTypes.VehicleEventsSchema = { + ...state, + points: { + ...state.points, + connections: { + ...state.points.connections, + targets: state.points.connections.targets.filter( + (target) => !deletedModelUUIDs.includes(target.modelUUID) + ), + }, + }, + }; + return updatedVehicle; + } + + // Handle StaticMachine + else if (state.type === "StaticMachine") { + const updatedStaticMachine: SimulationTypes.StaticMachineEventsSchema = + { + ...state, + points: { + ...state.points, + connections: { + ...state.points.connections, + targets: state.points.connections.targets.filter( + (target) => !deletedModelUUIDs.includes(target.modelUUID) + ), + }, + }, + }; + return updatedStaticMachine; + } + + // Handle ArmBot + else if (state.type === "ArmBot") { + const updatedArmBot: SimulationTypes.ArmBotEventsSchema = { + ...state, + points: { + ...state.points, + connections: { + ...state.points.connections, + + targets: state.points.connections.targets.filter( + (target: any) => !deletedModelUUIDs.includes(target.modelUUID) + ), + }, + actions: { + ...state.points.actions, + processes: (state.points.actions.processes = + state.points.actions.processes?.filter((process) => { + const matchedStates = simulationStates.filter((s) => + deletedModelUUIDs.includes(s.modeluuid) + ); + + if (matchedStates.length > 0) { + if (matchedStates[0]?.type === "StaticMachine") { + const trigPoints = matchedStates[0]?.points; + + return !( + process.triggerId === trigPoints?.triggers?.uuid + ); + } else if (matchedStates[0]?.type === "Conveyor") { + const trigPoints = matchedStates[0]?.points; + + if (Array.isArray(trigPoints)) { + const nonEmptyTriggers = trigPoints.filter( + (point) => + point && point.triggers && point.triggers.length > 0 + ); + + const allTriggerUUIDs = nonEmptyTriggers + .flatMap((point) => point.triggers) + .map((trigger) => trigger.uuid); + + return !allTriggerUUIDs.includes(process.triggerId); + } + } + } + return true; + })), + }, + }, + }; + return updatedArmBot; + } + + return state; + }); + + const filteredStates = updatedStates.filter( + (state) => !deletedModelUUIDs.includes(state.modeluuid) + ); + + updateBackend(filteredStates); + setSimulationStates(filteredStates); + }; + + return ( + <group name="simulationConnectionGroup" visible={!isPlaying}> + {simulationStates.flatMap((path) => { + if (path.type === "Conveyor") { + return path.points.flatMap((point) => + point.connections.targets.map((target, index) => { + const targetPath = simulationStates.find((p) => p.modeluuid === target.modelUUID); + if (targetPath?.type !== "Conveyor" && targetPath?.type !== "ArmBot") return null; + + const fromSphere = pathsGroupRef.current?.getObjectByProperty("uuid", point.uuid); + const toSphere = pathsGroupRef.current?.getObjectByProperty("uuid", target.pointUUID); + + if (fromSphere && toSphere) { + const fromWorldPosition = new THREE.Vector3(); + const toWorldPosition = new THREE.Vector3(); + fromSphere.getWorldPosition(fromWorldPosition); + toSphere.getWorldPosition(toWorldPosition); + + const distance = fromWorldPosition.distanceTo(toWorldPosition); + const heightFactor = Math.max(0.5, distance * 0.2); + const midPoint = new THREE.Vector3( + (fromWorldPosition.x + toWorldPosition.x) / 2, + Math.max(fromWorldPosition.y, toWorldPosition.y) + + heightFactor, + (fromWorldPosition.z + toWorldPosition.z) / 2 + ); + + return ( + <QuadraticBezierLine + key={`${point.uuid}-${target.pointUUID}-${index}`} + ref={(el) => (groupRefs.current[`${point.uuid}-${target.pointUUID}-${index}`] = el!)} + start={fromWorldPosition.toArray()} + end={toWorldPosition.toArray()} + mid={midPoint.toArray()} + color={deleteTool && hoveredLineKey === `${point.uuid}-${target.pointUUID}-${index}` ? "red" : targetPath?.type === "ArmBot" ? "#42a5f5" : "white"} + lineWidth={4} + dashed={deleteTool && hoveredLineKey === `${point.uuid}-${target.pointUUID}-${index}` ? false : true} + dashSize={0.75} + dashScale={20} + onPointerOver={() => setHoveredLineKey(`${point.uuid}-${target.pointUUID}-${index}`)} + onPointerOut={() => setHoveredLineKey(null)} + onClick={() => { + if (deleteTool) { + const connection1 = { + model: path.modeluuid, + point: point.uuid, + }; + const connection2 = { + model: target.modelUUID, + point: target.pointUUID, + }; + + removeConnection(connection1, connection2); + } + }} + userData={target} + /> + ); + } + return null; + }) + ); + } + + if (path.type === "Vehicle") { + return path.points.connections.targets.map((target, index) => { + const fromSphere = pathsGroupRef.current?.getObjectByProperty("uuid", path.points.uuid); + const toSphere = pathsGroupRef.current?.getObjectByProperty("uuid", target.pointUUID); + + if (fromSphere && toSphere) { + const fromWorldPosition = new THREE.Vector3(); + const toWorldPosition = new THREE.Vector3(); + fromSphere.getWorldPosition(fromWorldPosition); + toSphere.getWorldPosition(toWorldPosition); + + const distance = fromWorldPosition.distanceTo(toWorldPosition); + const heightFactor = Math.max(0.5, distance * 0.2); + const midPoint = new THREE.Vector3( + (fromWorldPosition.x + toWorldPosition.x) / 2, + Math.max(fromWorldPosition.y, toWorldPosition.y) + heightFactor, + (fromWorldPosition.z + toWorldPosition.z) / 2 + ); + + return ( + <QuadraticBezierLine + key={`${path.points.uuid}-${target.pointUUID}-${index}`} + ref={(el) => (groupRefs.current[`${path.points.uuid}-${target.pointUUID}-${index}`] = el!)} + start={fromWorldPosition.toArray()} + end={toWorldPosition.toArray()} + mid={midPoint.toArray()} + color={deleteTool && hoveredLineKey === `${path.points.uuid}-${target.pointUUID}-${index}` ? "red" : "orange"} + lineWidth={4} + dashed={deleteTool && hoveredLineKey === `${path.points.uuid}-${target.pointUUID}-${index}` ? false : true} + dashSize={0.75} + dashScale={20} + onPointerOver={() => setHoveredLineKey(`${path.points.uuid}-${target.pointUUID}-${index}`)} + onPointerOut={() => setHoveredLineKey(null)} + onClick={() => { + if (deleteTool) { + const connection1 = { + model: path.modeluuid, + point: path.points.uuid, + }; + const connection2 = { + model: target.modelUUID, + point: target.pointUUID, + }; + + removeConnection(connection1, connection2); + } + }} + userData={target} + /> + ); + } + return null; + }); + } + + if (path.type === "StaticMachine") { + return path.points.connections.targets.map((target, index) => { + const targetPath = simulationStates.find((p) => p.modeluuid === target.modelUUID); + if (targetPath?.type !== "ArmBot") return null; + + const fromSphere = pathsGroupRef.current?.getObjectByProperty("uuid", path.points.uuid); + const toSphere = pathsGroupRef.current?.getObjectByProperty("uuid", target.pointUUID); + + if (fromSphere && toSphere) { + const fromWorldPosition = new THREE.Vector3(); + const toWorldPosition = new THREE.Vector3(); + fromSphere.getWorldPosition(fromWorldPosition); + toSphere.getWorldPosition(toWorldPosition); + + const distance = fromWorldPosition.distanceTo(toWorldPosition); + const heightFactor = Math.max(0.5, distance * 0.2); + const midPoint = new THREE.Vector3( + (fromWorldPosition.x + toWorldPosition.x) / 2, + Math.max(fromWorldPosition.y, toWorldPosition.y) + heightFactor, + (fromWorldPosition.z + toWorldPosition.z) / 2 + ); + + return ( + <QuadraticBezierLine + key={`${path.points.uuid}-${target.pointUUID}-${index}`} + ref={(el) => (groupRefs.current[`${path.points.uuid}-${target.pointUUID}-${index}`] = el!)} + start={fromWorldPosition.toArray()} + end={toWorldPosition.toArray()} + mid={midPoint.toArray()} + color={deleteTool && hoveredLineKey === `${path.points.uuid}-${target.pointUUID}-${index}` ? "red" : "#42a5f5"} + lineWidth={4} + dashed={deleteTool && hoveredLineKey === `${path.points.uuid}-${target.pointUUID}-${index}` ? false : true} + dashSize={0.75} + dashScale={20} + onPointerOver={() => setHoveredLineKey(`${path.points.uuid}-${target.pointUUID}-${index}`)} + onPointerOut={() => setHoveredLineKey(null)} + onClick={() => { + if (deleteTool) { + const connection1 = { + model: path.modeluuid, + point: path.points.uuid, + }; + const connection2 = { + model: target.modelUUID, + point: target.pointUUID, + }; + + removeConnection(connection1, connection2); + } + }} + userData={target} + /> + ); + } + return null; + }); + } + + return []; + })} + + {currentLine && ( + <QuadraticBezierLine + start={currentLine.start.toArray()} + end={currentLine.end.toArray()} + mid={currentLine.mid.toArray()} + color={helperlineColor} lineWidth={4} - dashed={ - deleteTool && - hoveredLineKey === - `${point.uuid}-${target.pointUUID}-${index}` - ? false - : true - } - dashSize={0.75} + dashed + dashSize={1} dashScale={20} - onPointerOver={() => - setHoveredLineKey( - `${point.uuid}-${target.pointUUID}-${index}` - ) - } - onPointerOut={() => setHoveredLineKey(null)} - onClick={() => { - if (deleteTool) { - const connection1 = { - model: path.modeluuid, - point: point.uuid, - }; - const connection2 = { - model: target.modelUUID, - point: target.pointUUID, - }; - - removeConnections(connection1, connection2); - } - }} - userData={target} - /> - ); - } - return null; - }) - ); - } - - if (path.type === "Vehicle") { - return path.points.connections.targets.map((target, index) => { - const fromSphere = pathsGroupRef.current?.getObjectByProperty( - "uuid", - path.points.uuid - ); - const toSphere = pathsGroupRef.current?.getObjectByProperty( - "uuid", - target.pointUUID - ); - - if (fromSphere && toSphere) { - const fromWorldPosition = new THREE.Vector3(); - const toWorldPosition = new THREE.Vector3(); - fromSphere.getWorldPosition(fromWorldPosition); - toSphere.getWorldPosition(toWorldPosition); - - const distance = fromWorldPosition.distanceTo(toWorldPosition); - const heightFactor = Math.max(0.5, distance * 0.2); - const midPoint = new THREE.Vector3( - (fromWorldPosition.x + toWorldPosition.x) / 2, - Math.max(fromWorldPosition.y, toWorldPosition.y) + heightFactor, - (fromWorldPosition.z + toWorldPosition.z) / 2 - ); - - return ( - <QuadraticBezierLine - key={`${path.points.uuid}-${target.pointUUID}-${index}`} - ref={(el) => - (groupRefs.current[ - `${path.points.uuid}-${target.pointUUID}-${index}` - ] = el!) - } - start={fromWorldPosition.toArray()} - end={toWorldPosition.toArray()} - mid={midPoint.toArray()} - color={ - deleteTool && - hoveredLineKey === - `${path.points.uuid}-${target.pointUUID}-${index}` - ? "red" - : "orange" - } - lineWidth={4} - dashed={ - deleteTool && - hoveredLineKey === - `${path.points.uuid}-${target.pointUUID}-${index}` - ? false - : true - } - dashSize={0.75} - dashScale={20} - onPointerOver={() => - setHoveredLineKey( - `${path.points.uuid}-${target.pointUUID}-${index}` - ) - } - onPointerOut={() => setHoveredLineKey(null)} - onClick={() => { - if (deleteTool) { - const connection1 = { - model: path.modeluuid, - point: path.points.uuid, - }; - const connection2 = { - model: target.modelUUID, - point: target.pointUUID, - }; - - removeConnections(connection1, connection2); - } - }} - userData={target} /> - ); - } - return null; - }); - } - - if (path.type === "StaticMachine") { - return path.points.connections.targets.map((target, index) => { - const targetPath = simulationStates.find( - (p) => p.modeluuid === target.modelUUID - ); - if (targetPath?.type !== "ArmBot") return null; - - const fromSphere = pathsGroupRef.current?.getObjectByProperty( - "uuid", - path.points.uuid - ); - const toSphere = pathsGroupRef.current?.getObjectByProperty( - "uuid", - target.pointUUID - ); - - if (fromSphere && toSphere) { - const fromWorldPosition = new THREE.Vector3(); - const toWorldPosition = new THREE.Vector3(); - fromSphere.getWorldPosition(fromWorldPosition); - toSphere.getWorldPosition(toWorldPosition); - - const distance = fromWorldPosition.distanceTo(toWorldPosition); - const heightFactor = Math.max(0.5, distance * 0.2); - const midPoint = new THREE.Vector3( - (fromWorldPosition.x + toWorldPosition.x) / 2, - Math.max(fromWorldPosition.y, toWorldPosition.y) + heightFactor, - (fromWorldPosition.z + toWorldPosition.z) / 2 - ); - - return ( - <QuadraticBezierLine - key={`${path.points.uuid}-${target.pointUUID}-${index}`} - ref={(el) => - (groupRefs.current[ - `${path.points.uuid}-${target.pointUUID}-${index}` - ] = el!) - } - start={fromWorldPosition.toArray()} - end={toWorldPosition.toArray()} - mid={midPoint.toArray()} - color={ - deleteTool && - hoveredLineKey === - `${path.points.uuid}-${target.pointUUID}-${index}` - ? "red" - : "#42a5f5" - } - lineWidth={4} - dashed={ - deleteTool && - hoveredLineKey === - `${path.points.uuid}-${target.pointUUID}-${index}` - ? false - : true - } - dashSize={0.75} - dashScale={20} - onPointerOver={() => - setHoveredLineKey( - `${path.points.uuid}-${target.pointUUID}-${index}` - ) - } - onPointerOut={() => setHoveredLineKey(null)} - onClick={() => { - if (deleteTool) { - const connection1 = { - model: path.modeluuid, - point: path.points.uuid, - }; - const connection2 = { - model: target.modelUUID, - point: target.pointUUID, - }; - - removeConnections(connection1, connection2); - } - }} - userData={target} - /> - ); - } - return null; - }); - } - - return []; - })} - - {currentLine && ( - <QuadraticBezierLine - start={currentLine.start.toArray()} - end={currentLine.end.toArray()} - mid={currentLine.mid.toArray()} - color={helperlineColor} - lineWidth={4} - dashed - dashSize={1} - dashScale={20} - /> - )} - </group> - ); + )} + </group> + ); } export default PathConnector; From 83f92d4b015507c7b88d67ee77fb1d29f783b925 Mon Sep 17 00:00:00 2001 From: Jerald-Golden-B <jerald@hexrfactory.com> Date: Wed, 16 Apr 2025 10:16:54 +0530 Subject: [PATCH 08/20] feat: Enhance connection removal logic to handle deleted models and their points --- .../controls/selection/selectionControls.tsx | 61 ++++++++++------ .../modules/simulation/path/pathConnector.tsx | 73 ++++++++++--------- 2 files changed, 79 insertions(+), 55 deletions(-) diff --git a/app/src/modules/scene/controls/selection/selectionControls.tsx b/app/src/modules/scene/controls/selection/selectionControls.tsx index ece6924..8998bc9 100644 --- a/app/src/modules/scene/controls/selection/selectionControls.tsx +++ b/app/src/modules/scene/controls/selection/selectionControls.tsx @@ -270,6 +270,20 @@ const SelectionControls: React.FC = () => { }; const removeConnections = (deletedModelUUIDs: string[]) => { + + const deletedPointUUIDs = new Set<string>(); + simulationStates.forEach(state => { + if (deletedModelUUIDs.includes(state.modeluuid)) { + if (state.type === "Conveyor" && state.points) { + state.points.forEach(point => { + deletedPointUUIDs.add(point.uuid); + }); + } else if (state.points && 'uuid' in state.points) { + deletedPointUUIDs.add(state.points.uuid); + } + } + }); + const updatedStates = simulationStates.map((state) => { // Handle Conveyor if (state.type === "Conveyor") { @@ -333,38 +347,41 @@ const SelectionControls: React.FC = () => { ...state.points, connections: { ...state.points.connections, - targets: state.points.connections.targets.filter( (target: any) => !deletedModelUUIDs.includes(target.modelUUID) ), }, actions: { ...state.points.actions, - processes: (state.points.actions.processes = - state.points.actions.processes?.filter((process) => { - const matchedStates = simulationStates.filter((s) => deletedModelUUIDs.includes(s.modeluuid)); + processes: state.points.actions.processes?.filter((process) => { + // Check if trigger is from deleted model + const matchedStates = simulationStates.filter((s) => deletedModelUUIDs.includes(s.modeluuid)); - if (matchedStates.length > 0) { - if (matchedStates[0]?.type === "StaticMachine") { - const trigPoints = matchedStates[0]?.points; - - return !( - process.triggerId === trigPoints?.triggers?.uuid - ); - } else if (matchedStates[0]?.type === "Conveyor") { - const trigPoints = matchedStates[0]?.points; - - if (Array.isArray(trigPoints)) { - const nonEmptyTriggers = trigPoints.filter((point) => point && point.triggers && point.triggers.length > 0); - - const allTriggerUUIDs = nonEmptyTriggers.flatMap((point) => point.triggers).map((trigger) => trigger.uuid); - - return !allTriggerUUIDs.includes(process.triggerId); + if (matchedStates.length > 0) { + if (matchedStates[0]?.type === "StaticMachine") { + const trigPoints = matchedStates[0]?.points; + if (process.triggerId === trigPoints?.triggers?.uuid) { + return false; + } + } else if (matchedStates[0]?.type === "Conveyor") { + const trigPoints = matchedStates[0]?.points; + if (Array.isArray(trigPoints)) { + const nonEmptyTriggers = trigPoints.filter((point) => point && point.triggers && point.triggers.length > 0); + const allTriggerUUIDs = nonEmptyTriggers.flatMap((point) => point.triggers).map((trigger) => trigger.uuid); + if (allTriggerUUIDs.includes(process.triggerId)) { + return false; } } } - return true; - })), + } + + // Check if startPoint or endPoint is from deleted model + if (deletedPointUUIDs.has(process.startPoint) || deletedPointUUIDs.has(process.endPoint)) { + return false; + } + + return true; + }), }, }, }; diff --git a/app/src/modules/simulation/path/pathConnector.tsx b/app/src/modules/simulation/path/pathConnector.tsx index 4ca12a2..3e35925 100644 --- a/app/src/modules/simulation/path/pathConnector.tsx +++ b/app/src/modules/simulation/path/pathConnector.tsx @@ -964,8 +964,21 @@ function PathConnector({ pathsGroupRef, }: { pathsGroupRef: React.MutableRefObje }; const removeConnections = (deletedModelUUIDs: string[]) => { - const updatedStates = simulationStates.map((state) => { + const deletedPointUUIDs = new Set<string>(); + simulationStates.forEach(state => { + if (deletedModelUUIDs.includes(state.modeluuid)) { + if (state.type === "Conveyor" && state.points) { + state.points.forEach(point => { + deletedPointUUIDs.add(point.uuid); + }); + } else if (state.points && 'uuid' in state.points) { + deletedPointUUIDs.add(state.points.uuid); + } + } + }); + + const updatedStates = simulationStates.map((state) => { // Handle Conveyor if (state.type === "Conveyor") { const updatedConveyor: SimulationTypes.ConveyorEventsSchema = { @@ -1028,45 +1041,41 @@ function PathConnector({ pathsGroupRef, }: { pathsGroupRef: React.MutableRefObje ...state.points, connections: { ...state.points.connections, - targets: state.points.connections.targets.filter( (target: any) => !deletedModelUUIDs.includes(target.modelUUID) ), }, actions: { ...state.points.actions, - processes: (state.points.actions.processes = - state.points.actions.processes?.filter((process) => { - const matchedStates = simulationStates.filter((s) => - deletedModelUUIDs.includes(s.modeluuid) - ); + processes: state.points.actions.processes?.filter((process) => { + // Check if trigger is from deleted model + const matchedStates = simulationStates.filter((s) => deletedModelUUIDs.includes(s.modeluuid)); - if (matchedStates.length > 0) { - if (matchedStates[0]?.type === "StaticMachine") { - const trigPoints = matchedStates[0]?.points; - - return !( - process.triggerId === trigPoints?.triggers?.uuid - ); - } else if (matchedStates[0]?.type === "Conveyor") { - const trigPoints = matchedStates[0]?.points; - - if (Array.isArray(trigPoints)) { - const nonEmptyTriggers = trigPoints.filter( - (point) => - point && point.triggers && point.triggers.length > 0 - ); - - const allTriggerUUIDs = nonEmptyTriggers - .flatMap((point) => point.triggers) - .map((trigger) => trigger.uuid); - - return !allTriggerUUIDs.includes(process.triggerId); + if (matchedStates.length > 0) { + if (matchedStates[0]?.type === "StaticMachine") { + const trigPoints = matchedStates[0]?.points; + if (process.triggerId === trigPoints?.triggers?.uuid) { + return false; + } + } else if (matchedStates[0]?.type === "Conveyor") { + const trigPoints = matchedStates[0]?.points; + if (Array.isArray(trigPoints)) { + const nonEmptyTriggers = trigPoints.filter((point) => point && point.triggers && point.triggers.length > 0); + const allTriggerUUIDs = nonEmptyTriggers.flatMap((point) => point.triggers).map((trigger) => trigger.uuid); + if (allTriggerUUIDs.includes(process.triggerId)) { + return false; } } } - return true; - })), + } + + // Check if startPoint or endPoint is from deleted model + if (deletedPointUUIDs.has(process.startPoint) || deletedPointUUIDs.has(process.endPoint)) { + return false; + } + + return true; + }), }, }, }; @@ -1076,9 +1085,7 @@ function PathConnector({ pathsGroupRef, }: { pathsGroupRef: React.MutableRefObje return state; }); - const filteredStates = updatedStates.filter( - (state) => !deletedModelUUIDs.includes(state.modeluuid) - ); + const filteredStates = updatedStates.filter((state) => !deletedModelUUIDs.includes(state.modeluuid)); updateBackend(filteredStates); setSimulationStates(filteredStates); From 5c24d7ca71f82d474851c3b89e1c328e19f1f8ea Mon Sep 17 00:00:00 2001 From: Jerald-Golden-B <jerald@hexrfactory.com> Date: Wed, 16 Apr 2025 10:47:12 +0530 Subject: [PATCH 09/20] feat: Improve trigger processing and static machine status updates in IKAnimationController --- .../armbot/IKAnimationController.tsx | 57 +++++++++---------- 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/app/src/modules/simulation/armbot/IKAnimationController.tsx b/app/src/modules/simulation/armbot/IKAnimationController.tsx index 1044540..9f19797 100644 --- a/app/src/modules/simulation/armbot/IKAnimationController.tsx +++ b/app/src/modules/simulation/armbot/IKAnimationController.tsx @@ -114,10 +114,38 @@ const IKAnimationController = ({ if (prev.needsInitialMovement !== needsInitialMovement && !needsInitialMovement) { logStatus(`[Arm ${uuid}] Reached rest position, ready for animation`); + } if (prev.selectedTrigger !== selectedTrigger) { logStatus(`[Arm ${uuid}] Processing new trigger: ${selectedTrigger}`); + + + const currentProcess = process.find(p => p.triggerId === prev.selectedTrigger); + if (currentProcess) { + const triggerId = currentProcess.triggerId; + + const endPoint = armBot.actions.processes.find((process) => process.triggerId === triggerId)?.endPoint; + + // Search simulationStates for a StaticMachine that has a point matching this endPointId + const matchedStaticMachine = simulationStates.find( + (state) => + state.type === "StaticMachine" && + state.points?.uuid === endPoint// check for static machine with matching point uuid + ) as any; + + if (matchedStaticMachine) { + setStaticMachines((machines) => { + return machines.map((machine) => { + if (machine.uuid === matchedStaticMachine.modeluuid) { + return { ...machine, status: "running" }; + } else { + return machine; + } + }); + }); + } + } } // Update previous state @@ -249,35 +277,6 @@ const IKAnimationController = ({ } else if (progress >= startToEndRange[0] && progress < startToEndRange[1]) { currentSpeed = speed; currentStatus = "moving"; // Moving between points - if (1 - progress < 0.05) { - // Find the process that matches the current trigger - const currentProcess = process.find(p => p.triggerId === selectedTrigger); - if (currentProcess) { - const triggerId = currentProcess.triggerId; - - const endPoint = armBot.actions.processes.find((process) => process.triggerId === triggerId)?.endPoint; - - // Search simulationStates for a StaticMachine that has a point matching this endPointId - const matchedStaticMachine = simulationStates.find( - (state) => - state.type === "StaticMachine" && - state.points?.uuid === endPoint// check for static machine with matching point uuid - ) as any; - - if (matchedStaticMachine) { - setStaticMachines((machines) => { - return machines.map((machine) => { - if (machine.uuid === matchedStaticMachine.modeluuid) { - return { ...machine, status: "running" }; - } else { - return machine; - } - }); - }); - } - } - - } } else if (progress >= endToRestRange[0] && progress < 1) { currentSpeed = restSpeed; currentStatus = "moving"; // Returning to rest From e0082cb55a4ea64a7562f97ee16c10f763776445 Mon Sep 17 00:00:00 2001 From: Jerald-Golden-B <jerald@hexrfactory.com> Date: Wed, 16 Apr 2025 11:39:03 +0530 Subject: [PATCH 10/20] feat: Enhance ArmBotState with connections and isActive properties - Updated ArmBotState interface across multiple files to include connections (source and targets) and isActive properties. - Implemented logic in ProcessAnimator to check if processes are connected to active ArmBots, preventing future spawns if connected. - Adjusted animation state handling to account for active ArmBots, stopping animations and resetting states as necessary. - Refactored related functions for better clarity and maintainability. --- app/src/modules/simulation/armbot/ArmBot.tsx | 8 +- .../simulation/armbot/ArmBotInstances.tsx | 15 +- .../armbot/IKAnimationController.tsx | 79 +- .../modules/simulation/armbot/IkInstances.tsx | 17 +- .../simulation/process/processAnimator.tsx | 84 +- .../simulation/process/processContainer.tsx | 5 + .../process/useProcessAnimations.tsx | 1174 +++++++++-------- app/src/modules/simulation/simulation.tsx | 5 + .../staticMachine/staticMachine.tsx | 6 +- 9 files changed, 755 insertions(+), 638 deletions(-) diff --git a/app/src/modules/simulation/armbot/ArmBot.tsx b/app/src/modules/simulation/armbot/ArmBot.tsx index a6d8d23..1c7bb83 100644 --- a/app/src/modules/simulation/armbot/ArmBot.tsx +++ b/app/src/modules/simulation/armbot/ArmBot.tsx @@ -12,7 +12,12 @@ interface ArmBotState { status: string; material: string; triggerId: string; + connections: { + source: { modelUUID: string; pointUUID: string }; + targets: { modelUUID: string; pointUUID: string }[]; + }; actions: { uuid: string; name: string; speed: number; processes: { triggerId: string; startPoint: string; endPoint: string }[]; }; + isActive?: boolean; } interface StaticMachineState { @@ -45,7 +50,8 @@ const ArmBot = ({ armBots, setArmBots, setStaticMachines }: ArmBotProps) => { status: "idle", material: "default", triggerId: '', - actions: bot.points.actions + actions: bot.points.actions, + connections: bot.points.connections })); setArmBots(initialStates); }, [simulationStates]); diff --git a/app/src/modules/simulation/armbot/ArmBotInstances.tsx b/app/src/modules/simulation/armbot/ArmBotInstances.tsx index 10a49f6..8d20a87 100644 --- a/app/src/modules/simulation/armbot/ArmBotInstances.tsx +++ b/app/src/modules/simulation/armbot/ArmBotInstances.tsx @@ -18,16 +18,12 @@ interface ArmBotState { status: string; material: string; triggerId: string; - actions: { - uuid: string; - name: string; - speed: number; - processes: { - triggerId: string; - startPoint: string; - endPoint: string; - }[]; + connections: { + source: { modelUUID: string; pointUUID: string }; + targets: { modelUUID: string; pointUUID: string }[]; }; + actions: { uuid: string; name: string; speed: number; processes: { triggerId: string; startPoint: string; endPoint: string }[]; }; + isActive?: boolean; } interface StaticMachineState { @@ -87,6 +83,7 @@ export const ArmbotInstances: React.FC<ArmbotInstancesProps> = ({ index, armBot, rotation={armBot.rotation} processes={processes} armBot={armBot} + setArmBots={setArmBots} setStaticMachines={setStaticMachines} updateArmBotStatus={updateArmBotStatus} /> diff --git a/app/src/modules/simulation/armbot/IKAnimationController.tsx b/app/src/modules/simulation/armbot/IKAnimationController.tsx index 9f19797..d0aaec1 100644 --- a/app/src/modules/simulation/armbot/IKAnimationController.tsx +++ b/app/src/modules/simulation/armbot/IKAnimationController.tsx @@ -20,16 +20,12 @@ interface ArmBotState { status: string; material: string; triggerId: string; - actions: { - uuid: string; - name: string; - speed: number; - processes: { - triggerId: string; - startPoint: string; - endPoint: string; - }[]; + connections: { + source: { modelUUID: string; pointUUID: string }; + targets: { modelUUID: string; pointUUID: string }[]; }; + actions: { uuid: string; name: string; speed: number; processes: { triggerId: string; startPoint: string; endPoint: string }[]; }; + isActive?: boolean; } type IKAnimationControllerProps = { @@ -46,6 +42,7 @@ type IKAnimationControllerProps = { logStatus: (status: string) => void; groupRef: React.RefObject<THREE.Group>; armBot: ArmBotState; + setArmBots: React.Dispatch<React.SetStateAction<ArmBotState[]>>; setStaticMachines: React.Dispatch<React.SetStateAction<StaticMachineState[]>>; updateArmBotStatus: (status: string) => void; } @@ -59,6 +56,7 @@ const IKAnimationController = ({ logStatus, groupRef, armBot, + setArmBots, setStaticMachines, updateArmBotStatus }: IKAnimationControllerProps) => { @@ -120,30 +118,61 @@ const IKAnimationController = ({ if (prev.selectedTrigger !== selectedTrigger) { logStatus(`[Arm ${uuid}] Processing new trigger: ${selectedTrigger}`); - const currentProcess = process.find(p => p.triggerId === prev.selectedTrigger); if (currentProcess) { const triggerId = currentProcess.triggerId; const endPoint = armBot.actions.processes.find((process) => process.triggerId === triggerId)?.endPoint; - // Search simulationStates for a StaticMachine that has a point matching this endPointId - const matchedStaticMachine = simulationStates.find( - (state) => - state.type === "StaticMachine" && - state.points?.uuid === endPoint// check for static machine with matching point uuid - ) as any; + // Search simulationStates for a StaticMachine or Conveyor that has a point matching this endPointId + const matchedMachine = simulationStates.find((state) => { + if (state.type === "Conveyor") { + // For Conveyor, points is an array + return (state).points.some( + (point) => point.uuid === endPoint + ); + } else if (state.type === "StaticMachine") { + // For StaticMachine, points is an object + return state.points.uuid === endPoint; + } + return false; + }); - if (matchedStaticMachine) { - setStaticMachines((machines) => { - return machines.map((machine) => { - if (machine.uuid === matchedStaticMachine.modeluuid) { - return { ...machine, status: "running" }; - } else { - return machine; - } + if (matchedMachine) { + // Log if the end point is a conveyor + if (matchedMachine.type === "Conveyor") { + logStatus(`[Arm ${uuid}] Reached end point which is a conveyor (${matchedMachine.modelName})`); + } else { + logStatus(`[Arm ${uuid}] Reached end point which is a static machine (${matchedMachine.modelName})`); + } + + if (matchedMachine.type === "StaticMachine") { + setStaticMachines((machines) => { + return machines.map((machine) => { + if (machine.uuid === matchedMachine.modeluuid) { + return { ...machine, status: "running" }; + } else { + return machine; + } + }); }); - }); + } + + if (matchedMachine.type === "Conveyor") { + setArmBots((prev) => + prev.map((arm) => { + if (arm.uuid === uuid) { + return { + ...arm, + isActive: false + }; + } + else { + return arm; + } + }) + ); + } } } } diff --git a/app/src/modules/simulation/armbot/IkInstances.tsx b/app/src/modules/simulation/armbot/IkInstances.tsx index 50b8ffb..5a0d23b 100644 --- a/app/src/modules/simulation/armbot/IkInstances.tsx +++ b/app/src/modules/simulation/armbot/IkInstances.tsx @@ -23,16 +23,12 @@ interface ArmBotState { status: string; material: string; triggerId: string; - actions: { - uuid: string; - name: string; - speed: number; - processes: { - triggerId: string; - startPoint: string; - endPoint: string; - }[]; + connections: { + source: { modelUUID: string; pointUUID: string }; + targets: { modelUUID: string; pointUUID: string }[]; }; + actions: { uuid: string; name: string; speed: number; processes: { triggerId: string; startPoint: string; endPoint: string }[]; }; + isActive?: boolean; } const IkInstances = ({ @@ -43,6 +39,7 @@ const IkInstances = ({ position, rotation, armBot, + setArmBots, setStaticMachines, updateArmBotStatus }: { @@ -53,6 +50,7 @@ const IkInstances = ({ position: [number, number, number]; rotation: [number, number, number]; armBot: ArmBotState; + setArmBots: React.Dispatch<React.SetStateAction<ArmBotState[]>>; setStaticMachines: React.Dispatch<React.SetStateAction<StaticMachineState[]>>; updateArmBotStatus: (status: string) => void; }) => { @@ -141,6 +139,7 @@ const IkInstances = ({ logStatus={logStatus} groupRef={groupRef} armBot={armBot} + setArmBots={setArmBots} setStaticMachines={setStaticMachines} updateArmBotStatus={updateArmBotStatus} /> diff --git a/app/src/modules/simulation/process/processAnimator.tsx b/app/src/modules/simulation/process/processAnimator.tsx index 460de49..f01e799 100644 --- a/app/src/modules/simulation/process/processAnimator.tsx +++ b/app/src/modules/simulation/process/processAnimator.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useEffect, useMemo } from "react"; +import React, { useRef, useEffect, useMemo, useCallback } from "react"; import { useLoader, useFrame } from "@react-three/fiber"; import { GLTFLoader } from "three-stdlib"; import * as THREE from "three"; @@ -18,9 +18,13 @@ interface ArmBotState { status: string; material: string; triggerId: string; + connections: { + source: { modelUUID: string; pointUUID: string }; + targets: { modelUUID: string; pointUUID: string }[]; + }; actions: { uuid: string; name: string; speed: number; processes: { triggerId: string; startPoint: string; endPoint: string }[]; }; + isActive?: boolean; } - interface ProcessContainerProps { processes: ProcessData[]; setProcesses: React.Dispatch<React.SetStateAction<any[]>>; @@ -103,9 +107,33 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ // In processAnimator.tsx - only the relevant spawn logic part that needs fixes + + // Add this function to ProcessAnimator component + const isConnectedToActiveArmBot = useCallback( + (processId: any) => { + // Check if any active armbot is connected to this process + return armBots.some((armbot) => { + if (!armbot.isActive) return false; + + // Check if this armbot is connected to the process + return armbot.connections?.targets?.some((connection: any) => { + // Find the process that owns this modelUUID + const connectedProcess = processes.find((p) => + p.paths?.some((path) => path.modeluuid === connection.modelUUID) + ); + return connectedProcess?.id === processId; + }); + }); + }, + [armBots, processes] + ); + + // In processAnimator.tsx - only the relevant spawn logic part that needs fixes + useFrame(() => { // Spawn logic frame - const currentTime = clockRef.current.getElapsedTime() - elapsedBeforePauseRef.current; + const currentTime = + clockRef.current.getElapsedTime() - elapsedBeforePauseRef.current; setAnimationStates((prev) => { const newStates = { ...prev }; @@ -119,6 +147,14 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ return; } + if (isConnectedToActiveArmBot(process.id)) { + newStates[process.id] = { + ...processState, + nextSpawnTime: Infinity, // Prevent future spawns + }; + return; + } + const spawnPoint = findSpawnPoint(process); if (!spawnPoint || !spawnPoint.actions) return; @@ -133,7 +169,10 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ : parseFloat(spawnAction.spawnInterval as string) || 0; // Check if this is a zero interval spawn and we already spawned an object - if (spawnInterval === 0 && processState.hasSpawnedZeroIntervalObject === true) { + if ( + spawnInterval === 0 && + processState.hasSpawnedZeroIntervalObject === true + ) { return; // Don't spawn more objects for zero interval } @@ -183,6 +222,29 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ const processState = newStates[process.id]; if (!processState) return; + if (isConnectedToActiveArmBot(process.id)) { + newStates[process.id] = { + ...processState, + spawnedObjects: Object.entries(processState.spawnedObjects).reduce( + (acc, [id, obj]) => ({ + ...acc, + [id]: { + ...obj, + state: { + ...obj.state, + isAnimating: false, // Stop animation + isDelaying: false, // Clear delays + delayComplete: false, // Reset delays + progress: 0, // Reset progress + }, + }, + }), + {} + ), + }; + return; + } + if (processState.isProcessDelaying) { const effectiveDelayTime = processState.processDelayDuration / speedRef.current; @@ -338,10 +400,13 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ if (isLastPoint) { const isAgvPicking = agvRef.current.some( - (agv: any) => agv.processId === process.id && agv.status === "picking" + (agv: any) => + agv.processId === process.id && agv.status === "picking" ); - const shouldHide = !currentPointData?.actions || !hasNonInheritActions(currentPointData.actions); + const shouldHide = + !currentPointData?.actions || + !hasNonInheritActions(currentPointData.actions); if (shouldHide) { if (isAgvPicking) { @@ -372,7 +437,8 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ if (tempStackedObjectsRef.current[objectId]) { const isAgvPicking = agvRef.current.some( - (agv: any) => agv.processId === process.id && agv.status === "picking" + (agv: any) => + agv.processId === process.id && agv.status === "picking" ); if (isAgvPicking) { @@ -391,7 +457,8 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ if (!isLastPoint) { const nextPoint = path[nextPointIdx]; - const distance = path[stateRef.currentIndex].distanceTo(nextPoint); + const distance = + path[stateRef.currentIndex].distanceTo(nextPoint); const effectiveSpeed = stateRef.speed * speedRef.current; const movement = effectiveSpeed * delta; @@ -442,7 +509,6 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ return newStates; }); }); - if (!processedProcesses || processedProcesses.length === 0) { return null; } diff --git a/app/src/modules/simulation/process/processContainer.tsx b/app/src/modules/simulation/process/processContainer.tsx index 4cc7edf..0bcdb13 100644 --- a/app/src/modules/simulation/process/processContainer.tsx +++ b/app/src/modules/simulation/process/processContainer.tsx @@ -9,7 +9,12 @@ interface ArmBotState { status: string; material: string; triggerId: string; + connections: { + source: { modelUUID: string; pointUUID: string }; + targets: { modelUUID: string; pointUUID: string }[]; + }; actions: { uuid: string; name: string; speed: number; processes: { triggerId: string; startPoint: string; endPoint: string }[]; }; + isActive?: boolean; } interface ProcessContainerProps { diff --git a/app/src/modules/simulation/process/useProcessAnimations.tsx b/app/src/modules/simulation/process/useProcessAnimations.tsx index 8c0f20d..5fc96dc 100644 --- a/app/src/modules/simulation/process/useProcessAnimations.tsx +++ b/app/src/modules/simulation/process/useProcessAnimations.tsx @@ -1,645 +1,651 @@ import { useCallback, useEffect, useRef, useState } from "react"; import * as THREE from "three"; import { - ProcessData, - ProcessAnimationState, - SpawnedObject, - AnimationState, - ProcessPoint, - PointAction, - Trigger, + ProcessData, + ProcessAnimationState, + SpawnedObject, + AnimationState, + ProcessPoint, + PointAction, + Trigger, } from "./types"; import { - useAnimationPlaySpeed, - usePauseButtonStore, - usePlayButtonStore, - useResetButtonStore, + useAnimationPlaySpeed, + usePauseButtonStore, + usePlayButtonStore, + useResetButtonStore, } from "../../../store/usePlayButtonStore"; import { usePlayAgv } from "../../../store/store"; // Enhanced ProcessAnimationState with trigger tracking interface EnhancedProcessAnimationState extends ProcessAnimationState { - triggerCounts: Record<string, number>; - triggerLogs: Array<{ - timestamp: number; - pointId: string; - objectId: string; - triggerId: string; - }>; + triggerCounts: Record<string, number>; + triggerLogs: Array<{ + timestamp: number; + pointId: string; + objectId: string; + triggerId: string; + }>; } interface ProcessContainerProps { - processes: ProcessData[]; - setProcesses: React.Dispatch<React.SetStateAction<any[]>>; - agvRef: any; + processes: ProcessData[]; + setProcesses: React.Dispatch<React.SetStateAction<any[]>>; + agvRef: any; } interface PlayAgvState { - playAgv: Record<string, any>; - setPlayAgv: (data: any) => void; + playAgv: Record<string, any>; + setPlayAgv: (data: any) => void; } interface ArmBotState { - uuid: string; - position: [number, number, number]; - rotation: [number, number, number]; - status: string; - material: string; - triggerId: string; - actions: { uuid: string; name: string; speed: number; processes: { triggerId: string; startPoint: string; endPoint: string }[]; }; + uuid: string; + position: [number, number, number]; + rotation: [number, number, number]; + status: string; + material: string; + triggerId: string; + connections: { + source: { modelUUID: string; pointUUID: string }; + targets: { modelUUID: string; pointUUID: string }[]; + }; + actions: { uuid: string; name: string; speed: number; processes: { triggerId: string; startPoint: string; endPoint: string }[]; }; + isActive?: boolean; } export const useProcessAnimation = ( - processes: ProcessData[], - setProcesses: React.Dispatch<React.SetStateAction<any[]>>, - agvRef: any, - armBots: ArmBotState[], - setArmBots: React.Dispatch<React.SetStateAction<ArmBotState[]>> + processes: ProcessData[], + setProcesses: React.Dispatch<React.SetStateAction<any[]>>, + agvRef: any, + armBots: ArmBotState[], + setArmBots: React.Dispatch<React.SetStateAction<ArmBotState[]>> ) => { - // State and refs initialization - const { isPlaying, setIsPlaying } = usePlayButtonStore(); - const { isPaused, setIsPaused } = usePauseButtonStore(); - const { isReset, setReset } = useResetButtonStore(); - const debugRef = useRef<boolean>(false); - const clockRef = useRef<THREE.Clock>(new THREE.Clock()); - const pauseTimeRef = useRef<number>(0); - const elapsedBeforePauseRef = useRef<number>(0); - const animationStatesRef = useRef<Record<string, EnhancedProcessAnimationState>>({}); - const { speed } = useAnimationPlaySpeed(); - const prevIsPlaying = useRef<boolean | null>(null); - const [internalResetFlag, setInternalResetFlag] = useState(false); - const [animationStates, setAnimationStates] = useState<Record<string, EnhancedProcessAnimationState>>({}); - const speedRef = useRef<number>(speed); - const { PlayAgv, setPlayAgv } = usePlayAgv(); + // State and refs initialization + const { isPlaying, setIsPlaying } = usePlayButtonStore(); + const { isPaused, setIsPaused } = usePauseButtonStore(); + const { isReset, setReset } = useResetButtonStore(); + const debugRef = useRef<boolean>(false); + const clockRef = useRef<THREE.Clock>(new THREE.Clock()); + const pauseTimeRef = useRef<number>(0); + const elapsedBeforePauseRef = useRef<number>(0); + const animationStatesRef = useRef<Record<string, EnhancedProcessAnimationState>>({}); + const { speed } = useAnimationPlaySpeed(); + const prevIsPlaying = useRef<boolean | null>(null); + const [internalResetFlag, setInternalResetFlag] = useState(false); + const [animationStates, setAnimationStates] = useState<Record<string, EnhancedProcessAnimationState>>({}); + const speedRef = useRef<number>(speed); + const { PlayAgv, setPlayAgv } = usePlayAgv(); - // Effect hooks - useEffect(() => { - speedRef.current = speed; - }, [speed]); + // Effect hooks + useEffect(() => { + speedRef.current = speed; + }, [speed]); - useEffect(() => { - if (prevIsPlaying.current !== null || !isPlaying) { - setAnimationStates({}); - } - prevIsPlaying.current = isPlaying; - }, [isPlaying]); + useEffect(() => { + if (prevIsPlaying.current !== null || !isPlaying) { + setAnimationStates({}); + } + prevIsPlaying.current = isPlaying; + }, [isPlaying]); - useEffect(() => { - animationStatesRef.current = animationStates; - }, [animationStates]); + useEffect(() => { + animationStatesRef.current = animationStates; + }, [animationStates]); - // Reset handler - useEffect(() => { - if (isReset) { - setInternalResetFlag(true); - setIsPlaying(false); - setIsPaused(false); - setAnimationStates({}); - animationStatesRef.current = {}; - clockRef.current = new THREE.Clock(); - elapsedBeforePauseRef.current = 0; - pauseTimeRef.current = 0; - setReset(false); - setTimeout(() => { - setInternalResetFlag(false); - setIsPlaying(true); - }, 0); - } - }, [isReset, setReset, setIsPlaying, setIsPaused]); + // Reset handler + useEffect(() => { + if (isReset) { + setInternalResetFlag(true); + setIsPlaying(false); + setIsPaused(false); + setAnimationStates({}); + animationStatesRef.current = {}; + clockRef.current = new THREE.Clock(); + elapsedBeforePauseRef.current = 0; + pauseTimeRef.current = 0; + setReset(false); + setTimeout(() => { + setInternalResetFlag(false); + setIsPlaying(true); + }, 0); + } + }, [isReset, setReset, setIsPlaying, setIsPaused]); - // Pause handler - useEffect(() => { - if (isPaused) { - pauseTimeRef.current = clockRef.current.getElapsedTime(); - } else if (pauseTimeRef.current > 0) { - const pausedDuration = clockRef.current.getElapsedTime() - pauseTimeRef.current; - elapsedBeforePauseRef.current += pausedDuration; - } - }, [isPaused]); + // Pause handler + useEffect(() => { + if (isPaused) { + pauseTimeRef.current = clockRef.current.getElapsedTime(); + } else if (pauseTimeRef.current > 0) { + const pausedDuration = clockRef.current.getElapsedTime() - pauseTimeRef.current; + elapsedBeforePauseRef.current += pausedDuration; + } + }, [isPaused]); - // Initialize animation states with trigger tracking - useEffect(() => { - if (isPlaying && !internalResetFlag) { - const newStates: Record<string, EnhancedProcessAnimationState> = {}; + // Initialize animation states with trigger tracking + useEffect(() => { + if (isPlaying && !internalResetFlag) { + const newStates: Record<string, EnhancedProcessAnimationState> = {}; - processes.forEach((process) => { - const triggerCounts: Record<string, number> = {}; + processes.forEach((process) => { + const triggerCounts: Record<string, number> = {}; - // Initialize trigger counts for all On-Hit triggers - process.paths?.forEach((path) => { - path.points?.forEach((point) => { - point.triggers?.forEach((trigger: Trigger) => { - if (trigger.type === "On-Hit" && trigger.isUsed) { - triggerCounts[`${point.uuid}-${trigger.uuid}`] = 0; - } - }); - }); - }); - - newStates[process.id] = { - spawnedObjects: {}, - nextSpawnTime: 0, - objectIdCounter: 0, - isProcessDelaying: false, - processDelayStartTime: 0, - processDelayDuration: 0, - triggerCounts, - triggerLogs: [], - }; + // Initialize trigger counts for all On-Hit triggers + process.paths?.forEach((path) => { + path.points?.forEach((point) => { + point.triggers?.forEach((trigger: Trigger) => { + if (trigger.type === "On-Hit" && trigger.isUsed) { + triggerCounts[`${point.uuid}-${trigger.uuid}`] = 0; + } }); + }); + }); - setAnimationStates(newStates); - animationStatesRef.current = newStates; - clockRef.current.start(); - } - }, [isPlaying, processes, internalResetFlag]); + newStates[process.id] = { + spawnedObjects: {}, + nextSpawnTime: 0, + objectIdCounter: 0, + isProcessDelaying: false, + processDelayStartTime: 0, + processDelayDuration: 0, + triggerCounts, + triggerLogs: [], + }; + }); - useEffect(() => { - if (isPlaying && !internalResetFlag) { - const newStates: Record<string, EnhancedProcessAnimationState> = {}; + setAnimationStates(newStates); + animationStatesRef.current = newStates; + clockRef.current.start(); + } + }, [isPlaying, processes, internalResetFlag]); - // Initialize AGVs for each process first - processes.forEach((process) => { - // Find all vehicle paths for this process - const vehiclePaths = process.paths?.filter( - (path) => path.type === "Vehicle" - ) || []; + useEffect(() => { + if (isPlaying && !internalResetFlag) { + const newStates: Record<string, EnhancedProcessAnimationState> = {}; - // Initialize AGVs for each vehicle path - vehiclePaths.forEach((vehiclePath) => { - if (vehiclePath.points?.length > 0) { - const vehiclePoint = vehiclePath.points[0]; - const action = vehiclePoint.actions?.[0]; - const maxHitCount = action?.hitCount; + // Initialize AGVs for each process first + processes.forEach((process) => { + // Find all vehicle paths for this process + const vehiclePaths = process.paths?.filter( + (path) => path.type === "Vehicle" + ) || []; - const vehicleId = vehiclePath.modeluuid; - const processId = process.id; + // Initialize AGVs for each vehicle path + vehiclePaths.forEach((vehiclePath) => { + if (vehiclePath.points?.length > 0) { + const vehiclePoint = vehiclePath.points[0]; + const action = vehiclePoint.actions?.[0]; + const maxHitCount = action?.hitCount; - // Check if this AGV already exists - const existingAgv = agvRef.current.find( - (v: any) => v.vehicleId === vehicleId && v.processId === processId - ); + const vehicleId = vehiclePath.modeluuid; + const processId = process.id; - if (!existingAgv) { - // Initialize the AGV in a stationed state - agvRef.current.push({ - processId, - vehicleId, - maxHitCount: maxHitCount || 0, - isActive: false, - hitCount: 0, - status: 'stationed', - lastUpdated: 0 - }); - } - } - }); + // Check if this AGV already exists + const existingAgv = agvRef.current.find( + (v: any) => v.vehicleId === vehicleId && v.processId === processId + ); - // Then initialize trigger counts as before - const triggerCounts: Record<string, number> = {}; - process.paths?.forEach((path) => { - path.points?.forEach((point) => { - point.triggers?.forEach((trigger: Trigger) => { - if (trigger.type === "On-Hit" && trigger.isUsed) { - triggerCounts[`${point.uuid}-${trigger.uuid}`] = 0; - } - }); - }); - }); + if (!existingAgv) { + // Initialize the AGV in a stationed state + agvRef.current.push({ + processId, + vehicleId, + maxHitCount: maxHitCount || 0, + isActive: false, + hitCount: 0, + status: 'stationed', + lastUpdated: 0 + }); + } + } + }); - newStates[process.id] = { - spawnedObjects: {}, - nextSpawnTime: 0, - objectIdCounter: 0, - isProcessDelaying: false, - processDelayStartTime: 0, - processDelayDuration: 0, - triggerCounts, - triggerLogs: [], - }; + // Then initialize trigger counts as before + const triggerCounts: Record<string, number> = {}; + process.paths?.forEach((path) => { + path.points?.forEach((point) => { + point.triggers?.forEach((trigger: Trigger) => { + if (trigger.type === "On-Hit" && trigger.isUsed) { + triggerCounts[`${point.uuid}-${trigger.uuid}`] = 0; + } }); + }); + }); - setAnimationStates(newStates); - animationStatesRef.current = newStates; - clockRef.current.start(); - } - }, [isPlaying, processes, internalResetFlag]); + newStates[process.id] = { + spawnedObjects: {}, + nextSpawnTime: 0, + objectIdCounter: 0, + isProcessDelaying: false, + processDelayStartTime: 0, + processDelayDuration: 0, + triggerCounts, + triggerLogs: [], + }; + }); - // Helper functions - 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; - }; + setAnimationStates(newStates); + animationStatesRef.current = newStates; + clockRef.current.start(); + } + }, [isPlaying, processes, internalResetFlag]); - const findAnimationPathPoint = ( - process: ProcessData, - spawnPoint: ProcessPoint - ): THREE.Vector3 => { - if (process.animationPath && process.animationPath.length > 0) { - let pointIndex = 0; - for (const path of process.paths || []) { - for (let i = 0; i < (path.points?.length || 0); i++) { - const point = path.points?.[i]; - if (point && point.uuid === spawnPoint.uuid) { - if (process.animationPath[pointIndex]) { - const p = process.animationPath[pointIndex]; - return new THREE.Vector3(p.x, p.y, p.z); - } - } - pointIndex++; - } - } - } - return new THREE.Vector3( - spawnPoint.position[0], - spawnPoint.position[1], - spawnPoint.position[2] + // Helper functions + 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" ); - }; - - // Optimized object creation - const createSpawnedObject = useCallback( - ( - process: ProcessData, - currentTime: number, - materialType: string, - spawnPoint: ProcessPoint, - baseMaterials: Record<string, THREE.Material> - ): SpawnedObject => { - const processMaterials = { - ...baseMaterials, - ...(process.customMaterials || {}), - }; - - const spawnPosition = findAnimationPathPoint(process, spawnPoint); - const material = - processMaterials[materialType as keyof typeof processMaterials] || - baseMaterials.Default; - - return { - ref: { current: null }, - state: { - currentIndex: 0, - progress: 0, - isAnimating: true, - speed: process.speed || 1, - isDelaying: false, - delayStartTime: 0, - currentDelayDuration: 0, - delayComplete: false, - currentPathIndex: 0, - }, - visible: true, - material: material, - currentMaterialType: materialType, - spawnTime: currentTime, - position: spawnPosition, - }; - }, - [] - ); - - // Material handling - const handleMaterialSwap = useCallback( - ( - processId: string, - objectId: string, - materialType: string, - processes: ProcessData[], - baseMaterials: Record<string, THREE.Material> - ) => { - setAnimationStates((prev) => { - const processState = prev[processId]; - if (!processState || !processState.spawnedObjects[objectId]) - return prev; - - const process = processes.find((p) => p.id === processId); - if (!process) return prev; - - const processMaterials = { - ...baseMaterials, - ...(process.customMaterials || {}), - }; - - const newMaterial = - processMaterials[materialType as keyof typeof processMaterials]; - if (!newMaterial) return prev; - - return { - ...prev, - [processId]: { - ...processState, - spawnedObjects: { - ...processState.spawnedObjects, - [objectId]: { - ...processState.spawnedObjects[objectId], - material: newMaterial, - currentMaterialType: materialType, - }, - }, - }, - }; - }); - }, - [] - ); - - // Point action handler with trigger counting - const handlePointActions = useCallback( - ( - processId: string, - objectId: string, - actions: PointAction[] = [], - currentTime: number, - processes: ProcessData[], - baseMaterials: Record<string, THREE.Material> - ): 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.isProcessDelaying) return prev; - - const delayDuration = - typeof action.delay === "number" - ? action.delay - : parseFloat(action.delay as string) || 0; - - if (delayDuration > 0) { - return { - ...prev, - [processId]: { - ...processState, - isProcessDelaying: true, - processDelayStartTime: currentTime, - processDelayDuration: delayDuration, - spawnedObjects: { - ...processState.spawnedObjects, - [objectId]: { - ...processState.spawnedObjects[objectId], - state: { - ...processState.spawnedObjects[objectId].state, - isAnimating: false, - 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, - processes, - baseMaterials - ); - } - break; - - default: - break; - } - }); - - return shouldStopAnimation; - }, - [handleMaterialSwap] - ); - - const deferredArmBotUpdates = useRef<{ uuid: string; triggerId: string }[]>([]); - - // Trigger counting system - const checkAndCountTriggers = useCallback( - ( - processId: string, - objectId: string, - currentPointIndex: number, - processes: ProcessData[], - currentTime: number - ) => { - setAnimationStates((prev) => { - const processState = prev[processId]; - if (!processState) return prev; - - const process = processes.find((p) => p.id === processId); - if (!process) return prev; - - const point = getPointDataForAnimationIndex(process, currentPointIndex); - if (!point?.triggers) return prev; - - const onHitTriggers = point.triggers.filter((t: Trigger) => t.type === "On-Hit" && t.isUsed); - - if (onHitTriggers.length === 0) return prev; - - let newTriggerCounts = { ...processState.triggerCounts }; - const newTriggerLogs = [...processState.triggerLogs]; - let shouldLog = false; - - const vehiclePaths = process.paths.filter((path) => path.type === "Vehicle"); - const armBotPaths = process.paths.filter((path) => path.type === "ArmBot"); - - const activeVehicles = vehiclePaths.filter((path) => { - const vehicleId = path.modeluuid; - const vehicleEntry = agvRef.current.find((v: any) => v.vehicleId === vehicleId && v.processId === processId); - return vehicleEntry?.isActive; - }); - - // Check if any ArmBot is active for this process - // const activeArmBots = armBotPaths.filter((path) => { - // const armBotId = path.modeluuid; - // const armBotEntry = armBots.find((a: any) => a.uuid === armBotId); - // return armBotEntry; - // }); - - // Only count triggers if no vehicles and no ArmBots are active for this process - - if (activeVehicles.length === 0) { - onHitTriggers.forEach((trigger: Trigger) => { - const connections = point.connections?.targets || []; - - connections.forEach((connection) => { - const connectedModelUUID = connection.modelUUID; - - const matchingArmPath = armBotPaths.find((path) => path.modeluuid === connectedModelUUID); - - if (matchingArmPath) { - deferredArmBotUpdates.current.push({ - uuid: connectedModelUUID, - triggerId: trigger.uuid, - }); - } - }); - }); - } - - let processTotalHits = Object.values(newTriggerCounts).reduce((a, b) => a + b, 0); - - // Handle logic for vehicles and ArmBots when a trigger is hit - if (shouldLog) { - vehiclePaths.forEach((vehiclePath) => { - if (vehiclePath.points?.length > 0) { - const vehiclePoint = vehiclePath.points[0]; - const action = vehiclePoint.actions?.[0]; - const maxHitCount = action?.hitCount; - - if (maxHitCount !== undefined) { - const vehicleId = vehiclePath.modeluuid; - let vehicleEntry = agvRef.current.find( - (v: any) => - v.vehicleId === vehicleId && v.processId === processId - ); - - if (!vehicleEntry) { - vehicleEntry = { - processId, - vehicleId, - maxHitCount: maxHitCount, - isActive: false, - hitCount: 0, - status: "stationed", - }; - agvRef.current.push(vehicleEntry); - } - - if (!vehicleEntry.isActive) { - vehicleEntry.hitCount++; - vehicleEntry.lastUpdated = currentTime; - - if (vehicleEntry.hitCount >= vehicleEntry.maxHitCount) { - vehicleEntry.isActive = true; - newTriggerCounts = {}; - processTotalHits = 0; - } - } - } - } - }); - } - - return { - ...prev, - [processId]: { - ...processState, - triggerCounts: newTriggerCounts, - triggerLogs: newTriggerLogs, - totalHits: processTotalHits, - }, - }; - }); - }, []); - - useEffect(() => { - if (deferredArmBotUpdates.current.length > 0) { - const updates = [...deferredArmBotUpdates.current]; - deferredArmBotUpdates.current = []; - - setArmBots((prev) => - prev.map((bot) => { - const update = updates.find((u) => u.uuid === bot.uuid); - return update ? { ...bot, triggerId: update.triggerId } : bot; - }) - ); + if (spawnAction) { + return point; } - }, [animationStates]); + } + } + return null; + }; - // Utility functions - const hasNonInheritActions = useCallback( - (actions: PointAction[] = []): boolean => { - return actions.some( - (action) => action.isUsed && action.type !== "Inherit" - ); - }, - [] + const findAnimationPathPoint = ( + process: ProcessData, + spawnPoint: ProcessPoint + ): THREE.Vector3 => { + if (process.animationPath && process.animationPath.length > 0) { + let pointIndex = 0; + for (const path of process.paths || []) { + for (let i = 0; i < (path.points?.length || 0); i++) { + const point = path.points?.[i]; + if (point && point.uuid === spawnPoint.uuid) { + if (process.animationPath[pointIndex]) { + const p = process.animationPath[pointIndex]; + return new THREE.Vector3(p.x, p.y, p.z); + } + } + pointIndex++; + } + } + } + return new THREE.Vector3( + spawnPoint.position[0], + spawnPoint.position[1], + spawnPoint.position[2] ); + }; - const getPointDataForAnimationIndex = useCallback( - (process: ProcessData, index: number): ProcessPoint | null => { - if (!process.paths) return null; + // Optimized object creation + const createSpawnedObject = useCallback( + ( + process: ProcessData, + currentTime: number, + materialType: string, + spawnPoint: ProcessPoint, + baseMaterials: Record<string, THREE.Material> + ): SpawnedObject => { + const processMaterials = { + ...baseMaterials, + ...(process.customMaterials || {}), + }; - let cumulativePoints = 0; - for (const path of process.paths) { - const pointCount = path.points?.length || 0; + const spawnPosition = findAnimationPathPoint(process, spawnPoint); + const material = + processMaterials[materialType as keyof typeof processMaterials] || + baseMaterials.Default; - if (index < cumulativePoints + pointCount) { - const pointIndex = index - cumulativePoints; - return path.points?.[pointIndex] || null; + return { + ref: { current: null }, + state: { + currentIndex: 0, + progress: 0, + isAnimating: true, + speed: process.speed || 1, + isDelaying: false, + delayStartTime: 0, + currentDelayDuration: 0, + delayComplete: false, + currentPathIndex: 0, + }, + visible: true, + material: material, + currentMaterialType: materialType, + spawnTime: currentTime, + position: spawnPosition, + }; + }, + [] + ); + + // Material handling + const handleMaterialSwap = useCallback( + ( + processId: string, + objectId: string, + materialType: string, + processes: ProcessData[], + baseMaterials: Record<string, THREE.Material> + ) => { + setAnimationStates((prev) => { + const processState = prev[processId]; + if (!processState || !processState.spawnedObjects[objectId]) + return prev; + + const process = processes.find((p) => p.id === processId); + if (!process) return prev; + + const processMaterials = { + ...baseMaterials, + ...(process.customMaterials || {}), + }; + + const newMaterial = + processMaterials[materialType as keyof typeof processMaterials]; + if (!newMaterial) return prev; + + return { + ...prev, + [processId]: { + ...processState, + spawnedObjects: { + ...processState.spawnedObjects, + [objectId]: { + ...processState.spawnedObjects[objectId], + material: newMaterial, + currentMaterialType: materialType, + }, + }, + }, + }; + }); + }, + [] + ); + + // Point action handler with trigger counting + const handlePointActions = useCallback( + ( + processId: string, + objectId: string, + actions: PointAction[] = [], + currentTime: number, + processes: ProcessData[], + baseMaterials: Record<string, THREE.Material> + ): 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.isProcessDelaying) return prev; + + const delayDuration = + typeof action.delay === "number" + ? action.delay + : parseFloat(action.delay as string) || 0; + + if (delayDuration > 0) { + return { + ...prev, + [processId]: { + ...processState, + isProcessDelaying: true, + processDelayStartTime: currentTime, + processDelayDuration: delayDuration, + spawnedObjects: { + ...processState.spawnedObjects, + [objectId]: { + ...processState.spawnedObjects[objectId], + state: { + ...processState.spawnedObjects[objectId].state, + isAnimating: false, + 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, + processes, + baseMaterials + ); + } + break; + + default: + break; + } + }); + + return shouldStopAnimation; + }, + [handleMaterialSwap] + ); + + const deferredArmBotUpdates = useRef<{ uuid: string; triggerId: string }[]>([]); + + // Trigger counting system + const checkAndCountTriggers = useCallback( + ( + processId: string, + objectId: string, + currentPointIndex: number, + processes: ProcessData[], + currentTime: number + ) => { + setAnimationStates((prev) => { + const processState = prev[processId]; + if (!processState) return prev; + + const process = processes.find((p) => p.id === processId); + if (!process) return prev; + + const point = getPointDataForAnimationIndex(process, currentPointIndex); + if (!point?.triggers) return prev; + + const onHitTriggers = point.triggers.filter((t: Trigger) => t.type === "On-Hit" && t.isUsed); + + if (onHitTriggers.length === 0) return prev; + + let newTriggerCounts = { ...processState.triggerCounts }; + const newTriggerLogs = [...processState.triggerLogs]; + let shouldLog = false; + + const vehiclePaths = process.paths.filter((path) => path.type === "Vehicle"); + const armBotPaths = process.paths.filter((path) => path.type === "ArmBot"); + + const activeVehicles = vehiclePaths.filter((path) => { + const vehicleId = path.modeluuid; + const vehicleEntry = agvRef.current.find((v: any) => v.vehicleId === vehicleId && v.processId === processId); + return vehicleEntry?.isActive; + }); + + // Check if any ArmBot is active for this process + // const activeArmBots = armBotPaths.filter((path) => { + // const armBotId = path.modeluuid; + // const armBotEntry = armBots.find((a: any) => a.uuid === armBotId); + // return armBotEntry; + // }); + + // Only count triggers if no vehicles and no ArmBots are active for this process + + if (activeVehicles.length === 0) { + onHitTriggers.forEach((trigger: Trigger) => { + const connections = point.connections?.targets || []; + + connections.forEach((connection) => { + const connectedModelUUID = connection.modelUUID; + + const matchingArmPath = armBotPaths.find((path) => path.modeluuid === connectedModelUUID); + + if (matchingArmPath) { + deferredArmBotUpdates.current.push({ + uuid: connectedModelUUID, + triggerId: trigger.uuid, + }); + } + }); + }); + } + + let processTotalHits = Object.values(newTriggerCounts).reduce((a, b) => a + b, 0); + + // Handle logic for vehicles and ArmBots when a trigger is hit + if (shouldLog) { + vehiclePaths.forEach((vehiclePath) => { + if (vehiclePath.points?.length > 0) { + const vehiclePoint = vehiclePath.points[0]; + const action = vehiclePoint.actions?.[0]; + const maxHitCount = action?.hitCount; + + if (maxHitCount !== undefined) { + const vehicleId = vehiclePath.modeluuid; + let vehicleEntry = agvRef.current.find( + (v: any) => + v.vehicleId === vehicleId && v.processId === processId + ); + + if (!vehicleEntry) { + vehicleEntry = { + processId, + vehicleId, + maxHitCount: maxHitCount, + isActive: false, + hitCount: 0, + status: "stationed", + }; + agvRef.current.push(vehicleEntry); } - cumulativePoints += pointCount; + if (!vehicleEntry.isActive) { + vehicleEntry.hitCount++; + vehicleEntry.lastUpdated = currentTime; + + if (vehicleEntry.hitCount >= vehicleEntry.maxHitCount) { + vehicleEntry.isActive = true; + newTriggerCounts = {}; + processTotalHits = 0; + } + } + } } + }); + } - return null; - }, - [] - ); - - const getTriggerCounts = useCallback((processId: string) => { - return animationStatesRef.current[processId]?.triggerCounts || {}; + return { + ...prev, + [processId]: { + ...processState, + triggerCounts: newTriggerCounts, + triggerLogs: newTriggerLogs, + totalHits: processTotalHits, + }, + }; + }); }, []); - const getTriggerLogs = useCallback((processId: string) => { - return animationStatesRef.current[processId]?.triggerLogs || []; + useEffect(() => { + if (deferredArmBotUpdates.current.length > 0) { + const updates = [...deferredArmBotUpdates.current]; + deferredArmBotUpdates.current = []; + + setArmBots((prev) => + prev.map((bot) => { + const update = updates.find((u) => u.uuid === bot.uuid); + + return update + ? { ...bot, triggerId: update.triggerId, isActive: true } + : bot; + }) + ); + } + }, [animationStates]); + + // Utility functions + const hasNonInheritActions = useCallback( + (actions: PointAction[] = []): boolean => { + return actions.some( + (action) => action.isUsed && action.type !== "Inherit" + ); }, []); - return { - animationStates, - setAnimationStates, - clockRef, - elapsedBeforePauseRef, - speedRef, - debugRef, - findSpawnPoint, - createSpawnedObject, - handlePointActions, - hasNonInheritActions, - getPointDataForAnimationIndex, - checkAndCountTriggers, - getTriggerCounts, - getTriggerLogs, - processes, - }; + const getPointDataForAnimationIndex = useCallback( + (process: ProcessData, index: number): ProcessPoint | null => { + 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; + }, + [] + ); + + const getTriggerCounts = useCallback((processId: string) => { + return animationStatesRef.current[processId]?.triggerCounts || {}; + }, []); + + const getTriggerLogs = useCallback((processId: string) => { + return animationStatesRef.current[processId]?.triggerLogs || []; + }, []); + + return { + animationStates, + setAnimationStates, + clockRef, + elapsedBeforePauseRef, + speedRef, + debugRef, + findSpawnPoint, + createSpawnedObject, + handlePointActions, + hasNonInheritActions, + getPointDataForAnimationIndex, + checkAndCountTriggers, + getTriggerCounts, + getTriggerLogs, + processes, + }; }; diff --git a/app/src/modules/simulation/simulation.tsx b/app/src/modules/simulation/simulation.tsx index 2f66ab8..84d3651 100644 --- a/app/src/modules/simulation/simulation.tsx +++ b/app/src/modules/simulation/simulation.tsx @@ -15,7 +15,12 @@ interface ArmBotState { status: string; material: string; triggerId: string; + connections: { + source: { modelUUID: string; pointUUID: string }; + targets: { modelUUID: string; pointUUID: string }[]; + }; actions: { uuid: string; name: string; speed: number; processes: { triggerId: string; startPoint: string; endPoint: string }[]; }; + isActive?: boolean; } interface StaticMachineState { diff --git a/app/src/modules/simulation/staticMachine/staticMachine.tsx b/app/src/modules/simulation/staticMachine/staticMachine.tsx index ba9b4f0..26760eb 100644 --- a/app/src/modules/simulation/staticMachine/staticMachine.tsx +++ b/app/src/modules/simulation/staticMachine/staticMachine.tsx @@ -10,9 +10,13 @@ interface ArmBotState { status: string; material: string; triggerId: string; + connections: { + source: { modelUUID: string; pointUUID: string }; + targets: { modelUUID: string; pointUUID: string }[]; + }; actions: { uuid: string; name: string; speed: number; processes: { triggerId: string; startPoint: string; endPoint: string }[]; }; + isActive?: boolean; } - interface StaticMachineState { uuid: string; status: string; From 43d21a522c6da72e05727f9f34c6c41991a36dfc Mon Sep 17 00:00:00 2001 From: SreeNath14 <153710861+SreeNath14@users.noreply.github.com> Date: Wed, 16 Apr 2025 12:09:18 +0530 Subject: [PATCH 11/20] bug fix --- .../simulation/process/processAnimator.tsx | 173 ++++++++++++------ 1 file changed, 118 insertions(+), 55 deletions(-) diff --git a/app/src/modules/simulation/process/processAnimator.tsx b/app/src/modules/simulation/process/processAnimator.tsx index 19e55b8..f6cda0b 100644 --- a/app/src/modules/simulation/process/processAnimator.tsx +++ b/app/src/modules/simulation/process/processAnimator.tsx @@ -9,7 +9,6 @@ import { useProcessAnimation } from "./useProcessAnimations"; import ProcessObject from "./processObject"; import { ProcessData } from "./types"; - interface ArmBotState { uuid: string; position: [number, number, number]; @@ -21,7 +20,12 @@ interface ArmBotState { source: { modelUUID: string; pointUUID: string }; targets: { modelUUID: string; pointUUID: string }[]; }; - actions: { uuid: string; name: string; speed: number; processes: { triggerId: string; startPoint: string; endPoint: string }[]; }; + actions: { + uuid: string; + name: string; + speed: number; + processes: { triggerId: string; startPoint: string; endPoint: string }[]; + }; isActive?: boolean; } interface ProcessContainerProps { @@ -39,7 +43,7 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ agvRef, MaterialRef, armBots, - setArmBots + setArmBots, }) => { const gltf = useLoader(GLTFLoader, crate) as GLTF; const groupRef = useRef<THREE.Group>(null); @@ -114,7 +118,6 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ // In processAnimator.tsx - only the relevant spawn logic part that needs fixes - // Add this function to ProcessAnimator component const isConnectedToActiveArmBot = useCallback( (processId: any) => { @@ -123,7 +126,7 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ if (!armbot.isActive) return false; // Check if this armbot is connected to the process - return armbot.connections?.targets?.some((connection: any) => { + return armbot.connections?.targets?.some((connection) => { // Find the process that owns this modelUUID const connectedProcess = processes.find((p) => p.paths?.some((path) => path.modeluuid === connection.modelUUID) @@ -135,8 +138,7 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ [armBots, processes] ); - // In processAnimator.tsx - only the relevant spawn logic part that needs fixes - + // First useFrame for spawn logic useFrame(() => { // Spawn logic frame const currentTime = @@ -149,12 +151,15 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ const processState = newStates[process.id]; if (!processState) return; + // Check connection status + const isConnected = isConnectedToActiveArmBot(process.id); + if (processState.isProcessDelaying) { // Existing delay handling logic... return; } - if (isConnectedToActiveArmBot(process.id)) { + if (isConnected) { newStates[process.id] = { ...processState, nextSpawnTime: Infinity, // Prevent future spawns @@ -163,17 +168,24 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ } const spawnPoint = findSpawnPoint(process); - if (!spawnPoint || !spawnPoint.actions) return; + if (!spawnPoint || !spawnPoint.actions) { + console.log( + `Process ${process.id} has no valid spawn point or actions` + ); + return; + } const spawnAction = spawnPoint.actions.find( (a) => a.isUsed && a.type === "Spawn" ); - if (!spawnAction) return; + if (!spawnAction) { + return; + } const spawnInterval = typeof spawnAction.spawnInterval === "number" ? spawnAction.spawnInterval - : parseFloat(spawnAction.spawnInterval as string) || 0; + : parseFloat(spawnAction.spawnInterval || "0") || 0; // Check if this is a zero interval spawn and we already spawned an object if ( @@ -195,6 +207,15 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ baseMaterials ); + // Initialize state properly to ensure animation + newObject.state = { + ...newObject.state, + isAnimating: true, + isDelaying: false, + delayComplete: false, + progress: 0.005, // Start with tiny progress to ensure animation begins + }; + // Update state with the new object and flag for zero interval newStates[process.id] = { ...processState, @@ -217,6 +238,7 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ }); }); + // Second useFrame for animation logic useFrame((_, delta) => { // Animation logic frame const currentTime = @@ -227,9 +249,19 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ processedProcesses.forEach((process) => { const processState = newStates[process.id]; - if (!processState) return; + if (!processState) { + return; + } - if (isConnectedToActiveArmBot(process.id)) { + // Check connection status with debugging + const isConnected = isConnectedToActiveArmBot(process.id); + console.log( + `Process ${process.id} animation - connected:`, + isConnected + ); + + if (isConnected) { + // Stop all animations when connected to active arm bot newStates[process.id] = { ...processState, spawnedObjects: Object.entries(processState.spawnedObjects).reduce( @@ -252,6 +284,7 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ return; } + // Process delay handling if (processState.isProcessDelaying) { const effectiveDelayTime = processState.processDelayDuration / speedRef.current; @@ -260,6 +293,9 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ currentTime - processState.processDelayStartTime >= effectiveDelayTime ) { + console.log( + `Process ${process.id} delay completed, resuming animation` + ); newStates[process.id] = { ...processState, isProcessDelaying: false, @@ -283,26 +319,42 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ {} ), }; - return newStates; + return; } else { - return newStates; + return; } } + // Ensure we have a valid path to follow const path = process.animationPath?.map((p) => new THREE.Vector3(p.x, p.y, p.z)) || []; - if (path.length < 2) return; + + if (path.length < 2) { + console.log( + `Process ${process.id} has insufficient path points: ${path.length}` + ); + return; + } const updatedObjects = { ...processState.spawnedObjects }; + let animationOccurring = false; // Track if any animation is happening Object.entries(processState.spawnedObjects).forEach( ([objectId, obj]) => { - if (!obj.visible) return; + if (!obj.visible) { + return; + } const currentRef = gltf?.scene ? obj.ref.current : obj.ref.current; - if (!currentRef) return; + if (!currentRef) { + console.log( + `No reference for object ${objectId}, skipping animation` + ); + return; + } + // Initialize position for new objects if ( obj.position && obj.state.currentIndex === 0 && @@ -313,11 +365,22 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ const stateRef = obj.state; + // Ensure animation state is properly set for objects + if (!stateRef.isAnimating && !stateRef.isDelaying && !isConnected) { + stateRef.isAnimating = true; + stateRef.progress = + stateRef.progress > 0 ? stateRef.progress : 0.005; + } + + // Handle delay logic if (stateRef.isDelaying) { const effectiveDelayTime = stateRef.currentDelayDuration / speedRef.current; if (currentTime - stateRef.delayStartTime >= effectiveDelayTime) { + console.log( + `Delay complete for object ${objectId}, resuming animation` + ); stateRef.isDelaying = false; stateRef.delayComplete = true; stateRef.isAnimating = true; @@ -343,8 +406,17 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ } } - if (!stateRef.isAnimating) return; + // Skip non-animating objects + if (!stateRef.isAnimating) { + console.log( + `Object ${objectId} not animating, skipping animation updates` + ); + return; + } + animationOccurring = true; // Mark that animation is happening + + // Handle point actions const currentPointData = getPointDataForAnimationIndex( process, stateRef.currentIndex @@ -369,42 +441,7 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ const nextPointIdx = stateRef.currentIndex + 1; const isLastPoint = nextPointIdx >= path.length; - // if (isLastPoint) { - // if (currentPointData?.actions) { - // const shouldStop = !hasNonInheritActions( - // currentPointData.actions - // ); - // if (shouldStop) { - // return; - // } - // } - // } - - // if (isLastPoint) { - // if (currentPointData?.actions) { - // const hasNonInherit = hasNonInheritActions( - // currentPointData.actions - // ); - // if (!hasNonInherit) { - // // Remove the object if all actions are inherit - // updatedObjects[objectId] = { - // ...obj, - // visible: false, - // state: { ...stateRef, isAnimating: false }, - // }; - // return; - // } - // } else { - // // No actions at last point - remove the object - // updatedObjects[objectId] = { - // ...obj, - // visible: false, - // state: { ...stateRef, isAnimating: false }, - // }; - // return; - // } - // } - + // Handle objects at the last point if (isLastPoint) { const isAgvPicking = agvRef.current.some( (agv: any) => @@ -417,6 +454,9 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ if (shouldHide) { if (isAgvPicking) { + console.log( + `AGV picking at last point for object ${objectId}, hiding object` + ); updatedObjects[objectId] = { ...obj, visible: false, @@ -442,6 +482,7 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ } } + // Handle stacked objects when AGV picks if (tempStackedObjectsRef.current[objectId]) { const isAgvPicking = agvRef.current.some( (agv: any) => @@ -459,9 +500,12 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ isAnimating: false, }, }; + + return; } } + // Handle normal animation progress for objects not at last point if (!isLastPoint) { const nextPoint = path[nextPointIdx]; const distance = @@ -469,13 +513,21 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ const effectiveSpeed = stateRef.speed * speedRef.current; const movement = effectiveSpeed * delta; + // Ensure progress is always moving forward if (stateRef.delayComplete && stateRef.progress < 0.01) { stateRef.progress = 0.05; stateRef.delayComplete = false; + console.log( + `Boosting progress for object ${objectId} after delay` + ); } else { stateRef.progress += movement / distance; + console.log( + `Object ${objectId} progress: ${stateRef.progress.toFixed(3)}` + ); } + // Handle point transition if (stateRef.progress >= 1) { stateRef.currentIndex = nextPointIdx; stateRef.progress = 0; @@ -494,7 +546,10 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ process, stateRef.currentIndex ); + + // No action needed with newPointData here - will be handled in next frame } else { + // Update position with lerp currentRef.position.lerpVectors( path[stateRef.currentIndex], nextPoint, @@ -507,6 +562,13 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ } ); + // Log if no animation is occurring when it should + if (!animationOccurring && !isConnected) { + console.log( + `Warning: No animation occurring for process ${process.id} despite not being connected` + ); + } + newStates[process.id] = { ...processState, spawnedObjects: updatedObjects, @@ -516,6 +578,7 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ return newStates; }); }); + if (!processedProcesses || processedProcesses.length === 0) { return null; } From a59aa1d61c7e7d1c277058b01e9d73755a367bbd Mon Sep 17 00:00:00 2001 From: Jerald-Golden-B <jerald@hexrfactory.com> Date: Wed, 16 Apr 2025 13:22:47 +0530 Subject: [PATCH 12/20] feat: Add MaterialInstances component for rendering animated materials based on active curves refactor: Clean up console logs in ProcessAnimator for better readability fix: Update group visibility logic in Agv and PathNavigator components --- app/src/modules/builder/agv/agv.tsx | 3 +- app/src/modules/builder/agv/pathNavigator.tsx | 4 +- .../armbot/IKAnimationController.tsx | 19 ++++++++- .../simulation/armbot/MaterialInstances.tsx | 42 +++++++++++++++++++ .../simulation/process/processAnimator.tsx | 37 ++-------------- .../process/useProcessAnimations.tsx | 10 ++++- 6 files changed, 76 insertions(+), 39 deletions(-) create mode 100644 app/src/modules/simulation/armbot/MaterialInstances.tsx diff --git a/app/src/modules/builder/agv/agv.tsx b/app/src/modules/builder/agv/agv.tsx index c2e7783..0a2d14e 100644 --- a/app/src/modules/builder/agv/agv.tsx +++ b/app/src/modules/builder/agv/agv.tsx @@ -79,8 +79,9 @@ const Agv: React.FC<ProcessContainerProps> = ({ return ( <> {pathPoints.map((pair, i) => ( - <group key={i} visible={!isPlaying}> + <group key={i}> <PathNavigator + key={i} navMesh={navMesh} pathPoints={pair.points} id={pair.modelUuid} diff --git a/app/src/modules/builder/agv/pathNavigator.tsx b/app/src/modules/builder/agv/pathNavigator.tsx index a81925f..a82b53c 100644 --- a/app/src/modules/builder/agv/pathNavigator.tsx +++ b/app/src/modules/builder/agv/pathNavigator.tsx @@ -185,7 +185,7 @@ export default function PathNavigator({ function logAgvStatus(id: string, status: string) { // console.log( // `AGV ${id}: ${status}` - + // ); } @@ -456,7 +456,7 @@ export default function PathNavigator({ }, []); return ( - <group name="path-navigator-lines" visible={!isPlaying}> + <group name="path-navigator-lines"> {toPickupPath.length > 0 && ( <Line points={toPickupPath} diff --git a/app/src/modules/simulation/armbot/IKAnimationController.tsx b/app/src/modules/simulation/armbot/IKAnimationController.tsx index d0aaec1..dbb7386 100644 --- a/app/src/modules/simulation/armbot/IKAnimationController.tsx +++ b/app/src/modules/simulation/armbot/IKAnimationController.tsx @@ -3,6 +3,7 @@ import { useFrame } from "@react-three/fiber"; import * as THREE from "three"; import { usePlayButtonStore } from "../../../store/usePlayButtonStore"; import { useSimulationStates } from "../../../store/store"; +import MaterialInstances from "./MaterialInstances"; interface StaticMachineState { @@ -257,6 +258,10 @@ const IKAnimationController = ({ }); }, [process, groupRef, isPlaying]); + useEffect(() => { + console.log('processedCurves: ', processedCurves); + }, [processedCurves]) + const activeCurve = useMemo(() => { if (isPlaying && processedCurves) return processedCurves.find((c) => c?.trigger === selectedTrigger); @@ -337,7 +342,19 @@ const IKAnimationController = ({ ikSolver.update(); }); - return null; + return ( + <> + {armBot.status === 'moving' && activeCurve && + <MaterialInstances + groupRef={groupRef} + activeCurve={activeCurve} + progress={progress} + ikSolver={ikSolver} + targetBoneName={targetBoneName} + /> + } + </> + ); }; export default IKAnimationController; \ No newline at end of file diff --git a/app/src/modules/simulation/armbot/MaterialInstances.tsx b/app/src/modules/simulation/armbot/MaterialInstances.tsx new file mode 100644 index 0000000..778ae30 --- /dev/null +++ b/app/src/modules/simulation/armbot/MaterialInstances.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import * as THREE from 'three'; +import { Box } from '@react-three/drei'; + +type MaterialInstancesProps = { + groupRef: React.RefObject<THREE.Group>; + activeCurve: any; + progress: number; + ikSolver: any; + targetBoneName: string; +}; + +function MaterialInstances({ + groupRef, + activeCurve, + progress, + ikSolver, + targetBoneName +}: MaterialInstancesProps) { + const { endToRestRange, startToEndRange } = activeCurve; + + // Show the box from when we reach the start point until we leave the end point + const shouldShow = (progress >= startToEndRange[0] && progress < startToEndRange[1] && progress >= endToRestRange[0]); + + if (!shouldShow || !ikSolver) return null; + + const targetBone = ikSolver.mesh.skeleton.bones.find( + (b: any) => b.name === targetBoneName + ); + if (!targetBone) return null; + + const worldPos = new THREE.Vector3(); + targetBone.getWorldPosition(worldPos); + + return ( + <Box args={[0.5, 0.5, 0.5]} position={worldPos}> + <meshStandardMaterial color="orange" /> + </Box> + ); +} + +export default MaterialInstances; \ No newline at end of file diff --git a/app/src/modules/simulation/process/processAnimator.tsx b/app/src/modules/simulation/process/processAnimator.tsx index f6cda0b..a32b34b 100644 --- a/app/src/modules/simulation/process/processAnimator.tsx +++ b/app/src/modules/simulation/process/processAnimator.tsx @@ -169,9 +169,6 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ const spawnPoint = findSpawnPoint(process); if (!spawnPoint || !spawnPoint.actions) { - console.log( - `Process ${process.id} has no valid spawn point or actions` - ); return; } @@ -255,10 +252,6 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ // Check connection status with debugging const isConnected = isConnectedToActiveArmBot(process.id); - console.log( - `Process ${process.id} animation - connected:`, - isConnected - ); if (isConnected) { // Stop all animations when connected to active arm bot @@ -293,9 +286,6 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ currentTime - processState.processDelayStartTime >= effectiveDelayTime ) { - console.log( - `Process ${process.id} delay completed, resuming animation` - ); newStates[process.id] = { ...processState, isProcessDelaying: false, @@ -331,9 +321,6 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ []; if (path.length < 2) { - console.log( - `Process ${process.id} has insufficient path points: ${path.length}` - ); return; } @@ -348,9 +335,6 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ const currentRef = gltf?.scene ? obj.ref.current : obj.ref.current; if (!currentRef) { - console.log( - `No reference for object ${objectId}, skipping animation` - ); return; } @@ -378,9 +362,6 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ stateRef.currentDelayDuration / speedRef.current; if (currentTime - stateRef.delayStartTime >= effectiveDelayTime) { - console.log( - `Delay complete for object ${objectId}, resuming animation` - ); stateRef.isDelaying = false; stateRef.delayComplete = true; stateRef.isAnimating = true; @@ -408,9 +389,6 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ // Skip non-animating objects if (!stateRef.isAnimating) { - console.log( - `Object ${objectId} not animating, skipping animation updates` - ); return; } @@ -454,9 +432,6 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ if (shouldHide) { if (isAgvPicking) { - console.log( - `AGV picking at last point for object ${objectId}, hiding object` - ); updatedObjects[objectId] = { ...obj, visible: false, @@ -517,14 +492,8 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ if (stateRef.delayComplete && stateRef.progress < 0.01) { stateRef.progress = 0.05; stateRef.delayComplete = false; - console.log( - `Boosting progress for object ${objectId} after delay` - ); } else { stateRef.progress += movement / distance; - console.log( - `Object ${objectId} progress: ${stateRef.progress.toFixed(3)}` - ); } // Handle point transition @@ -564,9 +533,9 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ // Log if no animation is occurring when it should if (!animationOccurring && !isConnected) { - console.log( - `Warning: No animation occurring for process ${process.id} despite not being connected` - ); + // console.log( + // `Warning: No animation occurring for process ${process.id} despite not being connected` + // ); } newStates[process.id] = { diff --git a/app/src/modules/simulation/process/useProcessAnimations.tsx b/app/src/modules/simulation/process/useProcessAnimations.tsx index 9f08cf3..a46fe3c 100644 --- a/app/src/modules/simulation/process/useProcessAnimations.tsx +++ b/app/src/modules/simulation/process/useProcessAnimations.tsx @@ -510,6 +510,14 @@ export const useProcessAnimation = ( if (activeVehicles.length === 0) { onHitTriggers.forEach((trigger: Trigger) => { + const triggerKey = `${point.uuid}-${trigger.uuid}`; + + newTriggerCounts[triggerKey] = (newTriggerCounts[triggerKey] || 0) + 1; + + shouldLog = true; + + newTriggerLogs.push({ timestamp: currentTime, pointId: point.uuid, objectId, triggerId: trigger.uuid, }); + const connections = point.connections?.targets || []; connections.forEach((connection) => { @@ -529,7 +537,7 @@ export const useProcessAnimation = ( let processTotalHits = Object.values(newTriggerCounts).reduce((a, b) => a + b, 0); - // Handle logic for vehicles and ArmBots when a trigger is hit + // Handle logic for vehicles when a trigger is hit if (shouldLog) { vehiclePaths.forEach((vehiclePath) => { if (vehiclePath.points?.length > 0) { From 8b7b7f589a0e06f6477cc11f28aba96605622c8f Mon Sep 17 00:00:00 2001 From: Jerald-Golden-B <jerald@hexrfactory.com> Date: Wed, 16 Apr 2025 15:04:52 +0530 Subject: [PATCH 13/20] feat: Integrate reset functionality in ArmBot and StaticMachine components --- app/src/modules/simulation/armbot/ArmBot.tsx | 4 +- .../armbot/IKAnimationController.tsx | 358 ++++++++---------- .../modules/simulation/armbot/IkInstances.tsx | 2 +- .../staticMachine/staticMachine.tsx | 4 +- 4 files changed, 166 insertions(+), 202 deletions(-) diff --git a/app/src/modules/simulation/armbot/ArmBot.tsx b/app/src/modules/simulation/armbot/ArmBot.tsx index 1c7bb83..5d55791 100644 --- a/app/src/modules/simulation/armbot/ArmBot.tsx +++ b/app/src/modules/simulation/armbot/ArmBot.tsx @@ -4,6 +4,7 @@ import useModuleStore from "../../../store/useModuleStore"; import { useSimulationStates } from "../../../store/store"; import * as SimulationTypes from '../../../types/simulationTypes'; import { ArmbotInstances } from "./ArmBotInstances"; +import { useResetButtonStore } from "../../../store/usePlayButtonStore"; interface ArmBotState { uuid: string; @@ -38,6 +39,7 @@ const ArmBot = ({ armBots, setArmBots, setStaticMachines }: ArmBotProps) => { const { activeModule } = useModuleStore(); const { scene } = useThree(); const { simulationStates } = useSimulationStates(); + const { isReset } = useResetButtonStore(); useEffect(() => { const filtered = simulationStates.filter((s): s is SimulationTypes.ArmBotEventsSchema => s.type === "ArmBot"); @@ -54,7 +56,7 @@ const ArmBot = ({ armBots, setArmBots, setStaticMachines }: ArmBotProps) => { connections: bot.points.connections })); setArmBots(initialStates); - }, [simulationStates]); + }, [simulationStates, isReset]); useEffect(() => { armBots.forEach((bot) => { diff --git a/app/src/modules/simulation/armbot/IKAnimationController.tsx b/app/src/modules/simulation/armbot/IKAnimationController.tsx index dbb7386..efe88d4 100644 --- a/app/src/modules/simulation/armbot/IKAnimationController.tsx +++ b/app/src/modules/simulation/armbot/IKAnimationController.tsx @@ -31,7 +31,7 @@ interface ArmBotState { type IKAnimationControllerProps = { ikSolver: any; - process: { + processes: { triggerId: string; startPoint: THREE.Vector3; endPoint: THREE.Vector3; @@ -50,7 +50,7 @@ type IKAnimationControllerProps = { const IKAnimationController = ({ ikSolver, - process, + processes, selectedTrigger, targetBoneName, uuid, @@ -67,20 +67,10 @@ const IKAnimationController = ({ const [isInitializing, setIsInitializing] = useState(true); const restSpeed = 0.1; const restPosition = new THREE.Vector3(0, 2, 1.6); - const { isPlaying } = usePlayButtonStore(); + const { isPlaying } = usePlayButtonStore();; + const statusRef = useRef("idle"); const { simulationStates } = useSimulationStates(); - // Track previous states for comparison - const prevStateRef = useRef({ - isInitializing: true, - needsInitialMovement: true, - selectedTrigger: "", - progress: 0 - }); - - // Track previous status for comparison - const prevStatusRef = useRef(""); - const initialCurveRef = useRef<THREE.CatmullRomCurve3 | null>(null); const initialStartPositionRef = useRef<THREE.Vector3 | null>(null); @@ -101,91 +91,6 @@ const IKAnimationController = ({ } }, [ikSolver]); - // Log state changes - useEffect(() => { - const prev = prevStateRef.current; - - if (prev.isInitializing !== isInitializing) { - if (!isInitializing) { - logStatus(`[Arm ${uuid}] Completed initialization, now at rest position`); - } - } - - if (prev.needsInitialMovement !== needsInitialMovement && !needsInitialMovement) { - logStatus(`[Arm ${uuid}] Reached rest position, ready for animation`); - - } - - if (prev.selectedTrigger !== selectedTrigger) { - logStatus(`[Arm ${uuid}] Processing new trigger: ${selectedTrigger}`); - - const currentProcess = process.find(p => p.triggerId === prev.selectedTrigger); - if (currentProcess) { - const triggerId = currentProcess.triggerId; - - const endPoint = armBot.actions.processes.find((process) => process.triggerId === triggerId)?.endPoint; - - // Search simulationStates for a StaticMachine or Conveyor that has a point matching this endPointId - const matchedMachine = simulationStates.find((state) => { - if (state.type === "Conveyor") { - // For Conveyor, points is an array - return (state).points.some( - (point) => point.uuid === endPoint - ); - } else if (state.type === "StaticMachine") { - // For StaticMachine, points is an object - return state.points.uuid === endPoint; - } - return false; - }); - - if (matchedMachine) { - // Log if the end point is a conveyor - if (matchedMachine.type === "Conveyor") { - logStatus(`[Arm ${uuid}] Reached end point which is a conveyor (${matchedMachine.modelName})`); - } else { - logStatus(`[Arm ${uuid}] Reached end point which is a static machine (${matchedMachine.modelName})`); - } - - if (matchedMachine.type === "StaticMachine") { - setStaticMachines((machines) => { - return machines.map((machine) => { - if (machine.uuid === matchedMachine.modeluuid) { - return { ...machine, status: "running" }; - } else { - return machine; - } - }); - }); - } - - if (matchedMachine.type === "Conveyor") { - setArmBots((prev) => - prev.map((arm) => { - if (arm.uuid === uuid) { - return { - ...arm, - isActive: false - }; - } - else { - return arm; - } - }) - ); - } - } - } - } - - // Update previous state - prevStateRef.current = { - isInitializing, - needsInitialMovement, - selectedTrigger, - progress - }; - }, [isInitializing, needsInitialMovement, selectedTrigger, progress]); const calculateInitialCurve = (startPosition: THREE.Vector3) => { const direction = new THREE.Vector3().subVectors(restPosition, startPosition); @@ -211,61 +116,56 @@ const IKAnimationController = ({ ]); }; - const processedCurves = useMemo(() => { - if (isPlaying) - return process.map((p) => { - const tempLift = 0.5; - const localStart = groupRef.current?.worldToLocal(p.startPoint.clone().add(new THREE.Vector3(0, tempLift, 0))); - const localEnd = groupRef.current?.worldToLocal(p.endPoint.clone().add(new THREE.Vector3(0, tempLift, 0))); + const processCurves = useMemo(() => { + if (!isPlaying) return []; - if (localStart && localEnd) { + return processes.map(process => { + const localStart = groupRef.current?.worldToLocal(process.startPoint.clone()); + const localEnd = groupRef.current?.worldToLocal(process.endPoint.clone()); - const mid = new THREE.Vector3( - (localStart.x + localEnd.x) / 1, - Math.max(localStart.y, localEnd.y) + 0.8, - (localStart.z + localEnd.z) / 0.9 - ); + if (!localStart || !localEnd) return null; - const points = [ - restPosition.clone(), - localStart.clone(), - mid.clone(), - localEnd.clone(), - restPosition.clone(), - ]; - const curve = new THREE.CatmullRomCurve3(points); - const restToStartDist = points[0].distanceTo(points[1]); - const startToEndDist = points[1].distanceTo(points[3]); - const endToRestDist = points[3].distanceTo(points[4]); + const midPoint = new THREE.Vector3( + (localStart.x + localEnd.x) / 2, + Math.max(localStart.y, localEnd.y) + 1, + (localStart.z + localEnd.z) / 2 + ); + const restToStartCurve = new THREE.CatmullRomCurve3([ + restPosition, + new THREE.Vector3().lerpVectors(restPosition, localStart, 0.5), + localStart + ]); - const totalDist = restToStartDist + startToEndDist + endToRestDist; - const restToStartRange = [0, restToStartDist / totalDist]; - const startToEndRange = [ - restToStartRange[1], - restToStartRange[1] + startToEndDist / totalDist, - ]; - const endToRestRange = [startToEndRange[1], 1]; + const processCurve = new THREE.CatmullRomCurve3([ + localStart, + midPoint, + localEnd + ]); - return { - trigger: p.triggerId, - curve, - speed: p.speed, - restToStartRange, - startToEndRange, - endToRestRange, - }; - } - }); - }, [process, groupRef, isPlaying]); + const endToRestCurve = new THREE.CatmullRomCurve3([ + localEnd, + new THREE.Vector3().lerpVectors(localEnd, restPosition, 0.5), + restPosition + ]); - useEffect(() => { - console.log('processedCurves: ', processedCurves); - }, [processedCurves]) + return { + triggerId: process.triggerId, + restToStartCurve, + processCurve, + endToRestCurve, + speed: process.speed, + totalDistance: + restPosition.distanceTo(localStart) + + localStart.distanceTo(localEnd) + + localEnd.distanceTo(restPosition) + }; + }).filter(Boolean); + }, [processes, isPlaying]); - const activeCurve = useMemo(() => { - if (isPlaying && processedCurves) - return processedCurves.find((c) => c?.trigger === selectedTrigger); - }, [processedCurves, selectedTrigger, isPlaying]); + const activeProcess = useMemo(() => { + if (!selectedTrigger) return null; + return processCurves.find(p => p?.triggerId === selectedTrigger); + }, [processCurves, selectedTrigger]); // Initial movement to rest position useFrame((_, delta) => { @@ -293,68 +193,128 @@ const IKAnimationController = ({ // Main animation loop useFrame((_, delta) => { - if (!ikSolver || !activeCurve || isInitializing || !isPlaying) return; + if (isInitializing || !isPlaying || !selectedTrigger || !activeProcess || !ikSolver) return; - const { curve, speed, restToStartRange, startToEndRange, endToRestRange } = activeCurve; - const targetBone = ikSolver.mesh.skeleton.bones.find( - (b: any) => b.name === targetBoneName - ); - if (!targetBone) return; + const bone = ikSolver.mesh.skeleton.bones.find((b: any) => b.name === targetBoneName); + if (!bone) return; - let currentSpeed = restSpeed; - let currentStatus = "idle"; // Default status + const { + restToStartCurve, + processCurve, + endToRestCurve, + speed, + totalDistance + } = activeProcess; - // Determine current phase and status - if (progress < restToStartRange[1]) { - currentSpeed = restSpeed; - currentStatus = "moving"; // Moving to start point - } else if (progress >= startToEndRange[0] && progress < startToEndRange[1]) { - currentSpeed = speed; - currentStatus = "moving"; // Moving between points - } else if (progress >= endToRestRange[0] && progress < 1) { - currentSpeed = restSpeed; - currentStatus = "moving"; // Returning to rest - } else if (progress >= 1) { - currentStatus = "idle"; // Completed cycle - } + // Calculate current segment and progress + const restToStartDist = restPosition.distanceTo(restToStartCurve.points[2]); + const processDist = processCurve.getLength(); + const endToRestDist = endToRestCurve.getLength(); - // Update status when it changes - if (prevStatusRef.current !== currentStatus) { - updateArmBotStatus(currentStatus); - prevStatusRef.current = currentStatus; - } + const restToStartEnd = restToStartDist / totalDistance; + const processEnd = (restToStartDist + processDist) / totalDistance; - // Only update progress if we're not already at the end - if (progress < 1) { - setProgress((prev) => { - const next = prev + delta * currentSpeed; - return Math.min(next, 1); // Cap at 1 - }); - } + setProgress(prev => { + let currentStatus = statusRef.current; + let currentPosition: THREE.Vector3; + const newProgress = Math.min(prev + delta * ((currentStatus === 'returning to rest') ? restSpeed : speed), 1); - // Update bone position based on progress - if (progress < 1) { - targetBone.position.copy(curve.getPoint(progress)); - } else { - targetBone.position.copy(curve.getPoint(1)); - } + if (newProgress < restToStartEnd) { + // Moving from rest to start position + currentStatus = "moving to start"; + const segmentProgress = newProgress / restToStartEnd; + currentPosition = restToStartCurve.getPoint(segmentProgress); + } else if (newProgress < processEnd) { + // Processing - moving from start to end + currentStatus = "processing"; + const segmentProgress = (newProgress - restToStartEnd) / (processEnd - restToStartEnd); + currentPosition = processCurve.getPoint(segmentProgress); + } else { + // Returning to rest position + currentStatus = "returning to rest"; + const segmentProgress = (newProgress - processEnd) / (1 - processEnd); + currentPosition = endToRestCurve.getPoint(segmentProgress); + } - ikSolver.update(); + // Update status if changed + if (currentStatus !== statusRef.current) { + statusRef.current = currentStatus; + // updateArmBotStatus(currentStatus); + logStatus(`[Arm ${uuid}] Status: ${currentStatus}`); + } + + // Only trigger when the entire animation is complete (newProgress === 1) + if (newProgress === 1 && currentStatus === "returning to rest") { + updateConveyorOrStaticMachineStatus(selectedTrigger); + } + + bone.position.copy(currentPosition); + ikSolver.update(); + return newProgress; + }); }); - return ( - <> - {armBot.status === 'moving' && activeCurve && - <MaterialInstances - groupRef={groupRef} - activeCurve={activeCurve} - progress={progress} - ikSolver={ikSolver} - targetBoneName={targetBoneName} - /> + const updateConveyorOrStaticMachineStatus = (selectedTrigger: string) => { + const currentProcess = processes.find(p => p.triggerId === selectedTrigger); + if (currentProcess) { + const triggerId = currentProcess.triggerId; + + const endPoint = armBot.actions.processes.find((process) => process.triggerId === triggerId)?.endPoint; + + const matchedMachine = simulationStates.find((state) => { + if (state.type === "Conveyor") { + return (state).points.some( + (point) => point.uuid === endPoint + ); + } else if (state.type === "StaticMachine") { + return state.points.uuid === endPoint; + } + return false; + }); + + if (matchedMachine) { + if (matchedMachine.type === "Conveyor") { + logStatus(`[Arm ${uuid}] Reached end point which is a conveyor (${matchedMachine.modelName})`); + } else { + logStatus(`[Arm ${uuid}] Reached end point which is a static machine (${matchedMachine.modelName})`); + } + + setTimeout(() => { + if (matchedMachine.type === "StaticMachine") { + setStaticMachines((machines) => { + return machines.map((machine) => { + if (machine.uuid === matchedMachine.modeluuid) { + return { ...machine, status: "running" }; + } else { + return machine; + } + }); + }); + updateArmBotStatus('idle'); + } + + if (matchedMachine.type === "Conveyor") { + setArmBots((prev) => + prev.map((arm) => { + if (arm.uuid === uuid) { + return { + ...arm, + isActive: false, + status: "idle", + }; + } + else { + return arm; + } + }) + ); + } + }, 0); } - </> - ); + } + } + + return null; }; export default IKAnimationController; \ No newline at end of file diff --git a/app/src/modules/simulation/armbot/IkInstances.tsx b/app/src/modules/simulation/armbot/IkInstances.tsx index 5a0d23b..6e7dc13 100644 --- a/app/src/modules/simulation/armbot/IkInstances.tsx +++ b/app/src/modules/simulation/armbot/IkInstances.tsx @@ -132,7 +132,7 @@ const IkInstances = ({ </group> <IKAnimationController ikSolver={ikSolver} - process={processes} + processes={processes} selectedTrigger={selectedTrigger} targetBoneName={targetBoneName} uuid={uuid} diff --git a/app/src/modules/simulation/staticMachine/staticMachine.tsx b/app/src/modules/simulation/staticMachine/staticMachine.tsx index 26760eb..3f61993 100644 --- a/app/src/modules/simulation/staticMachine/staticMachine.tsx +++ b/app/src/modules/simulation/staticMachine/staticMachine.tsx @@ -2,6 +2,7 @@ import React, { useEffect } from 'react' import * as SimulationTypes from '../../../types/simulationTypes'; import { useSimulationStates } from '../../../store/store'; import StaticMachineInstances from './staticMachineInstances'; +import { useResetButtonStore } from '../../../store/usePlayButtonStore'; interface ArmBotState { uuid: string; @@ -34,6 +35,7 @@ type StaticMachineProps = { function StaticMachine({ setArmBots, staticMachines, setStaticMachines }: StaticMachineProps) { const { simulationStates } = useSimulationStates(); + const { isReset } = useResetButtonStore(); useEffect(() => { const filtered = simulationStates.filter((s): s is SimulationTypes.StaticMachineEventsSchema => s.type === "StaticMachine"); @@ -47,7 +49,7 @@ function StaticMachine({ setArmBots, staticMachines, setStaticMachines }: Static connectedArmBot: machine.points.connections.targets[0].modelUUID })); setStaticMachines(initialStates); - }, [simulationStates]); + }, [simulationStates, isReset]); const updateArmBotTriggerAndMachineStatus = (armBotUuid: string, triggerId: string, machineId: string) => { setArmBots((prevArmBots) => { From 0eedbdd58e27557a2b681f3ca3056bad9af1fa04 Mon Sep 17 00:00:00 2001 From: SreeNath14 <153710861+SreeNath14@users.noreply.github.com> Date: Wed, 16 Apr 2025 15:07:11 +0530 Subject: [PATCH 14/20] "multiple spawn bug fixed" --- .../simulation/process/processAnimator.tsx | 59 ++++++++++++++----- 1 file changed, 45 insertions(+), 14 deletions(-) diff --git a/app/src/modules/simulation/process/processAnimator.tsx b/app/src/modules/simulation/process/processAnimator.tsx index a32b34b..981846b 100644 --- a/app/src/modules/simulation/process/processAnimator.tsx +++ b/app/src/modules/simulation/process/processAnimator.tsx @@ -1,4 +1,10 @@ -import React, { useRef, useEffect, useMemo, useCallback } from "react"; +import React, { + useRef, + useEffect, + useMemo, + useCallback, + useState, +} from "react"; import { useLoader, useFrame } from "@react-three/fiber"; import { GLTFLoader } from "three-stdlib"; import * as THREE from "three"; @@ -48,6 +54,8 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ const gltf = useLoader(GLTFLoader, crate) as GLTF; const groupRef = useRef<THREE.Group>(null); const tempStackedObjectsRef = useRef<Record<string, boolean>>({}); + const [previouslyConnected, setPreviouslyConnected] = useState<any>({}); + const currentSpawnedObjectRef = useRef<any>(null); const { animationStates, @@ -116,8 +124,6 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ }); }, [animationStates, MaterialRef, agvRef]); - // In processAnimator.tsx - only the relevant spawn logic part that needs fixes - // Add this function to ProcessAnimator component const isConnectedToActiveArmBot = useCallback( (processId: any) => { @@ -153,6 +159,26 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ // Check connection status const isConnected = isConnectedToActiveArmBot(process.id); + const wasConnected = previouslyConnected[process.id] || false; + + // Track connection state changes + if (wasConnected !== isConnected) { + setTimeout(() => { + setPreviouslyConnected((prev: any) => ({ + ...prev, + [process.id]: isConnected, + })); + }, 0); + } + + // If just disconnected, reset the spawn timer to allow new spawns + if (wasConnected && !isConnected) { + newStates[process.id] = { + ...processState, + nextSpawnTime: currentTime + 0.1, + hasSpawnedZeroIntervalObject: false, + }; + } if (processState.isProcessDelaying) { // Existing delay handling logic... @@ -192,7 +218,10 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ return; // Don't spawn more objects for zero interval } - const effectiveSpawnInterval = spawnInterval / speedRef.current; + const effectiveSpawnInterval = Math.max( + 0.1, + spawnInterval / speedRef.current + ); if (currentTime >= processState.nextSpawnTime) { const objectId = `obj-${process.id}-${processState.objectIdCounter}`; @@ -213,6 +242,9 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ progress: 0.005, // Start with tiny progress to ensure animation begins }; + // Calculate next spawn time + const nextSpawnAt = currentTime + effectiveSpawnInterval; + // Update state with the new object and flag for zero interval newStates[process.id] = { ...processState, @@ -221,7 +253,7 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ [objectId]: newObject, }, objectIdCounter: processState.objectIdCounter + 1, - nextSpawnTime: currentTime + effectiveSpawnInterval, + nextSpawnTime: nextSpawnAt, // Mark that we've spawned an object for zero interval case hasSpawnedZeroIntervalObject: spawnInterval === 0 @@ -237,6 +269,8 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ // Second useFrame for animation logic useFrame((_, delta) => { + let currentProcessObject: any = null; + // Animation logic frame const currentTime = clockRef.current.getElapsedTime() - elapsedBeforePauseRef.current; @@ -250,7 +284,7 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ return; } - // Check connection status with debugging + // Check connection status const isConnected = isConnectedToActiveArmBot(process.id); if (isConnected) { @@ -263,6 +297,7 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ [id]: { ...obj, state: { + visible: true, ...obj.state, isAnimating: false, // Stop animation isDelaying: false, // Clear delays @@ -511,12 +546,8 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ currentTime ); - const newPointData = getPointDataForAnimationIndex( - process, - stateRef.currentIndex - ); - // No action needed with newPointData here - will be handled in next frame + getPointDataForAnimationIndex(process, stateRef.currentIndex); } else { // Update position with lerp currentRef.position.lerpVectors( @@ -533,9 +564,9 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ // Log if no animation is occurring when it should if (!animationOccurring && !isConnected) { - // console.log( - // `Warning: No animation occurring for process ${process.id} despite not being connected` - // ); + console.log( + `Warning: No animation occurring for process ${process.id} despite not being connected` + ); } newStates[process.id] = { From 496c8b0305fa7345aae514fd06d1d5f4d23ec5f7 Mon Sep 17 00:00:00 2001 From: SreeNath14 <153710861+SreeNath14@users.noreply.github.com> Date: Wed, 16 Apr 2025 16:08:47 +0530 Subject: [PATCH 15/20] new comit --- .../simulation/process/processAnimator.tsx | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/app/src/modules/simulation/process/processAnimator.tsx b/app/src/modules/simulation/process/processAnimator.tsx index 981846b..408cd37 100644 --- a/app/src/modules/simulation/process/processAnimator.tsx +++ b/app/src/modules/simulation/process/processAnimator.tsx @@ -55,7 +55,6 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ const groupRef = useRef<THREE.Group>(null); const tempStackedObjectsRef = useRef<Record<string, boolean>>({}); const [previouslyConnected, setPreviouslyConnected] = useState<any>({}); - const currentSpawnedObjectRef = useRef<any>(null); const { animationStates, @@ -124,6 +123,8 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ }); }, [animationStates, MaterialRef, agvRef]); + // In processAnimator.tsx - only the relevant spawn logic part that needs fixes + // Add this function to ProcessAnimator component const isConnectedToActiveArmBot = useCallback( (processId: any) => { @@ -163,6 +164,8 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ // Track connection state changes if (wasConnected !== isConnected) { + + // Update connection tracking (in a separate useEffect to avoid issues with setState in render) setTimeout(() => { setPreviouslyConnected((prev: any) => ({ ...prev, @@ -173,10 +176,11 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ // If just disconnected, reset the spawn timer to allow new spawns if (wasConnected && !isConnected) { + newStates[process.id] = { ...processState, - nextSpawnTime: currentTime + 0.1, - hasSpawnedZeroIntervalObject: false, + nextSpawnTime: currentTime + 0.1, // Start spawning soon after disconnect + hasSpawnedZeroIntervalObject: false, // Reset for zero interval spawns }; } @@ -195,6 +199,7 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ const spawnPoint = findSpawnPoint(process); if (!spawnPoint || !spawnPoint.actions) { + return; } @@ -210,6 +215,8 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ ? spawnAction.spawnInterval : parseFloat(spawnAction.spawnInterval || "0") || 0; + + // Check if this is a zero interval spawn and we already spawned an object if ( spawnInterval === 0 && @@ -224,6 +231,7 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ ); if (currentTime >= processState.nextSpawnTime) { + const objectId = `obj-${process.id}-${processState.objectIdCounter}`; const newObject = createSpawnedObject( process, @@ -269,8 +277,6 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ // Second useFrame for animation logic useFrame((_, delta) => { - let currentProcessObject: any = null; - // Animation logic frame const currentTime = clockRef.current.getElapsedTime() - elapsedBeforePauseRef.current; @@ -297,7 +303,6 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ [id]: { ...obj, state: { - visible: true, ...obj.state, isAnimating: false, // Stop animation isDelaying: false, // Clear delays @@ -321,6 +326,7 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ currentTime - processState.processDelayStartTime >= effectiveDelayTime ) { + newStates[process.id] = { ...processState, isProcessDelaying: false, @@ -356,6 +362,7 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ []; if (path.length < 2) { + return; } @@ -370,6 +377,7 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ const currentRef = gltf?.scene ? obj.ref.current : obj.ref.current; if (!currentRef) { + return; } @@ -397,6 +405,7 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ stateRef.currentDelayDuration / speedRef.current; if (currentTime - stateRef.delayStartTime >= effectiveDelayTime) { + stateRef.isDelaying = false; stateRef.delayComplete = true; stateRef.isAnimating = true; @@ -564,9 +573,7 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ // Log if no animation is occurring when it should if (!animationOccurring && !isConnected) { - console.log( - `Warning: No animation occurring for process ${process.id} despite not being connected` - ); + } newStates[process.id] = { From 9b9164c6005faf28f62cb8bacf89ff296f951304 Mon Sep 17 00:00:00 2001 From: SreeNath14 <153710861+SreeNath14@users.noreply.github.com> Date: Wed, 16 Apr 2025 16:25:02 +0530 Subject: [PATCH 16/20] minor changes in conveyor box spawn --- .../simulation/process/processAnimator.tsx | 93 +++++++++---------- 1 file changed, 43 insertions(+), 50 deletions(-) diff --git a/app/src/modules/simulation/process/processAnimator.tsx b/app/src/modules/simulation/process/processAnimator.tsx index 408cd37..f6cda0b 100644 --- a/app/src/modules/simulation/process/processAnimator.tsx +++ b/app/src/modules/simulation/process/processAnimator.tsx @@ -1,10 +1,4 @@ -import React, { - useRef, - useEffect, - useMemo, - useCallback, - useState, -} from "react"; +import React, { useRef, useEffect, useMemo, useCallback } from "react"; import { useLoader, useFrame } from "@react-three/fiber"; import { GLTFLoader } from "three-stdlib"; import * as THREE from "three"; @@ -54,7 +48,6 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ const gltf = useLoader(GLTFLoader, crate) as GLTF; const groupRef = useRef<THREE.Group>(null); const tempStackedObjectsRef = useRef<Record<string, boolean>>({}); - const [previouslyConnected, setPreviouslyConnected] = useState<any>({}); const { animationStates, @@ -160,29 +153,6 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ // Check connection status const isConnected = isConnectedToActiveArmBot(process.id); - const wasConnected = previouslyConnected[process.id] || false; - - // Track connection state changes - if (wasConnected !== isConnected) { - - // Update connection tracking (in a separate useEffect to avoid issues with setState in render) - setTimeout(() => { - setPreviouslyConnected((prev: any) => ({ - ...prev, - [process.id]: isConnected, - })); - }, 0); - } - - // If just disconnected, reset the spawn timer to allow new spawns - if (wasConnected && !isConnected) { - - newStates[process.id] = { - ...processState, - nextSpawnTime: currentTime + 0.1, // Start spawning soon after disconnect - hasSpawnedZeroIntervalObject: false, // Reset for zero interval spawns - }; - } if (processState.isProcessDelaying) { // Existing delay handling logic... @@ -199,7 +169,9 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ const spawnPoint = findSpawnPoint(process); if (!spawnPoint || !spawnPoint.actions) { - + console.log( + `Process ${process.id} has no valid spawn point or actions` + ); return; } @@ -215,8 +187,6 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ ? spawnAction.spawnInterval : parseFloat(spawnAction.spawnInterval || "0") || 0; - - // Check if this is a zero interval spawn and we already spawned an object if ( spawnInterval === 0 && @@ -225,13 +195,9 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ return; // Don't spawn more objects for zero interval } - const effectiveSpawnInterval = Math.max( - 0.1, - spawnInterval / speedRef.current - ); + const effectiveSpawnInterval = spawnInterval / speedRef.current; if (currentTime >= processState.nextSpawnTime) { - const objectId = `obj-${process.id}-${processState.objectIdCounter}`; const newObject = createSpawnedObject( process, @@ -250,9 +216,6 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ progress: 0.005, // Start with tiny progress to ensure animation begins }; - // Calculate next spawn time - const nextSpawnAt = currentTime + effectiveSpawnInterval; - // Update state with the new object and flag for zero interval newStates[process.id] = { ...processState, @@ -261,7 +224,7 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ [objectId]: newObject, }, objectIdCounter: processState.objectIdCounter + 1, - nextSpawnTime: nextSpawnAt, + nextSpawnTime: currentTime + effectiveSpawnInterval, // Mark that we've spawned an object for zero interval case hasSpawnedZeroIntervalObject: spawnInterval === 0 @@ -290,8 +253,12 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ return; } - // Check connection status + // Check connection status with debugging const isConnected = isConnectedToActiveArmBot(process.id); + console.log( + `Process ${process.id} animation - connected:`, + isConnected + ); if (isConnected) { // Stop all animations when connected to active arm bot @@ -326,7 +293,9 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ currentTime - processState.processDelayStartTime >= effectiveDelayTime ) { - + console.log( + `Process ${process.id} delay completed, resuming animation` + ); newStates[process.id] = { ...processState, isProcessDelaying: false, @@ -362,7 +331,9 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ []; if (path.length < 2) { - + console.log( + `Process ${process.id} has insufficient path points: ${path.length}` + ); return; } @@ -377,7 +348,9 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ const currentRef = gltf?.scene ? obj.ref.current : obj.ref.current; if (!currentRef) { - + console.log( + `No reference for object ${objectId}, skipping animation` + ); return; } @@ -405,7 +378,9 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ stateRef.currentDelayDuration / speedRef.current; if (currentTime - stateRef.delayStartTime >= effectiveDelayTime) { - + console.log( + `Delay complete for object ${objectId}, resuming animation` + ); stateRef.isDelaying = false; stateRef.delayComplete = true; stateRef.isAnimating = true; @@ -433,6 +408,9 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ // Skip non-animating objects if (!stateRef.isAnimating) { + console.log( + `Object ${objectId} not animating, skipping animation updates` + ); return; } @@ -476,6 +454,9 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ if (shouldHide) { if (isAgvPicking) { + console.log( + `AGV picking at last point for object ${objectId}, hiding object` + ); updatedObjects[objectId] = { ...obj, visible: false, @@ -536,8 +517,14 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ if (stateRef.delayComplete && stateRef.progress < 0.01) { stateRef.progress = 0.05; stateRef.delayComplete = false; + console.log( + `Boosting progress for object ${objectId} after delay` + ); } else { stateRef.progress += movement / distance; + console.log( + `Object ${objectId} progress: ${stateRef.progress.toFixed(3)}` + ); } // Handle point transition @@ -555,8 +542,12 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ currentTime ); + const newPointData = getPointDataForAnimationIndex( + process, + stateRef.currentIndex + ); + // No action needed with newPointData here - will be handled in next frame - getPointDataForAnimationIndex(process, stateRef.currentIndex); } else { // Update position with lerp currentRef.position.lerpVectors( @@ -573,7 +564,9 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ // Log if no animation is occurring when it should if (!animationOccurring && !isConnected) { - + console.log( + `Warning: No animation occurring for process ${process.id} despite not being connected` + ); } newStates[process.id] = { From ee319c28e440ccc8c10df9596e57eb9874d203f9 Mon Sep 17 00:00:00 2001 From: Jerald-Golden-B <jerald@hexrfactory.com> Date: Wed, 16 Apr 2025 16:33:48 +0530 Subject: [PATCH 17/20] refactor: Remove unnecessary console logs and improve connection limit checks in simulation components --- .../mechanics/ArmBotMechanics.tsx | 1 - .../simulation/armbot/ArmBotInstances.tsx | 1 - .../armbot/IKAnimationController.tsx | 63 ++++++- .../simulation/armbot/MaterialInstances.tsx | 21 +-- .../modules/simulation/path/pathConnector.tsx | 166 ++++++++++++++++-- .../simulation/process/processAnimator.tsx | 6 +- 6 files changed, 219 insertions(+), 39 deletions(-) diff --git a/app/src/components/layout/sidebarRight/mechanics/ArmBotMechanics.tsx b/app/src/components/layout/sidebarRight/mechanics/ArmBotMechanics.tsx index d3e5301..70e297b 100644 --- a/app/src/components/layout/sidebarRight/mechanics/ArmBotMechanics.tsx +++ b/app/src/components/layout/sidebarRight/mechanics/ArmBotMechanics.tsx @@ -150,7 +150,6 @@ const ArmBotMechanics: React.FC = () => { modeluuid: updatedPath.modeluuid, eventData: { type: "ArmBot", points: updatedPath.points } } - console.log('data: ', data); socket.emit('v2:model-asset:updateEventData', data); } diff --git a/app/src/modules/simulation/armbot/ArmBotInstances.tsx b/app/src/modules/simulation/armbot/ArmBotInstances.tsx index 8d20a87..a0657db 100644 --- a/app/src/modules/simulation/armbot/ArmBotInstances.tsx +++ b/app/src/modules/simulation/armbot/ArmBotInstances.tsx @@ -46,7 +46,6 @@ export const ArmbotInstances: React.FC<ArmbotInstancesProps> = ({ index, armBot, const [processes, setProcesses] = useState<Process[]>([]); useEffect(() => { - if (armBot.actions.processes.length > 0) { const mappedProcesses = armBot.actions.processes.map((process) => { return { diff --git a/app/src/modules/simulation/armbot/IKAnimationController.tsx b/app/src/modules/simulation/armbot/IKAnimationController.tsx index efe88d4..6b77bac 100644 --- a/app/src/modules/simulation/armbot/IKAnimationController.tsx +++ b/app/src/modules/simulation/armbot/IKAnimationController.tsx @@ -1,7 +1,7 @@ import { useEffect, useMemo, useState, useRef } from "react"; import { useFrame } from "@react-three/fiber"; import * as THREE from "three"; -import { usePlayButtonStore } from "../../../store/usePlayButtonStore"; +import { usePlayButtonStore, useResetButtonStore } from "../../../store/usePlayButtonStore"; import { useSimulationStates } from "../../../store/store"; import MaterialInstances from "./MaterialInstances"; @@ -70,6 +70,7 @@ const IKAnimationController = ({ const { isPlaying } = usePlayButtonStore();; const statusRef = useRef("idle"); const { simulationStates } = useSimulationStates(); + const { isReset } = useResetButtonStore(); const initialCurveRef = useRef<THREE.CatmullRomCurve3 | null>(null); const initialStartPositionRef = useRef<THREE.Vector3 | null>(null); @@ -78,6 +79,13 @@ const IKAnimationController = ({ setProgress(0); }, [selectedTrigger]); + useEffect(() => { + setProgress(0); + setNeedsInitialMovement(true); + setInitialProgress(0); + setIsInitializing(true); + }, [isReset]); + useEffect(() => { if (ikSolver) { const targetBone = ikSolver.mesh.skeleton.bones.find( @@ -229,6 +237,9 @@ const IKAnimationController = ({ currentStatus = "processing"; const segmentProgress = (newProgress - restToStartEnd) / (processEnd - restToStartEnd); currentPosition = processCurve.getPoint(segmentProgress); + if (statusRef.current !== "processing") { + updateConveyorOrStaticMachineStatusOnStart(selectedTrigger); + } } else { // Returning to rest position currentStatus = "returning to rest"; @@ -245,7 +256,7 @@ const IKAnimationController = ({ // Only trigger when the entire animation is complete (newProgress === 1) if (newProgress === 1 && currentStatus === "returning to rest") { - updateConveyorOrStaticMachineStatus(selectedTrigger); + updateConveyorOrStaticMachineStatusOnEnd(selectedTrigger); } bone.position.copy(currentPosition); @@ -254,7 +265,45 @@ const IKAnimationController = ({ }); }); - const updateConveyorOrStaticMachineStatus = (selectedTrigger: string) => { + const updateConveyorOrStaticMachineStatusOnStart = (selectedTrigger: string) => { + const currentProcess = processes.find(p => p.triggerId === selectedTrigger); + if (currentProcess) { + const triggerId = currentProcess.triggerId; + + const startPoint = armBot.actions.processes.find((process) => process.triggerId === triggerId)?.startPoint; + + const matchedMachine = simulationStates.find((state) => { + if (state.type === "Conveyor") { + return (state).points.some( + (point) => point.uuid === startPoint + ); + } else if (state.type === "StaticMachine") { + return state.points.uuid === startPoint; + } + return false; + }); + + if (matchedMachine) { + if (matchedMachine.type === "Conveyor") { + logStatus(`[Arm ${uuid}] start point which is a conveyor (${matchedMachine.modelName})`); + } else { + logStatus(`[Arm ${uuid}] started form start point which is a static machine (${matchedMachine.modelName})`); + } + + setTimeout(() => { + if (matchedMachine.type === "StaticMachine") { + updateArmBotStatus('dropping'); + } + + if (matchedMachine.type === "Conveyor") { + updateArmBotStatus('picking'); + } + }, 0); + } + } + } + + const updateConveyorOrStaticMachineStatusOnEnd = (selectedTrigger: string) => { const currentProcess = processes.find(p => p.triggerId === selectedTrigger); if (currentProcess) { const triggerId = currentProcess.triggerId; @@ -314,7 +363,13 @@ const IKAnimationController = ({ } } - return null; + return ( + <MaterialInstances + statusRef={statusRef} + ikSolver={ikSolver} + targetBoneName={targetBoneName} + /> + ); }; export default IKAnimationController; \ No newline at end of file diff --git a/app/src/modules/simulation/armbot/MaterialInstances.tsx b/app/src/modules/simulation/armbot/MaterialInstances.tsx index 778ae30..c83cd51 100644 --- a/app/src/modules/simulation/armbot/MaterialInstances.tsx +++ b/app/src/modules/simulation/armbot/MaterialInstances.tsx @@ -3,37 +3,26 @@ import * as THREE from 'three'; import { Box } from '@react-three/drei'; type MaterialInstancesProps = { - groupRef: React.RefObject<THREE.Group>; - activeCurve: any; - progress: number; + statusRef: React.RefObject<string>; ikSolver: any; targetBoneName: string; }; function MaterialInstances({ - groupRef, - activeCurve, - progress, + statusRef, ikSolver, targetBoneName }: MaterialInstancesProps) { - const { endToRestRange, startToEndRange } = activeCurve; + if (!ikSolver) return null; - // Show the box from when we reach the start point until we leave the end point - const shouldShow = (progress >= startToEndRange[0] && progress < startToEndRange[1] && progress >= endToRestRange[0]); - - if (!shouldShow || !ikSolver) return null; - - const targetBone = ikSolver.mesh.skeleton.bones.find( - (b: any) => b.name === targetBoneName - ); + const targetBone = ikSolver.mesh.skeleton.bones.find((b: any) => b.name === targetBoneName); if (!targetBone) return null; const worldPos = new THREE.Vector3(); targetBone.getWorldPosition(worldPos); return ( - <Box args={[0.5, 0.5, 0.5]} position={worldPos}> + <Box args={[0.5, 0.5, 0.5]} position={worldPos} visible={statusRef.current === 'processing'}> <meshStandardMaterial color="orange" /> </Box> ); diff --git a/app/src/modules/simulation/path/pathConnector.tsx b/app/src/modules/simulation/path/pathConnector.tsx index 3e35925..3db399e 100644 --- a/app/src/modules/simulation/path/pathConnector.tsx +++ b/app/src/modules/simulation/path/pathConnector.tsx @@ -40,6 +40,37 @@ function PathConnector({ pathsGroupRef, }: { pathsGroupRef: React.MutableRefObje }; const existingTargets = point.connections.targets || []; + // Check connection limits + const toPath = simulationStates.find(p => p.modeluuid === toModelUUID); + if (toPath) { + // Check if we already have this type of connection + const hasConveyor = existingTargets.some(t => { + const targetPath = simulationStates.find(p => p.modeluuid === t.modelUUID); + return targetPath?.type === "Conveyor"; + }); + const hasArmBot = existingTargets.some(t => { + const targetPath = simulationStates.find(p => p.modeluuid === t.modelUUID); + return targetPath?.type === "ArmBot"; + }); + const hasVehicle = existingTargets.some(t => { + const targetPath = simulationStates.find(p => p.modeluuid === t.modelUUID); + return targetPath?.type === "Vehicle"; + }); + + if (toPath.type === "Conveyor" && hasConveyor) { + console.log("Conveyor can only connect to one other conveyor"); + return point; + } + if (toPath.type === "ArmBot" && hasArmBot) { + console.log("Conveyor can only connect to one ArmBot"); + return point; + } + if (toPath.type === "Vehicle" && hasVehicle) { + console.log("Conveyor can only connect to one Vehicle"); + return point; + } + } + if (!existingTargets.some((target) => target.modelUUID === newTarget.modelUUID && target.pointUUID === newTarget.pointUUID)) { return { ...point, @@ -66,6 +97,36 @@ function PathConnector({ pathsGroupRef, }: { pathsGroupRef: React.MutableRefObje }; const existingTargets = point.connections.targets || []; + // Check connection limits + const fromPath = simulationStates.find(p => p.modeluuid === fromModelUUID); + if (fromPath) { + const hasConveyor = existingTargets.some(t => { + const targetPath = simulationStates.find(p => p.modeluuid === t.modelUUID); + return targetPath?.type === "Conveyor"; + }); + const hasArmBot = existingTargets.some(t => { + const targetPath = simulationStates.find(p => p.modeluuid === t.modelUUID); + return targetPath?.type === "ArmBot"; + }); + const hasVehicle = existingTargets.some(t => { + const targetPath = simulationStates.find(p => p.modeluuid === t.modelUUID); + return targetPath?.type === "Vehicle"; + }); + + if (fromPath.type === "Conveyor" && hasConveyor) { + console.log("Conveyor can only connect to one other conveyor"); + return point; + } + if (fromPath.type === "ArmBot" && hasArmBot) { + console.log("Conveyor can only connect to one ArmBot"); + return point; + } + if (fromPath.type === "Vehicle" && hasVehicle) { + console.log("Conveyor can only connect to one Vehicle"); + return point; + } + } + if (!existingTargets.some((target) => target.modelUUID === reverseTarget.modelUUID && target.pointUUID === reverseTarget.pointUUID)) { return { ...point, @@ -494,21 +555,59 @@ function PathConnector({ pathsGroupRef, }: { pathsGroupRef: React.MutableRefObje } } - // For non-Vehicle paths, check if already connected - if (intersected.userData.path.type !== "Vehicle") { - const isAlreadyConnected = simulationStates.some((path) => { - if (path.type === "Conveyor") { - return path.points.some( - (point) => - point.uuid === sphereUUID && - point.connections.targets.length > 0 - ); + // For Conveyors, check connection limits in BOTH DIRECTIONS + if (firstSelected && (firstPath?.type === "Conveyor" || secondPath?.type === "Conveyor")) { + const checkConveyorLimits = (path: any, pointUUID: string) => { + if (path?.type === "Conveyor") { + const point = path.points.find((p: { uuid: string }) => p.uuid === pointUUID); + if (point) { + return { + hasConveyor: point.connections.targets.some((t: { modelUUID: string }) => { + const targetPath = simulationStates.find((p: { modeluuid: string }) => p.modeluuid === t.modelUUID); + return targetPath?.type === "Conveyor"; + }), + hasArmBot: point.connections.targets.some((t: { modelUUID: string }) => { + const targetPath = simulationStates.find((p: { modeluuid: string }) => p.modeluuid === t.modelUUID); + return targetPath?.type === "ArmBot"; + }), + hasVehicle: point.connections.targets.some((t: { modelUUID: string }) => { + const targetPath = simulationStates.find(p => p.modeluuid === t.modelUUID); + return targetPath?.type === "Vehicle"; + }) + }; + } } - return false; - }); + return { hasConveyor: false, hasArmBot: false, hasVehicle: false }; + }; - if (isAlreadyConnected) { - console.log("Conveyor point is already connected. Ignoring."); + const firstConveyorLimits = checkConveyorLimits(firstPath, firstSelected?.sphereUUID); + const secondConveyorLimits = checkConveyorLimits(secondPath, sphereUUID); + + // Check if trying to connect two conveyors + if (firstPath?.type === "Conveyor" && secondPath?.type === "Conveyor") { + if (firstConveyorLimits.hasConveyor || secondConveyorLimits.hasConveyor) { + console.log("Conveyor can only connect to one other conveyor"); + return; + } + } + + // Check if trying to connect to an ArmBot when already connected to one + if (secondPath?.type === "ArmBot" && firstConveyorLimits.hasArmBot) { + console.log("Conveyor can only connect to one ArmBot"); + return; + } + if (firstPath?.type === "ArmBot" && secondConveyorLimits.hasArmBot) { + console.log("Conveyor can only connect to one ArmBot"); + return; + } + + // Check if trying to connect to a Vehicle when already connected to one + if (secondPath?.type === "Vehicle" && firstConveyorLimits.hasVehicle) { + console.log("Conveyor can only connect to one Vehicle"); + return; + } + if (firstPath?.type === "Vehicle" && secondConveyorLimits.hasVehicle) { + console.log("Conveyor can only connect to one Vehicle"); return; } } @@ -762,6 +861,45 @@ function PathConnector({ pathsGroupRef, }: { pathsGroupRef: React.MutableRefObje !(firstPath?.type === 'Conveyor' || firstPath?.type === 'StaticMachine' || secondPath?.type === 'Conveyor' || secondPath?.type === 'StaticMachine'); + // NEW: Check conveyor connection limits + let isConveyorAtMaxConnections = false; + if (firstPath?.type === 'Conveyor' || secondPath?.type === 'Conveyor') { + const conveyorPath = firstPath?.type === 'Conveyor' ? firstPath : secondPath; + const otherPath = firstPath?.type === 'Conveyor' ? secondPath : firstPath; + + if (conveyorPath) { + const conveyorPoint = Array.isArray(conveyorPath.points) + ? conveyorPath.points.find((p: { uuid: string }) => p.uuid === + (firstPath?.type === 'Conveyor' ? firstSelected.sphereUUID : sphereUUID)) + : undefined; + + if (conveyorPoint) { + const hasConveyor = conveyorPoint.connections.targets.some((t: { modelUUID: string }) => { + const targetPath = simulationStates.find((p: { modeluuid: string }) => p.modeluuid === t.modelUUID); + return targetPath?.type === 'Conveyor'; + }); + const hasArmBot = conveyorPoint.connections.targets.some((t: { modelUUID: string }) => { + const targetPath = simulationStates.find((p: { modeluuid: string }) => p.modeluuid === t.modelUUID); + return targetPath?.type === 'ArmBot'; + }); + const hasVehicle = conveyorPoint.connections.targets.some((t: { modelUUID: string }) => { + const targetPath = simulationStates.find(p => p.modeluuid === t.modelUUID); + return targetPath?.type === 'Vehicle'; + }); + + if (otherPath?.type === 'Conveyor' && hasConveyor) { + isConveyorAtMaxConnections = true; + } + if (otherPath?.type === 'ArmBot' && hasArmBot) { + isConveyorAtMaxConnections = true; + } + if (otherPath?.type === 'Vehicle' && hasVehicle) { + isConveyorAtMaxConnections = true; + } + } + } + } + if ( !isDuplicateConnection && !isVehicleToVehicle && @@ -773,6 +911,7 @@ function PathConnector({ pathsGroupRef, }: { pathsGroupRef: React.MutableRefObje !isArmBotToArmBot && !isArmBotToInvalidType && !isArmBotAlreadyConnectedToStatic && + !isConveyorAtMaxConnections && // NEW: Check conveyor limits firstSelected.sphereUUID !== sphereUUID && firstSelected.modelUUID !== modelUUID && (firstSelected.isCorner || isConnectable) && @@ -957,7 +1096,6 @@ function PathConnector({ pathsGroupRef, }: { pathsGroupRef: React.MutableRefObje state.modeluuid === connection2.model ); - console.log("updatedPaths: ", updatedPaths); updateBackend(updatedPaths); setSimulationStates(updatedStates); diff --git a/app/src/modules/simulation/process/processAnimator.tsx b/app/src/modules/simulation/process/processAnimator.tsx index 981846b..ddf21be 100644 --- a/app/src/modules/simulation/process/processAnimator.tsx +++ b/app/src/modules/simulation/process/processAnimator.tsx @@ -564,9 +564,9 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ // Log if no animation is occurring when it should if (!animationOccurring && !isConnected) { - console.log( - `Warning: No animation occurring for process ${process.id} despite not being connected` - ); + // console.log( + // `Warning: No animation occurring for process ${process.id} despite not being connected` + // ); } newStates[process.id] = { From 16cf1b96ccd3a5b0c64cc788586ad730749ff69c Mon Sep 17 00:00:00 2001 From: gabriel <gabriel@hexrfactory.com> Date: Wed, 16 Apr 2025 18:04:29 +0530 Subject: [PATCH 18/20] bug fix for data selection tab --- .../IotInputCards/BarChartInput.tsx | 5 +- .../FleetEfficiencyInputComponent.tsx | 7 +- .../IotInputCards/FlotingWidgetInput.tsx | 3 +- .../IotInputCards/LineGrapInput.tsx | 3 +- .../IotInputCards/PieChartInput.tsx | 3 +- .../IotInputCards/Progress1Input.tsx | 3 +- .../IotInputCards/Progress2Input.tsx | 3 +- .../WarehouseThroughputInputComponent.tsx | 3 +- .../IotInputCards/Widget2InputCard3D.tsx | 1 + .../IotInputCards/Widget3InputCard3D.tsx | 1 + .../IotInputCards/Widget4InputCard3D.tsx | 1 + .../ui/inputs/MultiLevelDropDown.tsx | 218 ++++-------------- .../floating/DroppedFloatingWidgets.tsx | 2 +- 13 files changed, 74 insertions(+), 179 deletions(-) diff --git a/app/src/components/layout/sidebarRight/visualization/IotInputCards/BarChartInput.tsx b/app/src/components/layout/sidebarRight/visualization/IotInputCards/BarChartInput.tsx index 8e2d4f1..9fb7b78 100644 --- a/app/src/components/layout/sidebarRight/visualization/IotInputCards/BarChartInput.tsx +++ b/app/src/components/layout/sidebarRight/visualization/IotInputCards/BarChartInput.tsx @@ -62,7 +62,7 @@ const BarChartInput = (props: Props) => { fetchSavedInputes(); - }, [selectedChartId.id]); + }, [selectedChartId]); // Sync Zustand state when component mounts useEffect(() => { @@ -138,7 +138,7 @@ const BarChartInput = (props: Props) => { <div className="inputs-wrapper"> <div className="datas"> <div className="datas__label">Title</div> - <RenameInput value={selectedChartId?.title || "untited"} onRename={handleNameChange}/> + <RenameInput value={widgetName || selectedChartId?.title} onRename={handleNameChange}/> </div> {[...Array(3)].map((_, index) => { const inputKey = `input${index + 1}`; @@ -152,6 +152,7 @@ const BarChartInput = (props: Props) => { onUnselect={() => handleSelect(inputKey, null)} selectedValue={selections[inputKey]} // Load from Zustand isLoading={isLoading} + allSelections={selections} /> <div className="icon"> <AddIcon /> diff --git a/app/src/components/layout/sidebarRight/visualization/IotInputCards/FleetEfficiencyInputComponent.tsx b/app/src/components/layout/sidebarRight/visualization/IotInputCards/FleetEfficiencyInputComponent.tsx index af6f735..78b82c8 100644 --- a/app/src/components/layout/sidebarRight/visualization/IotInputCards/FleetEfficiencyInputComponent.tsx +++ b/app/src/components/layout/sidebarRight/visualization/IotInputCards/FleetEfficiencyInputComponent.tsx @@ -23,6 +23,10 @@ const FleetEfficiencyInputComponent = (props: Props) => { const organization = email?.split("@")[1]?.split(".")[0] const [isLoading, setLoading] = useState<boolean>(true); + const isSelected = () => { + + } + useEffect(() => { const fetchZoneData = async () => { try { @@ -139,7 +143,7 @@ const FleetEfficiencyInputComponent = (props: Props) => { <div className="inputs-wrapper"> <div className="datas"> <div className="datas__label">Title</div> - <RenameInput value={selectedChartId?.header || "untited"} onRename={handleNameChange}/> + <RenameInput value={widgetName || selectedChartId?.header} onRename={handleNameChange}/> </div> {[...Array(1)].map((_, index) => { const inputKey = `input${index + 1}`; @@ -153,6 +157,7 @@ const FleetEfficiencyInputComponent = (props: Props) => { onUnselect={() => handleSelect(inputKey, null)} selectedValue={selections[inputKey]} // Load from Zustand isLoading={isLoading} + allSelections={selections} /> <div className="icon"> <AddIcon /> diff --git a/app/src/components/layout/sidebarRight/visualization/IotInputCards/FlotingWidgetInput.tsx b/app/src/components/layout/sidebarRight/visualization/IotInputCards/FlotingWidgetInput.tsx index 53ddbcc..271b4fc 100644 --- a/app/src/components/layout/sidebarRight/visualization/IotInputCards/FlotingWidgetInput.tsx +++ b/app/src/components/layout/sidebarRight/visualization/IotInputCards/FlotingWidgetInput.tsx @@ -139,7 +139,7 @@ const FlotingWidgetInput = (props: Props) => { <div className="inputs-wrapper"> <div className="datas"> <div className="datas__label">Title</div> - <RenameInput value={selectedChartId?.header || "untited"} onRename={handleNameChange}/> + <RenameInput value={widgetName || selectedChartId?.header} onRename={handleNameChange}/> </div> {[...Array(6)].map((_, index) => { const inputKey = `input${index + 1}`; @@ -153,6 +153,7 @@ const FlotingWidgetInput = (props: Props) => { onUnselect={() => handleSelect(inputKey, null)} selectedValue={selections[inputKey]} // Load from Zustand isLoading={isLoading} + allSelections={selections} /> <div className="icon"> <AddIcon /> diff --git a/app/src/components/layout/sidebarRight/visualization/IotInputCards/LineGrapInput.tsx b/app/src/components/layout/sidebarRight/visualization/IotInputCards/LineGrapInput.tsx index 3cf647e..50330d1 100644 --- a/app/src/components/layout/sidebarRight/visualization/IotInputCards/LineGrapInput.tsx +++ b/app/src/components/layout/sidebarRight/visualization/IotInputCards/LineGrapInput.tsx @@ -257,7 +257,7 @@ const LineGrapInput = (props: Props) => { <div className="inputs-wrapper"> <div className="datas"> <div className="datas__label">Title</div> - <RenameInput value={selectedChartId?.title || "untited"} onRename={handleNameChange}/> + <RenameInput value={widgetName || selectedChartId?.title} onRename={handleNameChange}/> </div> {[...Array(4)].map((_, index) => { const inputKey = `input${index + 1}`; @@ -271,6 +271,7 @@ const LineGrapInput = (props: Props) => { onUnselect={() => handleSelect(inputKey, null)} selectedValue={selections[inputKey]} // Load from Zustand isLoading={isLoading} + allSelections={selections} /> <div className="icon"> <AddIcon /> diff --git a/app/src/components/layout/sidebarRight/visualization/IotInputCards/PieChartInput.tsx b/app/src/components/layout/sidebarRight/visualization/IotInputCards/PieChartInput.tsx index 1d16358..e7486f2 100644 --- a/app/src/components/layout/sidebarRight/visualization/IotInputCards/PieChartInput.tsx +++ b/app/src/components/layout/sidebarRight/visualization/IotInputCards/PieChartInput.tsx @@ -138,7 +138,7 @@ const PieChartInput = (props: Props) => { <div className="inputs-wrapper"> <div className="datas"> <div className="datas__label">Title</div> - <RenameInput value={selectedChartId?.title || "untited"} onRename={handleNameChange}/> + <RenameInput value={widgetName || selectedChartId?.title} onRename={handleNameChange}/> </div> {[...Array(2)].map((_, index) => { const inputKey = `input${index + 1}`; @@ -152,6 +152,7 @@ const PieChartInput = (props: Props) => { onUnselect={() => handleSelect(inputKey, null)} selectedValue={selections[inputKey]} // Load from Zustand isLoading={isLoading} + allSelections={selections} /> <div className="icon"> <AddIcon /> diff --git a/app/src/components/layout/sidebarRight/visualization/IotInputCards/Progress1Input.tsx b/app/src/components/layout/sidebarRight/visualization/IotInputCards/Progress1Input.tsx index 5d9dd58..797f63a 100644 --- a/app/src/components/layout/sidebarRight/visualization/IotInputCards/Progress1Input.tsx +++ b/app/src/components/layout/sidebarRight/visualization/IotInputCards/Progress1Input.tsx @@ -132,7 +132,7 @@ const Progress1Input = (props: Props) => { <div className="inputs-wrapper"> <div className="datas"> <div className="datas__label">Title</div> - <RenameInput value={selectedChartId?.title || "untited"} onRename={handleNameChange}/> + <RenameInput value={widgetName || selectedChartId?.title} onRename={handleNameChange}/> </div> {[...Array(1)].map((_, index) => { const inputKey = `input${index + 1}`; @@ -146,6 +146,7 @@ const Progress1Input = (props: Props) => { onUnselect={() => handleSelect(inputKey, null)} selectedValue={selections[inputKey]} // Load from Zustand isLoading={isLoading} + allSelections={selections} /> <div className="icon"> <AddIcon /> diff --git a/app/src/components/layout/sidebarRight/visualization/IotInputCards/Progress2Input.tsx b/app/src/components/layout/sidebarRight/visualization/IotInputCards/Progress2Input.tsx index bc6059c..a7d2934 100644 --- a/app/src/components/layout/sidebarRight/visualization/IotInputCards/Progress2Input.tsx +++ b/app/src/components/layout/sidebarRight/visualization/IotInputCards/Progress2Input.tsx @@ -132,7 +132,7 @@ const Progress2Input = (props: Props) => { <div className="inputs-wrapper"> <div className="datas"> <div className="datas__label">Title</div> - <RenameInput value={selectedChartId?.title || "untited"} onRename={handleNameChange}/> + <RenameInput value={widgetName || selectedChartId?.title} onRename={handleNameChange}/> </div> {[...Array(2)].map((_, index) => { const inputKey = `input${index + 1}`; @@ -146,6 +146,7 @@ const Progress2Input = (props: Props) => { onUnselect={() => handleSelect(inputKey, null)} selectedValue={selections[inputKey]} // Load from Zustand isLoading={isLoading} + allSelections={selections} /> <div className="icon"> <AddIcon /> diff --git a/app/src/components/layout/sidebarRight/visualization/IotInputCards/WarehouseThroughputInputComponent.tsx b/app/src/components/layout/sidebarRight/visualization/IotInputCards/WarehouseThroughputInputComponent.tsx index 8d5e717..764be54 100644 --- a/app/src/components/layout/sidebarRight/visualization/IotInputCards/WarehouseThroughputInputComponent.tsx +++ b/app/src/components/layout/sidebarRight/visualization/IotInputCards/WarehouseThroughputInputComponent.tsx @@ -139,7 +139,7 @@ const WarehouseThroughputInputComponent = (props: Props) => { <div className="inputs-wrapper"> <div className="datas"> <div className="datas__label">Title</div> - <RenameInput value={selectedChartId?.header || "untited"} onRename={handleNameChange}/> + <RenameInput value={widgetName || selectedChartId?.header} onRename={handleNameChange}/> </div> {[...Array(1)].map((_, index) => { const inputKey = `input${index + 1}`; @@ -153,6 +153,7 @@ const WarehouseThroughputInputComponent = (props: Props) => { onUnselect={() => handleSelect(inputKey, null)} selectedValue={selections[inputKey]} // Load from Zustand isLoading={isLoading} + allSelections={selections} /> <div className="icon"> <AddIcon /> diff --git a/app/src/components/layout/sidebarRight/visualization/IotInputCards/Widget2InputCard3D.tsx b/app/src/components/layout/sidebarRight/visualization/IotInputCards/Widget2InputCard3D.tsx index a1b4360..345de09 100644 --- a/app/src/components/layout/sidebarRight/visualization/IotInputCards/Widget2InputCard3D.tsx +++ b/app/src/components/layout/sidebarRight/visualization/IotInputCards/Widget2InputCard3D.tsx @@ -151,6 +151,7 @@ const Widget2InputCard3D = (props: Props) => { onUnselect={() => handleSelect(inputKey, null)} selectedValue={selections[inputKey]} // Load from Zustand isLoading={isLoading} + allSelections={selections} /> <div className="icon"> <AddIcon /> diff --git a/app/src/components/layout/sidebarRight/visualization/IotInputCards/Widget3InputCard3D.tsx b/app/src/components/layout/sidebarRight/visualization/IotInputCards/Widget3InputCard3D.tsx index 43f8e55..c41447a 100644 --- a/app/src/components/layout/sidebarRight/visualization/IotInputCards/Widget3InputCard3D.tsx +++ b/app/src/components/layout/sidebarRight/visualization/IotInputCards/Widget3InputCard3D.tsx @@ -144,6 +144,7 @@ const Widget3InputCard3D = () => { onUnselect={() => handleSelect(inputKey, null)} selectedValue={selections[inputKey]} // Load from Zustand isLoading={isLoading} + allSelections={selections} /> <div className="icon"> <AddIcon /> diff --git a/app/src/components/layout/sidebarRight/visualization/IotInputCards/Widget4InputCard3D.tsx b/app/src/components/layout/sidebarRight/visualization/IotInputCards/Widget4InputCard3D.tsx index 4aa9855..20369bd 100644 --- a/app/src/components/layout/sidebarRight/visualization/IotInputCards/Widget4InputCard3D.tsx +++ b/app/src/components/layout/sidebarRight/visualization/IotInputCards/Widget4InputCard3D.tsx @@ -151,6 +151,7 @@ const Widget4InputCard3D = (props: Props) => { onUnselect={() => handleSelect(inputKey, null)} selectedValue={selections[inputKey]} // Load from Zustand isLoading={isLoading} + allSelections={selections} /> <div className="icon"> <AddIcon /> diff --git a/app/src/components/ui/inputs/MultiLevelDropDown.tsx b/app/src/components/ui/inputs/MultiLevelDropDown.tsx index 1293e4a..fa30a55 100644 --- a/app/src/components/ui/inputs/MultiLevelDropDown.tsx +++ b/app/src/components/ui/inputs/MultiLevelDropDown.tsx @@ -1,145 +1,3 @@ -// import React, { useState, useRef, useEffect } from "react"; - -// // Dropdown Item Component -// const DropdownItem = ({ -// label, -// href, -// onClick, -// }: { -// label: string; -// href?: string; -// onClick?: () => void; -// }) => ( -// <a -// href={href || "#"} -// className="dropdown-item" -// onClick={(e) => { -// e.preventDefault(); -// onClick?.(); -// }} -// > -// {label} -// </a> -// ); - -// // Nested Dropdown Component -// const NestedDropdown = ({ -// label, -// children, -// onSelect, -// }: { -// label: string; -// children: React.ReactNode; -// onSelect: (selectedLabel: string) => void; -// }) => { -// const [open, setOpen] = useState(false); - -// return ( -// <div className="nested-dropdown"> -// {/* Dropdown Trigger */} -// <div -// className={`dropdown-trigger ${open ? "open" : ""}`} -// onClick={() => setOpen(!open)} // Toggle submenu on click -// > -// {label} <span className="icon">{open ? "▼" : "▶"}</span> -// </div> - -// {/* Submenu */} -// {open && ( -// <div className="submenu"> -// {React.Children.map(children, (child) => { -// if (React.isValidElement(child)) { -// // Clone the element and pass the `onSelect` prop only if it's expected -// return React.cloneElement(child as React.ReactElement<any>, { onSelect }); -// } -// return child; // Return non-element children as-is -// })} -// </div> -// )} -// </div> -// ); -// }; - -// // Recursive Function to Render Nested Data -// const renderNestedData = ( -// data: Record<string, any>, -// onSelect: (selectedLabel: string) => void -// ) => { -// return Object.entries(data).map(([key, value]) => { -// if (typeof value === "object" && !Array.isArray(value)) { -// // If the value is an object, render it as a nested dropdown -// return ( -// <NestedDropdown key={key} label={key} onSelect={onSelect}> -// {renderNestedData(value, onSelect)} -// </NestedDropdown> -// ); -// } else if (Array.isArray(value)) { -// // If the value is an array, render each item as a dropdown item -// return value.map((item, index) => ( -// <DropdownItem key={index} label={item} onClick={() => onSelect(item)} /> -// )); -// } else { -// // If the value is a simple string, render it as a dropdown item -// return ( -// <DropdownItem key={key} label={value} onClick={() => onSelect(value)} /> -// ); -// } -// }); -// }; - -// // Main Multi-Level Dropdown Component -// const MultiLevelDropdown = ({ data }: { data: Record<string, any> }) => { -// const [open, setOpen] = useState(false); -// const [selectedLabel, setSelectedLabel] = useState("Dropdown trigger"); -// const dropdownRef = useRef<HTMLDivElement>(null); - -// // Handle outer click to close the dropdown -// useEffect(() => { -// const handleClickOutside = (event: MouseEvent) => { -// if ( -// dropdownRef.current && -// !dropdownRef.current.contains(event.target as Node) -// ) { -// setOpen(false); -// } -// }; - -// document.addEventListener("mousedown", handleClickOutside); -// return () => { -// document.removeEventListener("mousedown", handleClickOutside); -// }; -// }, []); - -// // Handle selection of an item -// const handleSelect = (selectedLabel: string) => { -// setSelectedLabel(selectedLabel); // Update the dropdown trigger text -// setOpen(false); // Close the dropdown -// }; - -// return ( -// <div className="multi-level-dropdown" ref={dropdownRef}> -// {/* Dropdown Trigger Button */} -// <button -// className={`dropdown-button ${open ? "open" : ""}`} -// onClick={() => setOpen(!open)} // Toggle main menu on click -// > -// {selectedLabel} <span className="icon">▾</span> -// </button> - -// {/* Dropdown Menu */} -// {open && ( -// <div className="dropdown-menu"> -// <div className="dropdown-content"> -// {renderNestedData(data, handleSelect)} -// </div> -// </div> -// )} -// </div> -// ); -// }; - -// export default MultiLevelDropdown; - import React, { useState, useRef, useEffect } from "react"; import { ArrowIcon } from "../../icons/ExportCommonIcons"; @@ -147,11 +5,19 @@ import { ArrowIcon } from "../../icons/ExportCommonIcons"; const DropdownItem = ({ label, onClick, + disabled = false, }: { label: string; onClick: () => void; + disabled?: boolean; }) => ( - <div className="dropdown-item" onClick={onClick}> + <div + className={`dropdown-item ${disabled ? "disabled" : ""}`} + onClick={() => { + if (!disabled) onClick(); + }} + style={{ cursor: disabled ? "not-allowed": "default", opacity: disabled ? 0.5 : 1 }} + > {label} </div> ); @@ -161,10 +27,12 @@ const NestedDropdown = ({ label, fields, onSelect, + disabledFields = [], }: { label: string; fields: string[]; onSelect: (selectedData: { name: string; fields: string }) => void; + disabledFields?: string[]; }) => { const [open, setOpen] = useState(false); @@ -184,13 +52,17 @@ const NestedDropdown = ({ </div> {open && ( <div className="submenu"> - {fields.map((field) => ( - <DropdownItem - key={field} - label={field} - onClick={() => onSelect({ name: label, fields: field })} - /> - ))} + {fields.map((field) => { + const isDisabled = disabledFields.includes(`${label}-${field}`); + return ( + <DropdownItem + key={field} + label={field} + onClick={() => onSelect({ name: label, fields: field })} + disabled={isDisabled} + /> + ); + })} </div> )} </div> @@ -203,6 +75,7 @@ interface MultiLevelDropdownProps { onSelect: (selectedData: { name: string; fields: string }) => void; onUnselect: () => void; selectedValue?: { name: string; fields: string }; + allSelections?: Record<string, { name: string; fields: string }>; isLoading?: boolean; } @@ -212,6 +85,7 @@ const MultiLevelDropdown = ({ onSelect, onUnselect, selectedValue, + allSelections = {}, isLoading = false, }: MultiLevelDropdownProps) => { const [open, setOpen] = useState(false); @@ -249,6 +123,14 @@ const MultiLevelDropdown = ({ ? `${selectedValue.name} - ${selectedValue.fields}` : "Dropdown trigger"; + // Build list of disabled selections + const disabledFieldsList = Object.values(allSelections) + .filter( + (sel) => + !(sel.name === selectedValue?.name && sel.fields === selectedValue?.fields) + ) + .map((sel) => `${sel.name}-${sel.fields}`); + return ( <div className="multi-level-dropdown" ref={dropdownRef}> <button @@ -260,25 +142,23 @@ const MultiLevelDropdown = ({ </button> {open && ( <div className="dropdown-menu"> - <div className="dropdown-content "> - - {/* loading list */} - - - {/* Unselect Option */} - <DropdownItem label="Unselect" onClick={handleItemUnselect} /> - {/* Nested Dropdown Items */} - { - isLoading ? <div className="loading" /> : - Object.entries(data).map(([key, value]) => ( - <NestedDropdown - key={key} - label={key} - fields={Object.keys(value)} - onSelect={handleItemSelect} - /> - )) - } + <div className="dropdown-content"> + {isLoading ? ( + <div className="loading" /> + ) : ( + <> + <DropdownItem label="Unselect" onClick={handleItemUnselect} /> + {Object.entries(data).map(([key, value]) => ( + <NestedDropdown + key={key} + label={key} + fields={Object.keys(value)} + onSelect={handleItemSelect} + disabledFields={disabledFieldsList} + /> + ))} + </> + )} </div> </div> )} diff --git a/app/src/modules/visualization/widgets/floating/DroppedFloatingWidgets.tsx b/app/src/modules/visualization/widgets/floating/DroppedFloatingWidgets.tsx index 62c1de7..243d2ef 100644 --- a/app/src/modules/visualization/widgets/floating/DroppedFloatingWidgets.tsx +++ b/app/src/modules/visualization/widgets/floating/DroppedFloatingWidgets.tsx @@ -558,7 +558,7 @@ const DroppedObjects: React.FC = () => { return ( <div - key={`${zoneName}-${index}`} + key={obj.id} className={`${obj.className} ${ selectedChartId?.id === obj.id && "activeChart" } `} From 64885f246e9cd130f31409da26372aa35352f941 Mon Sep 17 00:00:00 2001 From: Jerald-Golden-B <jerald@hexrfactory.com> Date: Wed, 16 Apr 2025 18:05:23 +0530 Subject: [PATCH 19/20] refactor: Improve error handling and variable naming in Assets and IKAnimationController components --- .../components/layout/sidebarLeft/Assets.tsx | 5 +++-- .../armbot/IKAnimationController.tsx | 20 +++++++++++-------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/app/src/components/layout/sidebarLeft/Assets.tsx b/app/src/components/layout/sidebarLeft/Assets.tsx index 1cdd9bd..5477790 100644 --- a/app/src/components/layout/sidebarLeft/Assets.tsx +++ b/app/src/components/layout/sidebarLeft/Assets.tsx @@ -73,7 +73,7 @@ const Assets: React.FC = () => { try { const filt = await fetchAssets(); setFiltereredAssets(filt); - } catch {} + } catch { } }; filteredAssets(); }, [categoryAssets]); @@ -135,7 +135,7 @@ const Assets: React.FC = () => { const res = await getCategoryAsset(asset); setCategoryAssets(res); setFiltereredAssets(res); - } catch (error) {} + } catch (error) { } } }; return ( @@ -234,6 +234,7 @@ const Assets: React.FC = () => { src={categoryInfo?.categoryImage || ""} alt={category} className="category-image" + draggable={false} /> <div className="category-name">{category}</div> </div> diff --git a/app/src/modules/simulation/armbot/IKAnimationController.tsx b/app/src/modules/simulation/armbot/IKAnimationController.tsx index 6b77bac..a58e2d8 100644 --- a/app/src/modules/simulation/armbot/IKAnimationController.tsx +++ b/app/src/modules/simulation/armbot/IKAnimationController.tsx @@ -4,6 +4,8 @@ import * as THREE from "three"; import { usePlayButtonStore, useResetButtonStore } from "../../../store/usePlayButtonStore"; import { useSimulationStates } from "../../../store/store"; import MaterialInstances from "./MaterialInstances"; +import { Line } from "react-chartjs-2"; +import { QuadraticBezierLine } from "@react-three/drei"; interface StaticMachineState { @@ -124,7 +126,7 @@ const IKAnimationController = ({ ]); }; - const processCurves = useMemo(() => { + const processedCurves = useMemo(() => { if (!isPlaying) return []; return processes.map(process => { @@ -172,8 +174,8 @@ const IKAnimationController = ({ const activeProcess = useMemo(() => { if (!selectedTrigger) return null; - return processCurves.find(p => p?.triggerId === selectedTrigger); - }, [processCurves, selectedTrigger]); + return processedCurves.find(p => p?.triggerId === selectedTrigger); + }, [processedCurves, selectedTrigger]); // Initial movement to rest position useFrame((_, delta) => { @@ -364,11 +366,13 @@ const IKAnimationController = ({ } return ( - <MaterialInstances - statusRef={statusRef} - ikSolver={ikSolver} - targetBoneName={targetBoneName} - /> + <> + <MaterialInstances + statusRef={statusRef} + ikSolver={ikSolver} + targetBoneName={targetBoneName} + /> + </> ); }; From 8e491a0002f6e8b3135c06681e64c324cd81f364 Mon Sep 17 00:00:00 2001 From: Jerald-Golden-B <jerald@hexrfactory.com> Date: Wed, 16 Apr 2025 18:30:43 +0530 Subject: [PATCH 20/20] feat: Add isActive property to ArmBot state and update IKAnimationController logic refactor: Remove commented console logs in ProcessAnimator and useProcessAnimations --- app/src/modules/simulation/armbot/ArmBot.tsx | 3 +- .../armbot/IKAnimationController.tsx | 2 +- .../simulation/process/processAnimator.tsx | 62 +++++++++---------- .../process/useProcessAnimations.tsx | 4 +- 4 files changed, 36 insertions(+), 35 deletions(-) diff --git a/app/src/modules/simulation/armbot/ArmBot.tsx b/app/src/modules/simulation/armbot/ArmBot.tsx index 5d55791..066779a 100644 --- a/app/src/modules/simulation/armbot/ArmBot.tsx +++ b/app/src/modules/simulation/armbot/ArmBot.tsx @@ -53,7 +53,8 @@ const ArmBot = ({ armBots, setArmBots, setStaticMachines }: ArmBotProps) => { material: "default", triggerId: '', actions: bot.points.actions, - connections: bot.points.connections + connections: bot.points.connections, + isActive: false })); setArmBots(initialStates); }, [simulationStates, isReset]); diff --git a/app/src/modules/simulation/armbot/IKAnimationController.tsx b/app/src/modules/simulation/armbot/IKAnimationController.tsx index a58e2d8..e2df7f5 100644 --- a/app/src/modules/simulation/armbot/IKAnimationController.tsx +++ b/app/src/modules/simulation/armbot/IKAnimationController.tsx @@ -347,7 +347,7 @@ const IKAnimationController = ({ if (matchedMachine.type === "Conveyor") { setArmBots((prev) => prev.map((arm) => { - if (arm.uuid === uuid) { + if (arm.uuid === uuid && arm.isActive === true) { return { ...arm, isActive: false, diff --git a/app/src/modules/simulation/process/processAnimator.tsx b/app/src/modules/simulation/process/processAnimator.tsx index 04ddf90..2cd574f 100644 --- a/app/src/modules/simulation/process/processAnimator.tsx +++ b/app/src/modules/simulation/process/processAnimator.tsx @@ -169,9 +169,9 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ const spawnPoint = findSpawnPoint(process); if (!spawnPoint || !spawnPoint.actions) { - console.log( - `Process ${process.id} has no valid spawn point or actions` - ); + // console.log( + // `Process ${process.id} has no valid spawn point or actions` + // ); return; } @@ -255,10 +255,10 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ // Check connection status with debugging const isConnected = isConnectedToActiveArmBot(process.id); - console.log( - `Process ${process.id} animation - connected:`, - isConnected - ); + // console.log( + // `Process ${process.id} animation - connected:`, + // isConnected + // ); if (isConnected) { // Stop all animations when connected to active arm bot @@ -293,9 +293,9 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ currentTime - processState.processDelayStartTime >= effectiveDelayTime ) { - console.log( - `Process ${process.id} delay completed, resuming animation` - ); + // console.log( + // `Process ${process.id} delay completed, resuming animation` + // ); newStates[process.id] = { ...processState, isProcessDelaying: false, @@ -331,9 +331,9 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ []; if (path.length < 2) { - console.log( - `Process ${process.id} has insufficient path points: ${path.length}` - ); + // console.log( + // `Process ${process.id} has insufficient path points: ${path.length}` + // ); return; } @@ -348,9 +348,9 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ const currentRef = gltf?.scene ? obj.ref.current : obj.ref.current; if (!currentRef) { - console.log( - `No reference for object ${objectId}, skipping animation` - ); + // console.log( + // `No reference for object ${objectId}, skipping animation` + // ); return; } @@ -378,9 +378,9 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ stateRef.currentDelayDuration / speedRef.current; if (currentTime - stateRef.delayStartTime >= effectiveDelayTime) { - console.log( - `Delay complete for object ${objectId}, resuming animation` - ); + // console.log( + // `Delay complete for object ${objectId}, resuming animation` + // ); stateRef.isDelaying = false; stateRef.delayComplete = true; stateRef.isAnimating = true; @@ -408,9 +408,9 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ // Skip non-animating objects if (!stateRef.isAnimating) { - console.log( - `Object ${objectId} not animating, skipping animation updates` - ); + // console.log( + // `Object ${objectId} not animating, skipping animation updates` + // ); return; } @@ -454,9 +454,9 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ if (shouldHide) { if (isAgvPicking) { - console.log( - `AGV picking at last point for object ${objectId}, hiding object` - ); + // console.log( + // `AGV picking at last point for object ${objectId}, hiding object` + // ); updatedObjects[objectId] = { ...obj, visible: false, @@ -517,14 +517,14 @@ const ProcessAnimator: React.FC<ProcessContainerProps> = ({ if (stateRef.delayComplete && stateRef.progress < 0.01) { stateRef.progress = 0.05; stateRef.delayComplete = false; - console.log( - `Boosting progress for object ${objectId} after delay` - ); + // console.log( + // `Boosting progress for object ${objectId} after delay` + // ); } else { stateRef.progress += movement / distance; - console.log( - `Object ${objectId} progress: ${stateRef.progress.toFixed(3)}` - ); + // console.log( + // `Object ${objectId} progress: ${stateRef.progress.toFixed(3)}` + // ); } // Handle point transition diff --git a/app/src/modules/simulation/process/useProcessAnimations.tsx b/app/src/modules/simulation/process/useProcessAnimations.tsx index a46fe3c..579ee3f 100644 --- a/app/src/modules/simulation/process/useProcessAnimations.tsx +++ b/app/src/modules/simulation/process/useProcessAnimations.tsx @@ -514,8 +514,6 @@ export const useProcessAnimation = ( newTriggerCounts[triggerKey] = (newTriggerCounts[triggerKey] || 0) + 1; - shouldLog = true; - newTriggerLogs.push({ timestamp: currentTime, pointId: point.uuid, objectId, triggerId: trigger.uuid, }); const connections = point.connections?.targets || []; @@ -530,6 +528,8 @@ export const useProcessAnimation = ( uuid: connectedModelUUID, triggerId: trigger.uuid, }); + } else { + shouldLog = true; } }); });