import { useFrame, useThree } from '@react-three/fiber'; import React, { useEffect, useState } from 'react'; import * as THREE from 'three'; import * as Types from '../../../types/world/worldTypes'; import { QuadraticBezierLine } from '@react-three/drei'; import { useIsConnecting, useSimulationPaths } from '../../../store/store'; import useModuleStore from '../../../store/useModuleStore'; function PathConnector({ pathsGroupRef }: { pathsGroupRef: React.MutableRefObject }) { const { activeModule } = useModuleStore(); const { gl, raycaster, scene, pointer, camera } = useThree(); const { setIsConnecting } = useIsConnecting(); const { simulationPaths, setSimulationPaths } = useSimulationPaths(); const [firstSelected, setFirstSelected] = useState<{ pathUUID: 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('red'); const updatePathConnections = ( fromPathUUID: string, fromPointUUID: string, toPathUUID: string, toPointUUID: string ) => { const updatedPaths = simulationPaths.map(path => { if (path.type === 'Conveyor') { if (path.modeluuid === fromPathUUID) { return { ...path, points: path.points.map(point => { if (point.uuid === fromPointUUID) { const newTarget = { pathUUID: toPathUUID, pointUUID: toPointUUID }; const existingTargets = point.connections.targets || []; if (!existingTargets.some(target => target.pathUUID === newTarget.pathUUID && target.pointUUID === newTarget.pointUUID )) { return { ...point, connections: { ...point.connections, targets: [...existingTargets, newTarget] } }; } } return point; }) }; } else if (path.modeluuid === toPathUUID) { return { ...path, points: path.points.map(point => { if (point.uuid === toPointUUID) { const reverseTarget = { pathUUID: fromPathUUID, pointUUID: fromPointUUID }; const existingTargets = point.connections.targets || []; if (!existingTargets.some(target => target.pathUUID === reverseTarget.pathUUID && target.pointUUID === reverseTarget.pointUUID )) { return { ...point, connections: { ...point.connections, targets: [...existingTargets, reverseTarget] } }; } } return point; }) }; } } // In the updatePathConnections function, modify the Vehicle handling section: else if (path.type === 'Vehicle') { // Handle outgoing connections from Vehicle if (path.modeluuid === fromPathUUID && path.point.uuid === fromPointUUID) { const newTarget = { pathUUID: toPathUUID, pointUUID: toPointUUID }; const existingTargets = path.point.connections.targets || []; // Check if we're trying to add a connection to a Conveyor const toPath = simulationPaths.find(p => p.modeluuid === toPathUUID); const isConnectingToConveyor = toPath?.type === 'Conveyor'; // Count existing connections if (existingTargets.length >= 2) { console.log("Vehicle can have maximum 2 connections"); return path; } // Check if we already have a Conveyor connection and trying to add another const hasConveyorConnection = existingTargets.some(target => { const targetPath = simulationPaths.find(p => p.modeluuid === target.pathUUID); return targetPath?.type === 'Conveyor'; }); if (hasConveyorConnection && isConnectingToConveyor) { console.log("Vehicle can only have one connection to a Conveyor"); return path; } if (!existingTargets.some(target => target.pathUUID === newTarget.pathUUID && target.pointUUID === newTarget.pointUUID )) { return { ...path, point: { ...path.point, connections: { ...path.point.connections, targets: [...existingTargets, newTarget] } } }; } } // Handle incoming connections to Vehicle else if (path.modeluuid === toPathUUID && path.point.uuid === toPointUUID) { const reverseTarget = { pathUUID: fromPathUUID, pointUUID: fromPointUUID }; const existingTargets = path.point.connections.targets || []; // Check if we're receiving a connection from a Conveyor const fromPath = simulationPaths.find(p => p.modeluuid === fromPathUUID); const isConnectingFromConveyor = fromPath?.type === 'Conveyor'; // Count existing connections if (existingTargets.length >= 2) { console.log("Vehicle can have maximum 2 connections"); return path; } // Check if we already have a Conveyor connection and trying to add another const hasConveyorConnection = existingTargets.some(target => { const targetPath = simulationPaths.find(p => p.modeluuid === target.pathUUID); return targetPath?.type === 'Conveyor'; }); if (hasConveyorConnection && isConnectingFromConveyor) { console.log("Vehicle can only have one connection to a Conveyor"); return path; } if (!existingTargets.some(target => target.pathUUID === reverseTarget.pathUUID && target.pointUUID === reverseTarget.pointUUID )) { return { ...path, point: { ...path.point, connections: { ...path.point.connections, targets: [...existingTargets, reverseTarget] } } }; } } return path; } return path; }); setSimulationPaths(updatedPaths); }; const handleAddConnection = (fromPathUUID: string, fromUUID: string, toPathUUID: string, toUUID: string) => { updatePathConnections(fromPathUUID, fromUUID, toPathUUID, 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 pathUUID = intersected.userData.path.modeluuid; const sphereUUID = intersected.uuid; const worldPosition = new THREE.Vector3(); intersected.getWorldPosition(worldPosition); let isStartOrEnd = false; if (intersected.userData.path.points) { 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.point) { isStartOrEnd = sphereUUID === intersected.userData.path.point.uuid; } if (pathUUID) { const firstPath = simulationPaths.find(p => p.modeluuid === firstSelected?.pathUUID); const secondPath = simulationPaths.find(p => p.modeluuid === pathUUID); // 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) { console.log("Conveyor middle points can only connect to non-conveyor paths"); return; } // Check if this specific connection already exists const isDuplicateConnection = firstSelected ? simulationPaths.some(path => { if (path.modeluuid === firstSelected.pathUUID) { if (path.type === 'Conveyor') { const point = path.points.find(p => p.uuid === firstSelected.sphereUUID); return point?.connections.targets.some(t => t.pathUUID === pathUUID && t.pointUUID === sphereUUID ); } else if (path.type === 'Vehicle') { return path.point.connections.targets.some(t => t.pathUUID === pathUUID && t.pointUUID === sphereUUID ); } } return false; }) : false; if (isDuplicateConnection) { console.log("These points are already connected. Ignoring."); return; } // For Vehicles, skip the "already connected" check since they can have multiple connections if (intersected.userData.path.type !== 'Vehicle') { const isAlreadyConnected = simulationPaths.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; } } // Check vehicle connection limits const checkVehicleConnections = (pathUUID: string) => { const path = simulationPaths.find(p => p.modeluuid === pathUUID); if (path?.type === 'Vehicle') { return path.point.connections.targets.length >= 2; } return false; }; if (firstSelected) { // Check if either selected point is from a Vehicle with max connections if (checkVehicleConnections(firstSelected.pathUUID) || checkVehicleConnections(pathUUID)) { console.log("Vehicle already has maximum connections"); return; } // Check if we're trying to add a second Conveyor connection to a Vehicle if (firstPath?.type === 'Vehicle' && secondPath?.type === 'Conveyor') { const hasConveyorConnection = firstPath.point.connections.targets.some(target => { const targetPath = simulationPaths.find(p => p.modeluuid === target.pathUUID); return targetPath?.type === 'Conveyor'; }); if (hasConveyorConnection) { console.log("Vehicle can only have one connection to a Conveyor"); return; } } if (secondPath?.type === 'Vehicle' && firstPath?.type === 'Conveyor') { const hasConveyorConnection = secondPath.point.connections.targets.some(target => { const targetPath = simulationPaths.find(p => p.modeluuid === target.pathUUID); return targetPath?.type === 'Conveyor'; }); if (hasConveyorConnection) { console.log("Vehicle can only have one connection to a Conveyor"); return; } } // Prevent same-path connections if (firstSelected.pathUUID === pathUUID) { console.log("Cannot connect spheres on the same path."); 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.pathUUID, firstSelected.sphereUUID, pathUUID, sphereUUID ); } else { // First selection - just store it setFirstSelected({ pathUUID, sphereUUID, position: worldPosition, isCorner: isStartOrEnd }); setIsConnecting(true); } } } } else { // Clicked outside - cancel connection setFirstSelected(null); setCurrentLine(null); setIsConnecting(false); } }; if (activeModule === 'simulation') { 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, simulationPaths]); 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, pathUUID: 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 pathUUID = pathData.modeluuid; const firstPath = simulationPaths.find(p => p.modeluuid === firstSelected.pathUUID); const secondPath = simulationPaths.find(p => p.modeluuid === pathUUID); const isVehicleToVehicle = firstPath?.type === 'Vehicle' && secondPath?.type === 'Vehicle'; // Inside the useFrame hook, where we check for snapped spheres: const isConnectable = (pathData.type === 'Vehicle' || (pathData.points.length > 0 && ( sphereUUID === pathData.points[0].uuid || sphereUUID === pathData.points[pathData.points.length - 1].uuid ))) && !isVehicleToVehicle && !(firstPath?.type === 'Conveyor' && pathData.type === 'Conveyor' && !firstSelected.isCorner); // Check for duplicate connection (regardless of path type) const isDuplicateConnection = simulationPaths.some(path => { if (path.modeluuid === firstSelected.pathUUID) { if (path.type === 'Conveyor') { const point = path.points.find(p => p.uuid === firstSelected.sphereUUID); return point?.connections.targets.some(t => t.pathUUID === pathUUID && t.pointUUID === sphereUUID ); } else if (path.type === 'Vehicle') { return path.point.connections.targets.some(t => t.pathUUID === pathUUID && t.pointUUID === sphereUUID ); } } return false; }); // For non-Vehicle paths, check if already connected const isNonVehicleAlreadyConnected = pathData.type !== 'Vehicle' && simulationPaths.some(path => { if (path.type === 'Conveyor') { return path.points.some(point => point.uuid === sphereUUID && point.connections.targets.length > 0 ); } return false; }); // Check vehicle connection limits const isVehicleAtMaxConnections = pathData.type === 'Vehicle' && pathData.point.connections.targets.length >= 2; const isVehicleConveyorConflict = (firstPath?.type === 'Vehicle' && secondPath?.type === 'Conveyor' && firstPath.point.connections.targets.some(t => { const targetPath = simulationPaths.find(p => p.modeluuid === t.pathUUID); return targetPath?.type === 'Conveyor'; })) || (secondPath?.type === 'Vehicle' && firstPath?.type === 'Conveyor' && secondPath.point.connections.targets.some(t => { const targetPath = simulationPaths.find(p => p.modeluuid === t.pathUUID); return targetPath?.type === 'Conveyor'; })); if ( !isDuplicateConnection && !isVehicleToVehicle && !isNonVehicleAlreadyConnected && !isVehicleAtMaxConnections && !isVehicleConveyorConflict && firstSelected.sphereUUID !== sphereUUID && firstSelected.pathUUID !== pathUUID && (firstSelected.isCorner || isConnectable) ) { snappedSphere = { sphereUUID, position: spherePosition, pathUUID, 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); } }); return ( <> {simulationPaths.flatMap(path => { if (path.type === 'Conveyor') { return path.points.flatMap(point => point.connections.targets.map((target, index) => { const targetPath = simulationPaths.find(p => p.modeluuid === target.pathUUID); if (targetPath?.type === 'Vehicle') 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 ( ); } return null; }) ); } else if (path.type === 'Vehicle') { return path.point.connections.targets.map((target, index) => { const fromSphere = pathsGroupRef.current?.getObjectByProperty('uuid', path.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 ( ); } return null; }); } return []; })} {currentLine && ( )} ); } export default PathConnector;