Dwinzo_dev/app/src/modules/simulation/events/arrows/arrows.tsx

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>
);
}