import { useFrame, useThree } from '@react-three/fiber'; import React, { useEffect, useState } from 'react'; import * as THREE from 'three'; 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.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; }) }; } 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("action-sphere")) { const pathUUID = intersected.userData.path.modeluuid; const sphereUUID = intersected.uuid; const worldPosition = new THREE.Vector3(); intersected.getWorldPosition(worldPosition); const 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 ); if (pathUUID) { // Check if sphere is already connected const isAlreadyConnected = simulationPaths.some(path => path.points.some(point => point.uuid === sphereUUID && point.connections.targets.length > 0 ) ); if (isAlreadyConnected) { console.log("Sphere is already connected. Ignoring."); return; } if (!firstSelected) { setFirstSelected({ pathUUID, sphereUUID, position: worldPosition, isCorner: isStartOrEnd }); setIsConnecting(true); } else { if (firstSelected.sphereUUID === sphereUUID) return; if (firstSelected.pathUUID === pathUUID) { console.log("Cannot connect spheres on the same path."); return; } if (!firstSelected.isCorner && !isStartOrEnd) { console.log("At least one of the selected spheres must be a start or end point."); return; } handleAddConnection( firstSelected.pathUUID, firstSelected.sphereUUID, pathUUID, sphereUUID ); } } } } else { 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("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("action-sphere") ); if (sphereIntersects.length > 0) { const sphere = sphereIntersects[0].object; const sphereUUID = sphere.uuid; const spherePosition = new THREE.Vector3(); sphere.getWorldPosition(spherePosition); const pathUUID = sphere.userData.path.modeluuid; const isStartOrEnd = sphere.userData.path.points.length > 0 && ( sphereUUID === sphere.userData.path.points[0].uuid || sphereUUID === sphere.userData.path.points[sphere.userData.path.points.length - 1].uuid ); const isAlreadyConnected = simulationPaths.some(path => path.points.some(point => point.uuid === sphereUUID && point.connections.targets.length > 0 ) ); if ( !isAlreadyConnected && firstSelected.sphereUUID !== sphereUUID && firstSelected.pathUUID !== pathUUID && (firstSelected.isCorner || isStartOrEnd) ) { snappedSphere = { sphereUUID, position: spherePosition, pathUUID, isCorner: isStartOrEnd }; } 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, }); setIsConnecting(true); if (sphereIntersects.length > 0) { setHelperLineColor(isInvalidConnection ? 'red' : '#6cf542'); } else { setHelperLineColor('yellow'); } } else { setCurrentLine(null); setIsConnecting(false); } } else { setCurrentLine(null); setIsConnecting(false); } }); // Render connections from simulationPaths return ( <> {simulationPaths.flatMap(path => path.points.flatMap(point => point.connections.targets.map((target, index) => { 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; }) ) )} {currentLine && ( )} ); } export default PathConnector;