Files
Dwinzo_Demo/app/src/modules/simulation/events/arrows/arrows.tsx

248 lines
8.9 KiB
TypeScript

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<string | null>(null);
const groupRef = useRef<THREE.Group>(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<string>();
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 (
<group
key={key}
onPointerOver={() => setHoveredArrowTrigger(trigger.triggerUuid)}
onPointerOut={() => setHoveredArrowTrigger(null)}
onClick={() => { removeConnection(trigger) }}
>
<mesh
geometry={shaftGeometry}
>
<meshStandardMaterial
color={toolMode === '3D-Delete' && hoveredArrowTrigger === trigger.triggerUuid ? "red" : "#42a5f5"}
/>
</mesh>
<mesh
position={end}
quaternion={rotation}
geometry={headGeometry}
>
<meshStandardMaterial
color={toolMode === '3D-Delete' && hoveredArrowTrigger === trigger.triggerUuid ? "red" : "#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 { 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 <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>
);
}