Compare commits
14 Commits
dev-api-so
...
feature/pr
| Author | SHA1 | Date | |
|---|---|---|---|
| c60f15db13 | |||
| 6da82895b7 | |||
| b44111a620 | |||
| c9536a13e0 | |||
| 6182862296 | |||
| a0e5115c6c | |||
| 5117e48527 | |||
| 7b5486590a | |||
| fe95ea8d0b | |||
| d090b976b0 | |||
| 7fb83417be | |||
| b623a92b9c | |||
| 3f808f167d | |||
| 5e025224d6 |
0
app/src/functions/findShortestPath.ts
Normal file
0
app/src/functions/findShortestPath.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -180,11 +180,11 @@ function PointsCreator() {
|
|||||||
drag = false;
|
drag = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onMouseUp = () => {
|
const onMouseUp = (e : MouseEvent) => {
|
||||||
if (selectedEventSphere && !drag) {
|
if (selectedEventSphere && !drag) {
|
||||||
raycaster.setFromCamera(pointer, camera);
|
raycaster.setFromCamera(pointer, camera);
|
||||||
const intersects = raycaster.intersectObjects(scene.children, true).filter((intersect) => intersect.object.name === "Event-Sphere");
|
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();
|
clearSelectedEventSphere();
|
||||||
setTransformMode(null);
|
setTransformMode(null);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -199,6 +199,7 @@ const VehicleUI = () => {
|
|||||||
steeringAngle: steeringRotation[1],
|
steeringAngle: steeringRotation[1],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,368 +1,409 @@
|
|||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useFrame, useThree, ThreeEvent } from "@react-three/fiber";
|
||||||
import { useFrame, useThree, ThreeEvent } from '@react-three/fiber';
|
import * as THREE from "three";
|
||||||
import * as THREE from 'three';
|
import { Line } from "@react-three/drei";
|
||||||
import { Line } from '@react-three/drei';
|
import {
|
||||||
import { useAnimationPlaySpeed, usePauseButtonStore, usePlayButtonStore, useResetButtonStore } from '../../../../../store/usePlayButtonStore';
|
useAnimationPlaySpeed,
|
||||||
import { useSceneContext } from '../../../../scene/sceneContext';
|
usePauseButtonStore,
|
||||||
import { useActiveTool, useSelectedPath } from '../../../../../store/builder/store';
|
usePlayButtonStore,
|
||||||
|
useResetButtonStore,
|
||||||
|
} from "../../../../../store/usePlayButtonStore";
|
||||||
|
import { useSceneContext } from "../../../../scene/sceneContext";
|
||||||
|
import {
|
||||||
|
useActiveTool,
|
||||||
|
useSelectedPath,
|
||||||
|
} from "../../../../../store/builder/store";
|
||||||
|
|
||||||
interface VehicleAnimatorProps {
|
interface VehicleAnimatorProps {
|
||||||
path: [number, number, number][];
|
path: [number, number, number][];
|
||||||
handleCallBack: () => void;
|
handleCallBack: () => void;
|
||||||
reset: () => void;
|
reset: () => void;
|
||||||
startUnloadingProcess: () => void;
|
startUnloadingProcess: () => void;
|
||||||
currentPhase: string;
|
currentPhase: string;
|
||||||
agvUuid: string;
|
agvUuid: string;
|
||||||
agvDetail: VehicleStatus;
|
agvDetail: VehicleStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
function VehicleAnimator({ path, handleCallBack, currentPhase, agvUuid, agvDetail, reset, startUnloadingProcess }: Readonly<VehicleAnimatorProps>) {
|
function VehicleAnimator({
|
||||||
const { vehicleStore } = useSceneContext();
|
path,
|
||||||
const { getVehicleById } = vehicleStore();
|
handleCallBack,
|
||||||
const { isPaused } = usePauseButtonStore();
|
currentPhase,
|
||||||
const { isPlaying } = usePlayButtonStore();
|
agvUuid,
|
||||||
const { speed } = useAnimationPlaySpeed();
|
agvDetail,
|
||||||
const { isReset, setReset } = useResetButtonStore();
|
reset,
|
||||||
const progressRef = useRef<number>(0);
|
startUnloadingProcess,
|
||||||
const movingForward = useRef<boolean>(true);
|
}: Readonly<VehicleAnimatorProps>) {
|
||||||
const completedRef = useRef<boolean>(false);
|
const { vehicleStore } = useSceneContext();
|
||||||
const [objectRotation, setObjectRotation] = useState<{ x: number; y: number; z: number } | undefined>(agvDetail.point?.action?.pickUpPoint?.rotation || { x: 0, y: 0, z: 0 })
|
const { getVehicleById } = vehicleStore();
|
||||||
const [restRotation, setRestingRotation] = useState<boolean>(true);
|
const { isPaused } = usePauseButtonStore();
|
||||||
const [currentPath, setCurrentPath] = useState<[number, number, number][]>([]);
|
const { isPlaying } = usePlayButtonStore();
|
||||||
const { scene, controls } = useThree();
|
const { speed } = useAnimationPlaySpeed();
|
||||||
const { selectedPath } = useSelectedPath();
|
const { isReset, setReset } = useResetButtonStore();
|
||||||
const [isAnyDragging, setIsAnyDragging] = useState<string>("");
|
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 [restRotation, setRestingRotation] = useState<boolean>(true);
|
||||||
|
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) {
|
||||||
|
setCurrentPath(path);
|
||||||
|
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);
|
||||||
|
setCurrentPath(path);
|
||||||
|
}
|
||||||
|
}, [currentPhase, path, objectRotation, selectedPath]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentPhase === 'stationed-pickup' && path.length > 0 && selectedPath === "auto") {
|
completedRef.current = false;
|
||||||
setCurrentPath(path);
|
}, [currentPath]);
|
||||||
setObjectRotation(agvDetail.point.action?.pickUpPoint?.rotation)
|
|
||||||
} else if (currentPhase === 'pickup-drop' && path.length > 0) {
|
useEffect(() => {
|
||||||
setObjectRotation(agvDetail.point.action?.unLoadPoint?.rotation)
|
if (isReset || !isPlaying) {
|
||||||
setCurrentPath(path);
|
reset();
|
||||||
} else if (currentPhase === 'drop-pickup' && path.length > 0) {
|
setCurrentPath([]);
|
||||||
setObjectRotation(agvDetail.point.action?.pickUpPoint?.rotation)
|
completedRef.current = false;
|
||||||
setCurrentPath(path);
|
movingForward.current = true;
|
||||||
|
progressRef.current = 0;
|
||||||
|
setReset(false);
|
||||||
|
setRestingRotation(true);
|
||||||
|
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]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [isReset, isPlaying]);
|
||||||
|
|
||||||
|
const lastTimeRef = useRef(performance.now());
|
||||||
|
|
||||||
|
useFrame(() => {
|
||||||
|
if (!isPlaying) return;
|
||||||
|
const now = performance.now();
|
||||||
|
const delta = (now - lastTimeRef.current) / 1000;
|
||||||
|
lastTimeRef.current = now;
|
||||||
|
|
||||||
|
const object = scene.getObjectByProperty("uuid", agvUuid);
|
||||||
|
if (!object || currentPath.length < 2) return;
|
||||||
|
if (isPaused) return;
|
||||||
|
|
||||||
|
let totalDistance = 0;
|
||||||
|
const distances = [];
|
||||||
|
let accumulatedDistance = 0;
|
||||||
|
let index = 0;
|
||||||
|
const rotationSpeed = 0.75;
|
||||||
|
|
||||||
|
for (let i = 0; i < currentPath.length - 1; i++) {
|
||||||
|
const start = new THREE.Vector3(...currentPath[i]);
|
||||||
|
const end = new THREE.Vector3(...currentPath[i + 1]);
|
||||||
|
const segmentDistance = start.distanceTo(end);
|
||||||
|
distances.push(segmentDistance);
|
||||||
|
totalDistance += segmentDistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (
|
||||||
|
index < distances.length &&
|
||||||
|
progressRef.current > accumulatedDistance + distances[index]
|
||||||
|
) {
|
||||||
|
accumulatedDistance += distances[index];
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index < distances.length) {
|
||||||
|
const start = new THREE.Vector3(...currentPath[index]);
|
||||||
|
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
|
||||||
|
);
|
||||||
|
targetQuaternion.multiply(y180);
|
||||||
|
|
||||||
|
const angle = object.quaternion.angleTo(targetQuaternion);
|
||||||
|
if (angle < 0.01) {
|
||||||
|
object.quaternion.copy(targetQuaternion);
|
||||||
|
} else {
|
||||||
|
const step = rotationSpeed * delta * speed * agvDetail.speed;
|
||||||
|
const angle = object.quaternion.angleTo(targetQuaternion);
|
||||||
|
|
||||||
|
if (angle < step) {
|
||||||
|
object.quaternion.copy(targetQuaternion);
|
||||||
|
} else {
|
||||||
|
object.quaternion.rotateTowards(targetQuaternion, step);
|
||||||
}
|
}
|
||||||
}, [currentPhase, path, objectRotation, selectedPath]);
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
const isAligned = angle < 0.01;
|
||||||
completedRef.current = false;
|
|
||||||
}, [currentPath]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
if (isAligned) {
|
||||||
if (isReset || !isPlaying) {
|
progressRef.current += delta * (speed * agvDetail.speed);
|
||||||
reset();
|
const t = (progressRef.current - accumulatedDistance) / segmentDistance;
|
||||||
setCurrentPath([]);
|
const position = start.clone().lerp(end, t);
|
||||||
completedRef.current = false;
|
object.position.copy(position);
|
||||||
movingForward.current = true;
|
}
|
||||||
progressRef.current = 0;
|
}
|
||||||
setReset(false);
|
|
||||||
setRestingRotation(true);
|
if (progressRef.current >= totalDistance) {
|
||||||
const object = scene.getObjectByProperty('uuid', agvUuid);
|
if (restRotation && objectRotation) {
|
||||||
const vehicle = getVehicleById(agvDetail.modelUuid);
|
const targetEuler = new THREE.Euler(
|
||||||
if (object && vehicle) {
|
0,
|
||||||
object.position.set(vehicle.position[0], vehicle.position[1], vehicle.position[2]);
|
objectRotation.y - agvDetail.point.action.steeringAngle,
|
||||||
object.rotation.set(vehicle.rotation[0], vehicle.rotation[1], vehicle.rotation[2]);
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
const baseQuaternion = new THREE.Quaternion().setFromEuler(targetEuler);
|
||||||
|
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);
|
||||||
|
} else {
|
||||||
|
const step = rotationSpeed * delta * speed * agvDetail.speed;
|
||||||
|
const angle = object.quaternion.angleTo(targetQuaternion);
|
||||||
|
|
||||||
|
if (angle < step) {
|
||||||
|
object.quaternion.copy(targetQuaternion);
|
||||||
|
} else {
|
||||||
|
object.quaternion.rotateTowards(targetQuaternion, step);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progressRef.current >= totalDistance) {
|
||||||
|
setRestingRotation(true);
|
||||||
|
progressRef.current = 0;
|
||||||
|
movingForward.current = !movingForward.current;
|
||||||
|
setCurrentPath([]);
|
||||||
|
handleCallBack();
|
||||||
|
if (currentPhase === "pickup-drop") {
|
||||||
|
requestAnimationFrame(startUnloadingProcess);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatePoint = (index: number, pos: THREE.Vector3) => {
|
||||||
|
const updated = [...currentPath];
|
||||||
|
updated[index] = pos.toArray() as [number, number, number];
|
||||||
|
setCurrentPath(updated);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{selectedPath === "auto" && (
|
||||||
|
<group visible={false}>
|
||||||
|
{currentPath.map((pos, i) => {
|
||||||
|
if (i < currentPath.length - 1) {
|
||||||
|
return (
|
||||||
|
<DraggableLineSegment
|
||||||
|
key={i}
|
||||||
|
index={i}
|
||||||
|
start={new THREE.Vector3(...currentPath[i])}
|
||||||
|
end={new THREE.Vector3(...currentPath[i + 1])}
|
||||||
|
updatePoints={(i0, p0, i1, p1) => {
|
||||||
|
const updated = [...currentPath];
|
||||||
|
updated[i0] = p0.toArray() as [number, number, number];
|
||||||
|
updated[i1] = p1.toArray() as [number, number, number];
|
||||||
|
setCurrentPath(updated);
|
||||||
|
}}
|
||||||
|
isAnyDragging={isAnyDragging}
|
||||||
|
setIsAnyDragging={setIsAnyDragging}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
return null;
|
||||||
}, [isReset, isPlaying])
|
})}
|
||||||
|
{currentPath.length > 0 && (
|
||||||
const lastTimeRef = useRef(performance.now());
|
<group
|
||||||
|
onPointerMissed={() => {
|
||||||
useFrame(() => {
|
if (controls) (controls as any).enabled = true;
|
||||||
if (!isPlaying) return
|
}}
|
||||||
const now = performance.now();
|
>
|
||||||
const delta = (now - lastTimeRef.current) / 1000;
|
{currentPath.map((pos, i) => (
|
||||||
lastTimeRef.current = now;
|
<DraggableSphere
|
||||||
|
key={i}
|
||||||
const object = scene.getObjectByProperty('uuid', agvUuid);
|
index={i}
|
||||||
if (!object || currentPath.length < 2) return;
|
position={new THREE.Vector3(...pos)}
|
||||||
if (isPaused) return;
|
onMove={updatePoint}
|
||||||
|
isAnyDragging={isAnyDragging}
|
||||||
let totalDistance = 0;
|
setIsAnyDragging={setIsAnyDragging}
|
||||||
const distances = [];
|
/>
|
||||||
let accumulatedDistance = 0;
|
))}
|
||||||
let index = 0;
|
</group>
|
||||||
const rotationSpeed = 0.75;
|
)}
|
||||||
|
</group>
|
||||||
for (let i = 0; i < currentPath.length - 1; i++) {
|
)}
|
||||||
const start = new THREE.Vector3(...currentPath[i]);
|
</>
|
||||||
const end = new THREE.Vector3(...currentPath[i + 1]);
|
);
|
||||||
const segmentDistance = start.distanceTo(end);
|
|
||||||
distances.push(segmentDistance);
|
|
||||||
totalDistance += segmentDistance;
|
|
||||||
}
|
|
||||||
|
|
||||||
while (index < distances.length && progressRef.current > accumulatedDistance + distances[index]) {
|
|
||||||
accumulatedDistance += distances[index];
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index < distances.length) {
|
|
||||||
const start = new THREE.Vector3(...currentPath[index]);
|
|
||||||
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);
|
|
||||||
targetQuaternion.multiply(y180);
|
|
||||||
|
|
||||||
const angle = object.quaternion.angleTo(targetQuaternion);
|
|
||||||
if (angle < 0.01) {
|
|
||||||
object.quaternion.copy(targetQuaternion);
|
|
||||||
} else {
|
|
||||||
const step = rotationSpeed * delta * speed * agvDetail.speed;
|
|
||||||
const angle = object.quaternion.angleTo(targetQuaternion);
|
|
||||||
|
|
||||||
if (angle < step) {
|
|
||||||
object.quaternion.copy(targetQuaternion);
|
|
||||||
} else {
|
|
||||||
object.quaternion.rotateTowards(targetQuaternion, step);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const isAligned = angle < 0.01;
|
|
||||||
|
|
||||||
if (isAligned) {
|
|
||||||
progressRef.current += delta * (speed * agvDetail.speed);
|
|
||||||
const t = (progressRef.current - accumulatedDistance) / segmentDistance;
|
|
||||||
const position = start.clone().lerp(end, t);
|
|
||||||
object.position.copy(position);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (progressRef.current >= totalDistance) {
|
|
||||||
if (restRotation && objectRotation) {
|
|
||||||
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 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);
|
|
||||||
|
|
||||||
if (angle < step) {
|
|
||||||
object.quaternion.copy(targetQuaternion);
|
|
||||||
} else {
|
|
||||||
object.quaternion.rotateTowards(targetQuaternion, step);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (progressRef.current >= totalDistance) {
|
|
||||||
setRestingRotation(true);
|
|
||||||
progressRef.current = 0;
|
|
||||||
movingForward.current = !movingForward.current;
|
|
||||||
setCurrentPath([]);
|
|
||||||
handleCallBack();
|
|
||||||
if (currentPhase === 'pickup-drop') {
|
|
||||||
requestAnimationFrame(startUnloadingProcess);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const updatePoint = (index: number, pos: THREE.Vector3) => {
|
|
||||||
const updated = [...currentPath];
|
|
||||||
updated[index] = pos.toArray() as [number, number, number];
|
|
||||||
setCurrentPath(updated);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{selectedPath === "auto" &&
|
|
||||||
<group visible={false}>
|
|
||||||
{currentPath.map((pos, i) => {
|
|
||||||
if (i < currentPath.length - 1) {
|
|
||||||
return (
|
|
||||||
<DraggableLineSegment
|
|
||||||
key={i}
|
|
||||||
index={i}
|
|
||||||
start={new THREE.Vector3(...currentPath[i])}
|
|
||||||
end={new THREE.Vector3(...currentPath[i + 1])}
|
|
||||||
updatePoints={(i0, p0, i1, p1) => {
|
|
||||||
const updated = [...currentPath];
|
|
||||||
updated[i0] = p0.toArray() as [number, number, number];
|
|
||||||
updated[i1] = p1.toArray() as [number, number, number];
|
|
||||||
setCurrentPath(updated);
|
|
||||||
}}
|
|
||||||
isAnyDragging={isAnyDragging}
|
|
||||||
setIsAnyDragging={setIsAnyDragging}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
})}
|
|
||||||
{currentPath.length > 0 && (
|
|
||||||
<group onPointerMissed={() => { if (controls) (controls as any).enabled = true; }}>
|
|
||||||
{currentPath.map((pos, i) =>
|
|
||||||
(
|
|
||||||
<DraggableSphere
|
|
||||||
key={i}
|
|
||||||
index={i}
|
|
||||||
position={new THREE.Vector3(...pos)}
|
|
||||||
onMove={updatePoint}
|
|
||||||
isAnyDragging={isAnyDragging}
|
|
||||||
setIsAnyDragging={setIsAnyDragging}
|
|
||||||
/>)
|
|
||||||
)}
|
|
||||||
</group >
|
|
||||||
)}
|
|
||||||
</group >
|
|
||||||
}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default VehicleAnimator;
|
export default VehicleAnimator;
|
||||||
|
|
||||||
function DraggableSphere({
|
function DraggableSphere({
|
||||||
index,
|
index,
|
||||||
position,
|
position,
|
||||||
onMove,
|
onMove,
|
||||||
isAnyDragging,
|
isAnyDragging,
|
||||||
setIsAnyDragging,
|
setIsAnyDragging,
|
||||||
}: {
|
}: {
|
||||||
index: number;
|
index: number;
|
||||||
position: THREE.Vector3;
|
position: THREE.Vector3;
|
||||||
onMove: (index: number, pos: THREE.Vector3) => void;
|
onMove: (index: number, pos: THREE.Vector3) => void;
|
||||||
isAnyDragging: string;
|
isAnyDragging: string;
|
||||||
setIsAnyDragging: (val: string) => void;
|
setIsAnyDragging: (val: string) => void;
|
||||||
}) {
|
}) {
|
||||||
const meshRef = useRef<THREE.Mesh>(null);
|
const meshRef = useRef<THREE.Mesh>(null);
|
||||||
const { gl, controls, raycaster } = useThree();
|
const { gl, controls, raycaster } = useThree();
|
||||||
const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
|
const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
|
||||||
const { activeTool } = useActiveTool();
|
const { activeTool } = useActiveTool();
|
||||||
|
|
||||||
const onPointerDown = (e: ThreeEvent<PointerEvent>) => {
|
const onPointerDown = (e: ThreeEvent<PointerEvent>) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation();
|
||||||
if (activeTool !== 'pen') return;
|
if (activeTool !== "pen") return;
|
||||||
setIsAnyDragging("point");
|
setIsAnyDragging("point");
|
||||||
gl.domElement.style.cursor = 'grabbing';
|
gl.domElement.style.cursor = "grabbing";
|
||||||
if (controls) (controls as any).enabled = false;
|
if (controls) (controls as any).enabled = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPointerMove = (e: ThreeEvent<PointerEvent>) => {
|
||||||
|
if (isAnyDragging !== "point" || activeTool !== "pen") return;
|
||||||
|
|
||||||
|
const intersect = new THREE.Vector3();
|
||||||
|
if (raycaster.ray.intersectPlane(plane, intersect)) {
|
||||||
|
meshRef.current!.position.copy(intersect);
|
||||||
|
onMove(index, intersect);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPointerUp = () => {
|
||||||
|
if (activeTool !== "pen") return;
|
||||||
|
setIsAnyDragging("");
|
||||||
|
gl.domElement.style.cursor = "default";
|
||||||
|
if (controls) (controls as any).enabled = true;
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
gl.domElement.addEventListener("pointerup", onPointerUp);
|
||||||
|
return () => {
|
||||||
|
gl.domElement.removeEventListener("pointerup", onPointerUp);
|
||||||
};
|
};
|
||||||
|
}, [activeTool]);
|
||||||
|
|
||||||
const onPointerMove = (e: ThreeEvent<PointerEvent>) => {
|
return (
|
||||||
if (isAnyDragging !== "point" || activeTool !== 'pen') return;
|
<mesh
|
||||||
|
ref={meshRef}
|
||||||
const intersect = new THREE.Vector3();
|
position={position}
|
||||||
if (raycaster.ray.intersectPlane(plane, intersect)) {
|
onPointerDown={onPointerDown}
|
||||||
meshRef.current!.position.copy(intersect);
|
onPointerMove={onPointerMove}
|
||||||
onMove(index, intersect);
|
onPointerUp={onPointerUp}
|
||||||
}
|
onPointerMissed={onPointerUp}
|
||||||
};
|
>
|
||||||
|
<sphereGeometry args={[0.2, 16, 16]} />
|
||||||
const onPointerUp = () => {
|
<meshStandardMaterial color="red" />
|
||||||
if (activeTool !== 'pen') return;
|
</mesh>
|
||||||
setIsAnyDragging("");
|
);
|
||||||
gl.domElement.style.cursor = 'default';
|
|
||||||
if (controls) (controls as any).enabled = true;
|
|
||||||
};
|
|
||||||
useEffect(() => {
|
|
||||||
gl.domElement.addEventListener("pointerup", onPointerUp);
|
|
||||||
return (() => {
|
|
||||||
gl.domElement.removeEventListener("pointerup", onPointerUp);
|
|
||||||
})
|
|
||||||
}, [activeTool])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<mesh
|
|
||||||
ref={meshRef}
|
|
||||||
position={position}
|
|
||||||
onPointerDown={onPointerDown}
|
|
||||||
onPointerMove={onPointerMove}
|
|
||||||
onPointerUp={onPointerUp}
|
|
||||||
onPointerMissed={onPointerUp}
|
|
||||||
>
|
|
||||||
<sphereGeometry args={[0.2, 16, 16]} />
|
|
||||||
<meshStandardMaterial color="red" />
|
|
||||||
</mesh>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function DraggableLineSegment({
|
function DraggableLineSegment({
|
||||||
index,
|
index,
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
updatePoints,
|
updatePoints,
|
||||||
isAnyDragging,
|
isAnyDragging,
|
||||||
setIsAnyDragging,
|
setIsAnyDragging,
|
||||||
}: {
|
}: {
|
||||||
index: number;
|
index: number;
|
||||||
start: THREE.Vector3;
|
start: THREE.Vector3;
|
||||||
end: THREE.Vector3;
|
end: THREE.Vector3;
|
||||||
updatePoints: (i0: number, p0: THREE.Vector3, i1: number, p1: THREE.Vector3) => void;
|
updatePoints: (
|
||||||
isAnyDragging: string;
|
i0: number,
|
||||||
setIsAnyDragging: (val: string) => void;
|
p0: THREE.Vector3,
|
||||||
|
i1: number,
|
||||||
|
p1: THREE.Vector3
|
||||||
|
) => void;
|
||||||
|
isAnyDragging: string;
|
||||||
|
setIsAnyDragging: (val: string) => void;
|
||||||
}) {
|
}) {
|
||||||
const { gl, raycaster, controls } = useThree();
|
const { gl, raycaster, controls } = useThree();
|
||||||
const { activeTool } = useActiveTool();
|
const { activeTool } = useActiveTool();
|
||||||
const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
|
const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
|
||||||
const dragStart = useRef<THREE.Vector3 | null>(null);
|
const dragStart = useRef<THREE.Vector3 | null>(null);
|
||||||
|
|
||||||
const onPointerDown = () => {
|
const onPointerDown = () => {
|
||||||
if (activeTool !== 'pen' || isAnyDragging) return;
|
if (activeTool !== "pen" || isAnyDragging) return;
|
||||||
setIsAnyDragging("line");
|
setIsAnyDragging("line");
|
||||||
gl.domElement.style.cursor = 'grabbing';
|
gl.domElement.style.cursor = "grabbing";
|
||||||
if (controls) (controls as any).enabled = false;
|
if (controls) (controls as any).enabled = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPointerMove = (e: ThreeEvent<PointerEvent>) => {
|
||||||
|
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 newStart = start.clone().add(offset);
|
||||||
|
const newEnd = end.clone().add(offset);
|
||||||
|
updatePoints(index, newStart, index + 1, newEnd);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPointerUp = () => {
|
||||||
|
if (activeTool !== "pen") return;
|
||||||
|
setIsAnyDragging("");
|
||||||
|
dragStart.current = null;
|
||||||
|
gl.domElement.style.cursor = "default";
|
||||||
|
if (controls) (controls as any).enabled = true;
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
gl.domElement.addEventListener("pointerup", onPointerUp);
|
||||||
|
return () => {
|
||||||
|
gl.domElement.removeEventListener("pointerup", onPointerUp);
|
||||||
};
|
};
|
||||||
|
}, [activeTool]);
|
||||||
const onPointerMove = (e: ThreeEvent<PointerEvent>) => {
|
return (
|
||||||
if (isAnyDragging !== "line" || activeTool !== 'pen') return;
|
<Line
|
||||||
|
points={[start, end]}
|
||||||
const intersect = new THREE.Vector3();
|
color="blue"
|
||||||
if (raycaster.ray.intersectPlane(plane, intersect)) {
|
lineWidth={5}
|
||||||
if (!dragStart.current) dragStart.current = intersect.clone();
|
onPointerDown={onPointerDown}
|
||||||
const offset = new THREE.Vector3().subVectors(intersect, dragStart.current);
|
onPointerMove={onPointerMove}
|
||||||
const newStart = start.clone().add(offset);
|
onPointerUp={onPointerUp}
|
||||||
const newEnd = end.clone().add(offset);
|
onPointerMissed={onPointerUp}
|
||||||
updatePoints(index, newStart, index + 1, newEnd);
|
/>
|
||||||
}
|
);
|
||||||
};
|
|
||||||
|
|
||||||
const onPointerUp = () => {
|
|
||||||
if (activeTool !== 'pen') return;
|
|
||||||
setIsAnyDragging("");
|
|
||||||
dragStart.current = null;
|
|
||||||
gl.domElement.style.cursor = 'default';
|
|
||||||
if (controls) (controls as any).enabled = true;
|
|
||||||
};
|
|
||||||
useEffect(() => {
|
|
||||||
gl.domElement.addEventListener("pointerup", onPointerUp);
|
|
||||||
return (() => {
|
|
||||||
gl.domElement.removeEventListener("pointerup", onPointerUp);
|
|
||||||
})
|
|
||||||
}, [activeTool])
|
|
||||||
return (
|
|
||||||
<Line
|
|
||||||
points={[start, end]}
|
|
||||||
color="blue"
|
|
||||||
lineWidth={5}
|
|
||||||
onPointerDown={onPointerDown}
|
|
||||||
onPointerMove={onPointerMove}
|
|
||||||
onPointerUp={onPointerUp}
|
|
||||||
onPointerMissed={onPointerUp}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 we’re 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]);
|
||||||
@@ -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} />;
|
||||||
|
}
|
||||||
@@ -1,21 +1,44 @@
|
|||||||
import React from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import VehicleInstance from "./instance/vehicleInstance";
|
import VehicleInstance from "./instance/vehicleInstance";
|
||||||
import VehicleContentUi from "../../ui3d/VehicleContentUi";
|
import VehicleContentUi from "../../ui3d/VehicleContentUi";
|
||||||
import { useSceneContext } from "../../../scene/sceneContext";
|
import { useSceneContext } from "../../../scene/sceneContext";
|
||||||
import { useViewSceneStore } from "../../../../store/builder/store";
|
import { useViewSceneStore } from "../../../../store/builder/store";
|
||||||
|
import PathCreator from "../pathCreator/pathCreator";
|
||||||
|
import VehicleInstance2 from "./instance/vehicleInstance2";
|
||||||
|
|
||||||
function VehicleInstances() {
|
function VehicleInstances() {
|
||||||
const { vehicleStore } = useSceneContext();
|
const { vehicleStore } = useSceneContext();
|
||||||
const { vehicles } = vehicleStore();
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{vehicles.map((vehicle: VehicleStatus) => (
|
{/* {vehicles.map((vehicle: VehicleStatus) => (
|
||||||
<React.Fragment key={vehicle.modelUuid}>
|
<React.Fragment key={vehicle.modelUuid}>
|
||||||
<VehicleInstance agvDetail={vehicle} />
|
<VehicleInstance agvDetail={vehicle} />
|
||||||
{viewSceneLabels && <VehicleContentUi vehicle={vehicle} />}
|
{viewSceneLabels && <VehicleContentUi vehicle={vehicle} />}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
))} */}
|
||||||
|
{vehiclesData.map((vehicle: VehicleStructure) => (
|
||||||
|
<React.Fragment key={vehicle.vehicleId}>
|
||||||
|
<VehicleInstance2
|
||||||
|
vehicleData={vehicle}
|
||||||
|
vehiclesData={vehiclesData}
|
||||||
|
setVehiclesData={setVehiclesData}
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
};
|
||||||
@@ -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]);
|
||||||
|
};
|
||||||
428
app/src/modules/simulation/vehicle/pathCreator/pathCreator.tsx
Normal file
428
app/src/modules/simulation/vehicle/pathCreator/pathCreator.tsx
Normal 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}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
189
app/src/modules/simulation/vehicle/pathCreator/pathHandler.tsx
Normal file
189
app/src/modules/simulation/vehicle/pathCreator/pathHandler.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
720
app/src/modules/simulation/vehicle/pathCreator/pointHandler.tsx
Normal file
720
app/src/modules/simulation/vehicle/pathCreator/pointHandler.tsx
Normal 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}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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]);
|
||||||
@@ -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 [];
|
||||||
|
}
|
||||||
@@ -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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export function handleContextMenu(
|
||||||
|
evt: MouseEvent,
|
||||||
|
setCurrentTempPath: (val: any[]) => void
|
||||||
|
) {
|
||||||
|
evt.preventDefault();
|
||||||
|
setCurrentTempPath([]);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export function handleMouseMove(
|
||||||
|
isLeftClickDown: { current: boolean },
|
||||||
|
isDragging: { current: boolean }
|
||||||
|
) {
|
||||||
|
if (isLeftClickDown.current) isDragging.current = true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export function handleMouseUp(
|
||||||
|
evt: MouseEvent,
|
||||||
|
isLeftClickDown: { current: boolean }
|
||||||
|
) {
|
||||||
|
if (evt.button === 0) isLeftClickDown.current = false;
|
||||||
|
}
|
||||||
@@ -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([]);
|
||||||
|
}
|
||||||
@@ -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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,36 +4,39 @@ import { usePlayButtonStore } from "../../../store/usePlayButtonStore";
|
|||||||
import VehicleInstances from "./instances/vehicleInstances";
|
import VehicleInstances from "./instances/vehicleInstances";
|
||||||
import VehicleUI from "../spatialUI/vehicle/vehicleUI";
|
import VehicleUI from "../spatialUI/vehicle/vehicleUI";
|
||||||
import { useSceneContext } from "../../scene/sceneContext";
|
import { useSceneContext } from "../../scene/sceneContext";
|
||||||
|
import PreDefinedPath from "./preDefinedPath/preDefinedPath";
|
||||||
|
import StructuredPath from "./structuredPath/structuredPath";
|
||||||
|
import PathCreator from "./pathCreator/pathCreator";
|
||||||
|
|
||||||
function Vehicles() {
|
function Vehicles() {
|
||||||
const { vehicleStore } = useSceneContext();
|
const { vehicleStore } = useSceneContext();
|
||||||
const { getVehicleById } = vehicleStore();
|
const { getVehicleById } = vehicleStore();
|
||||||
const { selectedEventSphere } = useSelectedEventSphere();
|
const { selectedEventSphere } = useSelectedEventSphere();
|
||||||
const { isPlaying } = usePlayButtonStore();
|
const { isPlaying } = usePlayButtonStore();
|
||||||
const [isVehicleSelected, setIsVehicleSelected] = useState(false);
|
const [isVehicleSelected, setIsVehicleSelected] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedEventSphere) {
|
if (selectedEventSphere) {
|
||||||
const selectedVehicle = getVehicleById(selectedEventSphere.userData.modelUuid);
|
const selectedVehicle = getVehicleById(
|
||||||
if (selectedVehicle) {
|
selectedEventSphere.userData.modelUuid
|
||||||
setIsVehicleSelected(true);
|
);
|
||||||
} else {
|
if (selectedVehicle) {
|
||||||
setIsVehicleSelected(false);
|
setIsVehicleSelected(true);
|
||||||
}
|
} else {
|
||||||
}
|
setIsVehicleSelected(false);
|
||||||
}, [getVehicleById, selectedEventSphere])
|
}
|
||||||
|
}
|
||||||
|
}, [getVehicleById, selectedEventSphere]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<PathCreator />
|
||||||
<VehicleInstances />
|
{/* <StructuredPath /> */}
|
||||||
|
{/* <PreDefinedPath /> */}
|
||||||
{isVehicleSelected && selectedEventSphere && !isPlaying &&
|
<VehicleInstances />
|
||||||
<VehicleUI />
|
{isVehicleSelected && selectedEventSphere && !isPlaying && <VehicleUI />}
|
||||||
}
|
</>
|
||||||
|
);
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Vehicles;
|
export default Vehicles;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user