import * as THREE from "three"; import { useMemo, useRef, useState } from "react"; import { useThree } from "@react-three/fiber"; import { useToolMode } from "../../../../store/builder/store"; interface ConnectionLine { id: string; startPointUuid: string; endPointUuid: string; trigger: TriggerSchema; } export function Arrows({ connections }: { readonly connections: ConnectionLine[] }) { const [hoveredLineKey, setHoveredLineKey] = useState(null); const groupRef = useRef(null); const { scene } = useThree(); const { toolMode } = useToolMode(); const getWorldPositionFromScene = (uuid: string): THREE.Vector3 | null => { const obj = scene.getObjectByProperty("uuid", uuid); if (!obj) return null; const pos = new THREE.Vector3(); obj.getWorldPosition(pos); return pos; }; const createArrow = ( key: string, fullCurve: THREE.QuadraticBezierCurve3, centerT: number, segmentSize: number, scale: number, reverse = false ) => { const t1 = Math.max(0, centerT - segmentSize / 2); const t2 = Math.min(1, centerT + segmentSize / 2); const subCurve = getSubCurve(fullCurve, t1, t2, reverse); const shaftGeometry = new THREE.TubeGeometry(subCurve, 8, 0.01 * scale, 8, false); const end = subCurve.getPoint(1); const tangent = subCurve.getTangent(1).normalize(); const arrowHeadLength = 0.15 * scale; const arrowRadius = 0.01 * scale; const arrowHeadRadius = arrowRadius * 2.5; const headGeometry = new THREE.ConeGeometry(arrowHeadRadius, arrowHeadLength, 8); headGeometry.translate(0, arrowHeadLength / 2, 0); const rotation = new THREE.Quaternion().setFromUnitVectors( new THREE.Vector3(0, 1, 0), tangent ); return ( setHoveredLineKey(key)} onPointerOut={() => setHoveredLineKey(null)} > setHoveredLineKey(key)} onPointerOut={() => setHoveredLineKey(null)} > ); }; const getSubCurve = ( curve: THREE.Curve, t1: number, t2: number, reverse = false ) => { const divisions = 10; const subPoints = Array.from({ length: divisions + 1 }, (_, i) => { const t = THREE.MathUtils.lerp(t1, t2, i / divisions); return curve.getPoint(t); }); if (reverse) subPoints.reverse(); return new THREE.CatmullRomCurve3(subPoints); }; const arrowGroups = connections.flatMap((connection) => { const start = getWorldPositionFromScene(connection.startPointUuid); const end = getWorldPositionFromScene(connection.endPointUuid); if (!start || !end) return []; const isBidirectional = connections.some( (other) => other.startPointUuid === connection.endPointUuid && other.endPointUuid === connection.startPointUuid ); if (isBidirectional && connection.startPointUuid < connection.endPointUuid) { const distance = start.distanceTo(end); const heightFactor = Math.max(0.5, distance * 0.2); const control = new THREE.Vector3( (start.x + end.x) / 2, Math.max(start.y, end.y) + heightFactor, (start.z + end.z) / 2 ); const curve = new THREE.QuadraticBezierCurve3(start, control, end); const scale = THREE.MathUtils.clamp(distance * 0.75, 0.5, 3); return [ createArrow(connection.id + "-fwd", curve, 0.33, 0.25, scale, true), createArrow(connection.id + "-bwd", curve, 0.66, 0.25, scale, false), ]; } if (!isBidirectional) { const distance = start.distanceTo(end); const heightFactor = Math.max(0.5, distance * 0.2); const control = new THREE.Vector3( (start.x + end.x) / 2, Math.max(start.y, end.y) + heightFactor, (start.z + end.z) / 2 ); const curve = new THREE.QuadraticBezierCurve3(start, control, end); const scale = THREE.MathUtils.clamp(distance * 0.75, 0.5, 5); return [ createArrow(connection.id, curve, 0.5, 0.3, scale) ]; } return []; }); return {arrowGroups}; } export function ArrowOnQuadraticBezier({ start, mid, end, color = "#42a5f5", }: { start: number[]; mid: number[]; end: number[]; color?: string; }) { const minScale = 0.5; const maxScale = 5; const segmentSize = 0.3; const startVec = useMemo(() => new THREE.Vector3(...start), [start]); const midVec = useMemo(() => new THREE.Vector3(...mid), [mid]); const endVec = useMemo(() => new THREE.Vector3(...end), [end]); const fullCurve = useMemo( () => new THREE.QuadraticBezierCurve3(startVec, midVec, endVec), [startVec, midVec, endVec] ); const distance = useMemo(() => startVec.distanceTo(endVec), [startVec, endVec]); const scale = useMemo(() => THREE.MathUtils.clamp(distance * 0.75, minScale, maxScale), [distance]); const arrowHeadLength = 0.15 * scale; const arrowRadius = 0.01 * scale; const arrowHeadRadius = arrowRadius * 2.5; const subCurve = useMemo(() => { const centerT = 0.5; const t1 = Math.max(0, centerT - segmentSize / 2); const t2 = Math.min(1, centerT + segmentSize / 2); const divisions = 10; const subPoints = Array.from({ length: divisions + 1 }, (_, i) => { const t = THREE.MathUtils.lerp(t1, t2, i / divisions); return fullCurve.getPoint(t); }); return new THREE.CatmullRomCurve3(subPoints); }, [fullCurve, segmentSize]); const tubeGeometry = useMemo( () => new THREE.TubeGeometry(subCurve, 20, arrowRadius, 8, false), [subCurve, arrowRadius] ); const arrowPosition = useMemo(() => subCurve.getPoint(1), [subCurve]); const arrowTangent = useMemo(() => subCurve.getTangent(1).normalize(), [subCurve]); const arrowRotation = useMemo(() => { return new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 1, 0), arrowTangent); }, [arrowTangent]); const coneGeometry = useMemo(() => { const geom = new THREE.ConeGeometry(arrowHeadRadius, arrowHeadLength, 8); geom.translate(0, arrowHeadLength / 2, 0); return geom; }, [arrowHeadRadius, arrowHeadLength]); return ( ); }