204 lines
6.9 KiB
TypeScript
204 lines
6.9 KiB
TypeScript
import * as THREE from "three";
|
|
import { useMemo, useRef } from "react";
|
|
import { useThree } from "@react-three/fiber";
|
|
|
|
interface ConnectionLine {
|
|
id: string;
|
|
startPointUuid: string;
|
|
endPointUuid: string;
|
|
trigger: TriggerSchema;
|
|
}
|
|
|
|
export function Arrows({ connections }: { connections: ConnectionLine[] }) {
|
|
const groupRef = useRef<THREE.Group>(null);
|
|
const { scene } = useThree();
|
|
|
|
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 (
|
|
<group key={key}>
|
|
<mesh geometry={shaftGeometry}>
|
|
<meshStandardMaterial color="#42a5f5" />
|
|
</mesh>
|
|
<mesh position={end} quaternion={rotation} geometry={headGeometry}>
|
|
<meshStandardMaterial color="#42a5f5" />
|
|
</mesh>
|
|
</group>
|
|
);
|
|
};
|
|
|
|
const getSubCurve = (
|
|
curve: THREE.Curve<THREE.Vector3>,
|
|
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 <group ref={groupRef} name="connectionArrows">{arrowGroups}</group>;
|
|
}
|
|
|
|
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 (
|
|
<group name="ArrowWithTube">
|
|
<mesh name="ArrowWithTube" geometry={tubeGeometry}>
|
|
<meshStandardMaterial color={color} />
|
|
</mesh>
|
|
<mesh name="ArrowWithTube" position={arrowPosition} quaternion={arrowRotation} geometry={coneGeometry}>
|
|
<meshStandardMaterial color={color} />
|
|
</mesh>
|
|
</group>
|
|
);
|
|
} |