import * as THREE from "three"; import { useMemo, useRef, useState } from "react"; import { useThree } from "@react-three/fiber"; import { useToolMode } from "../../../../store/builder/store"; import { useSceneContext } from "../../../scene/sceneContext"; import { useVersionContext } from "../../../builder/version/versionContext"; import { useProductContext } from "../../products/productContext"; import { useParams } from "react-router-dom"; import { upsertProductOrEventApi } from "../../../../services/simulation/products/UpsertProductOrEventApi"; interface ConnectionLine { id: string; startPointUuid: string; endPointUuid: string; trigger: TriggerSchema; } export function Arrows({ connections }: { readonly connections: ConnectionLine[] }) { const [hoveredArrowTrigger, setHoveredArrowTrigger] = useState(null); const groupRef = useRef(null); const { scene } = useThree(); const { toolMode } = useToolMode(); const { productStore } = useSceneContext(); const { removeTrigger } = productStore(); const { selectedVersionStore } = useVersionContext(); const { selectedVersion } = selectedVersionStore(); const { selectedProductStore } = useProductContext(); const { selectedProduct } = selectedProductStore(); const { projectId } = useParams(); const updateBackend = ( productName: string, productUuid: string, projectId: string, eventData: EventsSchema ) => { upsertProductOrEventApi({ productName: productName, productUuid: productUuid, projectId: projectId, eventDatas: eventData, versionId: selectedVersion?.versionId || '', }) } 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 processedArrows = new Set(); const createArrow = ( key: string, fullCurve: THREE.QuadraticBezierCurve3, centerT: number, segmentSize: number, scale: number, reverse: boolean, trigger: TriggerSchema ) => { if (processedArrows.has(trigger.triggerUuid)) return []; processedArrows.add(trigger.triggerUuid); 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 ); const removeConnection = (trigger: TriggerSchema) => { if (trigger.triggerUuid) { const event = removeTrigger(selectedProduct.productUuid, trigger.triggerUuid); if (event) { updateBackend( selectedProduct.productName, selectedProduct.productUuid, projectId || '', event ); } } }; return ( setHoveredArrowTrigger(trigger.triggerUuid)} onPointerOut={() => setHoveredArrowTrigger(null)} onClick={() => { removeConnection(trigger) }} > ); }; 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 { startPointUuid, endPointUuid } = connection; const start = getWorldPositionFromScene(startPointUuid); const end = getWorldPositionFromScene(endPointUuid); if (!start || !end) return []; const isBidirectional = connections.some((other) => other.startPointUuid === endPointUuid && other.endPointUuid === startPointUuid); 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); if (isBidirectional) { return [ createArrow(connection.id + "-fwd", curve, 0.33, 0.25, scale, true, connection.trigger), createArrow(connection.id + "-bwd", curve, 0.66, 0.25, scale, false, connection.trigger), ]; } else { return [ createArrow(connection.id, curve, 0.5, 0.3, scale, false, connection.trigger) ]; } }); 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 ( ); }