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 { useConnections, 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 { connections, setConnections, addConnection } = useConnections(); const { isConnecting, 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 [hoveredSphere, setHoveredSphere] = useState<{ sphereUUID: string, position: THREE.Vector3 } | null>(null); const [helperlineColor, setHelperLineColor] = useState('red'); 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) { const isAlreadyConnected = connections.some((connection) => connection.fromUUID === sphereUUID || connection.toConnections.some(conn => conn.toUUID === sphereUUID) ); 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; } addConnection({ fromPathUUID: firstSelected.pathUUID, fromUUID: firstSelected.sphereUUID, toConnections: [{ toPathUUID: pathUUID, toUUID: sphereUUID }] }); setFirstSelected(null); setCurrentLine(null); setIsConnecting(false); setHoveredSphere(null); } } } } else { setFirstSelected(null); setCurrentLine(null); setIsConnecting(false); setHoveredSphere(null); } }; 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); setHoveredSphere(null); } return () => { canvasElement.removeEventListener("mousedown", onMouseDown); canvasElement.removeEventListener("mouseup", onMouseUp); canvasElement.removeEventListener("mousemove", onMouseMove); canvasElement.removeEventListener("contextmenu", onContextMenu); }; }, [camera, scene, raycaster, firstSelected, connections]); 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 = connections.some((connection) => connection.fromUUID === sphereUUID || connection.toConnections.some(conn => conn.toUUID === sphereUUID) ); if ( !isAlreadyConnected && firstSelected.sphereUUID !== sphereUUID && firstSelected.pathUUID !== pathUUID && (firstSelected.isCorner || isStartOrEnd) ) { snappedSphere = { sphereUUID, position: spherePosition, pathUUID, isCorner: isStartOrEnd }; } else { isInvalidConnection = true; } } if (snappedSphere) { setHoveredSphere(snappedSphere); point = snappedSphere.position; } else { setHoveredSphere(null); } 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); } }); useEffect(() => { console.log('connections: ', connections); }, [connections]); return ( <> {connections.map((connection, index) => { if (!pathsGroupRef.current) return; const fromSphere = pathsGroupRef.current.getObjectByProperty('uuid', connection.fromUUID); const toSphere = pathsGroupRef.current.getObjectByProperty('uuid', connection.toConnections[0].toUUID); 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;