Compare commits

...

14 Commits

31 changed files with 6425 additions and 1271 deletions

View File

View File

@@ -1,20 +1,28 @@
import * as THREE from 'three'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useThree } from '@react-three/fiber';
import { useActiveLayer, useSocketStore, useToggleView, useToolMode } from '../../../../store/builder/store';
import { useBuilderStore } from '../../../../store/builder/useBuilderStore';
import { useParams } from 'react-router-dom';
import { useVersionContext } from '../../version/versionContext';
import { useSceneContext } from '../../../scene/sceneContext';
import ReferenceAisle from './referenceAisle';
import ReferencePoint from '../../point/reference/referencePoint';
import { getUserData } from '../../../../functions/getUserData';
import * as THREE from "three";
import { useEffect, useMemo, useRef, useState } from "react";
import { useThree } from "@react-three/fiber";
import {
useActiveLayer,
useSocketStore,
useToggleView,
useToolMode,
} from "../../../../store/builder/store";
import { useBuilderStore } from "../../../../store/builder/useBuilderStore";
import { useParams } from "react-router-dom";
import { useVersionContext } from "../../version/versionContext";
import { useSceneContext } from "../../../scene/sceneContext";
import ReferenceAisle from "./referenceAisle";
import ReferencePoint from "../../point/reference/referencePoint";
import { getUserData } from "../../../../functions/getUserData";
// import { upsertAisleApi } from '../../../../services/factoryBuilder/aisle/upsertAisleApi';
function AisleCreator() {
const { scene, camera, raycaster, gl, pointer } = useThree();
const plane = useMemo(() => new THREE.Plane(new THREE.Vector3(0, 1, 0), 0), []);
const plane = useMemo(
() => new THREE.Plane(new THREE.Vector3(0, 1, 0), 0),
[]
);
const { toggleView } = useToggleView();
const { toolMode } = useToolMode();
const { activeLayer } = useActiveLayer();
@@ -31,7 +39,20 @@ function AisleCreator() {
const [tempPoints, setTempPoints] = useState<Point[]>([]);
const [isCreating, setIsCreating] = useState(false);
const { aisleType, aisleWidth, aisleColor, dashLength, gapLength, dotRadius, aisleLength, isFlipped, snappedPosition, snappedPoint, setSnappedPosition, setSnappedPoint } = useBuilderStore();
const {
aisleType,
aisleWidth,
aisleColor,
dashLength,
gapLength,
dotRadius,
aisleLength,
isFlipped,
snappedPosition,
snappedPoint,
setSnappedPosition,
setSnappedPoint,
} = useBuilderStore();
useEffect(() => {
const canvasElement = gl.domElement;
@@ -63,13 +84,15 @@ function AisleCreator() {
let position = raycaster.ray.intersectPlane(plane, intersectionPoint);
if (!position) return;
const intersects = raycaster.intersectObjects(scene.children).find((intersect) => intersect.object.name === 'Aisle-Point');
const intersects = raycaster
.intersectObjects(scene.children)
.find((intersect) => intersect.object.name === "Aisle-Point");
const newPoint: Point = {
pointUuid: THREE.MathUtils.generateUUID(),
pointType: 'Aisle',
pointType: "Aisle",
position: [position.x, position.y, position.z],
layer: activeLayer
layer: activeLayer,
};
if (snappedPosition && snappedPoint) {
@@ -78,7 +101,9 @@ function AisleCreator() {
newPoint.layer = snappedPoint.layer;
}
if (snappedPoint && snappedPoint.pointUuid === tempPoints[0]?.pointUuid) { return }
if (snappedPoint && snappedPoint.pointUuid === tempPoints[0]?.pointUuid) {
return;
}
if (snappedPosition && !snappedPoint) {
newPoint.position = snappedPosition;
@@ -93,8 +118,7 @@ function AisleCreator() {
}
}
if (aisleType === 'solid-aisle') {
if (aisleType === "solid-aisle") {
if (tempPoints.length === 0) {
setTempPoints([newPoint]);
setIsCreating(true);
@@ -103,50 +127,48 @@ function AisleCreator() {
aisleUuid: THREE.MathUtils.generateUUID(),
points: [tempPoints[0], newPoint],
type: {
aisleType: 'solid-aisle',
aisleType: "solid-aisle",
aisleColor: aisleColor,
aisleWidth: aisleWidth
}
aisleWidth: aisleWidth,
},
};
addAisle(aisle);
push2D({
type: 'Draw',
type: "Draw",
actions: [
{
actionType: 'Line-Create',
actionType: "Line-Create",
point: {
type: 'Aisle',
type: "Aisle",
lineData: aisle,
timeStamp: new Date().toISOString(),
}
}
},
},
],
})
});
if (projectId) {
// API
// upsertAisleApi(aisle.aisleUuid, aisle.points, aisle.type, projectId, selectedVersion?.versionId || '')
// SOCKET
socket.emit('v1:model-aisle:add', {
socket.emit("v1:model-aisle:add", {
projectId: projectId,
versionId: selectedVersion?.versionId || '',
versionId: selectedVersion?.versionId || "",
userId: userId,
organization: organization,
aisleUuid: aisle.aisleUuid,
points: aisle.points,
type: aisle.type
})
type: aisle.type,
});
}
setTempPoints([newPoint]);
}
} else if (aisleType === 'dashed-aisle') {
} else if (aisleType === "dashed-aisle") {
if (tempPoints.length === 0) {
setTempPoints([newPoint]);
setIsCreating(true);
@@ -155,52 +177,50 @@ function AisleCreator() {
aisleUuid: THREE.MathUtils.generateUUID(),
points: [tempPoints[0], newPoint],
type: {
aisleType: 'dashed-aisle',
aisleType: "dashed-aisle",
aisleColor: aisleColor,
aisleWidth: aisleWidth,
dashLength: dashLength,
gapLength: gapLength
}
gapLength: gapLength,
},
};
addAisle(aisle);
push2D({
type: 'Draw',
type: "Draw",
actions: [
{
actionType: 'Line-Create',
actionType: "Line-Create",
point: {
type: 'Aisle',
type: "Aisle",
lineData: aisle,
timeStamp: new Date().toISOString(),
}
}
},
},
],
})
});
if (projectId) {
// API
// upsertAisleApi(aisle.aisleUuid, aisle.points, aisle.type, projectId, selectedVersion?.versionId || '')
// SOCKET
socket.emit('v1:model-aisle:add', {
socket.emit("v1:model-aisle:add", {
projectId: projectId,
versionId: selectedVersion?.versionId || '',
versionId: selectedVersion?.versionId || "",
userId: userId,
organization: organization,
aisleUuid: aisle.aisleUuid,
points: aisle.points,
type: aisle.type
})
type: aisle.type,
});
}
setTempPoints([newPoint]);
}
} else if (aisleType === 'dotted-aisle') {
} else if (aisleType === "dotted-aisle") {
if (tempPoints.length === 0) {
setTempPoints([newPoint]);
setIsCreating(true);
@@ -209,51 +229,49 @@ function AisleCreator() {
aisleUuid: THREE.MathUtils.generateUUID(),
points: [tempPoints[0], newPoint],
type: {
aisleType: 'dotted-aisle',
aisleType: "dotted-aisle",
aisleColor: aisleColor,
dotRadius: dotRadius,
gapLength: gapLength
}
gapLength: gapLength,
},
};
addAisle(aisle);
push2D({
type: 'Draw',
type: "Draw",
actions: [
{
actionType: 'Line-Create',
actionType: "Line-Create",
point: {
type: 'Aisle',
type: "Aisle",
lineData: aisle,
timeStamp: new Date().toISOString(),
}
}
},
},
],
})
});
if (projectId) {
// API
// upsertAisleApi(aisle.aisleUuid, aisle.points, aisle.type, projectId, selectedVersion?.versionId || '')
// SOCKET
socket.emit('v1:model-aisle:add', {
socket.emit("v1:model-aisle:add", {
projectId: projectId,
versionId: selectedVersion?.versionId || '',
versionId: selectedVersion?.versionId || "",
userId: userId,
organization: organization,
aisleUuid: aisle.aisleUuid,
points: aisle.points,
type: aisle.type
})
type: aisle.type,
});
}
setTempPoints([newPoint]);
}
} else if (aisleType === 'arrow-aisle') {
} else if (aisleType === "arrow-aisle") {
if (tempPoints.length === 0) {
setTempPoints([newPoint]);
setIsCreating(true);
@@ -262,50 +280,48 @@ function AisleCreator() {
aisleUuid: THREE.MathUtils.generateUUID(),
points: [tempPoints[0], newPoint],
type: {
aisleType: 'arrow-aisle',
aisleType: "arrow-aisle",
aisleColor: aisleColor,
aisleWidth: aisleWidth
}
aisleWidth: aisleWidth,
},
};
addAisle(aisle);
push2D({
type: 'Draw',
type: "Draw",
actions: [
{
actionType: 'Line-Create',
actionType: "Line-Create",
point: {
type: 'Aisle',
type: "Aisle",
lineData: aisle,
timeStamp: new Date().toISOString(),
}
}
},
},
],
})
});
if (projectId) {
// API
// upsertAisleApi(aisle.aisleUuid, aisle.points, aisle.type, projectId, selectedVersion?.versionId || '')
// SOCKET
socket.emit('v1:model-aisle:add', {
socket.emit("v1:model-aisle:add", {
projectId: projectId,
versionId: selectedVersion?.versionId || '',
versionId: selectedVersion?.versionId || "",
userId: userId,
organization: organization,
aisleUuid: aisle.aisleUuid,
points: aisle.points,
type: aisle.type
})
type: aisle.type,
});
}
setTempPoints([newPoint]);
}
} else if (aisleType === 'arrows-aisle') {
} else if (aisleType === "arrows-aisle") {
if (tempPoints.length === 0) {
setTempPoints([newPoint]);
setIsCreating(true);
@@ -314,52 +330,50 @@ function AisleCreator() {
aisleUuid: THREE.MathUtils.generateUUID(),
points: [tempPoints[0], newPoint],
type: {
aisleType: 'arrows-aisle',
aisleType: "arrows-aisle",
aisleColor: aisleColor,
aisleWidth: aisleWidth,
aisleLength: aisleLength,
gapLength: gapLength
}
gapLength: gapLength,
},
};
addAisle(aisle);
push2D({
type: 'Draw',
type: "Draw",
actions: [
{
actionType: 'Line-Create',
actionType: "Line-Create",
point: {
type: 'Aisle',
type: "Aisle",
lineData: aisle,
timeStamp: new Date().toISOString(),
}
}
},
},
],
})
});
if (projectId) {
// API
// upsertAisleApi(aisle.aisleUuid, aisle.points, aisle.type, projectId, selectedVersion?.versionId || '')
// SOCKET
socket.emit('v1:model-aisle:add', {
socket.emit("v1:model-aisle:add", {
projectId: projectId,
versionId: selectedVersion?.versionId || '',
versionId: selectedVersion?.versionId || "",
userId: userId,
organization: organization,
aisleUuid: aisle.aisleUuid,
points: aisle.points,
type: aisle.type
})
type: aisle.type,
});
}
setTempPoints([newPoint]);
}
} else if (aisleType === 'arc-aisle') {
} else if (aisleType === "arc-aisle") {
if (tempPoints.length === 0) {
setTempPoints([newPoint]);
setIsCreating(true);
@@ -368,51 +382,49 @@ function AisleCreator() {
aisleUuid: THREE.MathUtils.generateUUID(),
points: [tempPoints[0], newPoint],
type: {
aisleType: 'arc-aisle',
aisleType: "arc-aisle",
aisleColor: aisleColor,
aisleWidth: aisleWidth,
isFlipped: isFlipped
}
isFlipped: isFlipped,
},
};
addAisle(aisle);
push2D({
type: 'Draw',
type: "Draw",
actions: [
{
actionType: 'Line-Create',
actionType: "Line-Create",
point: {
type: 'Aisle',
type: "Aisle",
lineData: aisle,
timeStamp: new Date().toISOString(),
}
}
},
},
],
})
});
if (projectId) {
// API
// upsertAisleApi(aisle.aisleUuid, aisle.points, aisle.type, projectId, selectedVersion?.versionId || '')
// SOCKET
socket.emit('v1:model-aisle:add', {
socket.emit("v1:model-aisle:add", {
projectId: projectId,
versionId: selectedVersion?.versionId || '',
versionId: selectedVersion?.versionId || "",
userId: userId,
organization: organization,
aisleUuid: aisle.aisleUuid,
points: aisle.points,
type: aisle.type
})
type: aisle.type,
});
}
setTempPoints([newPoint]);
}
} else if (aisleType === 'circle-aisle') {
} else if (aisleType === "circle-aisle") {
if (tempPoints.length === 0) {
setTempPoints([newPoint]);
setIsCreating(true);
@@ -421,98 +433,95 @@ function AisleCreator() {
aisleUuid: THREE.MathUtils.generateUUID(),
points: [tempPoints[0], newPoint],
type: {
aisleType: 'circle-aisle',
aisleColor: aisleColor,
aisleWidth: aisleWidth
}
};
addAisle(aisle);
push2D({
type: 'Draw',
actions: [
{
actionType: 'Line-Create',
point: {
type: 'Aisle',
lineData: aisle,
timeStamp: new Date().toISOString(),
}
}
],
})
if (projectId) {
// API
// upsertAisleApi(aisle.aisleUuid, aisle.points, aisle.type, projectId, selectedVersion?.versionId || '')
// SOCKET
socket.emit('v1:model-aisle:add', {
projectId: projectId,
versionId: selectedVersion?.versionId || '',
userId: userId,
organization: organization,
aisleUuid: aisle.aisleUuid,
points: aisle.points,
type: aisle.type
})
}
setTempPoints([newPoint]);
}
} else if (aisleType === 'junction-aisle') {
if (tempPoints.length === 0) {
setTempPoints([newPoint]);
setIsCreating(true);
} else {
const aisle: Aisle = {
aisleUuid: THREE.MathUtils.generateUUID(),
points: [tempPoints[0], newPoint],
type: {
aisleType: 'junction-aisle',
aisleType: "circle-aisle",
aisleColor: aisleColor,
aisleWidth: aisleWidth,
isFlipped: isFlipped
}
},
};
addAisle(aisle);
push2D({
type: 'Draw',
type: "Draw",
actions: [
{
actionType: 'Line-Create',
actionType: "Line-Create",
point: {
type: 'Aisle',
type: "Aisle",
lineData: aisle,
timeStamp: new Date().toISOString(),
}
}
},
},
],
})
});
if (projectId) {
// API
// upsertAisleApi(aisle.aisleUuid, aisle.points, aisle.type, projectId, selectedVersion?.versionId || '')
// SOCKET
socket.emit('v1:model-aisle:add', {
socket.emit("v1:model-aisle:add", {
projectId: projectId,
versionId: selectedVersion?.versionId || '',
versionId: selectedVersion?.versionId || "",
userId: userId,
organization: organization,
aisleUuid: aisle.aisleUuid,
points: aisle.points,
type: aisle.type
})
type: aisle.type,
});
}
setTempPoints([newPoint]);
}
} else if (aisleType === "junction-aisle") {
if (tempPoints.length === 0) {
setTempPoints([newPoint]);
setIsCreating(true);
} else {
const aisle: Aisle = {
aisleUuid: THREE.MathUtils.generateUUID(),
points: [tempPoints[0], newPoint],
type: {
aisleType: "junction-aisle",
aisleColor: aisleColor,
aisleWidth: aisleWidth,
isFlipped: isFlipped,
},
};
addAisle(aisle);
push2D({
type: "Draw",
actions: [
{
actionType: "Line-Create",
point: {
type: "Aisle",
lineData: aisle,
timeStamp: new Date().toISOString(),
},
},
],
});
if (projectId) {
// API
// upsertAisleApi(aisle.aisleUuid, aisle.points, aisle.type, projectId, selectedVersion?.versionId || '')
// SOCKET
socket.emit("v1:model-aisle:add", {
projectId: projectId,
versionId: selectedVersion?.versionId || "",
userId: userId,
organization: organization,
aisleUuid: aisle.aisleUuid,
points: aisle.points,
type: aisle.type,
});
}
setTempPoints([newPoint]);
}
@@ -556,23 +565,46 @@ function AisleCreator() {
canvasElement.removeEventListener("click", onMouseClick);
canvasElement.removeEventListener("contextmenu", onContext);
};
}, [gl, camera, scene, raycaster, pointer, plane, toggleView, toolMode, activeLayer, socket, tempPoints, isCreating, addAisle, getAislePointById, aisleType, aisleWidth, aisleColor, dashLength, gapLength, dotRadius, aisleLength, snappedPosition, snappedPoint, selectedVersion?.versionId]);
}, [
gl,
camera,
scene,
raycaster,
pointer,
plane,
toggleView,
toolMode,
activeLayer,
socket,
tempPoints,
isCreating,
addAisle,
getAislePointById,
aisleType,
aisleWidth,
aisleColor,
dashLength,
gapLength,
dotRadius,
aisleLength,
snappedPosition,
snappedPoint,
selectedVersion?.versionId,
]);
return (
<>
{toggleView &&
{toggleView && (
<>
<group name='Aisle-Reference-Points-Group'>
<group name="Aisle-Reference-Points-Group">
{tempPoints.map((point) => (
<ReferencePoint key={point.pointUuid} point={point} />
))}
</group>
{tempPoints.length > 0 &&
<ReferenceAisle tempPoints={tempPoints} />
}
{tempPoints.length > 0 && <ReferenceAisle tempPoints={tempPoints} />}
</>
}
)}
</>
);
}

View File

@@ -180,11 +180,11 @@ function PointsCreator() {
drag = false;
};
const onMouseUp = () => {
const onMouseUp = (e : MouseEvent) => {
if (selectedEventSphere && !drag) {
raycaster.setFromCamera(pointer, camera);
const intersects = raycaster.intersectObjects(scene.children, true).filter((intersect) => intersect.object.name === "Event-Sphere");
if (intersects.length === 0) {
if (intersects.length === 0 && e.button === 0) {
clearSelectedEventSphere();
setTransformMode(null);
}

View File

@@ -199,6 +199,7 @@ const VehicleUI = () => {
steeringAngle: steeringRotation[1],
},
},
}
);

View File

@@ -1,12 +1,18 @@
import { useEffect, useRef, useState } from 'react'
import { useFrame, useThree, ThreeEvent } from '@react-three/fiber';
import * as THREE from 'three';
import { Line } from '@react-three/drei';
import { useAnimationPlaySpeed, usePauseButtonStore, usePlayButtonStore, useResetButtonStore } from '../../../../../store/usePlayButtonStore';
import { useSceneContext } from '../../../../scene/sceneContext';
import { useActiveTool, useSelectedPath } from '../../../../../store/builder/store';
import { useEffect, useRef, useState } from "react";
import { useFrame, useThree, ThreeEvent } from "@react-three/fiber";
import * as THREE from "three";
import { Line } from "@react-three/drei";
import {
useAnimationPlaySpeed,
usePauseButtonStore,
usePlayButtonStore,
useResetButtonStore,
} from "../../../../../store/usePlayButtonStore";
import { useSceneContext } from "../../../../scene/sceneContext";
import {
useActiveTool,
useSelectedPath,
} from "../../../../../store/builder/store";
interface VehicleAnimatorProps {
path: [number, number, number][];
@@ -18,7 +24,15 @@ interface VehicleAnimatorProps {
agvDetail: VehicleStatus;
}
function VehicleAnimator({ path, handleCallBack, currentPhase, agvUuid, agvDetail, reset, startUnloadingProcess }: Readonly<VehicleAnimatorProps>) {
function VehicleAnimator({
path,
handleCallBack,
currentPhase,
agvUuid,
agvDetail,
reset,
startUnloadingProcess,
}: Readonly<VehicleAnimatorProps>) {
const { vehicleStore } = useSceneContext();
const { getVehicleById } = vehicleStore();
const { isPaused } = usePauseButtonStore();
@@ -28,23 +42,26 @@ function VehicleAnimator({ path, handleCallBack, currentPhase, agvUuid, agvDetai
const progressRef = useRef<number>(0);
const movingForward = useRef<boolean>(true);
const completedRef = useRef<boolean>(false);
const [objectRotation, setObjectRotation] = useState<{ x: number; y: number; z: number } | undefined>(agvDetail.point?.action?.pickUpPoint?.rotation || { x: 0, y: 0, z: 0 })
const [objectRotation, setObjectRotation] = useState<
{ x: number; y: number; z: number } | undefined
>(agvDetail.point?.action?.pickUpPoint?.rotation || { x: 0, y: 0, z: 0 });
const [restRotation, setRestingRotation] = useState<boolean>(true);
const [currentPath, setCurrentPath] = useState<[number, number, number][]>([]);
const [currentPath, setCurrentPath] = useState<[number, number, number][]>(
[]
);
const { scene, controls } = useThree();
const { selectedPath } = useSelectedPath();
const [isAnyDragging, setIsAnyDragging] = useState<string>("");
useEffect(() => {
if (currentPhase === 'stationed-pickup' && path.length > 0 && selectedPath === "auto") {
if (currentPhase === "stationed-pickup" && path.length > 0) {
setCurrentPath(path);
setObjectRotation(agvDetail.point.action?.pickUpPoint?.rotation)
} else if (currentPhase === 'pickup-drop' && path.length > 0) {
setObjectRotation(agvDetail.point.action?.unLoadPoint?.rotation)
setObjectRotation(agvDetail.point.action?.pickUpPoint?.rotation);
} else if (currentPhase === "pickup-drop" && path.length > 0) {
setObjectRotation(agvDetail.point.action?.unLoadPoint?.rotation);
setCurrentPath(path);
} else if (currentPhase === 'drop-pickup' && path.length > 0) {
setObjectRotation(agvDetail.point.action?.pickUpPoint?.rotation)
} else if (currentPhase === "drop-pickup" && path.length > 0) {
setObjectRotation(agvDetail.point.action?.pickUpPoint?.rotation);
setCurrentPath(path);
}
}, [currentPhase, path, objectRotation, selectedPath]);
@@ -62,24 +79,32 @@ function VehicleAnimator({ path, handleCallBack, currentPhase, agvUuid, agvDetai
progressRef.current = 0;
setReset(false);
setRestingRotation(true);
const object = scene.getObjectByProperty('uuid', agvUuid);
const object = scene.getObjectByProperty("uuid", agvUuid);
const vehicle = getVehicleById(agvDetail.modelUuid);
if (object && vehicle) {
object.position.set(vehicle.position[0], vehicle.position[1], vehicle.position[2]);
object.rotation.set(vehicle.rotation[0], vehicle.rotation[1], vehicle.rotation[2]);
object.position.set(
vehicle.position[0],
vehicle.position[1],
vehicle.position[2]
);
object.rotation.set(
vehicle.rotation[0],
vehicle.rotation[1],
vehicle.rotation[2]
);
}
}
}, [isReset, isPlaying])
}, [isReset, isPlaying]);
const lastTimeRef = useRef(performance.now());
useFrame(() => {
if (!isPlaying) return
if (!isPlaying) return;
const now = performance.now();
const delta = (now - lastTimeRef.current) / 1000;
lastTimeRef.current = now;
const object = scene.getObjectByProperty('uuid', agvUuid);
const object = scene.getObjectByProperty("uuid", agvUuid);
if (!object || currentPath.length < 2) return;
if (isPaused) return;
@@ -97,7 +122,10 @@ function VehicleAnimator({ path, handleCallBack, currentPhase, agvUuid, agvDetai
totalDistance += segmentDistance;
}
while (index < distances.length && progressRef.current > accumulatedDistance + distances[index]) {
while (
index < distances.length &&
progressRef.current > accumulatedDistance + distances[index]
) {
accumulatedDistance += distances[index];
index++;
}
@@ -107,8 +135,13 @@ function VehicleAnimator({ path, handleCallBack, currentPhase, agvUuid, agvDetai
const end = new THREE.Vector3(...currentPath[index + 1]);
const segmentDistance = distances[index];
const targetQuaternion = new THREE.Quaternion().setFromRotationMatrix(new THREE.Matrix4().lookAt(start, end, new THREE.Vector3(0, 1, 0)));
const y180 = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI);
const targetQuaternion = new THREE.Quaternion().setFromRotationMatrix(
new THREE.Matrix4().lookAt(start, end, new THREE.Vector3(0, 1, 0))
);
const y180 = new THREE.Quaternion().setFromAxisAngle(
new THREE.Vector3(0, 1, 0),
Math.PI
);
targetQuaternion.multiply(y180);
const angle = object.quaternion.angleTo(targetQuaternion);
@@ -137,26 +170,23 @@ function VehicleAnimator({ path, handleCallBack, currentPhase, agvUuid, agvDetai
if (progressRef.current >= totalDistance) {
if (restRotation && objectRotation) {
const targetEuler = new THREE.Euler(0, objectRotation.y - agvDetail.point.action.steeringAngle, 0);
const targetEuler = new THREE.Euler(
0,
objectRotation.y - agvDetail.point.action.steeringAngle,
0
);
const baseQuaternion = new THREE.Quaternion().setFromEuler(targetEuler);
const y180 = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI);
const y180 = new THREE.Quaternion().setFromAxisAngle(
new THREE.Vector3(0, 1, 0),
Math.PI
);
const targetQuaternion = baseQuaternion.multiply(y180);
const angle = object.quaternion.angleTo(targetQuaternion);
if (angle < 0.01) {
object.quaternion.copy(targetQuaternion);
setRestingRotation(false);
setTimeout(() => {
setRestingRotation(true);
progressRef.current = 0;
movingForward.current = !movingForward.current;
setCurrentPath([]);
handleCallBack();
if (currentPhase === 'pickup-drop') {
requestAnimationFrame(startUnloadingProcess);
}
}, 0)
} else {
const step = rotationSpeed * delta * speed * agvDetail.speed;
const angle = object.quaternion.angleTo(targetQuaternion);
@@ -177,7 +207,7 @@ function VehicleAnimator({ path, handleCallBack, currentPhase, agvUuid, agvDetai
movingForward.current = !movingForward.current;
setCurrentPath([]);
handleCallBack();
if (currentPhase === 'pickup-drop') {
if (currentPhase === "pickup-drop") {
requestAnimationFrame(startUnloadingProcess);
}
}
@@ -191,7 +221,7 @@ function VehicleAnimator({ path, handleCallBack, currentPhase, agvUuid, agvDetai
return (
<>
{selectedPath === "auto" &&
{selectedPath === "auto" && (
<group visible={false}>
{currentPath.map((pos, i) => {
if (i < currentPath.length - 1) {
@@ -215,9 +245,12 @@ function VehicleAnimator({ path, handleCallBack, currentPhase, agvUuid, agvDetai
return null;
})}
{currentPath.length > 0 && (
<group onPointerMissed={() => { if (controls) (controls as any).enabled = true; }}>
{currentPath.map((pos, i) =>
(
<group
onPointerMissed={() => {
if (controls) (controls as any).enabled = true;
}}
>
{currentPath.map((pos, i) => (
<DraggableSphere
key={i}
index={i}
@@ -225,12 +258,12 @@ function VehicleAnimator({ path, handleCallBack, currentPhase, agvUuid, agvDetai
onMove={updatePoint}
isAnyDragging={isAnyDragging}
setIsAnyDragging={setIsAnyDragging}
/>)
/>
))}
</group>
)}
</group >
</group>
)}
</group >
}
</>
);
}
@@ -256,15 +289,15 @@ function DraggableSphere({
const { activeTool } = useActiveTool();
const onPointerDown = (e: ThreeEvent<PointerEvent>) => {
e.stopPropagation()
if (activeTool !== 'pen') return;
e.stopPropagation();
if (activeTool !== "pen") return;
setIsAnyDragging("point");
gl.domElement.style.cursor = 'grabbing';
gl.domElement.style.cursor = "grabbing";
if (controls) (controls as any).enabled = false;
};
const onPointerMove = (e: ThreeEvent<PointerEvent>) => {
if (isAnyDragging !== "point" || activeTool !== 'pen') return;
if (isAnyDragging !== "point" || activeTool !== "pen") return;
const intersect = new THREE.Vector3();
if (raycaster.ray.intersectPlane(plane, intersect)) {
@@ -274,17 +307,17 @@ function DraggableSphere({
};
const onPointerUp = () => {
if (activeTool !== 'pen') return;
if (activeTool !== "pen") return;
setIsAnyDragging("");
gl.domElement.style.cursor = 'default';
gl.domElement.style.cursor = "default";
if (controls) (controls as any).enabled = true;
};
useEffect(() => {
gl.domElement.addEventListener("pointerup", onPointerUp);
return (() => {
return () => {
gl.domElement.removeEventListener("pointerup", onPointerUp);
})
}, [activeTool])
};
}, [activeTool]);
return (
<mesh
@@ -312,7 +345,12 @@ function DraggableLineSegment({
index: number;
start: THREE.Vector3;
end: THREE.Vector3;
updatePoints: (i0: number, p0: THREE.Vector3, i1: number, p1: THREE.Vector3) => void;
updatePoints: (
i0: number,
p0: THREE.Vector3,
i1: number,
p1: THREE.Vector3
) => void;
isAnyDragging: string;
setIsAnyDragging: (val: string) => void;
}) {
@@ -322,19 +360,22 @@ function DraggableLineSegment({
const dragStart = useRef<THREE.Vector3 | null>(null);
const onPointerDown = () => {
if (activeTool !== 'pen' || isAnyDragging) return;
if (activeTool !== "pen" || isAnyDragging) return;
setIsAnyDragging("line");
gl.domElement.style.cursor = 'grabbing';
gl.domElement.style.cursor = "grabbing";
if (controls) (controls as any).enabled = false;
};
const onPointerMove = (e: ThreeEvent<PointerEvent>) => {
if (isAnyDragging !== "line" || activeTool !== 'pen') return;
if (isAnyDragging !== "line" || activeTool !== "pen") return;
const intersect = new THREE.Vector3();
if (raycaster.ray.intersectPlane(plane, intersect)) {
if (!dragStart.current) dragStart.current = intersect.clone();
const offset = new THREE.Vector3().subVectors(intersect, dragStart.current);
const offset = new THREE.Vector3().subVectors(
intersect,
dragStart.current
);
const newStart = start.clone().add(offset);
const newEnd = end.clone().add(offset);
updatePoints(index, newStart, index + 1, newEnd);
@@ -342,18 +383,18 @@ function DraggableLineSegment({
};
const onPointerUp = () => {
if (activeTool !== 'pen') return;
if (activeTool !== "pen") return;
setIsAnyDragging("");
dragStart.current = null;
gl.domElement.style.cursor = 'default';
gl.domElement.style.cursor = "default";
if (controls) (controls as any).enabled = true;
};
useEffect(() => {
gl.domElement.addEventListener("pointerup", onPointerUp);
return (() => {
return () => {
gl.domElement.removeEventListener("pointerup", onPointerUp);
})
}, [activeTool])
};
}, [activeTool]);
return (
<Line
points={[start, end]}

View File

@@ -0,0 +1,297 @@
import { useFrame, useThree } from "@react-three/fiber";
import React, { useEffect, useMemo, useRef, useState } from "react";
import { Quaternion, Vector3 } from "three";
import {
useAnimationPlaySpeed,
usePlayButtonStore,
} from "../../../../../store/usePlayButtonStore";
import { usePathManager } from "../../pathCreator/function/usePathManager";
import { useCreatedPaths } from "../../../../../store/builder/store";
interface VehicleAnimatorProps {
vehiclesData: VehicleStructure[];
}
type ManagerData = {
pathId: string;
vehicleId: string;
};
export default function VehicleAnimator2({
vehiclesData,
}: VehicleAnimatorProps) {
const [managerData, setManagerData] = useState<ManagerData>();
const { scene } = useThree();
const { speed } = useAnimationPlaySpeed();
const { isPlaying } = usePlayButtonStore();
const { paths, allPaths, setAllPaths } = useCreatedPaths();
const vehicleMovementState = useRef<
Record<
string,
{
index: number;
progress: number;
hasStarted: boolean;
pathIndex: number;
pointIndex: number;
}
>
>({});
const managerRef = useRef<ManagerData>();
// Initialize all paths into allPaths store
useEffect(() => {
if (!paths || paths.length === 0) return;
const newPaths = useCreatedPaths.getState().paths.map((val: any) => ({
pathId: val.pathId,
isAvailable: true,
vehicleId: null,
}));
const merged = [...useCreatedPaths.getState().allPaths];
newPaths.forEach((p: any) => {
if (!merged.find((m) => m.pathId === p.pathId)) {
merged.push(p);
}
});
if (merged.length !== useCreatedPaths.getState().allPaths.length) {
setAllPaths(merged);
}
}, [paths, allPaths, setAllPaths]);
useFrame((_, delta) => {
if (!isPlaying) return;
vehiclesData.forEach((vehicle) => {
const { vehicleId, route } = vehicle;
if (!route || route.length === 0) return;
const mesh = scene.getObjectByProperty("uuid", vehicleId);
if (!mesh) return;
const uuid = vehicleId;
// ✅ Initialize state if not already
if (!vehicleMovementState.current[uuid]) {
vehicleMovementState.current[uuid] = {
index: 0,
pointIndex: 0,
pathIndex: 0,
progress: 0,
hasStarted: false,
};
}
const state = vehicleMovementState.current[uuid];
let currentPath = route[state.pathIndex];
if (
!currentPath ||
!currentPath.pathPoints ||
currentPath.pathPoints.length < 2
)
return;
let pathPoints = currentPath.pathPoints;
const pathStart = new Vector3(...pathPoints[0].position);
/**
* 🟢 STEP 1: Move vehicle to the starting point of its first path
*/
if (!state.hasStarted) {
const distanceToStart = mesh.position.distanceTo(pathStart);
const step = speed * delta;
if (distanceToStart <= step) {
mesh.position.copy(pathStart);
mesh.quaternion.identity();
state.hasStarted = true;
return;
}
const direction = pathStart.clone().sub(mesh.position).normalize();
mesh.position.add(direction.clone().multiplyScalar(step));
const forward = new Vector3(0, 0, 1);
const targetQuat = new Quaternion().setFromUnitVectors(
forward,
direction
);
mesh.quaternion.slerp(targetQuat, 0.1);
return;
}
/**
* 🟢 STEP 2: Move along path once at start
*/
const currentPoint = new Vector3(
...pathPoints[state.pointIndex].position
);
const nextPoint = new Vector3(
...pathPoints[state.pointIndex + 1].position
);
const segmentVector = new Vector3().subVectors(nextPoint, currentPoint);
const segmentLength = segmentVector.length();
const direction = segmentVector.clone().normalize();
const moveDistance = speed * delta;
state.progress += moveDistance / segmentLength;
if (state.progress >= 1) {
state.pointIndex++;
state.progress = 0;
if (state.pointIndex >= pathPoints.length - 1) {
state.pathIndex++;
state.pointIndex = 0;
if (state.pathIndex >= route.length) {
return;
}
currentPath = route[state.pathIndex];
pathPoints = currentPath.pathPoints;
}
}
// Interpolate position
const newPos = new Vector3().lerpVectors(
new Vector3(...pathPoints[state.pointIndex].position),
new Vector3(...pathPoints[state.pointIndex + 1].position),
state.progress
);
mesh.position.copy(newPos);
// Smooth rotation
const forward = new Vector3(0, 0, 1);
const targetQuat = new Quaternion().setFromUnitVectors(
forward,
direction
);
mesh.quaternion.slerp(targetQuat, 0.1);
const pathCheck = handlePathCheck(currentPath.pathId, vehicleId);
console.log("pathCheck: ", pathCheck);
//
// 🟢 Log current pathId while moving
// console.log(
// `🚗 Vehicle ${uuid} moving on pathId: ${currentPath.pathId}`,
// "→",
// newPos.toArray()
// );
});
});
// const handlePathCheck = (pathId: string, vehicleId: string) => {
// const { paths, allPaths, setAllPaths } = useCreatedPaths.getState();
// const normalize = (v: any) => String(v ?? "").trim();
// // Find path
// const path = paths.find(
// (p: any) => normalize(p.pathId) === normalize(pathId)
// );
// if (!path) {
//
// return false;
// }
// // If path already reserved → always reject
// if (!path.isAvailable) {
// console.log(
// `🚨 Path ${pathId} is already reserved by vehicle ${vehicleId}`
// );
// return false;
// }
//
// // Reserve the path for this vehicle
// const updated = allPaths.map((p: any) =>
// normalize(p.pathId) === normalize(pathId)
// ? { ...p, vehicleId, isAvailable: false }
// : p
// );
// setAllPaths(updated);
//
// return true;
// };
const handlePathCheck = (pathId: string, vehicleId: string) => {
const normalize = (v: any) => String(v ?? "").trim();
// Find path
const path = useCreatedPaths
.getState()
.allPaths.find((p: any) => normalize(p.pathId) === normalize(pathId));
if (!path) {
return false;
}
// If path already reserved → reject always
if (!path.isAvailable) {
// console.warn(
// `🚨 Path ${pathId} is already reserved by vehicle ${vehicleId}`
// );
echo.warn(`Path ${pathId}`);
return false;
} else {
console.log("path is reserved");
}
// Reserve the path properly with vehicleId
const updated = allPaths.map((p: any) =>
normalize(p.pathId) === normalize(pathId)
? { ...p, vehicleId, isAvailable: false }
: p
);
console.log("updated: ", updated);
setAllPaths(updated);
return true;
};
// const manager = usePathManager(
// managerRef.current?.pathId,
// managerRef.current?.vehicleId
// );
return null;
}
// const handlePathCheck = (pathId: string, vehicleId: string) => {
// const allPaths = useCreatedPaths.getState().allPaths;
// // find the path were checking
// const path = allPaths.find((p: any) => p.pathId === pathId);
// if (!path) {
//
// return false;
// }
// // if path is available, update it with vehicleId and mark unavailable
// if (path.isAvailable) {
// const updated = allPaths.map((p: any) =>
// p.pathId === pathId ? { ...p, vehicleId, isAvailable: false } : p
// );
//
// //
// // setAllPaths(updated); // uncomment if you want to persist update
// return true; // path was available
// }
//
// return false; // path not available
// };
// useEffect(() => {
//
// if (managerRef.current) {
//
// }
// }, [manager, managerRef.current]);

View File

@@ -0,0 +1,271 @@
import React, { useEffect, useMemo, useRef, useState } from "react";
import { useCreatedPaths } from "../../../../../store/builder/store";
import { useThree } from "@react-three/fiber";
import { Vector3 } from "three";
import { useSceneContext } from "../../../../scene/sceneContext";
import { useProductContext } from "../../../products/productContext";
import { useSelectedEventSphere } from "../../../../../store/simulation/useSimulationStore";
import VehicleAnimator2 from "../animator/vehicleAnimator2";
function dist(a: PointData, b: PointData): number {
return Math.sqrt(
(a.position[0] - b.position[0]) ** 2 +
(a.position[1] - b.position[1]) ** 2 +
(a.position[2] - b.position[2]) ** 2
);
}
type SegmentPoint = {
position: Vector3;
originalPoint?: PointData;
pathId?: string;
startId?: string;
endId?: string;
};
/** --- A* Algorithm --- */
type AStarResult = {
pointIds: string[];
distance: number;
};
function aStarShortestPath(
startId: string,
goalId: string,
points: PointData[],
paths: PathData
): AStarResult | null {
const pointById = new Map(points.map((p) => [p.pointId, p]));
const start = pointById.get(startId);
const goal = pointById.get(goalId);
if (!start || !goal) return null;
const openSet = new Set<string>([startId]);
const cameFrom: Record<string, string | null> = {};
const gScore: Record<string, number> = {};
const fScore: Record<string, number> = {};
for (const p of points) {
cameFrom[p.pointId] = null;
gScore[p.pointId] = Infinity;
fScore[p.pointId] = Infinity;
}
gScore[startId] = 0;
fScore[startId] = dist(start, goal);
const neighborsOf = (id: string): { id: string; cost: number }[] => {
const me = pointById.get(id)!;
const out: { id: string; cost: number }[] = [];
for (const edge of paths) {
const [a, b] = edge.pathPoints;
if (a.pointId === id) out.push({ id: b.pointId, cost: dist(me, b) });
else if (b.pointId === id) out.push({ id: a.pointId, cost: dist(me, a) });
}
return out;
};
while (openSet.size > 0) {
let current: string = [...openSet].reduce((a, b) =>
fScore[a] < fScore[b] ? a : b
);
if (current === goalId) {
const ids: string[] = [];
let node: string | null = current;
while (node) {
ids.unshift(node);
node = cameFrom[node];
}
return { pointIds: ids, distance: gScore[goalId] };
}
openSet.delete(current);
for (const nb of neighborsOf(current)) {
const tentativeG = gScore[current] + nb.cost;
if (tentativeG < gScore[nb.id]) {
cameFrom[nb.id] = current;
gScore[nb.id] = tentativeG;
fScore[nb.id] = tentativeG + dist(pointById.get(nb.id)!, goal);
openSet.add(nb.id);
}
}
}
return null;
}
/** --- Convert node path to edges --- */
function nodePathToEdges(
pointIds: string[],
points: PointData[],
paths: PathData
): PathData {
const byId = new Map(points.map((p) => [p.pointId, p]));
const edges: PathData = [];
for (let i = 0; i < pointIds.length - 1; i++) {
const a = pointIds[i];
const b = pointIds[i + 1];
const edge = paths.find(
(p) =>
(p.pathPoints[0].pointId === a && p.pathPoints[1].pointId === b) ||
(p.pathPoints[0].pointId === b && p.pathPoints[1].pointId === a)
);
if (edge) {
const [p1, p2] = edge.pathPoints;
edges.push({
pathId: edge.pathId,
pathPoints:
p1.pointId === a
? ([p1, p2] as [PointData, PointData])
: ([p2, p1] as [PointData, PointData]),
});
} else {
const pa = byId.get(a)!;
const pb = byId.get(b)!;
edges.push({
pathId: `synthetic-${a}-${b}`,
pathPoints: [pa, pb],
});
}
}
return edges;
}
interface VehicleInstanceProps {
vehicleData: VehicleStructure;
vehiclesData: VehicleStructure[];
setVehiclesData: React.Dispatch<React.SetStateAction<VehicleStructure[]>>;
}
export default function VehicleInstance2({
vehicleData,
vehiclesData,
setVehiclesData,
}: VehicleInstanceProps) {
const { paths, setPaths } = useCreatedPaths();
const { vehicleStore, productStore } = useSceneContext();
const { vehicles, getVehicleById } = vehicleStore();
const { selectedProductStore } = useProductContext();
const { selectedProduct } = selectedProductStore();
const { updateEvent, updateAction } = productStore();
const { selectedEventSphere } = useSelectedEventSphere();
const { scene, gl, raycaster } = useThree();
const [selected, setSelected] = useState<any>([]);
const allPoints = useMemo(() => {
const points: PointData[] = [];
const seen = new Set<string>();
useCreatedPaths.getState().paths?.forEach((path: PathDataInterface) => {
path.pathPoints.forEach((p) => {
if (!seen.has(p.pointId)) {
seen.add(p.pointId);
points.push(p);
}
});
});
return points;
}, [paths]);
const vehiclesDataRef = useRef(vehiclesData);
const selectedEventSphereRef = useRef(selectedEventSphere);
useEffect(() => {
vehiclesDataRef.current = vehiclesData;
}, [vehiclesData]);
useEffect(() => {
selectedEventSphereRef.current = selectedEventSphere;
}, [selectedEventSphere]);
const handleContextMenu = (e: any) => {
const intersectObject = raycaster.intersectObjects(scene.children);
if (intersectObject.length > 0) {
const pathPoint = intersectObject[0].object;
if (pathPoint.name === "Path-Point") {
const point: any = pathPoint.userData;
const pointIndex = allPoints.findIndex(
(p) => p.pointId === point.pointId
);
if (pointIndex === -1) return;
setSelected((prev: any) => {
if (prev.length === 0) {
return [pointIndex];
}
if (prev.length === 1) {
const prevPoint = allPoints[prev[0]];
const newPoint = allPoints[pointIndex];
//
//
if (prevPoint.pointId === newPoint.pointId) return prev;
const result = aStarShortestPath(
prevPoint.pointId,
newPoint.pointId,
allPoints,
paths
);
if (result) {
const edges = nodePathToEdges(result.pointIds, allPoints, paths);
setTimeout(() => {
const modelUuid = selectedEventSphere?.userData?.modelUuid;
// const index = vehiclesData.findIndex(
// (v) => v.vehicleId === modelUuid
// );
// if (index !== -1) {
// const updatedVehicles = [...vehiclesData];
// updatedVehicles[index] = {
// ...updatedVehicles[index],
// startPoint: prevPoint.position,
// endPoint: newPoint.position,
// route: edges,
// };
//
// setVehiclesData(updatedVehicles);
// }
// }, 0);
const index = vehiclesDataRef.current.findIndex(
(v) => v.vehicleId === modelUuid
);
if (index !== -1) {
const updatedVehicles = [...vehiclesDataRef.current];
updatedVehicles[index] = {
...vehiclesDataRef.current[index],
startPoint: prevPoint.position,
endPoint: newPoint.position,
route: edges,
};
setVehiclesData(updatedVehicles);
}
}, 0);
}
return [prev[0], pointIndex];
}
return [pointIndex];
});
}
}
};
useEffect(() => {
const canvasElement = gl.domElement;
canvasElement.addEventListener("contextmenu", handleContextMenu);
console.log("vehiclesDataRef.current: ", vehiclesDataRef.current);
return () => {
canvasElement.removeEventListener("contextmenu", handleContextMenu);
};
}, [raycaster, setVehiclesData, vehiclesData, selectedEventSphere]);
return <VehicleAnimator2 vehiclesData={vehiclesDataRef.current} />;
}

View File

@@ -1,21 +1,44 @@
import React from "react";
import React, { useEffect, useState } from "react";
import VehicleInstance from "./instance/vehicleInstance";
import VehicleContentUi from "../../ui3d/VehicleContentUi";
import { useSceneContext } from "../../../scene/sceneContext";
import { useViewSceneStore } from "../../../../store/builder/store";
import PathCreator from "../pathCreator/pathCreator";
import VehicleInstance2 from "./instance/vehicleInstance2";
function VehicleInstances() {
const { vehicleStore } = useSceneContext();
const { vehicles } = vehicleStore();
const { viewSceneLabels } = useViewSceneStore();
const [vehiclesData, setVehiclesData] = useState<VehicleStructure[]>([]);
useEffect(() => {
const updatedVehicles = vehicles.map((val) => ({
vehicleId: val.modelUuid,
position: val.position,
rotation: val.rotation,
startPoint: null,
endPoint: null,
selectedPointId: val.point.uuid,
}));
setVehiclesData(updatedVehicles);
}, []);
return (
<>
{vehicles.map((vehicle: VehicleStatus) => (
{/* {vehicles.map((vehicle: VehicleStatus) => (
<React.Fragment key={vehicle.modelUuid}>
<VehicleInstance agvDetail={vehicle} />
{viewSceneLabels && <VehicleContentUi vehicle={vehicle} />}
</React.Fragment>
))} */}
{vehiclesData.map((vehicle: VehicleStructure) => (
<React.Fragment key={vehicle.vehicleId}>
<VehicleInstance2
vehicleData={vehicle}
vehiclesData={vehiclesData}
setVehiclesData={setVehiclesData}
/>
</React.Fragment>
))}
</>
);

View File

@@ -0,0 +1,13 @@
export const getPathPointByPoints = (point: any, paths: any) => {
for (const path of paths) {
if (
(path.pathPoints[0].pointId === point[0].pointId ||
path.pathPoints[1].pointId === point[0].pointId) &&
(path.pathPoints[0].pointId === point[1].pointId ||
path.pathPoints[1].pointId === point[1].pointId)
) {
return path;
}
}
return undefined;
};

View File

@@ -0,0 +1,48 @@
import { useCallback } from "react";
type PointData = {
pointId: string;
position: [number, number, number];
isCurved?: boolean;
handleA?: [number, number, number] | null;
handleB?: [number, number, number] | null;
};
interface PathDataInterface {
pathId: string;
isActive?: boolean;
isCurved?: boolean;
pathPoints: [PointData, PointData];
}
type PathData = PathDataInterface[];
type setPathPositionProps = (
pointUuid: string,
position: [number, number, number],
setPaths: React.Dispatch<React.SetStateAction<PathData>>
) => void;
export const getPathsByPointId = (pointId: any, paths: PathData) => {
return paths.filter((a) => a.pathPoints.some((p) => p.pointId === pointId));
};
export const setPathPosition = (
pointUuid: string,
position: [number, number, number],
setPaths: React.Dispatch<React.SetStateAction<PathData>>,
paths: PathData
) => {
const newPaths = paths.map((path: any) => {
if (path?.pathPoints.some((p: any) => p.pointId === pointUuid)) {
return {
...path,
pathPoints: path.pathPoints.map((p: any) =>
p.pointId === pointUuid ? { ...p, position } : p
) as [PointData, PointData], // 👈 force back to tuple
};
}
return path;
});
setPaths(newPaths);
};

View File

@@ -0,0 +1,49 @@
import { useEffect, useMemo } from "react";
import { useCreatedPaths } from "../../../../../store/builder/store";
export const usePathManager = (pathId?: string, vehicleId?: string) => {
const { paths, allPaths, setAllPaths } = useCreatedPaths();
// Initialize all paths into allPaths store
useEffect(() => {
if (!paths || paths.length === 0) return;
const newPaths = paths.map((val: any) => ({
pathId: val.pathId,
isAvailable: true,
vehicleId: null,
}));
const merged = [...allPaths];
newPaths.forEach((p: any) => {
if (!merged.find((m) => m.pathId === p.pathId)) {
merged.push(p);
}
});
if (merged.length !== allPaths.length) {
setAllPaths(merged);
}
}, [paths, allPaths, setAllPaths]);
// Assign vehicle to a path
useEffect(() => {
if (!pathId || !vehicleId) return;
const updated = allPaths.map((p: any) =>
p.pathId === pathId ? { ...p, vehicleId, isAvailable: false } : p
);
const hasChanged = JSON.stringify(updated) !== JSON.stringify(allPaths);
if (hasChanged) {
setAllPaths(updated);
}
}, [pathId, vehicleId, allPaths, setAllPaths]);
// ✅ return true if path exists & isAvailable, else false
return useMemo(() => {
if (!pathId) return false;
const path = allPaths.find((p: any) => p.pathId === pathId);
return path ? path.isAvailable : false;
}, [pathId, allPaths]);
};

View File

@@ -0,0 +1,428 @@
import { DragControls, Line } from "@react-three/drei";
import { useFrame, useThree } from "@react-three/fiber";
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { LineCurve3, MathUtils, Plane, Vector3 } from "three";
import {
useActiveTool,
useCreatedPaths,
useToolMode,
} from "../../../../store/builder/store";
import PointHandler from "./pointHandler";
import { getPathPointByPoints } from "./function/getPathPointByPoints";
import PathHandler from "./pathHandler";
type PointData = {
pointId: string;
position: [number, number, number];
isCurved?: boolean;
handleA?: [number, number, number] | null;
handleB?: [number, number, number] | null;
};
interface PathDataInterface {
pathId: string;
isActive?: boolean;
isCurved?: boolean;
pathPoints: [PointData, PointData];
}
type PathData = PathDataInterface[];
export default function PathCreator() {
const { paths, setPaths } = useCreatedPaths();
const { activeTool } = useActiveTool();
const { toolMode } = useToolMode();
const [draftPoints, setDraftPoints] = useState<PointData[]>([]);
const [mousePos, setMousePos] = useState<[number, number, number] | null>(
null
);
const [snappedPosition, setSnappedPosition] = useState<
[number, number, number] | null
>(null);
const [snappedPoint, setSnappedPoint] = useState<PointData | null>(null);
const finalPosition = useRef<[number, number, number] | null>(null);
const [hoveredLine, setHoveredLine] = useState<PathDataInterface | null>(
null
);
const [hoveredPoint, setHoveredPoint] = useState<PointData | null>(null);
const [pathPointsList, setPathPointsList] = useState<PointData[]>([]);
const [selectedPointIndices, setSelectedPointIndices] = useState<number[]>(
[]
);
const plane = useMemo(() => new Plane(new Vector3(0, 1, 0), 0), []);
const { scene, raycaster, gl } = useThree();
const POINT_SNAP_THRESHOLD = 0.5;
const CAN_POINT_SNAP = true;
const getAllOtherPathPoints = useCallback((): PointData[] => {
if (draftPoints.length === 0) return [];
return (
paths?.flatMap((path: any) =>
path.pathPoints.filter(
(pt: PointData) => pt.pointId !== draftPoints[0].pointId
)
) ?? []
);
}, [paths, draftPoints]);
useEffect(() => {
const stored = localStorage.getItem("paths");
setPaths(stored ? JSON.parse(stored) : []);
}, []);
const snapPathPoint = useCallback(
(position: [number, number, number]) => {
if (draftPoints.length === 0 || !CAN_POINT_SNAP) {
return {
position,
isSnapped: false,
snappedPoint: null as PointData | null,
};
}
const otherPoints = getAllOtherPathPoints();
const currentVec = new Vector3(...position);
for (const point of otherPoints) {
const pointVec = new Vector3(...point.position);
const distance = currentVec.distanceTo(pointVec);
if (distance <= POINT_SNAP_THRESHOLD) {
return {
position: point.position,
isSnapped: true,
snappedPoint: point,
};
}
}
return { position, isSnapped: false, snappedPoint: null };
},
[draftPoints, getAllOtherPathPoints]
);
// ---- RAYCAST ----
useFrame(() => {
const intersectionPoint = new Vector3();
raycaster.ray.intersectPlane(plane, intersectionPoint);
if (!intersectionPoint) return;
const snapped = snapPathPoint([
intersectionPoint.x,
intersectionPoint.y,
intersectionPoint.z,
]);
if (snapped.isSnapped && snapped.snappedPoint) {
finalPosition.current = snapped.position;
setSnappedPosition(snapped.position);
setSnappedPoint(snapped.snappedPoint);
} else {
finalPosition.current = [
intersectionPoint.x,
intersectionPoint.y,
intersectionPoint.z,
];
setSnappedPosition(null);
setSnappedPoint(null);
}
if (!finalPosition.current) return;
const paths: [PointData, PointData] = [
draftPoints[0],
{ pointId: "temp-point", position: finalPosition.current },
];
});
const getPathPointById = (uuid: any) => {
for (const path of paths) {
const point = path.pathPoints.find((p: PointData) => p.pointId === uuid);
if (point) return point;
}
return undefined;
};
const handleClick = (e: any) => {
if (activeTool !== "pen") return;
if (toolMode === "3D-Delete") return;
if (e.ctrlKey) return;
const intersectionPoint = new Vector3();
const pos = raycaster.ray.intersectPlane(plane, intersectionPoint);
if (!pos) return;
const pointIntersect = raycaster
.intersectObjects(scene.children)
.find((intersect) => intersect.object.name === "Path-Point");
const pathIntersect = raycaster
.intersectObjects(scene.children)
.find((intersect) => intersect.object.name === "Path-Line");
// --- Case 1: Split path ---
if (!pointIntersect && pathIntersect) {
const hitLine = pathIntersect.object;
const clickedPath = getPathPointByPoints(
hitLine.userData.pathPoints,
paths
);
if (clickedPath) {
const hitPath = paths.find(
(p: PathDataInterface) => p.pathId === clickedPath.pathId
);
if (!hitPath) return;
const [p1, p2] = clickedPath.pathPoints;
const point1Vec = new Vector3(...p1.position);
const point2Vec = new Vector3(...p2.position);
// Project clicked point onto line
const lineDir = new Vector3()
.subVectors(point2Vec, point1Vec)
.normalize();
const point1ToClick = new Vector3().subVectors(
pathIntersect.point,
point1Vec
);
const dot = point1ToClick.dot(lineDir);
const projection = new Vector3()
.copy(lineDir)
.multiplyScalar(dot)
.add(point1Vec);
const lineLength = point1Vec.distanceTo(point2Vec);
let t = point1Vec.distanceTo(projection) / lineLength;
t = Math.max(0, Math.min(1, t));
const closestPoint = new Vector3().lerpVectors(point1Vec, point2Vec, t);
// const filteredPath = paths.filter(
// (p: PathDataInterface) =>
// String(p.pathId).trim() !== String(clickedPath.pathId).trim()
// );
// setPaths(filteredPath);
const filteredPath = useCreatedPaths
.getState()
.paths.filter(
(p: PathDataInterface) =>
String(p.pathId).trim() !== String(clickedPath.pathId).trim()
);
setPaths(filteredPath);
const point1: PointData = {
pointId: clickedPath?.pathPoints[0].pointId,
position: clickedPath?.pathPoints[0].position,
};
const point2: PointData = {
pointId: clickedPath?.pathPoints[1].pointId,
position: clickedPath?.pathPoints[1].position,
};
const splitPoint: PointData = {
pointId: MathUtils.generateUUID(),
position: closestPoint.toArray() as [number, number, number],
};
if (draftPoints.length === 0) {
const path1: PathDataInterface = {
pathId: MathUtils.generateUUID(),
pathPoints: [point1, splitPoint],
};
const path2: PathDataInterface = {
pathId: MathUtils.generateUUID(),
pathPoints: [point2, splitPoint],
};
setDraftPoints([splitPoint]);
// Instead of relying on "paths" from the component:
setPaths([
...useCreatedPaths.getState().paths, // 👈 always current from store
path1,
path2,
]);
// setPaths([...paths, path1, path2]);
} else {
const newPath: PathDataInterface = {
pathId: MathUtils.generateUUID(),
pathPoints: [draftPoints[0], splitPoint],
};
const firstPath: PathDataInterface = {
pathId: MathUtils.generateUUID(),
pathPoints: [point1, splitPoint],
};
const secondPath: PathDataInterface = {
pathId: MathUtils.generateUUID(),
pathPoints: [point2, splitPoint],
};
setPaths([
...useCreatedPaths.getState().paths,
newPath,
firstPath,
secondPath,
]);
setDraftPoints([splitPoint]);
}
return;
}
}
const newPoint: PointData = {
pointId: MathUtils.generateUUID(),
position: [pos.x, pos.y, pos.z],
};
// --- Case 2: Normal path creation ---
let clickedPoint: PointData | null = null;
for (const pt of allPoints) {
if (new Vector3(...pt.position).distanceTo(pos) <= POINT_SNAP_THRESHOLD) {
clickedPoint = pt;
break;
}
}
if (snappedPosition && snappedPoint) {
newPoint.pointId = snappedPoint.pointId;
newPoint.position = snappedPosition;
}
if (snappedPoint && snappedPoint.pointId == draftPoints[0]?.pointId) {
return;
}
if (snappedPosition && !snappedPoint) {
newPoint.position = snappedPosition;
}
if (pointIntersect && !snappedPoint) {
const point = getPathPointById(pointIntersect.object.userData.pointId);
if (point) {
newPoint.pointId = point.pointId;
newPoint.position = point.position;
}
}
setPathPointsList((prev) => {
if (!prev.find((p) => p.pointId === newPoint.pointId))
return [...prev, newPoint];
return prev;
});
if (draftPoints.length === 0) {
setDraftPoints([newPoint]);
} else {
const newPath: PathDataInterface = {
pathId: MathUtils.generateUUID(),
pathPoints: [draftPoints[0], newPoint],
};
setPaths([...useCreatedPaths.getState().paths, newPath]);
setDraftPoints([newPoint]);
}
};
const handleContextMenu = (event: any) => {
event.preventDefault();
setDraftPoints([]);
};
const handleMouseMove = () => {
const intersectionPoint = new Vector3();
const pos = raycaster.ray.intersectPlane(plane, intersectionPoint);
if (pos) {
setMousePos([pos.x, pos.y, pos.z]);
}
};
useEffect(() => {
const canvasElement = gl.domElement;
canvasElement.addEventListener("click", handleClick);
canvasElement.addEventListener("mousemove", handleMouseMove);
canvasElement.addEventListener("contextmenu", handleContextMenu);
return () => {
canvasElement.removeEventListener("click", handleClick);
canvasElement.removeEventListener("mousemove", handleMouseMove);
canvasElement.removeEventListener("contextmenu", handleContextMenu);
};
}, [gl, draftPoints, paths, toolMode]);
const allPoints = useMemo(() => {
const points: PointData[] = [];
const seen = new Set<string>();
paths?.forEach((path: PathDataInterface) => {
path.pathPoints.forEach((p) => {
if (!seen.has(p.pointId)) {
seen.add(p.pointId);
points.push(p);
}
});
});
return points;
}, [paths]);
useEffect(() => {
localStorage.setItem("paths", JSON.stringify(paths));
console.log("paths: ", paths);
}, [paths]);
return (
<>
{/* Draft points (red) */}
{draftPoints.map((point) => (
<mesh key={point.pointId} position={point.position}>
<sphereGeometry args={[0.2, 16, 16]} />
<meshBasicMaterial color="red" />
</mesh>
))}
{/* Saved points */}
{allPoints.map((point: PointData, i: any) => (
<PointHandler
points={allPoints}
pointIndex={i}
key={point.pointId}
point={point}
setPaths={setPaths}
paths={paths}
setHoveredPoint={setHoveredPoint}
hoveredLine={hoveredLine}
hoveredPoint={hoveredPoint}
selected={selectedPointIndices}
setSelected={setSelectedPointIndices}
/>
))}
{/* Preview line */}
{draftPoints.length > 0 && mousePos && (
<Line
points={[draftPoints[0].position, mousePos]}
color="orange"
lineWidth={2}
dashed
/>
)}
{/* Permanent paths */}
{paths.map((path: PathDataInterface) => (
<PathHandler
key={path.pathId}
selectedPath={path}
setPaths={setPaths}
paths={paths}
points={path.pathPoints}
setHoveredLine={setHoveredLine}
hoveredLine={hoveredLine}
hoveredPoint={hoveredPoint}
/>
))}
</>
);
}

View File

@@ -0,0 +1,189 @@
import { DragControls, Line } from "@react-three/drei";
import React, { useMemo, useState } from "react";
import { useActiveTool, useToolMode } from "../../../../store/builder/store";
import { useThree } from "@react-three/fiber";
import { LineCurve3, Plane, Vector3 } from "three";
import { getPathsByPointId, setPathPosition } from "./function/getPaths";
import { handleCanvasCursors } from "../../../../utils/mouseUtils/handleCanvasCursors";
type PointData = {
pointId: string;
position: [number, number, number];
isCurved?: boolean;
handleA?: [number, number, number] | null;
handleB?: [number, number, number] | null;
};
interface PathDataInterface {
pathId: string;
isActive?: boolean;
isCurved?: boolean;
pathPoints: [PointData, PointData];
}
type PathData = PathDataInterface[];
type PathHandlerProps = {
selectedPath: PathDataInterface;
points: [PointData, PointData];
paths: PathData;
setPaths: React.Dispatch<React.SetStateAction<PathData>>;
setHoveredLine: React.Dispatch<
React.SetStateAction<PathDataInterface | null>
>;
hoveredLine: PathDataInterface | null;
hoveredPoint: PointData | null;
};
export default function PathHandler({
selectedPath,
setPaths,
points,
paths,
setHoveredLine,
hoveredLine,
hoveredPoint,
}: PathHandlerProps) {
const { toolMode } = useToolMode();
const { scene, raycaster } = useThree();
const [dragOffset, setDragOffset] = useState<Vector3 | null>(null);
const [initialPositions, setInitialPositions] = useState<{
paths?: any;
}>({});
const [isHovered, setIsHovered] = useState(false);
const { activeTool } = useActiveTool();
const plane = useMemo(() => new Plane(new Vector3(0, 1, 0), 0), []);
const path = useMemo(() => {
const [start, end] = points.map((p) => new Vector3(...p.position));
return new LineCurve3(start, end);
}, [points]);
const removePath = (pathId: string) => {
setPaths((prevPaths) => prevPaths.filter((p) => p.pathId !== pathId));
};
const handlePathClick = (pointId: string) => {
if (toolMode === "3D-Delete") {
removePath(pointId);
}
};
const handleDragStart = (points: [PointData, PointData]) => {
if (activeTool !== "cursor") return;
const intersectionPoint = new Vector3();
const hit = raycaster.ray.intersectPlane(plane, intersectionPoint);
if (hit) {
const start = new Vector3(...points[0].position);
const end = new Vector3(...points[1].position);
const midPoint = new Vector3().addVectors(start, end).multiplyScalar(0.5);
const offset = new Vector3().subVectors(midPoint, hit);
setDragOffset(offset);
const pathSet = getPathsByPointId(points[0].pointId, paths);
setInitialPositions({ paths: pathSet });
}
};
// const handleDrag = (points: [PointData, PointData]) => {
// if (isHovered && dragOffset) {
// const intersectionPoint = new Vector3();
// const hit = raycaster.ray.intersectPlane(plane, intersectionPoint);
// if (hit) {
// handleCanvasCursors("grabbing");
// const positionWithOffset = new Vector3().addVectors(hit, dragOffset);
// const start = new Vector3(...points[0].position);
// const end = new Vector3(...points[1].position);
// const midPoint = new Vector3()
// .addVectors(start, end)
// .multiplyScalar(0.5);
// const delta = new Vector3().subVectors(positionWithOffset, midPoint);
// const newStart = new Vector3().addVectors(start, delta);
// const newEnd = new Vector3().addVectors(end, delta);
// setPathPosition(
// points[0].pointId,
// [newStart.x, newStart.y, newStart.z],
// setPaths
// );
// setPathPosition(
// points[1].pointId,
// [newEnd.x, newEnd.y, newEnd.z],
// setPaths
// );
// }
// }
// };
const handleDrag = (points: [PointData, PointData]) => {
if (isHovered && dragOffset) {
const intersectionPoint = new Vector3();
const hit = raycaster.ray.intersectPlane(plane, intersectionPoint);
if (hit) {
handleCanvasCursors("grabbing");
const positionWithOffset = new Vector3().addVectors(hit, dragOffset);
const start = new Vector3(...points[0].position);
const end = new Vector3(...points[1].position);
const midPoint = new Vector3()
.addVectors(start, end)
.multiplyScalar(0.5);
const delta = new Vector3().subVectors(positionWithOffset, midPoint);
const newStart: [number, number, number] = [
start.x + delta.x,
start.y + delta.y,
start.z + delta.z,
];
const newEnd: [number, number, number] = [
end.x + delta.x,
end.y + delta.y,
end.z + delta.z,
];
}
}
};
const handleDragEnd = (points: [PointData, PointData]) => {};
return (
<>
<DragControls
axisLock="y"
autoTransform={false}
onDragStart={() => handleDragStart(points)}
onDrag={() => handleDrag(points)}
onDragEnd={() => handleDragEnd(points)}
>
<Line
name="Path-Line"
key={selectedPath.pathId}
points={[points[0].position, points[1].position]}
color="purple"
lineWidth={5}
userData={selectedPath}
onClick={(e) => {
e.stopPropagation();
handlePathClick(selectedPath.pathId);
}}
onPointerOver={(e) => {
if (e.buttons === 0 && !e.ctrlKey) {
setHoveredLine(selectedPath);
setIsHovered(true);
if (!hoveredPoint) {
// handleCanvasCursors("grab");
}
}
}}
onPointerOut={() => {
if (isHovered && hoveredLine) {
setHoveredLine(null);
if (!hoveredPoint) {
// handleCanvasCursors("default");
}
}
setIsHovered(false);
}}
/>
</DragControls>
</>
);
}

View File

@@ -0,0 +1,720 @@
import { DragControls, Line } from "@react-three/drei";
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import {
useActiveTool,
useCreatedPaths,
useToolMode,
} from "../../../../store/builder/store";
import { CubicBezierCurve3, Plane, Quaternion, Vector3 } from "three";
import { useFrame, useThree } from "@react-three/fiber";
import { handleCanvasCursors } from "../../../../utils/mouseUtils/handleCanvasCursors";
import { getPathsByPointId, setPathPosition } from "./function/getPaths";
import { aStar } from "../structuredPath/functions/aStar";
import {
useAnimationPlaySpeed,
usePlayButtonStore,
} from "../../../../store/usePlayButtonStore";
import { useSceneContext } from "../../../scene/sceneContext";
import { usePathManager } from "./function/usePathManager";
import { useProductContext } from "../../products/productContext";
import { useSelectedEventSphere } from "../../../../store/simulation/useSimulationStore";
type PointData = {
pointId: string;
position: [number, number, number];
isCurved?: boolean;
handleA?: [number, number, number] | null;
handleB?: [number, number, number] | null;
neighbors?: string[];
};
interface PathDataInterface {
pathId: string;
isActive?: boolean;
isCurved?: boolean;
pathPoints: [PointData, PointData];
}
type PathData = PathDataInterface[];
type PointHandlerProps = {
point: PointData;
hoveredPoint: PointData | null;
setPaths: React.Dispatch<React.SetStateAction<PathData>>;
paths: PathDataInterface[];
setHoveredPoint: React.Dispatch<React.SetStateAction<PointData | null>>;
hoveredLine: PathDataInterface | null;
pointIndex: any;
points: PointData[];
selected: number[];
setSelected: React.Dispatch<React.SetStateAction<number[]>>;
};
function dist(a: PointData, b: PointData): number {
return Math.sqrt(
(a.position[0] - b.position[0]) ** 2 +
(a.position[1] - b.position[1]) ** 2 +
(a.position[2] - b.position[2]) ** 2
);
}
type SegmentPoint = {
position: Vector3;
originalPoint?: PointData;
pathId?: string;
startId?: string;
endId?: string;
};
/** --- A* Algorithm --- */
type AStarResult = {
pointIds: string[];
distance: number;
};
function aStarShortestPath(
startId: string,
goalId: string,
points: PointData[],
paths: PathData
): AStarResult | null {
const pointById = new Map(points.map((p) => [p.pointId, p]));
const start = pointById.get(startId);
const goal = pointById.get(goalId);
if (!start || !goal) return null;
const openSet = new Set<string>([startId]);
const cameFrom: Record<string, string | null> = {};
const gScore: Record<string, number> = {};
const fScore: Record<string, number> = {};
for (const p of points) {
cameFrom[p.pointId] = null;
gScore[p.pointId] = Infinity;
fScore[p.pointId] = Infinity;
}
gScore[startId] = 0;
fScore[startId] = dist(start, goal);
const neighborsOf = (id: string): { id: string; cost: number }[] => {
const me = pointById.get(id)!;
const out: { id: string; cost: number }[] = [];
for (const edge of paths) {
const [a, b] = edge.pathPoints;
if (a.pointId === id) out.push({ id: b.pointId, cost: dist(me, b) });
else if (b.pointId === id) out.push({ id: a.pointId, cost: dist(me, a) });
}
return out;
};
while (openSet.size > 0) {
let current: string = [...openSet].reduce((a, b) =>
fScore[a] < fScore[b] ? a : b
);
if (current === goalId) {
const ids: string[] = [];
let node: string | null = current;
while (node) {
ids.unshift(node);
node = cameFrom[node];
}
return { pointIds: ids, distance: gScore[goalId] };
}
openSet.delete(current);
for (const nb of neighborsOf(current)) {
const tentativeG = gScore[current] + nb.cost;
if (tentativeG < gScore[nb.id]) {
cameFrom[nb.id] = current;
gScore[nb.id] = tentativeG;
fScore[nb.id] = tentativeG + dist(pointById.get(nb.id)!, goal);
openSet.add(nb.id);
}
}
}
return null;
}
/** --- Convert node path to edges --- */
function nodePathToEdges(
pointIds: string[],
points: PointData[],
paths: PathData
): PathData {
const byId = new Map(points.map((p) => [p.pointId, p]));
const edges: PathData = [];
for (let i = 0; i < pointIds.length - 1; i++) {
const a = pointIds[i];
const b = pointIds[i + 1];
const edge = paths.find(
(p) =>
(p.pathPoints[0].pointId === a && p.pathPoints[1].pointId === b) ||
(p.pathPoints[0].pointId === b && p.pathPoints[1].pointId === a)
);
if (edge) {
const [p1, p2] = edge.pathPoints;
edges.push({
pathId: edge.pathId,
pathPoints:
p1.pointId === a
? ([p1, p2] as [PointData, PointData])
: ([p2, p1] as [PointData, PointData]),
});
} else {
const pa = byId.get(a)!;
const pb = byId.get(b)!;
edges.push({
pathId: `synthetic-${a}-${b}`,
pathPoints: [pa, pb],
});
}
}
return edges;
}
type VehicleDetails = {
vehicleId: string;
vehiclePosition: [number, number, number];
};
type Manager = {
pathId: string;
vehicleId: string;
};
export default function PointHandler({
point,
// setPaths,
// paths,
setHoveredPoint,
hoveredLine,
hoveredPoint,
pointIndex,
points,
setSelected,
selected,
}: PointHandlerProps) {
const { isPlaying } = usePlayButtonStore();
const [multiPaths, setMultiPaths] = useState<
{ id: number; path: PathData }[]
>([]);
const { vehicleStore, productStore } = useSceneContext();
const { vehicles, getVehicleById } = vehicleStore();
const { selectedProductStore } = useProductContext();
const { selectedProduct } = selectedProductStore();
const { updateEvent, updateAction } = productStore();
const { selectedEventSphere } = useSelectedEventSphere();
const pathIdRef = useRef(1); // To ensure unique incremental IDs
const { toolMode } = useToolMode();
const { activeTool } = useActiveTool();
const { scene, raycaster } = useThree();
const [isHovered, setIsHovered] = useState(false);
const [dragOffset, setDragOffset] = useState<Vector3 | null>(null);
const plane = useMemo(() => new Plane(new Vector3(0, 1, 0), 0), []);
const [initialPositions, setInitialPositions] = useState<{
paths?: any;
}>({});
const [shortestPaths, setShortestPaths] = useState<PathData>([]);
const POINT_SNAP_THRESHOLD = 0.5; // Distance threshold for snapping in meters
const [vehicleUuids, setVehicleUuids] = useState<any>();
const CAN_POINT_SNAP = true;
const CAN_ANGLE_SNAP = true;
const ANGLE_SNAP_DISTANCE_THRESHOLD = 0.5;
const [selectedPointIndices, setSelectedPointIndices] = useState<number[]>(
[]
);
const [shortestEdges, setShortestEdges] = useState<PathData>([]);
const { speed } = useAnimationPlaySpeed();
const { assetStore } = useSceneContext();
const { assets } = assetStore();
const vehicleMovementState = useRef<any>({});
const [activeVehicleIndex, setActiveVehicleIndex] = useState(0);
const [vehicleData, setVehicleData] = useState<VehicleDetails[]>([]);
const { paths, setPaths } = useCreatedPaths();
const [managerData, setManagerData] = useState<Manager>();
useEffect(() => {
const findVehicle = assets
.filter((val) => val.eventData?.type === "Vehicle")
?.map((val) => val.modelUuid);
const findVehicleDatas = assets
.filter((val) => val.eventData?.type === "Vehicle")
?.map((val) => val);
findVehicleDatas.forEach((val) => {
const vehicledId = val.modelUuid;
const vehiclePosition: [number, number, number] = val.position;
setVehicleData((prev) => [
...prev,
{ vehicleId: vehicledId, vehiclePosition },
]);
});
setVehicleUuids(findVehicle);
setActiveVehicleIndex(0); // Reset to first vehicle
vehicleMovementState.current = {};
findVehicle.forEach((uuid) => {
vehicleMovementState.current[uuid] = {
index: 0,
progress: 0,
hasStarted: false,
};
});
}, [assets]);
const removePathByPoint = (pointId: string): PathDataInterface[] => {
const removedPaths: PathDataInterface[] = [];
const newPaths = paths.filter((path: PathDataInterface) => {
const hasPoint = path.pathPoints.some(
(p: PointData) => p.pointId === pointId
);
if (hasPoint) {
removedPaths.push(JSON.parse(JSON.stringify(path))); // keep a copy
return false; // remove this path
}
return true; // keep this path
});
setPaths(newPaths);
return removedPaths;
};
const getConnectedPoints = (uuid: string): PointData[] => {
const connected: PointData[] = [];
for (const path of paths) {
for (const point of path.pathPoints) {
if (point.pointId === uuid) {
connected.push(
...path.pathPoints.filter((p: PointData) => p.pointId !== uuid)
);
}
}
}
return connected;
};
const snapPathAngle = useCallback(
(
newPosition: [number, number, number],
pointId: string
): {
position: [number, number, number];
isSnapped: boolean;
snapSources: Vector3[];
} => {
if (!pointId || !CAN_ANGLE_SNAP) {
return { position: newPosition, isSnapped: false, snapSources: [] };
}
const connectedPoints: PointData[] = getConnectedPoints(pointId) || [];
if (connectedPoints.length === 0) {
return {
position: newPosition,
isSnapped: false,
snapSources: [],
};
}
const newPos = new Vector3(...newPosition);
let closestX: { pos: Vector3; dist: number } | null = null;
let closestZ: { pos: Vector3; dist: number } | null = null;
for (const connectedPoint of connectedPoints) {
const cPos = new Vector3(...connectedPoint.position);
const xDist = Math.abs(newPos.x - cPos.x);
const zDist = Math.abs(newPos.z - cPos.z);
if (xDist < ANGLE_SNAP_DISTANCE_THRESHOLD) {
if (!closestX || xDist < closestX.dist) {
closestX = { pos: cPos, dist: xDist };
}
}
if (zDist < ANGLE_SNAP_DISTANCE_THRESHOLD) {
if (!closestZ || zDist < closestZ.dist) {
closestZ = { pos: cPos, dist: zDist };
}
}
}
const snappedPos = newPos.clone();
const snapSources: Vector3[] = [];
if (closestX) {
snappedPos.x = closestX.pos.x;
snapSources.push(closestX.pos.clone());
}
if (closestZ) {
snappedPos.z = closestZ.pos.z;
snapSources.push(closestZ.pos.clone());
}
const isSnapped = snapSources.length > 0;
return {
position: [snappedPos.x, snappedPos.y, snappedPos.z],
isSnapped,
snapSources,
};
},
[]
);
const getAllOtherPathPoints = useCallback((): PointData[] => {
return (
paths?.flatMap((path: PathDataInterface) =>
path.pathPoints.filter((pt: PointData) => pt.pointId !== point.pointId)
) ?? []
);
}, [paths]);
const snapPathPoint = useCallback(
(position: [number, number, number], pointId?: string) => {
if (!CAN_POINT_SNAP)
return { position: position, isSnapped: false, snappedPoint: null };
const otherPoints = getAllOtherPathPoints();
const currentVec = new Vector3(...position);
for (const point of otherPoints) {
const pointVec = new Vector3(...point.position);
const distance = currentVec.distanceTo(pointVec);
if (distance <= POINT_SNAP_THRESHOLD) {
return {
position: point.position,
isSnapped: true,
snappedPoint: point,
};
}
}
return { position: position, isSnapped: false, snappedPoint: null };
},
[getAllOtherPathPoints]
);
const handlePointClick = (e: any, point: PointData) => {
e.stopPropagation();
if (toolMode === "3D-Delete") {
removePathByPoint(point.pointId);
return;
}
};
let selectedVehiclePaths: Array<
Array<{
vehicleId: string;
pathId: string;
isActive?: boolean;
isCurved?: boolean;
pathPoints: [PointData, PointData];
}>
> = [];
function assignPathToSelectedVehicle(
selectedVehicleId: string,
currentPath: PathData
) {
const vehiclePathSegments = currentPath.map((path) => ({
vehicleId: selectedVehicleId,
...path,
}));
return selectedVehiclePaths.push(vehiclePathSegments);
}
const handleContextMenu = (e: any, point: PointData) => {
// if (e.shiftKey && e.button === 2) {
const pointIndex = points.findIndex((p) => p.pointId === point.pointId);
if (pointIndex === -1) {
return;
}
setSelected((prev) => {
if (prev.length === 0) {
return [pointIndex];
}
// if (prev.length === 1) {
// setTimeout(() => {
//
// const prevPoint = points[prev[0]];
//
// const newPoint = points[pointIndex];
//
// const result = aStarShortestPath(
// prevPoint.pointId,
// newPoint.pointId,
// points,
// paths
// );
// if (result) {
// const edges = nodePathToEdges(result.pointIds, points, paths);
//
// setShortestPaths(edges);
// setShortestEdges(edges);
// } else {
// setShortestPaths([]);
// setShortestEdges([]);
// }
// if (prevPoint.pointId === newPoint.pointId) {
// return prev;
// }
// }, 0);
// return [prev[0], pointIndex];
// }
// More than two points — reset
if (prev.length === 1) {
setTimeout(() => {
const prevPoint = points[prev[0]];
const newPoint = points[pointIndex];
console.log(
"selectedEventSphere?.userData.modelUuid: ",
selectedEventSphere?.userData.modelUuid
);
if (selectedEventSphere?.userData.modelUuid) {
const updatedVehicle = getVehicleById(
selectedEventSphere.userData.modelUuid
);
const startPoint = new Vector3(...prevPoint.position);
const endPoint = new Vector3(...newPoint.position);
if (updatedVehicle && startPoint && endPoint) {
if (updatedVehicle.type === "vehicle") {
const event = updateAction(
selectedProduct.productUuid,
updatedVehicle.point?.action.actionUuid,
{
pickUpPoint: {
position: {
x: startPoint.x,
y: 0,
z: startPoint.z,
},
rotation: {
x:
updatedVehicle.point.action.pickUpPoint?.rotation.x ??
0,
y:
updatedVehicle.point.action.pickUpPoint?.rotation.y ??
0,
z:
updatedVehicle.point.action.pickUpPoint?.rotation.z ??
0,
},
},
unLoadPoint: {
position: {
x: endPoint.x,
y: endPoint.y,
z: endPoint.z,
},
rotation: {
x:
updatedVehicle.point.action.unLoadPoint?.rotation.x ??
0,
y:
updatedVehicle.point.action.unLoadPoint?.rotation.y ??
0,
z:
updatedVehicle.point.action.unLoadPoint?.rotation.z ??
0,
},
},
}
);
if (prevPoint.pointId === newPoint.pointId) return;
const result = aStarShortestPath(
prevPoint.pointId,
newPoint.pointId,
points,
paths
);
if (result) {
const edges = nodePathToEdges(result.pointIds, points, paths);
// Create a new path object/
const newPathObj = {
id: pathIdRef.current++,
path: edges,
};
const shortPath = assignPathToSelectedVehicle(
updatedVehicle?.modelUuid,
edges
);
console.log("shortPath: ", shortPath);
setShortestPaths(edges);
setShortestEdges(edges);
// Append it to the list of paths
setMultiPaths((prevPaths) => [...prevPaths, newPathObj]);
}
}
}
}
// Reset selection to allow new pair selection
}, 0);
return [prev[0], pointIndex];
}
setShortestPaths([]);
return [pointIndex];
});
// }
};
const handleDragStart = (point: PointData) => {
if (activeTool !== "cursor") return;
const intersectionPoint = new Vector3();
const hit = raycaster.ray.intersectPlane(plane, intersectionPoint);
if (hit) {
const currentPosition = new Vector3(...point.position);
const offset = new Vector3().subVectors(currentPosition, hit);
setDragOffset(offset);
const pathIntersection = getPathsByPointId(point.pointId, paths);
setInitialPositions({ paths: pathIntersection });
}
};
const handleDrag = (point: PointData) => {
if (isHovered && dragOffset) {
const intersectionPoint = new Vector3();
const hit = raycaster.ray.intersectPlane(plane, intersectionPoint);
if (hit) {
// handleCanvasCursors("grabbing");
const positionWithOffset = new Vector3().addVectors(hit, dragOffset);
const newPosition: [number, number, number] = [
positionWithOffset.x,
positionWithOffset.y,
positionWithOffset.z,
];
// ✅ Pass newPosition and pointId
const pathSnapped = snapPathAngle(newPosition, point.pointId);
const finalSnapped = snapPathPoint(pathSnapped.position);
setPathPosition(point.pointId, finalSnapped.position, setPaths, paths);
}
}
};
const handleDragEnd = (point: PointData) => {
const pathIntersection = getPathsByPointId(point.pointId, paths);
if (pathIntersection && pathIntersection.length > 0) {
pathIntersection.forEach((update) => {});
}
};
const pathSegments = useMemo(() => {
if (!shortestPaths || shortestPaths.length === 0) return [];
const segments: SegmentPoint[] = [];
shortestPaths.forEach((path) => {
const [start, end] = path.pathPoints;
const startPos = new Vector3(...start.position);
const endPos = new Vector3(...end.position);
segments.push(
{ position: startPos, originalPoint: start, startId: start.pointId },
{ position: endPos, originalPoint: end, endId: end.pointId }
);
});
return segments.filter(
(v, i, arr) => i === 0 || !v.position.equals(arr[i - 1].position)
);
}, [shortestPaths]);
function getPathIdByPoints(
startId: string | undefined,
endId: string | undefined,
shortestPaths: any[]
) {
for (const path of shortestPaths) {
for (let i = 0; i < path.pathPoints.length - 1; i++) {
const s = path.pathPoints[i];
const e = path.pathPoints[i + 1];
if (
(s.pointId === startId && e.pointId === endId) ||
(s.pointId === endId && e.pointId === startId) // handle both directions
) {
return path.pathId;
}
}
}
return null; // not found
}
return (
<>
<DragControls
axisLock="y"
autoTransform={false}
onDragStart={() => handleDragStart(point)}
onDrag={() => handleDrag(point)}
onDragEnd={() => handleDragEnd(point)}
>
<mesh
key={point.pointId}
position={point.position}
name="Path-Point"
userData={point}
onClick={(e) => {
handlePointClick(e, point);
}}
// onContextMenu={(e) => handleContextMenu(e, point)}
onPointerOver={(e) => {
if (!hoveredPoint && e.buttons === 0 && !e.ctrlKey) {
setHoveredPoint(point);
setIsHovered(true);
// handleCanvasCursors("default");
}
}}
onPointerOut={() => {
if (hoveredPoint) {
setHoveredPoint(null);
if (!hoveredLine) {
// handleCanvasCursors("default");
}
}
setIsHovered(false);
}}
>
<sphereGeometry args={[0.3, 16, 16]} />
<meshBasicMaterial color="pink" />
</mesh>
</DragControls>
{shortestEdges.map((edge) => (
<Line
key={`sp-${edge.pathId}`}
points={edge.pathPoints.map((p) => p.position)}
color="yellow"
lineWidth={3}
/>
))}
</>
);
}

View File

@@ -0,0 +1,360 @@
import React, { useRef, useState, useEffect } from "react";
import { useThree, useFrame } from "@react-three/fiber";
import * as THREE from "three";
import { Line } from "@react-three/drei";
type PointData = {
pointId: string;
position: [number, number, number];
isCurved: boolean;
handleA: [number, number, number] | null;
handleB: [number, number, number] | null;
};
interface PointProps {
point: any;
pointIndex: number;
groupIndex: number;
selected: number[];
mainShapeOnly?: PointData[];
setSelected: React.Dispatch<React.SetStateAction<number[]>>;
pointsGroups: any[][];
setPointsGroups: React.Dispatch<React.SetStateAction<any[][]>>;
shortestPath: number[]; // <- add this
setShortestPath: React.Dispatch<React.SetStateAction<number[]>>; // <- add this
setShortestDistance?: React.Dispatch<React.SetStateAction<number>>; // optional
}
export default function PointHandle({
point,
pointIndex,
groupIndex,
selected,
setSelected,
pointsGroups,
setPointsGroups,
setShortestDistance,
shortestPath,
setShortestPath,
}: PointProps) {
const meshRef = useRef<THREE.Mesh>(null);
const handleARef = useRef<THREE.Mesh>(null);
const handleBRef = useRef<THREE.Mesh>(null);
const lineRef = useRef<THREE.Line>(null!);
// const pathLineRef = useRef<THREE.Line>(null!);
const { camera, gl, controls } = useThree();
const [dragging, setDragging] = useState<
null | "main" | "handleA" | "handleB"
>(null);
const dragOffset = useRef(new THREE.Vector3());
// const [shortestPath, setShortestPath] = useState<number[]>([]);
/** Shift-click or ctrl-click handling */
const onPointClick = (e: any) => {
e.stopPropagation();
if (e.ctrlKey) {
// Toggle handles
setPointsGroups((prev) => {
const newGroups = [...prev];
const group = [...newGroups[groupIndex]];
const idx = group.findIndex((p) => p.pointId === point.pointId);
const updated = { ...group[idx] };
if (!updated.handleA && !updated.handleB) {
updated.handleA = [
updated.position[0] + 1,
updated.position[1],
updated.position[2],
];
updated.handleB = [
updated.position[0] - 1,
updated.position[1],
updated.position[2],
];
updated.isCurved = true;
} else {
updated.handleA = null;
updated.handleB = null;
updated.isCurved = false;
}
group[idx] = updated;
newGroups[groupIndex] = group;
return newGroups;
});
} else if (e.shiftKey) {
// Shift-click for multi-select
setSelected((prev) => {
if (prev.includes(pointIndex)) return prev; // keep selection
const newSelection = [...prev, pointIndex];
return newSelection.slice(-2); // keep only 2 points
});
} else {
// Single selection
setSelected([pointIndex]);
}
};
/** Dragging logic */
const startDrag = (target: "main" | "handleA" | "handleB", e: any) => {
e.stopPropagation();
setDragging(target);
const targetRef =
target === "main"
? meshRef.current
: target === "handleA"
? handleARef.current
: handleBRef.current;
if (targetRef) dragOffset.current.copy(targetRef.position).sub(e.point);
if (controls) (controls as any).enabled = false;
gl.domElement.style.cursor = "grabbing";
};
const stopDrag = () => {
setDragging(null);
gl.domElement.style.cursor = "auto";
if (controls) (controls as any).enabled = true;
};
useFrame(({ raycaster, mouse }) => {
if (!dragging) return;
const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
raycaster.setFromCamera(mouse, camera);
const intersection = new THREE.Vector3();
if (raycaster.ray.intersectPlane(plane, intersection)) {
const newPos = intersection.add(dragOffset.current);
setPointsGroups((prev) => {
const newGroups = [...prev];
const group = [...newGroups[groupIndex]];
const idx = group.findIndex((p) => p.pointId === point.pointId);
const updated = { ...group[idx] };
if (dragging === "main") {
const delta = new THREE.Vector3()
.fromArray(newPos.toArray())
.sub(new THREE.Vector3().fromArray(updated.position));
updated.position = newPos.toArray() as [number, number, number];
if (updated.handleA) {
updated.handleA = new THREE.Vector3()
.fromArray(updated.handleA)
.add(delta)
.toArray() as [number, number, number];
}
if (updated.handleB) {
updated.handleB = new THREE.Vector3()
.fromArray(updated.handleB)
.add(delta)
.toArray() as [number, number, number];
}
} else {
updated[dragging] = newPos.toArray() as [number, number, number];
if (updated.isCurved) {
const mainPos = new THREE.Vector3().fromArray(updated.position);
const thisHandle = new THREE.Vector3().fromArray(
updated[dragging]!
);
const mirrorHandle = mainPos
.clone()
.sub(thisHandle.clone().sub(mainPos));
if (dragging === "handleA")
updated.handleB = mirrorHandle.toArray() as [
number,
number,
number
];
if (dragging === "handleB")
updated.handleA = mirrorHandle.toArray() as [
number,
number,
number
];
}
}
group[idx] = updated;
newGroups[groupIndex] = group;
return newGroups;
});
}
});
/** Update handle lines */
useFrame(() => {
if (lineRef.current && point.handleA && point.handleB) {
const positions = lineRef.current.geometry.attributes.position
.array as Float32Array;
positions[0] = point.handleA[0];
positions[1] = point.handleA[1];
positions[2] = point.handleA[2];
positions[3] = point.handleB[0];
positions[4] = point.handleB[1];
positions[5] = point.handleB[2];
lineRef.current.geometry.attributes.position.needsUpdate = true;
}
});
useEffect(() => {
if (selected.length === 2) {
const groupPoints = pointsGroups[groupIndex];
if (!groupPoints) return;
const pathPoints = selected
.map((i) => groupPoints[i])
.filter((p) => p !== undefined)
.map((p) => p.position);
setShortestPath(pathPoints);
// compute distance
let totalDistance = 0;
for (let i = 0; i < pathPoints.length - 1; i++) {
const p1 = new THREE.Vector3().fromArray(pathPoints[i]);
const p2 = new THREE.Vector3().fromArray(pathPoints[i + 1]);
totalDistance += p1.distanceTo(p2);
}
setShortestDistance?.(totalDistance);
} else {
setShortestPath([]);
setShortestDistance?.(0);
}
}, [selected, pointsGroups]);
return (
<>
{/* Main point */}
<mesh
ref={meshRef}
position={point.position}
onClick={onPointClick}
onPointerDown={(e) => startDrag("main", e)}
onPointerUp={stopDrag}
>
<sphereGeometry args={[0.3, 16, 16]} />
<meshStandardMaterial
color={selected.includes(pointIndex) ? "red" : "pink"}
/>
</mesh>
{/* Handles + line */}
{point.isCurved && point.handleA && point.handleB && (
<>
<Line
points={[point.handleA, point.handleB]}
color="gray"
lineWidth={1}
/>
<mesh
ref={handleARef}
position={point.handleA}
onPointerDown={(e) => startDrag("handleA", e)}
onPointerUp={stopDrag}
>
<sphereGeometry args={[0.15, 8, 8]} />
<meshStandardMaterial color="orange" />
</mesh>
<mesh
ref={handleBRef}
position={point.handleB}
onPointerDown={(e) => startDrag("handleB", e)}
onPointerUp={stopDrag}
>
<sphereGeometry args={[0.15, 8, 8]} />
<meshStandardMaterial color="green" />
</mesh>
</>
)}
{/* Highlight shortest path */}
{shortestPath.length > 1 && (
<Line
points={shortestPath} // <- just use the positions array
color="blue"
lineWidth={2}
/>
)}
</>
);
}
/** Build adjacency list for shortest path */
// const buildGraph = (points: any[]) => {
// const graph: Record<number, { neighbor: number; distance: number }[]> = {};
// points.forEach((p, idx) => {
// graph[idx] = [];
// points.forEach((q, j) => {
// if (idx !== j) {
// const d = new THREE.Vector3()
// .fromArray(p.position)
// .distanceTo(new THREE.Vector3().fromArray(q.position));
// graph[idx].push({ neighbor: j, distance: d });
// }
// });
// });
// return graph;
// };
// /** Dijkstra shortest path */
// const findShortestPath = (graph: any, startIdx: number, endIdx: number) => {
// const distances: number[] = Array(Object.keys(graph).length).fill(Infinity);
// const previous: (number | null)[] = Array(distances.length).fill(null);
// distances[startIdx] = 0;
// const queue = new Set(Object.keys(graph).map(Number));
// while (queue.size) {
// let current = [...queue].reduce((a, b) =>
// distances[a] < distances[b] ? a : b
// );
// if (current === endIdx) break;
// queue.delete(current);
// for (const { neighbor, distance } of graph[current]) {
// const alt = distances[current] + distance;
// if (alt < distances[neighbor]) {
// distances[neighbor] = alt;
// previous[neighbor] = current;
// }
// }
// }
// const path: number[] = [];
// let u: number | null = endIdx;
// while (u !== null) {
// path.unshift(u);
// u = previous[u];
// }
//
// return path;
// };
// /** Calculate shortest path when 2 points are selected */
// useEffect(() => {
// if (selected.length === 2) {
// const groupPoints = pointsGroups[groupIndex];
// const graph = buildGraph(groupPoints);
// const path = findShortestPath(graph, selected[0], selected[1]);
// setShortestPath(path);
// // Calculate distance
// if (setShortestDistance) {
// let totalDistance = 0;
// for (let i = 0; i < path.length - 1; i++) {
// const p1 = new THREE.Vector3().fromArray(
// groupPoints[path[i]].position
// );
// const p2 = new THREE.Vector3().fromArray(
// groupPoints[path[i + 1]].position
// );
// totalDistance += p1.distanceTo(p2);
// }
// setShortestDistance?.(totalDistance);
// }
// } else {
// setShortestPath([]);
// if (setShortestDistance) setShortestDistance(0);
// }
// }, [selected, pointsGroups]);

View File

@@ -0,0 +1,22 @@
export function findShortestPath(
startIndex: number,
endIndex: number,
adjacency: number[][]
) {
const queue = [[startIndex]];
const visited = new Set<number>();
while (queue.length > 0) {
const path = queue.shift()!;
const node = path[path.length - 1];
if (node === endIndex) return path;
if (!visited.has(node)) {
visited.add(node);
for (const neighbor of adjacency[node]) {
queue.push([...path, neighbor]);
}
}
}
return [];
}

View File

@@ -0,0 +1,185 @@
// import { Line } from "@react-three/drei";
// import { useThree } from "@react-three/fiber";
// import { useEffect, useMemo, useRef } from "react";
// import { CubicBezierCurve3, LineCurve3, Plane, Vector3 } from "three";
// export default function LineSegment({
// index,
// createdPoints,
// updatePoints,
// insertPoint,
// }: {
// index: number;
// createdPoints: any[]; // Array of points with position, isCurved, handleA, handleB
// updatePoints: (i0: number, p0: Vector3, i1: number, p1: Vector3) => void;
// insertPoint?: (index: number, point: Vector3) => void;
// }) {
// const { gl, raycaster, camera, controls } = useThree();
// const plane = new Plane(new Vector3(0, 1, 0), 0);
// const dragStart = useRef<Vector3 | null>(null);
// // ======== Curve or Line Points ========
// const curvePoints = useMemo(() => {
// if (!createdPoints || index + 1 >= createdPoints.length) return [];
// const current = createdPoints[index];
// const next = createdPoints[index + 1];
// const starts = new Vector3(...current.position);
// const ends = new Vector3(...next.position);
// const useCurve =
// (current.isCurved && current.handleB) || (next.isCurved && next.handleA);
// const hB = current.handleB ? new Vector3(...current.handleB) : starts;
// const hA = next.handleA ? new Vector3(...next.handleA) : ends;
// const curve = useCurve
// ? new CubicBezierCurve3(starts, hB, hA, ends)
// : new LineCurve3(starts, ends);
// return curve.getPoints(useCurve ? 100 : 2);
// }, [createdPoints, index]);
// // ======== Events ========
// const onPointerUp = () => {
// dragStart.current = null;
// gl.domElement.style.cursor = "default";
// if (controls) (controls as any).enabled = true;
// };
// const onClickLine = () => {
// const intersection = new Vector3();
// if (raycaster.ray.intersectPlane(plane, intersection)) {
// const start = new Vector3(...createdPoints[index].position);
// const end = new Vector3(...createdPoints[index + 1].position);
// const segLen = start.distanceTo(end);
// const distToStart = start.distanceTo(intersection);
// const distToEnd = end.distanceTo(intersection);
// if (
// distToStart > 0.01 &&
// distToEnd > 0.01 &&
// distToStart + distToEnd <= segLen + 0.01
// ) {
// insertPoint?.(index + 1, intersection);
// }
// }
// };
// useEffect(() => {
// gl.domElement.addEventListener("pointerup", onPointerUp);
// return () => {
// gl.domElement.removeEventListener("pointerup", onPointerUp);
// };
// }, []);
// // ======== Render ========
// return (
// <Line
// points={curvePoints}
// color="purple"
// lineWidth={2}
// onPointerDown={onClickLine}
// onPointerUp={onPointerUp}
// />
// );
// }
import { Line } from "@react-three/drei";
import { useThree } from "@react-three/fiber";
import { useEffect, useMemo, useRef } from "react";
import { CubicBezierCurve3, LineCurve3, Plane, Vector3 } from "three";
export default function LineSegment({
index,
createdPoints,
updatePoints,
insertPoint,
}: {
index: number;
createdPoints: any[];
updatePoints: (i0: number, p0: Vector3, i1: number, p1: Vector3) => void;
insertPoint?: (index: number, point: Vector3) => void;
}) {
const { gl, raycaster, camera, controls } = useThree();
const plane = new Plane(new Vector3(0, 1, 0), 0);
const dragStart = useRef<Vector3 | null>(null);
const curvePoints = useMemo(() => {
if (!createdPoints || index + 1 >= createdPoints.length) return [];
const current = createdPoints[index];
const next = createdPoints[index + 1];
// Force y = 0
const starts = new Vector3(current.position[0], 0, current.position[2]);
const ends = new Vector3(next.position[0], 0, next.position[2]);
const useCurve =
(current.isCurved && current.handleB) || (next.isCurved && next.handleA);
const hB = current.handleB
? new Vector3(current.handleB[0], 0, current.handleB[2])
: starts;
const hA = next.handleA
? new Vector3(next.handleA[0], 0, next.handleA[2])
: ends;
const curve = useCurve
? new CubicBezierCurve3(starts, hB, hA, ends)
: new LineCurve3(starts, ends);
return curve.getPoints(useCurve ? 100 : 2);
}, [createdPoints, index]);
const onPointerUp = () => {
dragStart.current = null;
gl.domElement.style.cursor = "default";
if (controls) (controls as any).enabled = true;
};
const onClickLine = () => {
const intersection = new Vector3();
if (raycaster.ray.intersectPlane(plane, intersection)) {
const start = new Vector3(
createdPoints[index].position[0],
0,
createdPoints[index].position[2]
);
const end = new Vector3(
createdPoints[index + 1].position[0],
0,
createdPoints[index + 1].position[2]
);
const segLen = start.distanceTo(end);
const distToStart = start.distanceTo(intersection);
const distToEnd = end.distanceTo(intersection);
if (
distToStart > 0.01 &&
distToEnd > 0.01 &&
distToStart + distToEnd <= segLen + 0.01
) {
insertPoint?.(index + 1, intersection);
}
}
};
useEffect(() => {
gl.domElement.addEventListener("pointerup", onPointerUp);
return () => {
gl.domElement.removeEventListener("pointerup", onPointerUp);
};
}, []);
return (
<Line
points={curvePoints}
color="purple"
lineWidth={2}
onPointerDown={onClickLine}
onPointerUp={onPointerUp}
/>
);
}

View File

@@ -0,0 +1,298 @@
import { Line, Plane } from "@react-three/drei";
import { ThreeEvent, useFrame, useThree } from "@react-three/fiber";
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { Matrix4, Mesh, Quaternion, Vector3 } from "three";
import {
useAnimationPlaySpeed,
usePlayButtonStore,
} from "../../../../store/usePlayButtonStore";
import { useSceneContext } from "../../../scene/sceneContext";
import { findShortestPath } from "./functions/findShortestPath";
import * as THREE from "three";
import PointHandle from "./PointHandle";
import LineSegment from "./lineSegment";
type PointData = {
pointId: string;
position: [number, number, number];
isCurved: boolean;
handleA: [number, number, number] | null;
handleB: [number, number, number] | null;
};
interface PathDataInterface {
pathId: string;
pathPoints: [PointData, PointData];
}
type PathData = PathDataInterface[];
export default function PreDefinedPath() {
const { gl, raycaster } = useThree();
const plane = useRef(new THREE.Plane(new THREE.Vector3(0, 1, 0), 0));
const [mainShapeOnly, setMainShapeOnly] = useState<PointData[][]>([]);
const [pointsGroups, setPointsGroups] = useState<PointData[][]>([[]]);
const [definedPath, setDefinedPath] = useState<PointData[][] | PointData[]>(
[]
);
const [selected, setSelected] = useState<number[]>([]);
const downPosition = useRef<{ x: number; y: number } | null>(null);
const hasClicked = useRef(false);
const handleMouseDown = useCallback((e: any) => {
hasClicked.current = false;
downPosition.current = { x: e.clientX, y: e.clientY };
}, []);
const SNAP_DISTANCE = 0.5;
const handleClick = useCallback(
(e: any) => {
console.log("e.ctrlKey: ", e.ctrlKey);
if (e.ctrlKey) return;
if (e.button === 2) {
setPointsGroups((prev) => [...prev, []]);
setSelected([]);
return;
}
if (e.button !== 0) return;
if (hasClicked.current) return;
hasClicked.current = true;
if (
!downPosition.current ||
Math.abs(downPosition.current.x - e.clientX) > 2 ||
Math.abs(downPosition.current.y - e.clientY) > 2
)
return;
const intersection = new THREE.Vector3();
if (raycaster.ray.intersectPlane(plane.current, intersection)) {
const pointArray = intersection.toArray() as [number, number, number];
setPointsGroups((prev) => {
const newGroups = [...prev];
const currentGroup = [...newGroups[newGroups.length - 1]];
// 1⃣ Find nearest existing point
let nearestPos: [number, number, number] | null = null;
newGroups.forEach((group) => {
group.forEach((p) => {
const dist = Math.sqrt(
Math.pow(p.position[0] - pointArray[0], 2) +
Math.pow(p.position[1] - pointArray[1], 2) +
Math.pow(p.position[2] - pointArray[2], 2)
);
if (dist <= SNAP_DISTANCE && !nearestPos) {
nearestPos = p.position; // take only the position
}
});
});
if (nearestPos) {
// 2⃣ Reuse the position, but create NEW pointId
const snapPoint: PointData = {
pointId: crypto.randomUUID(),
position: nearestPos,
isCurved: false,
handleA: null,
handleB: null,
};
currentGroup.push(snapPoint);
newGroups[newGroups.length - 1] = currentGroup;
return newGroups;
}
// 3⃣ Otherwise, create brand new point
const newPoint: PointData = {
pointId: crypto.randomUUID(),
position: pointArray,
isCurved: false,
handleA: null,
handleB: null,
};
currentGroup.push(newPoint);
newGroups[newGroups.length - 1] = currentGroup;
return newGroups;
});
}
},
[raycaster]
);
function findConnectedComponents(groups: PointData[][]) {
const visited = new Set<string>();
const components: PointData[][] = [];
const arePointsEqual = (p1: PointData, p2: PointData) =>
Math.abs(p1.position[0] - p2.position[0]) < 0.001 &&
Math.abs(p1.position[1] - p2.position[1]) < 0.001 &&
Math.abs(p1.position[2] - p2.position[2]) < 0.001;
const dfs = (point: PointData, component: PointData[][]) => {
if (visited.has(point.pointId)) return;
visited.add(point.pointId);
for (const group of groups) {
if (group.some((gp) => arePointsEqual(gp, point))) {
if (!component.includes(group)) {
component.push(group);
for (const gp of group) dfs(gp, component);
}
}
}
};
for (const group of groups) {
for (const point of group) {
if (!visited.has(point.pointId)) {
const newComponent: PointData[][] = [];
dfs(point, newComponent);
if (newComponent.length > 0) components.push(newComponent.flat());
}
}
}
return components;
}
useEffect(() => {
const newDefinedPath = pointsGroups.filter((g) => g.length > 0);
setDefinedPath(newDefinedPath);
const connected = findConnectedComponents(newDefinedPath);
if (connected.length > 0) {
let mainShape = [...connected[0]];
const isolatedPoints = connected
.slice(1)
.filter((arr) => arr.length === 1);
const updatedMainShapeOnly = [mainShape, ...isolatedPoints];
setMainShapeOnly(updatedMainShapeOnly);
} else {
setMainShapeOnly([]);
}
}, [pointsGroups]);
useEffect(() => {
setDefinedPath(() => {
if (pointsGroups.length === 1) {
return [...pointsGroups[0]];
} else {
return pointsGroups.filter((group) => group.length > 0);
}
});
}, [pointsGroups]);
const [shortestPath, setShortestPath] = useState<number[]>([]);
const [shortestDistance, setShortestDistance] = useState<number>(0);
useEffect(() => {
const domElement = gl.domElement;
domElement.addEventListener("contextmenu", (e) => e.preventDefault());
domElement.addEventListener("mousedown", handleMouseDown);
domElement.addEventListener("mouseup", handleClick);
return () => {
domElement.removeEventListener("mousedown", handleMouseDown);
domElement.removeEventListener("mouseup", handleClick);
};
}, [handleClick, handleMouseDown]);
return (
<>
{pointsGroups.map((group, gIdx) => (
<React.Fragment key={gIdx}>
{group.map((point, idx) => (
<PointHandle
key={point.pointId}
point={point}
groupIndex={gIdx}
pointIndex={idx}
// mainShapeOnly={mainShapeOnly}
setPointsGroups={setPointsGroups}
pointsGroups={pointsGroups} // <-- pass the full groups
selected={selected}
setSelected={setSelected} // <-- pass setter for multi-selection
shortestPath={shortestPath}
setShortestPath={setShortestPath}
setShortestDistance={setShortestDistance}
/>
))}
{group.map((point, i) => {
if (i < group.length - 1) {
return (
<LineSegment
key={i}
index={i}
createdPoints={group} // pass the whole group here
updatePoints={(i0, p0, i1, p1) => {
setPointsGroups((prev) => {
const newGroups = [...prev];
const newGroup = [...newGroups[gIdx]];
newGroup[i0] = {
...newGroup[i0],
position: p0.toArray() as [number, number, number],
};
newGroup[i1] = {
...newGroup[i1],
position: p1.toArray() as [number, number, number],
};
newGroups[gIdx] = newGroup;
return newGroups;
});
}}
insertPoint={(index, pointVec) => {
setPointsGroups((prev) => {
const newGroups = [...prev];
const groupToSplit = newGroups[gIdx];
// Create the new point
const newPoint = {
pointId: crypto.randomUUID(),
position: pointVec.toArray() as [
number,
number,
number
],
isCurved: false,
handleA: null,
handleB: null,
};
// First half: everything from start to clicked segment
const firstHalf = [
...groupToSplit.slice(0, index),
newPoint,
];
// Second half: new point + everything after clicked segment
const secondHalf = [
newPoint,
...groupToSplit.slice(index),
];
// Replace the original group with the first half
newGroups[gIdx] = firstHalf;
// Insert the second half as a new group right after
newGroups.splice(gIdx + 1, 0, secondHalf);
return newGroups;
});
}}
/>
);
}
return null;
})}
</React.Fragment>
))}
</>
);
}

View File

@@ -0,0 +1,213 @@
type PointData = {
pointId: string;
position: [number, number, number];
isCurved?: boolean;
handleA?: [number, number, number] | null;
handleB?: [number, number, number] | null;
};
interface PathDataInterface {
pathId: string;
isActive?: boolean;
isCurved?: boolean;
pathPoints: [PointData, PointData];
}
type PathData = PathDataInterface[];
function dist(a: PointData, b: PointData): number {
return Math.sqrt(
(a.position[0] - b.position[0]) ** 2 +
(a.position[1] - b.position[1]) ** 2 +
(a.position[2] - b.position[2]) ** 2
);
}
/** --- A* Algorithm --- */
type AStarResult = {
pointIds: string[];
distance: number;
};
// function aStarShortestPath(
// startId: string,
// goalId: string,
// points: PointData[],
// paths: PathData
// ): AStarResult | null {
// const pointById = new Map(points.map((p) => [p.pointId, p]));
// const start = pointById.get(startId);
// const goal = pointById.get(goalId);
// if (!start || !goal) return null;
// const openSet = new Set<string>([startId]);
// const cameFrom: Record<string, string | null> = {};
// const gScore: Record<string, number> = {};
// const fScore: Record<string, number> = {};
// for (const p of points) {
// cameFrom[p.pointId] = null;
// gScore[p.pointId] = Infinity;
// fScore[p.pointId] = Infinity;
// }
// gScore[startId] = 0;
// fScore[startId] = dist(start, goal);
// const neighborsOf = (id: string): { id: string; cost: number }[] => {
// const me = pointById.get(id)!;
// const out: { id: string; cost: number }[] = [];
// for (const edge of paths) {
// const [a, b] = edge.pathPoints;
// if (a.pointId === id) out.push({ id: b.pointId, cost: dist(me, b) });
// else if (b.pointId === id) out.push({ id: a.pointId, cost: dist(me, a) });
// }
// return out;
// };
// while (openSet.size > 0) {
// let current: string = [...openSet].reduce((a, b) =>
// fScore[a] < fScore[b] ? a : b
// );
// if (current === goalId) {
// const ids: string[] = [];
// let node: string | null = current;
// while (node) {
// ids.unshift(node);
// node = cameFrom[node];
// }
// return { pointIds: ids, distance: gScore[goalId] };
// }
// openSet.delete(current);
// for (const nb of neighborsOf(current)) {
// const tentativeG = gScore[current] + nb.cost;
// if (tentativeG < gScore[nb.id]) {
// cameFrom[nb.id] = current;
// gScore[nb.id] = tentativeG;
// fScore[nb.id] = tentativeG + dist(pointById.get(nb.id)!, goal);
// openSet.add(nb.id);
// }
// }
// }
// return null;
// }
/** --- Convert node path to edges --- */
function nodePathToEdges(
pointIds: string[],
points: PointData[],
paths: PathData
): PathData {
const byId = new Map(points.map((p) => [p.pointId, p]));
const edges: PathData = [];
for (let i = 0; i < pointIds.length - 1; i++) {
const a = pointIds[i];
const b = pointIds[i + 1];
const edge = paths.find(
(p) =>
(p.pathPoints[0].pointId === a && p.pathPoints[1].pointId === b) ||
(p.pathPoints[0].pointId === b && p.pathPoints[1].pointId === a)
);
if (edge) {
const [p1, p2] = edge.pathPoints;
edges.push({
pathId: edge.pathId,
pathPoints:
p1.pointId === a
? ([p1, p2] as [PointData, PointData])
: ([p2, p1] as [PointData, PointData]),
});
} else {
const pa = byId.get(a)!;
const pb = byId.get(b)!;
edges.push({
pathId: `synthetic-${a}-${b}`,
pathPoints: [pa, pb],
});
}
}
return edges;
}
export function aStar(
start: string,
end: string,
points: PointData[],
paths: PathData
) {
// Map points by id for quick access
const pointMap = new Map(points.map((p) => [p.pointId, p]));
// Build adjacency list from paths
const graph = new Map<string, string[]>();
paths.forEach((path) => {
const pathPoints = path.pathPoints;
for (let i = 0; i < pathPoints.length - 1; i++) {
const a = pathPoints[i].pointId;
const b = pathPoints[i + 1].pointId;
if (!graph.has(a)) graph.set(a, []);
if (!graph.has(b)) graph.set(b, []);
graph.get(a)!.push(b);
graph.get(b)!.push(a);
}
});
// Manhattan distance heuristic (you can use Euclidean instead)
const heuristic = (a: string, b: string) => {
const pa = pointMap.get(a)!.position;
const pb = pointMap.get(b)!.position;
return (
Math.abs(pa[0] - pb[0]) +
Math.abs(pa[1] - pb[1]) +
Math.abs(pa[2] - pb[2])
);
};
const openSet = new Set([start]);
const cameFrom = new Map<string, string>();
const gScore = new Map(points.map((p) => [p.pointId, Infinity]));
const fScore = new Map(points.map((p) => [p.pointId, Infinity]));
gScore.set(start, 0);
fScore.set(start, heuristic(start, end));
while (openSet.size > 0) {
// get node in openSet with lowest fScore
let current = [...openSet].reduce((a, b) =>
fScore.get(a)! < fScore.get(b)! ? a : b
);
if (current === end) {
// reconstruct path
const path: string[] = [];
while (cameFrom.has(current)) {
path.unshift(current);
current = cameFrom.get(current)!;
}
path.unshift(start);
return path;
}
openSet.delete(current);
for (const neighbor of graph.get(current) || []) {
const tentativeG = gScore.get(current)! + heuristic(current, neighbor);
if (tentativeG < gScore.get(neighbor)!) {
cameFrom.set(neighbor, current);
gScore.set(neighbor, tentativeG);
fScore.set(neighbor, tentativeG + heuristic(neighbor, end));
if (!openSet.has(neighbor)) openSet.add(neighbor);
}
}
}
return null; // no path
}

View File

@@ -0,0 +1,95 @@
type PointData = {
pointId: string;
position: [number, number, number];
isCurved: boolean;
handleA: [number, number, number] | null;
handleB: [number, number, number] | null;
};
interface PathDataInterface {
pathId: string;
pathPoints: [PointData, PointData];
}
type PathData = PathDataInterface[];
function distance(a: PointData, b: PointData): number {
const dx = a.position[0] - b.position[0];
const dy = a.position[1] - b.position[1];
const dz = a.position[2] - b.position[2];
return Math.sqrt(dx * dx + dy * dy + dz * dz);
}
type AStarResult = {
path: PointData[]; // ordered list of points along the path
distance: number; // total distance
};
export function aStarShortestPath(
startId: string,
goalId: string,
points: PointData[],
paths: PathData
): AStarResult | null {
const openSet = new Set<string>([startId]);
const cameFrom: Record<string, string | null> = {};
const gScore: Record<string, number> = {};
const fScore: Record<string, number> = {};
points.forEach((p) => {
gScore[p.pointId] = Infinity;
fScore[p.pointId] = Infinity;
cameFrom[p.pointId] = null;
});
gScore[startId] = 0;
fScore[startId] = 0;
while (openSet.size > 0) {
// Pick node with lowest fScore
let current = [...openSet].reduce((a, b) =>
fScore[a] < fScore[b] ? a : b
);
if (current === goalId) {
// ✅ Reconstruct path
const path: PointData[] = [];
let node: string | null = current;
while (node) {
const pt = points.find((p) => p.pointId === node);
if (pt) path.unshift(pt);
node = cameFrom[node];
}
return {
path,
distance: gScore[goalId],
};
}
openSet.delete(current);
// Find neighbors from paths
const neighbors = paths.filter((p) =>
p.pathPoints.some((pt) => pt.pointId === current)
);
for (let n of neighbors) {
const [p1, p2] = n.pathPoints;
const neighbor = p1.pointId === current ? p2 : p1;
const tentativeG =
gScore[current] +
distance(points.find((pt) => pt.pointId === current)!, neighbor);
if (tentativeG < gScore[neighbor.pointId]) {
cameFrom[neighbor.pointId] = current;
gScore[neighbor.pointId] = tentativeG;
fScore[neighbor.pointId] = tentativeG; // no heuristic for now
openSet.add(neighbor.pointId);
}
}
}
return null; // no path found
}

View File

@@ -0,0 +1,7 @@
export function handleContextMenu(
evt: MouseEvent,
setCurrentTempPath: (val: any[]) => void
) {
evt.preventDefault();
setCurrentTempPath([]);
}

View File

@@ -0,0 +1,130 @@
import * as THREE from "three";
type PointData = {
pointId: string;
position: [number, number, number];
isCurved: boolean;
handleA: [number, number, number] | null;
handleB: [number, number, number] | null;
};
interface PathDataInterface {
pathId: string;
pathPoints: [PointData, PointData]; // always two points
}
type PathData = PathDataInterface[];
export const POLYGON_CLOSE_THRESHOLD = 0.3;
export const SNAP_POINT_THRESHOLD = 0.2;
export const SNAP_LINE_THRESHOLD = 0.2;
export function handleMouseClick({
evt,
isDragging,
raycaster,
plane,
pointer,
currentTempPath,
setCurrentTempPath,
pathPointsList,
allPaths,
setAllPaths,
addPointToCurrentTemp,
}: {
evt: MouseEvent;
isDragging: { current: boolean };
raycaster: THREE.Raycaster;
plane: THREE.Plane;
pointer: { x: number; y: number };
currentTempPath: any[];
setCurrentTempPath: (val: any[]) => void;
pathPointsList: any[];
allPaths: any[];
setAllPaths: React.Dispatch<React.SetStateAction<PathData>>;
addPointToCurrentTemp: (point: any) => void;
}) {
if (isDragging.current) return;
if (evt.ctrlKey || evt.shiftKey) return;
const intersectPoint = new THREE.Vector3();
const pos = raycaster.ray.intersectPlane(plane, intersectPoint);
if (!pos) return;
let clickedPoint = new THREE.Vector3(pos.x, pos.y, pos.z);
let snapPoint: any = null;
for (let p of pathPointsList) {
const pVec = new THREE.Vector3(...p.position);
if (pVec.distanceTo(clickedPoint) < SNAP_POINT_THRESHOLD) {
snapPoint = p;
clickedPoint = pVec;
break;
}
}
let newPoint = snapPoint ?? {
pointId: THREE.MathUtils.generateUUID(),
position: [clickedPoint.x, 0, clickedPoint.z],
isCurved: false,
handleA: null,
handleB: null,
};
if (currentTempPath.length > 2) {
const firstVec = new THREE.Vector3(...currentTempPath[0].position);
if (firstVec.distanceTo(clickedPoint) < POLYGON_CLOSE_THRESHOLD) {
const closingPoint = { ...currentTempPath[0] };
console.log("closingPoint: ", closingPoint);
addPointToCurrentTemp(closingPoint);
setCurrentTempPath([]);
return;
}
}
const getNearestPointOnLine = (
a: THREE.Vector3,
b: THREE.Vector3,
p: THREE.Vector3
) => {
const ab = new THREE.Vector3().subVectors(b, a);
const t = Math.max(
0,
Math.min(1, p.clone().sub(a).dot(ab) / ab.lengthSq())
);
return a.clone().add(ab.multiplyScalar(t));
};
for (let path of allPaths) {
const a = new THREE.Vector3(...path.pathPoints[0].position);
const b = new THREE.Vector3(...path.pathPoints[1].position);
const closest = getNearestPointOnLine(a, b, clickedPoint);
if (closest.distanceTo(clickedPoint) < SNAP_LINE_THRESHOLD) {
const splitPoint = {
pointId: THREE.MathUtils.generateUUID(),
position: closest.toArray() as [number, number, number],
isCurved: false,
handleA: null,
handleB: null,
};
setAllPaths((prev: any) =>
prev
.filter((pa: any) => pa.pathId !== path.pathId)
.concat([
{
pathId: THREE.MathUtils.generateUUID(),
pathPoints: [path.pathPoints[0], splitPoint],
},
{
pathId: THREE.MathUtils.generateUUID(),
pathPoints: [splitPoint, path.pathPoints[1]],
},
])
);
addPointToCurrentTemp(splitPoint);
return;
}
}
addPointToCurrentTemp(newPoint);
}

View File

@@ -0,0 +1,13 @@
export function handleMouseDown(
evt: MouseEvent,
isLeftClickDown: { current: boolean },
isDragging: { current: boolean }
) {
if (evt.button === 0) {
if (evt.ctrlKey || evt.shiftKey) return;
isLeftClickDown.current = true;
isDragging.current = false;
}
}

View File

@@ -0,0 +1,6 @@
export function handleMouseMove(
isLeftClickDown: { current: boolean },
isDragging: { current: boolean }
) {
if (isLeftClickDown.current) isDragging.current = true;
}

View File

@@ -0,0 +1,6 @@
export function handleMouseUp(
evt: MouseEvent,
isLeftClickDown: { current: boolean }
) {
if (evt.button === 0) isLeftClickDown.current = false;
}

View File

@@ -0,0 +1,209 @@
import * as THREE from "three";
export const POLYGON_CLOSE_THRESHOLD = 0.1;
export const SNAP_POINT_THRESHOLD = 0.2;
export const SNAP_LINE_THRESHOLD = 0.2;
type PointData = {
pointId: string;
position: [number, number, number];
isCurved: boolean;
handleA: [number, number, number] | null;
handleB: [number, number, number] | null;
};
interface PathDataInterface {
pathId: string;
pathPoints: [PointData, PointData]; // always two points
}
type PathData = PathDataInterface[];
export function handleMouseDown(
evt: MouseEvent,
isLeftClickDown: React.MutableRefObject<boolean>,
isDragging: React.MutableRefObject<boolean>
) {
if (evt.button === 0) {
if (evt.ctrlKey || evt.shiftKey) return;
isLeftClickDown.current = true;
isDragging.current = false;
}
}
export function handleMouseUp(
evt: MouseEvent,
isLeftClickDown: React.MutableRefObject<boolean>
) {
if (evt.button === 0) isLeftClickDown.current = false;
}
export function handleMouseMove(
isLeftClickDown: React.MutableRefObject<boolean>,
isDragging: React.MutableRefObject<boolean>
) {
if (isLeftClickDown.current) isDragging.current = true;
}
export function handleMouseClick({
evt,
isDragging,
raycaster,
plane,
pointer,
currentTempPath,
setCurrentTempPath,
pathPointsList,
allPaths,
setAllPaths,
addPointToCurrentTemp,
}: {
evt: MouseEvent;
isDragging: React.MutableRefObject<boolean>;
raycaster: THREE.Raycaster;
plane: THREE.Plane;
pointer: { x: number; y: number };
currentTempPath: any[];
setCurrentTempPath: (val: any[]) => void;
pathPointsList: any[];
allPaths: PathData;
setAllPaths: React.Dispatch<React.SetStateAction<PathData>>;
addPointToCurrentTemp: (point: any) => void;
}) {
if (isDragging.current) return;
if (evt.ctrlKey || evt.shiftKey) return;
const intersectPoint = new THREE.Vector3();
const pos = raycaster.ray.intersectPlane(plane, intersectPoint);
if (!pos) return;
let clickedPoint = new THREE.Vector3(pos.x, 0, pos.z); // force y = 0
let snapPoint: any = null;
for (let p of pathPointsList) {
const pVec = new THREE.Vector3(p.position[0], 0, p.position[2]); // force y = 0
if (pVec.distanceTo(clickedPoint) < SNAP_POINT_THRESHOLD) {
snapPoint = {
...p,
position: [p.position[0], 0, p.position[2]], // force y = 0
};
clickedPoint = pVec;
break;
}
}
let newPoint = snapPoint ?? {
pointId: THREE.MathUtils.generateUUID(),
position: [clickedPoint.x, 0, clickedPoint.z], // y = 0
isCurved: false,
handleA: null,
handleB: null,
};
if (currentTempPath.length > 2) {
const firstVec = new THREE.Vector3(
currentTempPath[0].position[0],
0,
currentTempPath[0].position[2]
); // y = 0
if (firstVec.distanceTo(clickedPoint) < POLYGON_CLOSE_THRESHOLD) {
const closingPoint = {
...currentTempPath[0],
position: [
currentTempPath[0].position[0],
0,
currentTempPath[0].position[2],
], // y = 0
};
addPointToCurrentTemp(closingPoint);
setCurrentTempPath([]);
return;
}
}
const getNearestPointOnLine = (
a: THREE.Vector3,
b: THREE.Vector3,
p: THREE.Vector3
) => {
const ab = new THREE.Vector3().subVectors(b, a);
const t = Math.max(
0,
Math.min(1, p.clone().sub(a).dot(ab) / ab.lengthSq())
);
return a.clone().add(ab.multiplyScalar(t));
};
for (let path of allPaths) {
const a = new THREE.Vector3(
path.pathPoints[0].position[0],
0,
path.pathPoints[0].position[2]
);
const b = new THREE.Vector3(
path.pathPoints[1].position[0],
0,
path.pathPoints[1].position[2]
);
const closest = getNearestPointOnLine(a, b, clickedPoint);
closest.y = 0; // force y = 0
if (closest.distanceTo(clickedPoint) < SNAP_LINE_THRESHOLD) {
const splitPoint = {
pointId: THREE.MathUtils.generateUUID(),
position: [closest.x, 0, closest.z], // y = 0
isCurved: false,
handleA: null,
handleB: null,
};
setAllPaths((prev) =>
prev
.filter((pa) => pa.pathId !== path.pathId)
.concat([
{
pathId: THREE.MathUtils.generateUUID(),
pathPoints: [
{
...path.pathPoints[0],
position: [
path.pathPoints[0].position[0],
0,
path.pathPoints[0].position[2],
] as [number, number, number],
},
splitPoint,
] as [PointData, PointData],
},
{
pathId: THREE.MathUtils.generateUUID(),
pathPoints: [
splitPoint,
{
...path.pathPoints[1],
position: [
path.pathPoints[1].position[0],
0,
path.pathPoints[1].position[2],
] as [number, number, number],
},
] as [PointData, PointData],
},
])
);
console.log("path.pathPoints[1]: ", path.pathPoints);
addPointToCurrentTemp(splitPoint);
return;
}
}
addPointToCurrentTemp(newPoint);
}
export function handleContextMenu(
evt: MouseEvent,
setCurrentTempPath: (val: any[]) => void
) {
evt.preventDefault();
setCurrentTempPath([]);
}

View File

@@ -0,0 +1,256 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { useThree } from "@react-three/fiber";
import * as THREE from "three";
import { Line } from "@react-three/drei";
type PointData = {
pointId: string;
position: [number, number, number];
isCurved: boolean;
handleA: [number, number, number] | null;
handleB: [number, number, number] | null;
};
interface PathDataInterface {
pathId: string;
pathPoints: [PointData, PointData];
}
type PathData = PathDataInterface[];
interface LineSegmentProps {
index: number;
paths: PathDataInterface[];
pathIndex: number;
setPaths: React.Dispatch<React.SetStateAction<PathData>>;
insertPoint?: (pathIndex: number, point: THREE.Vector3) => void;
}
export default function LineSegment({
index,
paths,
setPaths,
insertPoint,
pathIndex,
}: LineSegmentProps) {
const { gl, raycaster, camera, controls } = useThree();
const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
const [curve, setCurve] = useState<any>();
const [curveState, setCurveState] = useState<string>("");
const curvePoints = useMemo(() => {
if (!paths || index >= paths.length) return [];
const path = paths[index];
const [current, next] = path.pathPoints;
const start = new THREE.Vector3(...current.position);
const end = new THREE.Vector3(...next.position);
// 1⃣ Case 1: use predefined handles
const useCurve =
(current.isCurved && current.handleB) || (next.isCurved && next.handleA);
if (useCurve) {
const hB = current.handleB
? new THREE.Vector3(...current.handleB)
: start;
const hA = next.handleA ? new THREE.Vector3(...next.handleA) : end;
const curve = new THREE.CubicBezierCurve3(start, hB, hA, end);
return curve.getPoints(100);
}
// 2⃣ Case 2: use curveState-generated curve
if (curveState) {
const direction = new THREE.Vector3().subVectors(end, start).normalize();
const up = new THREE.Vector3(0, 1, 0);
const perpendicular = new THREE.Vector3()
.crossVectors(direction, up)
.normalize();
const distance = start.distanceTo(end);
const controlDistance = distance / 6;
let controlPoint1, controlPoint2;
// if (curveState === "arc") {
// const direction = new THREE.Vector3()
// .subVectors(end, start)
// .normalize();
// const perpendicular = new THREE.Vector3(
// -direction.z,
// 0,
// direction.x
// ).normalize();
// controlPoint1 = new THREE.Vector3().addVectors(
// start,
// perpendicular.clone().multiplyScalar(-controlDistance) // negative fixes to "C"
// );
// controlPoint2 = new THREE.Vector3().addVectors(
// end,
// perpendicular.clone().multiplyScalar(-controlDistance)
// );
// }
if (curveState === "arc") {
const direction = new THREE.Vector3()
.subVectors(end, start)
.normalize();
// Perpendicular direction in XZ plane
const perpendicular = new THREE.Vector3(-direction.z, 0, direction.x);
const distance = start.distanceTo(end);
const controlDistance = distance / 4;
const controlPoint1 = new THREE.Vector3()
.addVectors(start, direction.clone().multiplyScalar(distance / 3))
.add(perpendicular.clone().multiplyScalar(-controlDistance)); // ← flipped
const controlPoint2 = new THREE.Vector3()
.addVectors(end, direction.clone().multiplyScalar(-distance / 3))
.add(perpendicular.clone().multiplyScalar(-controlDistance)); // ← flipped
const curve = new THREE.CubicBezierCurve3(
start,
controlPoint1,
controlPoint2,
end
);
return curve.getPoints(64);
}
// if (curveState === "arc") {
// const direction = new THREE.Vector3()
// .subVectors(end, start)
// .normalize();
// // XZ-plane perpendicular
// const perpendicular = new THREE.Vector3(-direction.z, 0, direction.x);
// const distance = start.distanceTo(end);
// const controlDistance = distance / 6; // ← increase this for more curvature
// const controlPoint1 = new THREE.Vector3().addVectors(
// start,
// perpendicular.clone().multiplyScalar(-controlDistance)
// );
// const controlPoint2 = new THREE.Vector3().addVectors(
// end,
// perpendicular.clone().multiplyScalar(-controlDistance)
// );
// const curve = new THREE.CubicBezierCurve3(
// start,
// controlPoint1,
// controlPoint2,
// end
// );
// return curve.getPoints(64);
// }
const curve = new THREE.CubicBezierCurve3(
start,
controlPoint1,
controlPoint2,
end
);
return curve.getPoints(32);
}
// 3⃣ Case 3: fallback straight line
const line = new THREE.LineCurve3(start, end);
return line.getPoints(2);
}, [paths, index, curveState]);
// const curvePoints = useMemo(() => {
// if (!paths || index >= paths.length) return [];
// const path = paths[index];
// const [current, next] = path.pathPoints;
// const start = new THREE.Vector3(...current.position);
// const end = new THREE.Vector3(...next.position);
// // 1⃣ Case 1: Use predefined curve handles if present
// const useCurve =
// (current.isCurved && current.handleB) || (next.isCurved && next.handleA);
// if (useCurve) {
// const handleB = current.handleB
// ? new THREE.Vector3(...current.handleB)
// : start;
// const handleA = next.handleA ? new THREE.Vector3(...next.handleA) : end;
// const curve = new THREE.CubicBezierCurve3(start, handleB, handleA, end);
// return curve.getPoints(100);
// }
// // 2⃣ Case 2: Use curveState-generated arc (gentle C-shaped)
// // if (curveState === "arc") {
// // const direction = new THREE.Vector3().subVectors(end, start).normalize();
// // const distance = start.distanceTo(end);
// // // Get perpendicular in XZ plane
// // const perpendicular = new THREE.Vector3(-direction.z, 0, direction.x);
// // const controlOffset = perpendicular.multiplyScalar(distance / 8);
// // // Create gentle symmetric control points for a "C" arc
// // const controlPoint1 = start.clone().add(controlOffset.clone().negate());
// // const controlPoint2 = end.clone().add(controlOffset.clone().negate());
// // const curve = new THREE.CubicBezierCurve3(
// // start,
// // controlPoint1,
// // controlPoint2,
// // end
// // );
// // return curve.getPoints(64); // 64 for smoother shape
// // }
// if (curveState === "arc") {
// const direction = new THREE.Vector3().subVectors(end, start).normalize();
// const distance = start.distanceTo(end);
// // const curveHeight = distance * 0.25; // 25% of distance
// // 🔺 Control height: Raise control points on Y-axis
// const curveHeight = distance / 4; // adjust 4 → higher = taller arc
// // Control points directly above the midpoint
// const mid = start.clone().add(end).multiplyScalar(0.5);
// const controlPoint = mid
// .clone()
// .add(new THREE.Vector3(0, curveHeight, 0));
// // Use Quadratic Bezier for simple arc
// const curve = new THREE.QuadraticBezierCurve3(start, controlPoint, end);
// return curve.getPoints(64);
// }
// // 3⃣ Case 3: Fallback to straight line
// const line = new THREE.LineCurve3(start, end);
// return line.getPoints(2);
// }, [paths, index, curveState]);
const handleClick = (evt: any) => {
if (evt.ctrlKey) {
setCurveState("arc");
}
};
// const bendFactor = 1 / 10; // tweak this dynamically
// const controlOffset = perpendicular.multiplyScalar(distance * bendFactor);
return (
<Line
points={curvePoints}
color="purple"
lineWidth={3.5}
onClick={handleClick}
/>
);
}

View File

@@ -0,0 +1,831 @@
// import { Line } from "@react-three/drei";
// import { useFrame, useThree } from "@react-three/fiber";
// import React, { useRef, useState } from "react";
// import * as THREE from "three";
// /** --- Types --- */
// type PointData = {
// pointId: string;
// position: [number, number, number];
// isCurved: boolean;
// handleA: [number, number, number] | null;
// handleB: [number, number, number] | null;
// };
// interface PathDataInterface {
// pathId: string;
// pathPoints: [PointData, PointData]; // always two points
// }
// type PathData = PathDataInterface[];
// interface PointHandleProps {
// point: PointData;
// pointIndex: number;
// points: PointData[];
// setPoints: React.Dispatch<React.SetStateAction<PointData[]>>;
// setPaths: React.Dispatch<React.SetStateAction<PathData>>;
// paths: PathData;
// selected: number[];
// setSelected: React.Dispatch<React.SetStateAction<number[]>>;
// setShortestPath: React.Dispatch<React.SetStateAction<PathData>>;
// }
// /** --- Math helpers --- */
// function dist(a: PointData, b: PointData): number {
// return Math.sqrt(
// (a.position[0] - b.position[0]) ** 2 +
// (a.position[1] - b.position[1]) ** 2 +
// (a.position[2] - b.position[2]) ** 2
// );
// }
// /** --- A* Algorithm --- */
// type AStarResult = {
// pointIds: string[];
// distance: number;
// };
// function aStarShortestPath(
// startId: string,
// goalId: string,
// points: PointData[],
// paths: PathData
// ): AStarResult | null {
// const pointById = new Map(points.map((p) => [p.pointId, p]));
// const start = pointById.get(startId);
// const goal = pointById.get(goalId);
// if (!start || !goal) return null;
// const openSet = new Set<string>([startId]);
// const cameFrom: Record<string, string | null> = {};
// const gScore: Record<string, number> = {};
// const fScore: Record<string, number> = {};
// for (const p of points) {
// cameFrom[p.pointId] = null;
// gScore[p.pointId] = Infinity;
// fScore[p.pointId] = Infinity;
// }
// gScore[startId] = 0;
// fScore[startId] = dist(start, goal);
// const neighborsOf = (id: string): { id: string; cost: number }[] => {
// const me = pointById.get(id)!;
// const out: { id: string; cost: number }[] = [];
// for (const edge of paths) {
// const [a, b] = edge.pathPoints;
// if (a.pointId === id) out.push({ id: b.pointId, cost: dist(me, b) });
// else if (b.pointId === id) out.push({ id: a.pointId, cost: dist(me, a) });
// }
// return out;
// };
// while (openSet.size > 0) {
// let current: string = [...openSet].reduce((a, b) =>
// fScore[a] < fScore[b] ? a : b
// );
// if (current === goalId) {
// const ids: string[] = [];
// let node: string | null = current;
// while (node) {
// ids.unshift(node);
// node = cameFrom[node];
// }
// return { pointIds: ids, distance: gScore[goalId] };
// }
// openSet.delete(current);
// for (const nb of neighborsOf(current)) {
// const tentativeG = gScore[current] + nb.cost;
// if (tentativeG < gScore[nb.id]) {
// cameFrom[nb.id] = current;
// gScore[nb.id] = tentativeG;
// fScore[nb.id] = tentativeG + dist(pointById.get(nb.id)!, goal);
// openSet.add(nb.id);
// }
// }
// }
// return null;
// }
// /** --- Convert node path to edges --- */
// // function nodePathToEdges(
// // pointIds: string[],
// // points: PointData[],
// // paths: PathData
// // ): PathData {
// // const byId = new Map(points.map((p) => [p.pointId, p]));
// // const edges: PathData = [];
// // for (let i = 0; i < pointIds.length - 1; i++) {
// // const a = pointIds[i];
// // const b = pointIds[i + 1];
// //
// //
// // const edge = paths.find(
// // (p) =>
// // (p.pathPoints[0].pointId === a && p.pathPoints[1].pointId === b) ||
// // (p.pathPoints[0].pointId === b && p.pathPoints[1].pointId === a)
// // );
// // if (edge) edges.push(edge);
// // else {
// // const pa = byId.get(a)!;
// // const pb = byId.get(b)!;
// // edges.push({
// // pathId: `synthetic-${a}-${b}`,
// // pathPoints: [pa, pb],
// // });
// // }
// // }
// //
// // return edges;
// // }
// function nodePathToEdges(
// pointIds: string[],
// points: PointData[],
// paths: PathData
// ): PathData {
// const byId = new Map(points.map((p) => [p.pointId, p]));
// const edges: PathData = [];
// for (let i = 0; i < pointIds.length - 1; i++) {
// const a = pointIds[i];
// const b = pointIds[i + 1];
// const edge = paths.find(
// (p) =>
// (p.pathPoints[0].pointId === a && p.pathPoints[1].pointId === b) ||
// (p.pathPoints[0].pointId === b && p.pathPoints[1].pointId === a)
// );
// if (edge) {
// // Ensure correct order in edge
// const [p1, p2] = edge.pathPoints;
// edges.push({
// pathId: edge.pathId,
// pathPoints:
// p1.pointId === a
// ? ([p1, p2] as [PointData, PointData])
// : ([p2, p1] as [PointData, PointData]),
// });
// } else {
// const pa = byId.get(a)!;
// const pb = byId.get(b)!;
// edges.push({
// pathId: `synthetic-${a}-${b}`,
// pathPoints: [pa, pb],
// });
// }
// }
// return edges;
// }
// /** --- React Component --- */
// export default function PointHandlers({
// point,
// pointIndex,
// points,
// setPoints,
// setPaths,
// paths,
// selected,
// setSelected,
// setShortestPath,
// }: PointHandleProps) {
// const meshRef = useRef<THREE.Mesh>(null);
// const handleARef = useRef<THREE.Mesh>(null);
// const handleBRef = useRef<THREE.Mesh>(null);
// const lineRef = useRef<any>(null!);
// const { camera, gl, controls } = useThree();
// const [dragging, setDragging] = useState<
// null | "main" | "handleA" | "handleB"
// >(null);
// const dragOffset = useRef(new THREE.Vector3());
// const [shortestEdges, setShortestEdges] = useState<PathData>([]);
// /** Click handling */
// const onPointClick = (e: any) => {
// e.stopPropagation();
// if (e.shiftKey) {
// setSelected((prev) => {
// if (prev.length === 0) return [pointIndex];
// else if (prev.length === 1) {
// const p1 = points[prev[0]];
// const p2 = points[pointIndex];
// const result = aStarShortestPath(
// p1.pointId,
// p2.pointId,
// points,
// paths
// );
// if (result) {
// const edges = nodePathToEdges(result.pointIds, points, paths);
// setShortestEdges(edges);
// setShortestPath(edges);
// } else {
// setShortestEdges([]);
// }
// return [prev[0], pointIndex];
// } else {
// setShortestEdges([]);
// return [pointIndex];
// }
// });
// } else if (e.ctrlKey) {
// setPoints((prev) => {
// const updated = [...prev];
// const p = { ...updated[pointIndex] };
// if (!p.handleA && !p.handleB) {
// p.handleA = [p.position[0] + 1, p.position[1], p.position[2]];
// p.handleB = [p.position[0] - 1, p.position[1], p.position[2]];
// p.isCurved = true;
// } else {
// p.handleA = null;
// p.handleB = null;
// p.isCurved = false;
// }
// updated[pointIndex] = p;
// return updated;
// });
// }
// };
// /** Dragging logic */
// const startDrag = (target: "main" | "handleA" | "handleB", e: any) => {
// e.stopPropagation();
// setDragging(target);
// const targetRef =
// target === "main"
// ? meshRef.current
// : target === "handleA"
// ? handleARef.current
// : handleBRef.current;
// if (targetRef) dragOffset.current.copy(targetRef.position).sub(e.point);
// if (controls) (controls as any).enabled = false;
// gl.domElement.style.cursor = "grabbing";
// };
// const stopDrag = () => {
// setDragging(null);
// gl.domElement.style.cursor = "auto";
// if (controls) (controls as any).enabled = true;
// };
// useFrame(({ raycaster, mouse }) => {
// if (!dragging) return;
// const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
// raycaster.setFromCamera(mouse, camera);
// const intersection = new THREE.Vector3();
// if (!raycaster.ray.intersectPlane(plane, intersection)) return;
// const newPos = intersection.add(dragOffset.current);
// setPoints((prevPoints) => {
// const updatedPoints = [...prevPoints];
// const point = { ...updatedPoints[pointIndex] };
// if (dragging === "main") {
// // Calculate delta movement
// const delta = newPos
// .clone()
// .sub(new THREE.Vector3().fromArray(point.position));
// // Move main point
// point.position = newPos.toArray() as [number, number, number];
// // Move handles with main point
// if (point.handleA)
// point.handleA = new THREE.Vector3()
// .fromArray(point.handleA)
// .add(delta)
// .toArray() as [number, number, number];
// if (point.handleB)
// point.handleB = new THREE.Vector3()
// .fromArray(point.handleB)
// .add(delta)
// .toArray() as [number, number, number];
// } else {
// // Dragging a handle
// point[dragging] = newPos.toArray() as [number, number, number];
// if (point.isCurved) {
// // Mirror the opposite handle
// const mainPos = new THREE.Vector3().fromArray(point.position);
// const thisHandle = new THREE.Vector3().fromArray(point[dragging]!);
// const mirrorHandle = mainPos
// .clone()
// .sub(thisHandle.clone().sub(mainPos));
// if (dragging === "handleA")
// point.handleB = mirrorHandle.toArray() as [number, number, number];
// if (dragging === "handleB")
// point.handleA = mirrorHandle.toArray() as [number, number, number];
// }
// }
// updatedPoints[pointIndex] = point;
// // Update all paths that include this point
// setPaths((prevPaths: any) =>
// prevPaths.map((path: any) => {
// const updatedPathPoints = path.pathPoints.map((p: any) =>
// p.pointId === point.pointId ? point : p
// );
// return { ...path, pathPoints: updatedPathPoints };
// })
// );
// return updatedPoints;
// });
// });
// /** Update line between handles */
// useFrame(() => {
// if (lineRef.current && point.handleA && point.handleB) {
// const positions = lineRef.current.geometry.attributes.position
// .array as Float32Array;
// positions[0] = point.handleA[0];
// positions[1] = point.handleA[1];
// positions[2] = point.handleA[2];
// positions[3] = point.handleB[0];
// positions[4] = point.handleB[1];
// positions[5] = point.handleB[2];
// lineRef.current.geometry.attributes.position.needsUpdate = true;
// }
// });
// return (
// <>
// {/* Main point */}
// <mesh
// ref={meshRef}
// position={point.position}
// onClick={onPointClick}
// onPointerDown={(e) => startDrag("main", e)}
// onPointerUp={stopDrag}
// >
// <sphereGeometry args={[0.3, 16, 16]} />
// <meshStandardMaterial
// color={selected.includes(pointIndex) ? "red" : "pink"}
// />
// </mesh>
// {/* Curve handles */}
// {point.isCurved && point.handleA && point.handleB && (
// <>
// <Line
// ref={lineRef}
// points={[point.handleA, point.handleB]}
// color="gray"
// lineWidth={1}
// />
// <mesh
// ref={handleARef}
// position={point.handleA}
// onPointerDown={(e) => startDrag("handleA", e)}
// onPointerUp={stopDrag}
// >
// <sphereGeometry args={[0.15, 8, 8]} />
// <meshStandardMaterial color="orange" />
// </mesh>
// <mesh
// ref={handleBRef}
// position={point.handleB}
// onPointerDown={(e) => startDrag("handleB", e)}
// onPointerUp={stopDrag}
// >
// <sphereGeometry args={[0.15, 8, 8]} />
// <meshStandardMaterial color="green" />
// </mesh>
// </>
// )}
// {/* Draw connected paths */}
// {/* Highlight shortest path */}
// {shortestEdges.map((edge) => (
// <Line
// key={`sp-${edge.pathId}`}
// points={edge.pathPoints.map((p) => p.position)}
// color="yellow"
// lineWidth={3}
// />
// ))}
// </>
// );
// }
/** --- Types --- */
import { Line } from "@react-three/drei";
import { useFrame, useThree } from "@react-three/fiber";
import React, { useRef, useState } from "react";
import * as THREE from "three";
/** --- Types --- */
type PointData = {
pointId: string;
position: [number, number, number];
isCurved: boolean;
handleA: [number, number, number] | null;
handleB: [number, number, number] | null;
};
interface PathDataInterface {
pathId: string;
pathPoints: [PointData, PointData];
}
type PathData = PathDataInterface[];
interface PointHandleProps {
point: PointData;
pointIndex: number;
points: PointData[];
setPoints: React.Dispatch<React.SetStateAction<PointData[]>>;
setPaths: React.Dispatch<React.SetStateAction<PathData>>;
paths: PathData;
selected: number[];
setSelected: React.Dispatch<React.SetStateAction<number[]>>;
setShortestPath: React.Dispatch<React.SetStateAction<PathData>>;
}
/** --- Math helpers --- */
function dist(a: PointData, b: PointData): number {
return Math.sqrt(
(a.position[0] - b.position[0]) ** 2 +
(a.position[1] - b.position[1]) ** 2 +
(a.position[2] - b.position[2]) ** 2
);
}
/** --- A* Algorithm --- */
type AStarResult = {
pointIds: string[];
distance: number;
};
function aStarShortestPath(
startId: string,
goalId: string,
points: PointData[],
paths: PathData
): AStarResult | null {
const pointById = new Map(points.map((p) => [p.pointId, p]));
const start = pointById.get(startId);
const goal = pointById.get(goalId);
if (!start || !goal) return null;
const openSet = new Set<string>([startId]);
const cameFrom: Record<string, string | null> = {};
const gScore: Record<string, number> = {};
const fScore: Record<string, number> = {};
for (const p of points) {
cameFrom[p.pointId] = null;
gScore[p.pointId] = Infinity;
fScore[p.pointId] = Infinity;
}
gScore[startId] = 0;
fScore[startId] = dist(start, goal);
const neighborsOf = (id: string): { id: string; cost: number }[] => {
const me = pointById.get(id)!;
const out: { id: string; cost: number }[] = [];
for (const edge of paths) {
const [a, b] = edge.pathPoints;
if (a.pointId === id) out.push({ id: b.pointId, cost: dist(me, b) });
else if (b.pointId === id) out.push({ id: a.pointId, cost: dist(me, a) });
}
return out;
};
while (openSet.size > 0) {
let current: string = [...openSet].reduce((a, b) =>
fScore[a] < fScore[b] ? a : b
);
if (current === goalId) {
const ids: string[] = [];
let node: string | null = current;
while (node) {
ids.unshift(node);
node = cameFrom[node];
}
return { pointIds: ids, distance: gScore[goalId] };
}
openSet.delete(current);
for (const nb of neighborsOf(current)) {
const tentativeG = gScore[current] + nb.cost;
if (tentativeG < gScore[nb.id]) {
cameFrom[nb.id] = current;
gScore[nb.id] = tentativeG;
fScore[nb.id] = tentativeG + dist(pointById.get(nb.id)!, goal);
openSet.add(nb.id);
}
}
}
return null;
}
/** --- Convert node path to edges --- */
function nodePathToEdges(
pointIds: string[],
points: PointData[],
paths: PathData
): PathData {
const byId = new Map(points.map((p) => [p.pointId, p]));
const edges: PathData = [];
for (let i = 0; i < pointIds.length - 1; i++) {
const a = pointIds[i];
const b = pointIds[i + 1];
const edge = paths.find(
(p) =>
(p.pathPoints[0].pointId === a && p.pathPoints[1].pointId === b) ||
(p.pathPoints[0].pointId === b && p.pathPoints[1].pointId === a)
);
if (edge) {
const [p1, p2] = edge.pathPoints;
edges.push({
pathId: edge.pathId,
pathPoints:
p1.pointId === a
? ([p1, p2] as [PointData, PointData])
: ([p2, p1] as [PointData, PointData]),
});
} else {
const pa = byId.get(a)!;
const pb = byId.get(b)!;
edges.push({
pathId: `synthetic-${a}-${b}`,
pathPoints: [pa, pb],
});
}
}
return edges;
}
/** --- React Component --- */
export default function PointHandlers({
point,
pointIndex,
points,
setPoints,
setPaths,
paths,
selected,
setSelected,
setShortestPath,
}: PointHandleProps) {
const meshRef = useRef<THREE.Mesh>(null);
const handleARef = useRef<THREE.Mesh>(null);
const handleBRef = useRef<THREE.Mesh>(null);
const lineRef = useRef<any>(null);
const { gl, controls, raycaster } = useThree();
const [dragging, setDragging] = useState<
null | "main" | "handleA" | "handleB"
>(null);
const dragOffset = useRef(new THREE.Vector3());
const [shortestEdges, setShortestEdges] = useState<PathData>([]);
/** Click handling */
const onPointClick = (e: any) => {
e.stopPropagation();
if (e.shiftKey) {
setSelected((prev) => {
console.log("prev: ", prev);
console.log("pointIndex: ", pointIndex);
if (prev.length === 0) return [pointIndex];
if (prev.length === 1) {
// defer shortest path calculation
setTimeout(() => {
console.log("points: ", points);
const p1 = points[prev[0]];
const p2 = points[pointIndex];
const result = aStarShortestPath(
p1.pointId,
p2.pointId,
points,
paths
);
if (result) {
const edges = nodePathToEdges(result.pointIds, points, paths);
console.log("edges: ", edges);
setShortestEdges(edges);
setShortestPath(edges);
} else {
setShortestEdges([]);
}
}, 0);
return [prev[0], pointIndex];
}
return [pointIndex];
});
} else if (e.ctrlKey) {
setPoints((prev) => {
const updated = [...prev];
const p = { ...updated[pointIndex] };
if (!p.handleA && !p.handleB) {
p.handleA = [p.position[0] + 1, 0, p.position[2]];
p.handleB = [p.position[0] - 1, 0, p.position[2]];
p.isCurved = true;
} else {
p.handleA = null;
p.handleB = null;
p.isCurved = false;
}
updated[pointIndex] = p;
return updated;
});
}
};
/** Dragging logic */
const startDrag = (target: "main" | "handleA" | "handleB", e: any) => {
e.stopPropagation();
setDragging(target);
const targetRef =
target === "main"
? meshRef.current
: target === "handleA"
? handleARef.current
: handleBRef.current;
if (targetRef && targetRef.position) {
dragOffset.current
.copy(new THREE.Vector3(targetRef.position.x, 0, targetRef.position.z))
.sub(e.point);
}
if (controls) (controls as any).enabled = false;
gl.domElement.style.cursor = "grabbing";
};
const stopDrag = () => {
setDragging(null);
gl.domElement.style.cursor = "auto";
if (controls) (controls as any).enabled = true;
};
/** Update position in useFrame */
useFrame(({ mouse }) => {
if (!dragging) return;
const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
const intersection = new THREE.Vector3();
if (!raycaster.ray.intersectPlane(plane, intersection)) return;
const newPos = intersection.add(dragOffset.current);
setPoints((prevPoints) => {
const updatedPoints = [...prevPoints];
const p = { ...updatedPoints[pointIndex] };
if (dragging === "main") {
const delta = newPos
.clone()
.sub(new THREE.Vector3().fromArray(p.position));
p.position = [newPos.x, 0, newPos.z];
if (p.handleA) {
p.handleA = new THREE.Vector3()
.fromArray(p.handleA)
.add(new THREE.Vector3(delta.x, 0, delta.z))
.toArray() as [number, number, number];
}
if (p.handleB) {
p.handleB = new THREE.Vector3()
.fromArray(p.handleB)
.add(new THREE.Vector3(delta.x, 0, delta.z))
.toArray() as [number, number, number];
}
} else {
p[dragging] = [newPos.x, 0, newPos.z];
if (p.isCurved) {
const mainPos = new THREE.Vector3().fromArray(p.position);
const thisHandle = new THREE.Vector3().fromArray(p[dragging]!);
const mirrorHandle = mainPos
.clone()
.sub(thisHandle.clone().sub(mainPos));
console.log("mirrorHandle: ", mirrorHandle);
if (dragging === "handleA")
p.handleB = mirrorHandle.toArray() as [number, number, number];
if (dragging === "handleB")
p.handleA = mirrorHandle.toArray() as [number, number, number];
}
}
updatedPoints[pointIndex] = p;
setPaths((prevPaths: any) =>
prevPaths.map((path: any) => ({
...path,
pathPoints: path.pathPoints.map((pp: any) =>
pp.pointId === p.pointId ? p : pp
),
}))
);
return updatedPoints;
});
});
/** Update line between handles */
useFrame(() => {
if (lineRef.current && point.handleA && point.handleB) {
const positions = lineRef.current.geometry.attributes.position
.array as Float32Array;
positions[0] = point.handleA[0];
positions[1] = point.handleA[1];
positions[2] = point.handleA[2];
positions[3] = point.handleB[0];
positions[4] = point.handleB[1];
positions[5] = point.handleB[2];
lineRef.current.geometry.attributes.position.needsUpdate = true;
}
});
return (
<>
{/* Main point */}
<mesh
ref={meshRef}
position={point.position}
onClick={onPointClick}
onPointerDown={(e) => startDrag("main", e)}
onPointerUp={stopDrag}
>
<sphereGeometry args={[0.1, 16, 16]} />
<meshStandardMaterial
color={selected.includes(pointIndex) ? "red" : "pink"}
/>
</mesh>
{/* Curve handles */}
{point.isCurved && point.handleA && point.handleB && (
<>
<Line
ref={lineRef}
points={[point.handleA, point.handleB]}
color="gray"
lineWidth={1}
/>
<mesh
ref={handleARef}
position={point.handleA}
onPointerDown={(e) => startDrag("handleA", e)}
onPointerUp={stopDrag}
>
<sphereGeometry args={[0.15, 8, 8]} />
<meshStandardMaterial color="orange" />
</mesh>
<mesh
ref={handleBRef}
position={point.handleB}
onPointerDown={(e) => startDrag("handleB", e)}
onPointerUp={stopDrag}
>
<sphereGeometry args={[0.15, 8, 8]} />
<meshStandardMaterial color="green" />
</mesh>
</>
)}
{/* Highlight shortest path */}
{shortestEdges.map((edge) => (
<Line
key={`sp-${edge.pathId}`}
points={edge.pathPoints.map((p) => p.position)}
color="yellow"
lineWidth={3}
/>
))}
</>
);
}

View File

@@ -0,0 +1,318 @@
import * as THREE from "three";
import { useRef, useState, useMemo, useEffect } from "react";
import { useThree, useFrame } from "@react-three/fiber";
import { Line } from "@react-three/drei";
import { useSceneContext } from "../../../scene/sceneContext";
import {
useAnimationPlaySpeed,
usePlayButtonStore,
} from "../../../../store/usePlayButtonStore";
import PointHandles from "./pointHandlers";
import LineSegment from "./lineSegment";
import {
handleContextMenu,
handleMouseClick,
handleMouseDown,
handleMouseMove,
handleMouseUp,
} from "./functions/pathMouseHandler";
type PointData = {
pointId: string;
position: [number, number, number];
isCurved: boolean;
handleA: [number, number, number] | null;
handleB: [number, number, number] | null;
};
interface PathDataInterface {
pathId: string;
isActive?: boolean;
isCurved?: boolean;
pathPoints: [PointData, PointData];
}
type PathData = PathDataInterface[];
type SegmentPoint = {
position: THREE.Vector3;
originalPoint?: PointData;
};
export default function StructuredPath() {
const { scene, camera, raycaster, gl, pointer } = useThree();
const plane = useMemo(
() => new THREE.Plane(new THREE.Vector3(0, 1, 0), 0),
[]
);
const { speed } = useAnimationPlaySpeed();
const { assetStore } = useSceneContext();
const { assets } = assetStore();
// --- State Variables ---
const [pathPointsList, setPathPointsList] = useState<PointData[]>([]);
const [allPaths, setAllPaths] = useState<PathData>([]);
const [computedShortestPath, setComputedShortestPath] = useState<PathData>(
[]
);
const [currentTempPath, setCurrentTempPath] = useState<PointData[]>([]);
const [currentMousePos, setCurrentMousePos] = useState<
[number, number, number] | null
>(null);
const [selectedPointIndices, setSelectedPointIndices] = useState<number[]>(
[]
);
const [vehicleUuids, setVehicleUuids] = useState<any>();
// --- Constants & Refs ---
const isLeftClickDown = useRef<boolean>(false);
const isDragging = useRef<boolean>(false);
const vehicleMovementState = useRef<any>({});
const activeVehicleIndexRef = useRef(0);
const { isPlaying } = usePlayButtonStore();
// --- Computed Path Segments ---
const pathSegments = useMemo(() => {
if (!computedShortestPath || computedShortestPath.length === 0) return [];
const segments: SegmentPoint[] = [];
computedShortestPath.forEach((path) => {
const [start, end] = path.pathPoints;
const startPos = new THREE.Vector3(...start.position);
const endPos = new THREE.Vector3(...end.position);
// Start point has curve handles
if (start.isCurved && start.handleA && start.handleB) {
const handleA = new THREE.Vector3(...start.handleA);
const handleB = new THREE.Vector3(...start.handleB);
const curve = new THREE.CubicBezierCurve3(
startPos,
handleA,
handleB,
endPos
);
const points = curve.getPoints(20).map((pos) => ({
position: pos,
originalPoint: start,
}));
segments.push(...points);
}
// End point has curve handles
else if (end.isCurved && end.handleA && end.handleB) {
const handleA = new THREE.Vector3(...end.handleA);
const handleB = new THREE.Vector3(...end.handleB);
const curve = new THREE.CubicBezierCurve3(
startPos,
handleA,
handleB,
endPos
);
const points = curve.getPoints(20).map((pos) => ({
position: pos,
originalPoint: end,
}));
segments.push(...points);
}
// No curves — just straight line
else {
segments.push(
{ position: startPos, originalPoint: start },
{ position: endPos, originalPoint: end }
);
}
});
// Filter out duplicate consecutive points
return segments.filter(
(v, i, arr) => i === 0 || !v.position.equals(arr[i - 1].position)
);
}, [computedShortestPath]);
// --- Initialize Vehicles ---
useEffect(() => {
const findVehicle = assets
.filter((val) => val.eventData?.type === "Vehicle")
?.map((val) => val.modelUuid);
setVehicleUuids(findVehicle);
vehicleMovementState.current = {};
findVehicle.forEach((uuid) => {
vehicleMovementState.current[uuid] = { index: 0, progress: 0 };
});
}, [assets]);
// --- Vehicle Movement ---
useFrame((_, delta) => {
if (!isPlaying || pathSegments.length < 2) return;
const object = scene.getObjectByProperty(
"uuid",
vehicleUuids[activeVehicleIndexRef.current]
);
if (!object) return;
const state =
vehicleMovementState.current[vehicleUuids[activeVehicleIndexRef.current]];
if (!state) return;
const startSeg = pathSegments[state.index];
const endSeg = pathSegments[state.index + 1];
const segmentDistance = startSeg.position.distanceTo(endSeg.position);
state.progress += (speed * delta) / segmentDistance;
if (state.progress >= 1) {
state.progress = 0;
state.index++;
if (state.index >= pathSegments.length - 1) {
state.index = 0;
activeVehicleIndexRef.current =
(activeVehicleIndexRef.current + 1) % vehicleUuids.length;
}
}
const newPos = startSeg.position
.clone()
.lerp(endSeg.position, state.progress);
object.position.copy(newPos);
const direction = endSeg.position
.clone()
.sub(startSeg.position)
.normalize();
const forward = new THREE.Vector3(0, 0, 1);
object.quaternion.setFromUnitVectors(forward, direction);
});
// --- Update Mouse Position ---
useFrame(() => {
if (currentTempPath.length === 0) return;
raycaster.setFromCamera(pointer, camera);
const intersect = new THREE.Vector3();
if (raycaster.ray.intersectPlane(plane, intersect)) {
setCurrentMousePos([intersect.x, intersect.y, intersect.z]);
}
});
const addPointToCurrentTemp = (newPoint: PointData) => {
setCurrentTempPath((prev) => {
const updated = [...prev, newPoint];
if (prev.length > 0) {
const lastPoint = prev[prev.length - 1];
const newPath: PathDataInterface = {
pathId: THREE.MathUtils.generateUUID(),
pathPoints: [lastPoint, newPoint],
};
setAllPaths((prevPaths) => [...prevPaths, newPath]);
}
return updated;
});
setPathPointsList((prev) => {
if (!prev.find((p) => p.pointId === newPoint.pointId))
return [...prev, newPoint];
return prev;
});
};
useEffect(() => {
const canvas = gl.domElement;
const onMouseDown = (evt: MouseEvent) =>
handleMouseDown(evt, isLeftClickDown, isDragging);
const onMouseUp = (evt: MouseEvent) => handleMouseUp(evt, isLeftClickDown);
const onMouseMove = () => handleMouseMove(isLeftClickDown, isDragging);
const onClick = (evt: MouseEvent) =>
handleMouseClick({
evt,
isDragging,
raycaster,
plane,
pointer,
currentTempPath,
setCurrentTempPath,
pathPointsList,
allPaths,
setAllPaths,
addPointToCurrentTemp,
});
const onContextMenu = (evt: MouseEvent) =>
handleContextMenu(evt, setCurrentTempPath);
canvas.addEventListener("mousedown", onMouseDown);
canvas.addEventListener("mouseup", onMouseUp);
canvas.addEventListener("mousemove", onMouseMove);
canvas.addEventListener("click", onClick);
canvas.addEventListener("contextmenu", onContextMenu);
return () => {
canvas.removeEventListener("mousedown", onMouseDown);
canvas.removeEventListener("mouseup", onMouseUp);
canvas.removeEventListener("mousemove", onMouseMove);
canvas.removeEventListener("click", onClick);
canvas.removeEventListener("contextmenu", onContextMenu);
};
}, [
gl,
camera,
raycaster,
pointer,
plane,
currentTempPath,
pathPointsList,
allPaths,
]);
// --- Render ---
return (
<>
{allPaths.map((path, pathIndex) => (
<LineSegment
key={path.pathId}
index={pathIndex}
pathIndex={pathIndex}
paths={allPaths}
setPaths={setAllPaths}
/>
))}
{pathPointsList.map((point, index) => (
<PointHandles
key={point.pointId}
point={point}
pointIndex={index}
points={pathPointsList}
setPoints={setPathPointsList}
paths={allPaths}
setPaths={setAllPaths}
setShortestPath={setComputedShortestPath}
selected={selectedPointIndices}
setSelected={setSelectedPointIndices}
/>
))}
{currentTempPath.length > 0 && currentMousePos && (
<Line
points={[
new THREE.Vector3(
...currentTempPath[currentTempPath.length - 1].position
),
new THREE.Vector3(...currentMousePos),
]}
color="orange"
lineWidth={2}
dashed
/>
)}
</>
);
}

View File

@@ -4,6 +4,9 @@ import { usePlayButtonStore } from "../../../store/usePlayButtonStore";
import VehicleInstances from "./instances/vehicleInstances";
import VehicleUI from "../spatialUI/vehicle/vehicleUI";
import { useSceneContext } from "../../scene/sceneContext";
import PreDefinedPath from "./preDefinedPath/preDefinedPath";
import StructuredPath from "./structuredPath/structuredPath";
import PathCreator from "./pathCreator/pathCreator";
function Vehicles() {
const { vehicleStore } = useSceneContext();
@@ -14,24 +17,24 @@ function Vehicles() {
useEffect(() => {
if (selectedEventSphere) {
const selectedVehicle = getVehicleById(selectedEventSphere.userData.modelUuid);
const selectedVehicle = getVehicleById(
selectedEventSphere.userData.modelUuid
);
if (selectedVehicle) {
setIsVehicleSelected(true);
} else {
setIsVehicleSelected(false);
}
}
}, [getVehicleById, selectedEventSphere])
}, [getVehicleById, selectedEventSphere]);
return (
<>
<PathCreator />
{/* <StructuredPath /> */}
{/* <PreDefinedPath /> */}
<VehicleInstances />
{isVehicleSelected && selectedEventSphere && !isPlaying &&
<VehicleUI />
}
{isVehicleSelected && selectedEventSphere && !isPlaying && <VehicleUI />}
</>
);
}

View File

@@ -669,6 +669,96 @@ export const useContextActionStore = create<any>((set: any) => ({
contextAction: null,
setContextAction: (x: any) => set({ contextAction: x }),
}));
type PointData = {
pointId: string;
position: [number, number, number];
isCurved?: boolean;
handleA?: [number, number, number] | null;
handleB?: [number, number, number] | null;
};
interface PathDataInterface {
pathId: string;
isActive?: boolean;
isCurved?: boolean;
pathPoints: [PointData, PointData];
}
interface allPaths {
paths: string;
isAvailable: boolean;
vehicleId: string;
}
type PathData = PathDataInterface[];
export const useCreatedPaths = create<any>((set: any) => ({
paths: [
// {
// pathId: "276724c5-05a3-4b5e-a127-a60b3533ccce",
// pathPoints: [
// {
// pointId: "19c3f429-f214-4f87-8906-7eaaedd925da",
// position: [2.33155763270131, 0, -20.538859668988927],
// },
// {
// pointId: "ea73c7c8-0e26-4aae-9ed8-2349ff2d6718",
// position: [17.13371069714903, 0, -22.156135485080462],
// },
// ],
// },
// {
// pathId: "2736b20b-a433-443c-a5c9-5ba4348ac682",
// pathPoints: [
// {
// pointId: "ea73c7c8-0e26-4aae-9ed8-2349ff2d6718",
// position: [17.13371069714903, 0, -22.156135485080462],
// },
// {
// pointId: "2212bb52-c63c-4289-8b50-5ffd229d13e5",
// position: [16.29236816120279, 0, -10.819973445497789],
// },
// ],
// },
// {
// pathId: "3144a2df-7aad-483d-bbc7-de7f7b5b3bfc",
// pathPoints: [
// {
// pointId: "2212bb52-c63c-4289-8b50-5ffd229d13e5",
// position: [16.29236816120279, 0, -10.819973445497789],
// },
// {
// pointId: "adfd05a7-4e16-403f-81d0-ce99f2e34f5f",
// position: [4.677047323894161, 0, -8.279486846767094],
// },
// ],
// },
// {
// pathId: "e0a1b5da-27c2-44a0-81db-759b5a5eb416",
// pathPoints: [
// {
// pointId: "adfd05a7-4e16-403f-81d0-ce99f2e34f5f",
// position: [4.677047323894161, 0, -8.279486846767094],
// },
// {
// pointId: "19c3f429-f214-4f87-8906-7eaaedd925da",
// position: [2.33155763270131, 0, -20.538859668988927],
// },
// ],
// },
],
setPaths: (x: PathData) => set({ paths: x }),
allPaths: [],
setAllPaths: (x: allPaths) => set({ allPaths: x }),
}));
// route?: {
// pathId: string;
// pathPoints: {
// pointId: string;
// position: [number, number, number];
// isCurved?: boolean;
// handleA?: [number, number, number] | null;
// handleB: [number, number, number] | null;
// }[];
// Define the store's state and actions type