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
@@ -1,20 +1,28 @@
|
||||
import * as THREE from 'three'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useThree } from '@react-three/fiber';
|
||||
import { useActiveLayer, useSocketStore, useToggleView, useToolMode } from '../../../../store/builder/store';
|
||||
import { useBuilderStore } from '../../../../store/builder/useBuilderStore';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useVersionContext } from '../../version/versionContext';
|
||||
import { useSceneContext } from '../../../scene/sceneContext';
|
||||
import ReferenceAisle from './referenceAisle';
|
||||
import ReferencePoint from '../../point/reference/referencePoint';
|
||||
import { getUserData } from '../../../../functions/getUserData';
|
||||
import * as THREE from "three";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useThree } from "@react-three/fiber";
|
||||
import {
|
||||
useActiveLayer,
|
||||
useSocketStore,
|
||||
useToggleView,
|
||||
useToolMode,
|
||||
} from "../../../../store/builder/store";
|
||||
import { useBuilderStore } from "../../../../store/builder/useBuilderStore";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useVersionContext } from "../../version/versionContext";
|
||||
import { useSceneContext } from "../../../scene/sceneContext";
|
||||
import ReferenceAisle from "./referenceAisle";
|
||||
import ReferencePoint from "../../point/reference/referencePoint";
|
||||
import { getUserData } from "../../../../functions/getUserData";
|
||||
|
||||
// import { upsertAisleApi } from '../../../../services/factoryBuilder/aisle/upsertAisleApi';
|
||||
|
||||
function AisleCreator() {
|
||||
const { scene, camera, raycaster, gl, pointer } = useThree();
|
||||
const plane = useMemo(() => new THREE.Plane(new THREE.Vector3(0, 1, 0), 0), []);
|
||||
const plane = useMemo(
|
||||
() => new THREE.Plane(new THREE.Vector3(0, 1, 0), 0),
|
||||
[]
|
||||
);
|
||||
const { toggleView } = useToggleView();
|
||||
const { toolMode } = useToolMode();
|
||||
const { activeLayer } = useActiveLayer();
|
||||
@@ -31,7 +39,20 @@ function AisleCreator() {
|
||||
|
||||
const [tempPoints, setTempPoints] = useState<Point[]>([]);
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const { aisleType, aisleWidth, aisleColor, dashLength, gapLength, dotRadius, aisleLength, isFlipped, snappedPosition, snappedPoint, setSnappedPosition, setSnappedPoint } = useBuilderStore();
|
||||
const {
|
||||
aisleType,
|
||||
aisleWidth,
|
||||
aisleColor,
|
||||
dashLength,
|
||||
gapLength,
|
||||
dotRadius,
|
||||
aisleLength,
|
||||
isFlipped,
|
||||
snappedPosition,
|
||||
snappedPoint,
|
||||
setSnappedPosition,
|
||||
setSnappedPoint,
|
||||
} = useBuilderStore();
|
||||
|
||||
useEffect(() => {
|
||||
const canvasElement = gl.domElement;
|
||||
@@ -63,13 +84,15 @@ function AisleCreator() {
|
||||
let position = raycaster.ray.intersectPlane(plane, intersectionPoint);
|
||||
if (!position) return;
|
||||
|
||||
const intersects = raycaster.intersectObjects(scene.children).find((intersect) => intersect.object.name === 'Aisle-Point');
|
||||
const intersects = raycaster
|
||||
.intersectObjects(scene.children)
|
||||
.find((intersect) => intersect.object.name === "Aisle-Point");
|
||||
|
||||
const newPoint: Point = {
|
||||
pointUuid: THREE.MathUtils.generateUUID(),
|
||||
pointType: 'Aisle',
|
||||
pointType: "Aisle",
|
||||
position: [position.x, position.y, position.z],
|
||||
layer: activeLayer
|
||||
layer: activeLayer,
|
||||
};
|
||||
|
||||
if (snappedPosition && snappedPoint) {
|
||||
@@ -78,7 +101,9 @@ function AisleCreator() {
|
||||
newPoint.layer = snappedPoint.layer;
|
||||
}
|
||||
|
||||
if (snappedPoint && snappedPoint.pointUuid === tempPoints[0]?.pointUuid) { return }
|
||||
if (snappedPoint && snappedPoint.pointUuid === tempPoints[0]?.pointUuid) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (snappedPosition && !snappedPoint) {
|
||||
newPoint.position = snappedPosition;
|
||||
@@ -93,8 +118,7 @@ function AisleCreator() {
|
||||
}
|
||||
}
|
||||
|
||||
if (aisleType === 'solid-aisle') {
|
||||
|
||||
if (aisleType === "solid-aisle") {
|
||||
if (tempPoints.length === 0) {
|
||||
setTempPoints([newPoint]);
|
||||
setIsCreating(true);
|
||||
@@ -103,50 +127,48 @@ function AisleCreator() {
|
||||
aisleUuid: THREE.MathUtils.generateUUID(),
|
||||
points: [tempPoints[0], newPoint],
|
||||
type: {
|
||||
aisleType: 'solid-aisle',
|
||||
aisleType: "solid-aisle",
|
||||
aisleColor: aisleColor,
|
||||
aisleWidth: aisleWidth
|
||||
}
|
||||
aisleWidth: aisleWidth,
|
||||
},
|
||||
};
|
||||
|
||||
addAisle(aisle);
|
||||
|
||||
push2D({
|
||||
type: 'Draw',
|
||||
type: "Draw",
|
||||
actions: [
|
||||
{
|
||||
actionType: 'Line-Create',
|
||||
actionType: "Line-Create",
|
||||
point: {
|
||||
type: 'Aisle',
|
||||
type: "Aisle",
|
||||
lineData: aisle,
|
||||
timeStamp: new Date().toISOString(),
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
});
|
||||
|
||||
if (projectId) {
|
||||
|
||||
// API
|
||||
|
||||
// upsertAisleApi(aisle.aisleUuid, aisle.points, aisle.type, projectId, selectedVersion?.versionId || '')
|
||||
|
||||
// SOCKET
|
||||
|
||||
socket.emit('v1:model-aisle:add', {
|
||||
socket.emit("v1:model-aisle:add", {
|
||||
projectId: projectId,
|
||||
versionId: selectedVersion?.versionId || '',
|
||||
versionId: selectedVersion?.versionId || "",
|
||||
userId: userId,
|
||||
organization: organization,
|
||||
aisleUuid: aisle.aisleUuid,
|
||||
points: aisle.points,
|
||||
type: aisle.type
|
||||
})
|
||||
type: aisle.type,
|
||||
});
|
||||
}
|
||||
setTempPoints([newPoint]);
|
||||
}
|
||||
} else if (aisleType === 'dashed-aisle') {
|
||||
|
||||
} else if (aisleType === "dashed-aisle") {
|
||||
if (tempPoints.length === 0) {
|
||||
setTempPoints([newPoint]);
|
||||
setIsCreating(true);
|
||||
@@ -155,52 +177,50 @@ function AisleCreator() {
|
||||
aisleUuid: THREE.MathUtils.generateUUID(),
|
||||
points: [tempPoints[0], newPoint],
|
||||
type: {
|
||||
aisleType: 'dashed-aisle',
|
||||
aisleType: "dashed-aisle",
|
||||
aisleColor: aisleColor,
|
||||
aisleWidth: aisleWidth,
|
||||
dashLength: dashLength,
|
||||
gapLength: gapLength
|
||||
}
|
||||
gapLength: gapLength,
|
||||
},
|
||||
};
|
||||
|
||||
addAisle(aisle);
|
||||
|
||||
push2D({
|
||||
type: 'Draw',
|
||||
type: "Draw",
|
||||
actions: [
|
||||
{
|
||||
actionType: 'Line-Create',
|
||||
actionType: "Line-Create",
|
||||
point: {
|
||||
type: 'Aisle',
|
||||
type: "Aisle",
|
||||
lineData: aisle,
|
||||
timeStamp: new Date().toISOString(),
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
});
|
||||
|
||||
if (projectId) {
|
||||
|
||||
// API
|
||||
|
||||
// upsertAisleApi(aisle.aisleUuid, aisle.points, aisle.type, projectId, selectedVersion?.versionId || '')
|
||||
|
||||
// SOCKET
|
||||
|
||||
socket.emit('v1:model-aisle:add', {
|
||||
socket.emit("v1:model-aisle:add", {
|
||||
projectId: projectId,
|
||||
versionId: selectedVersion?.versionId || '',
|
||||
versionId: selectedVersion?.versionId || "",
|
||||
userId: userId,
|
||||
organization: organization,
|
||||
aisleUuid: aisle.aisleUuid,
|
||||
points: aisle.points,
|
||||
type: aisle.type
|
||||
})
|
||||
type: aisle.type,
|
||||
});
|
||||
}
|
||||
setTempPoints([newPoint]);
|
||||
}
|
||||
} else if (aisleType === 'dotted-aisle') {
|
||||
|
||||
} else if (aisleType === "dotted-aisle") {
|
||||
if (tempPoints.length === 0) {
|
||||
setTempPoints([newPoint]);
|
||||
setIsCreating(true);
|
||||
@@ -209,51 +229,49 @@ function AisleCreator() {
|
||||
aisleUuid: THREE.MathUtils.generateUUID(),
|
||||
points: [tempPoints[0], newPoint],
|
||||
type: {
|
||||
aisleType: 'dotted-aisle',
|
||||
aisleType: "dotted-aisle",
|
||||
aisleColor: aisleColor,
|
||||
dotRadius: dotRadius,
|
||||
gapLength: gapLength
|
||||
}
|
||||
gapLength: gapLength,
|
||||
},
|
||||
};
|
||||
|
||||
addAisle(aisle);
|
||||
|
||||
push2D({
|
||||
type: 'Draw',
|
||||
type: "Draw",
|
||||
actions: [
|
||||
{
|
||||
actionType: 'Line-Create',
|
||||
actionType: "Line-Create",
|
||||
point: {
|
||||
type: 'Aisle',
|
||||
type: "Aisle",
|
||||
lineData: aisle,
|
||||
timeStamp: new Date().toISOString(),
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
});
|
||||
|
||||
if (projectId) {
|
||||
|
||||
// API
|
||||
|
||||
// upsertAisleApi(aisle.aisleUuid, aisle.points, aisle.type, projectId, selectedVersion?.versionId || '')
|
||||
|
||||
// SOCKET
|
||||
|
||||
socket.emit('v1:model-aisle:add', {
|
||||
socket.emit("v1:model-aisle:add", {
|
||||
projectId: projectId,
|
||||
versionId: selectedVersion?.versionId || '',
|
||||
versionId: selectedVersion?.versionId || "",
|
||||
userId: userId,
|
||||
organization: organization,
|
||||
aisleUuid: aisle.aisleUuid,
|
||||
points: aisle.points,
|
||||
type: aisle.type
|
||||
})
|
||||
type: aisle.type,
|
||||
});
|
||||
}
|
||||
setTempPoints([newPoint]);
|
||||
}
|
||||
} else if (aisleType === 'arrow-aisle') {
|
||||
|
||||
} else if (aisleType === "arrow-aisle") {
|
||||
if (tempPoints.length === 0) {
|
||||
setTempPoints([newPoint]);
|
||||
setIsCreating(true);
|
||||
@@ -262,50 +280,48 @@ function AisleCreator() {
|
||||
aisleUuid: THREE.MathUtils.generateUUID(),
|
||||
points: [tempPoints[0], newPoint],
|
||||
type: {
|
||||
aisleType: 'arrow-aisle',
|
||||
aisleType: "arrow-aisle",
|
||||
aisleColor: aisleColor,
|
||||
aisleWidth: aisleWidth
|
||||
}
|
||||
aisleWidth: aisleWidth,
|
||||
},
|
||||
};
|
||||
|
||||
addAisle(aisle);
|
||||
|
||||
push2D({
|
||||
type: 'Draw',
|
||||
type: "Draw",
|
||||
actions: [
|
||||
{
|
||||
actionType: 'Line-Create',
|
||||
actionType: "Line-Create",
|
||||
point: {
|
||||
type: 'Aisle',
|
||||
type: "Aisle",
|
||||
lineData: aisle,
|
||||
timeStamp: new Date().toISOString(),
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
});
|
||||
|
||||
if (projectId) {
|
||||
|
||||
// API
|
||||
|
||||
// upsertAisleApi(aisle.aisleUuid, aisle.points, aisle.type, projectId, selectedVersion?.versionId || '')
|
||||
|
||||
// SOCKET
|
||||
|
||||
socket.emit('v1:model-aisle:add', {
|
||||
socket.emit("v1:model-aisle:add", {
|
||||
projectId: projectId,
|
||||
versionId: selectedVersion?.versionId || '',
|
||||
versionId: selectedVersion?.versionId || "",
|
||||
userId: userId,
|
||||
organization: organization,
|
||||
aisleUuid: aisle.aisleUuid,
|
||||
points: aisle.points,
|
||||
type: aisle.type
|
||||
})
|
||||
type: aisle.type,
|
||||
});
|
||||
}
|
||||
setTempPoints([newPoint]);
|
||||
}
|
||||
} else if (aisleType === 'arrows-aisle') {
|
||||
|
||||
} else if (aisleType === "arrows-aisle") {
|
||||
if (tempPoints.length === 0) {
|
||||
setTempPoints([newPoint]);
|
||||
setIsCreating(true);
|
||||
@@ -314,52 +330,50 @@ function AisleCreator() {
|
||||
aisleUuid: THREE.MathUtils.generateUUID(),
|
||||
points: [tempPoints[0], newPoint],
|
||||
type: {
|
||||
aisleType: 'arrows-aisle',
|
||||
aisleType: "arrows-aisle",
|
||||
aisleColor: aisleColor,
|
||||
aisleWidth: aisleWidth,
|
||||
aisleLength: aisleLength,
|
||||
gapLength: gapLength
|
||||
}
|
||||
gapLength: gapLength,
|
||||
},
|
||||
};
|
||||
|
||||
addAisle(aisle);
|
||||
|
||||
push2D({
|
||||
type: 'Draw',
|
||||
type: "Draw",
|
||||
actions: [
|
||||
{
|
||||
actionType: 'Line-Create',
|
||||
actionType: "Line-Create",
|
||||
point: {
|
||||
type: 'Aisle',
|
||||
type: "Aisle",
|
||||
lineData: aisle,
|
||||
timeStamp: new Date().toISOString(),
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
});
|
||||
|
||||
if (projectId) {
|
||||
|
||||
// API
|
||||
|
||||
// upsertAisleApi(aisle.aisleUuid, aisle.points, aisle.type, projectId, selectedVersion?.versionId || '')
|
||||
|
||||
// SOCKET
|
||||
|
||||
socket.emit('v1:model-aisle:add', {
|
||||
socket.emit("v1:model-aisle:add", {
|
||||
projectId: projectId,
|
||||
versionId: selectedVersion?.versionId || '',
|
||||
versionId: selectedVersion?.versionId || "",
|
||||
userId: userId,
|
||||
organization: organization,
|
||||
aisleUuid: aisle.aisleUuid,
|
||||
points: aisle.points,
|
||||
type: aisle.type
|
||||
})
|
||||
type: aisle.type,
|
||||
});
|
||||
}
|
||||
setTempPoints([newPoint]);
|
||||
}
|
||||
} else if (aisleType === 'arc-aisle') {
|
||||
|
||||
} else if (aisleType === "arc-aisle") {
|
||||
if (tempPoints.length === 0) {
|
||||
setTempPoints([newPoint]);
|
||||
setIsCreating(true);
|
||||
@@ -368,51 +382,49 @@ function AisleCreator() {
|
||||
aisleUuid: THREE.MathUtils.generateUUID(),
|
||||
points: [tempPoints[0], newPoint],
|
||||
type: {
|
||||
aisleType: 'arc-aisle',
|
||||
aisleType: "arc-aisle",
|
||||
aisleColor: aisleColor,
|
||||
aisleWidth: aisleWidth,
|
||||
isFlipped: isFlipped
|
||||
}
|
||||
isFlipped: isFlipped,
|
||||
},
|
||||
};
|
||||
|
||||
addAisle(aisle);
|
||||
|
||||
push2D({
|
||||
type: 'Draw',
|
||||
type: "Draw",
|
||||
actions: [
|
||||
{
|
||||
actionType: 'Line-Create',
|
||||
actionType: "Line-Create",
|
||||
point: {
|
||||
type: 'Aisle',
|
||||
type: "Aisle",
|
||||
lineData: aisle,
|
||||
timeStamp: new Date().toISOString(),
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
});
|
||||
|
||||
if (projectId) {
|
||||
|
||||
// API
|
||||
|
||||
// upsertAisleApi(aisle.aisleUuid, aisle.points, aisle.type, projectId, selectedVersion?.versionId || '')
|
||||
|
||||
// SOCKET
|
||||
|
||||
socket.emit('v1:model-aisle:add', {
|
||||
socket.emit("v1:model-aisle:add", {
|
||||
projectId: projectId,
|
||||
versionId: selectedVersion?.versionId || '',
|
||||
versionId: selectedVersion?.versionId || "",
|
||||
userId: userId,
|
||||
organization: organization,
|
||||
aisleUuid: aisle.aisleUuid,
|
||||
points: aisle.points,
|
||||
type: aisle.type
|
||||
})
|
||||
type: aisle.type,
|
||||
});
|
||||
}
|
||||
setTempPoints([newPoint]);
|
||||
}
|
||||
} else if (aisleType === 'circle-aisle') {
|
||||
|
||||
} else if (aisleType === "circle-aisle") {
|
||||
if (tempPoints.length === 0) {
|
||||
setTempPoints([newPoint]);
|
||||
setIsCreating(true);
|
||||
@@ -421,98 +433,95 @@ function AisleCreator() {
|
||||
aisleUuid: THREE.MathUtils.generateUUID(),
|
||||
points: [tempPoints[0], newPoint],
|
||||
type: {
|
||||
aisleType: 'circle-aisle',
|
||||
aisleColor: aisleColor,
|
||||
aisleWidth: aisleWidth
|
||||
}
|
||||
};
|
||||
|
||||
addAisle(aisle);
|
||||
|
||||
push2D({
|
||||
type: 'Draw',
|
||||
actions: [
|
||||
{
|
||||
actionType: 'Line-Create',
|
||||
point: {
|
||||
type: 'Aisle',
|
||||
lineData: aisle,
|
||||
timeStamp: new Date().toISOString(),
|
||||
}
|
||||
}
|
||||
],
|
||||
})
|
||||
|
||||
if (projectId) {
|
||||
|
||||
// API
|
||||
|
||||
// upsertAisleApi(aisle.aisleUuid, aisle.points, aisle.type, projectId, selectedVersion?.versionId || '')
|
||||
|
||||
// SOCKET
|
||||
|
||||
socket.emit('v1:model-aisle:add', {
|
||||
projectId: projectId,
|
||||
versionId: selectedVersion?.versionId || '',
|
||||
userId: userId,
|
||||
organization: organization,
|
||||
aisleUuid: aisle.aisleUuid,
|
||||
points: aisle.points,
|
||||
type: aisle.type
|
||||
})
|
||||
}
|
||||
setTempPoints([newPoint]);
|
||||
}
|
||||
} else if (aisleType === 'junction-aisle') {
|
||||
|
||||
if (tempPoints.length === 0) {
|
||||
setTempPoints([newPoint]);
|
||||
setIsCreating(true);
|
||||
} else {
|
||||
const aisle: Aisle = {
|
||||
aisleUuid: THREE.MathUtils.generateUUID(),
|
||||
points: [tempPoints[0], newPoint],
|
||||
type: {
|
||||
aisleType: 'junction-aisle',
|
||||
aisleType: "circle-aisle",
|
||||
aisleColor: aisleColor,
|
||||
aisleWidth: aisleWidth,
|
||||
isFlipped: isFlipped
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
addAisle(aisle);
|
||||
|
||||
push2D({
|
||||
type: 'Draw',
|
||||
type: "Draw",
|
||||
actions: [
|
||||
{
|
||||
actionType: 'Line-Create',
|
||||
actionType: "Line-Create",
|
||||
point: {
|
||||
type: 'Aisle',
|
||||
type: "Aisle",
|
||||
lineData: aisle,
|
||||
timeStamp: new Date().toISOString(),
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
});
|
||||
|
||||
if (projectId) {
|
||||
|
||||
// API
|
||||
|
||||
// upsertAisleApi(aisle.aisleUuid, aisle.points, aisle.type, projectId, selectedVersion?.versionId || '')
|
||||
|
||||
// SOCKET
|
||||
|
||||
socket.emit('v1:model-aisle:add', {
|
||||
socket.emit("v1:model-aisle:add", {
|
||||
projectId: projectId,
|
||||
versionId: selectedVersion?.versionId || '',
|
||||
versionId: selectedVersion?.versionId || "",
|
||||
userId: userId,
|
||||
organization: organization,
|
||||
aisleUuid: aisle.aisleUuid,
|
||||
points: aisle.points,
|
||||
type: aisle.type
|
||||
})
|
||||
type: aisle.type,
|
||||
});
|
||||
}
|
||||
setTempPoints([newPoint]);
|
||||
}
|
||||
} else if (aisleType === "junction-aisle") {
|
||||
if (tempPoints.length === 0) {
|
||||
setTempPoints([newPoint]);
|
||||
setIsCreating(true);
|
||||
} else {
|
||||
const aisle: Aisle = {
|
||||
aisleUuid: THREE.MathUtils.generateUUID(),
|
||||
points: [tempPoints[0], newPoint],
|
||||
type: {
|
||||
aisleType: "junction-aisle",
|
||||
aisleColor: aisleColor,
|
||||
aisleWidth: aisleWidth,
|
||||
isFlipped: isFlipped,
|
||||
},
|
||||
};
|
||||
|
||||
addAisle(aisle);
|
||||
|
||||
push2D({
|
||||
type: "Draw",
|
||||
actions: [
|
||||
{
|
||||
actionType: "Line-Create",
|
||||
point: {
|
||||
type: "Aisle",
|
||||
lineData: aisle,
|
||||
timeStamp: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (projectId) {
|
||||
// API
|
||||
|
||||
// upsertAisleApi(aisle.aisleUuid, aisle.points, aisle.type, projectId, selectedVersion?.versionId || '')
|
||||
|
||||
// SOCKET
|
||||
|
||||
socket.emit("v1:model-aisle:add", {
|
||||
projectId: projectId,
|
||||
versionId: selectedVersion?.versionId || "",
|
||||
userId: userId,
|
||||
organization: organization,
|
||||
aisleUuid: aisle.aisleUuid,
|
||||
points: aisle.points,
|
||||
type: aisle.type,
|
||||
});
|
||||
}
|
||||
setTempPoints([newPoint]);
|
||||
}
|
||||
@@ -556,23 +565,46 @@ function AisleCreator() {
|
||||
canvasElement.removeEventListener("click", onMouseClick);
|
||||
canvasElement.removeEventListener("contextmenu", onContext);
|
||||
};
|
||||
}, [gl, camera, scene, raycaster, pointer, plane, toggleView, toolMode, activeLayer, socket, tempPoints, isCreating, addAisle, getAislePointById, aisleType, aisleWidth, aisleColor, dashLength, gapLength, dotRadius, aisleLength, snappedPosition, snappedPoint, selectedVersion?.versionId]);
|
||||
}, [
|
||||
gl,
|
||||
camera,
|
||||
scene,
|
||||
raycaster,
|
||||
pointer,
|
||||
plane,
|
||||
toggleView,
|
||||
toolMode,
|
||||
activeLayer,
|
||||
socket,
|
||||
tempPoints,
|
||||
isCreating,
|
||||
addAisle,
|
||||
getAislePointById,
|
||||
aisleType,
|
||||
aisleWidth,
|
||||
aisleColor,
|
||||
dashLength,
|
||||
gapLength,
|
||||
dotRadius,
|
||||
aisleLength,
|
||||
snappedPosition,
|
||||
snappedPoint,
|
||||
selectedVersion?.versionId,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{toggleView &&
|
||||
{toggleView && (
|
||||
<>
|
||||
<group name='Aisle-Reference-Points-Group'>
|
||||
<group name="Aisle-Reference-Points-Group">
|
||||
{tempPoints.map((point) => (
|
||||
<ReferencePoint key={point.pointUuid} point={point} />
|
||||
))}
|
||||
</group>
|
||||
|
||||
{tempPoints.length > 0 &&
|
||||
<ReferenceAisle tempPoints={tempPoints} />
|
||||
}
|
||||
{tempPoints.length > 0 && <ReferenceAisle tempPoints={tempPoints} />}
|
||||
</>
|
||||
}
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -180,11 +180,11 @@ function PointsCreator() {
|
||||
drag = false;
|
||||
};
|
||||
|
||||
const onMouseUp = () => {
|
||||
const onMouseUp = (e : MouseEvent) => {
|
||||
if (selectedEventSphere && !drag) {
|
||||
raycaster.setFromCamera(pointer, camera);
|
||||
const intersects = raycaster.intersectObjects(scene.children, true).filter((intersect) => intersect.object.name === "Event-Sphere");
|
||||
if (intersects.length === 0) {
|
||||
if (intersects.length === 0 && e.button === 0) {
|
||||
clearSelectedEventSphere();
|
||||
setTransformMode(null);
|
||||
}
|
||||
|
||||
@@ -199,6 +199,7 @@ const VehicleUI = () => {
|
||||
steeringAngle: steeringRotation[1],
|
||||
},
|
||||
},
|
||||
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useFrame, useThree, ThreeEvent } from '@react-three/fiber';
|
||||
import * as THREE from 'three';
|
||||
import { Line } from '@react-three/drei';
|
||||
import { useAnimationPlaySpeed, usePauseButtonStore, usePlayButtonStore, useResetButtonStore } from '../../../../../store/usePlayButtonStore';
|
||||
import { useSceneContext } from '../../../../scene/sceneContext';
|
||||
import { useActiveTool, useSelectedPath } from '../../../../../store/builder/store';
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useFrame, useThree, ThreeEvent } from "@react-three/fiber";
|
||||
import * as THREE from "three";
|
||||
import { Line } from "@react-three/drei";
|
||||
import {
|
||||
useAnimationPlaySpeed,
|
||||
usePauseButtonStore,
|
||||
usePlayButtonStore,
|
||||
useResetButtonStore,
|
||||
} from "../../../../../store/usePlayButtonStore";
|
||||
import { useSceneContext } from "../../../../scene/sceneContext";
|
||||
import {
|
||||
useActiveTool,
|
||||
useSelectedPath,
|
||||
} from "../../../../../store/builder/store";
|
||||
|
||||
interface VehicleAnimatorProps {
|
||||
path: [number, number, number][];
|
||||
@@ -18,7 +24,15 @@ interface VehicleAnimatorProps {
|
||||
agvDetail: VehicleStatus;
|
||||
}
|
||||
|
||||
function VehicleAnimator({ path, handleCallBack, currentPhase, agvUuid, agvDetail, reset, startUnloadingProcess }: Readonly<VehicleAnimatorProps>) {
|
||||
function VehicleAnimator({
|
||||
path,
|
||||
handleCallBack,
|
||||
currentPhase,
|
||||
agvUuid,
|
||||
agvDetail,
|
||||
reset,
|
||||
startUnloadingProcess,
|
||||
}: Readonly<VehicleAnimatorProps>) {
|
||||
const { vehicleStore } = useSceneContext();
|
||||
const { getVehicleById } = vehicleStore();
|
||||
const { isPaused } = usePauseButtonStore();
|
||||
@@ -28,23 +42,26 @@ function VehicleAnimator({ path, handleCallBack, currentPhase, agvUuid, agvDetai
|
||||
const progressRef = useRef<number>(0);
|
||||
const movingForward = useRef<boolean>(true);
|
||||
const completedRef = useRef<boolean>(false);
|
||||
const [objectRotation, setObjectRotation] = useState<{ x: number; y: number; z: number } | undefined>(agvDetail.point?.action?.pickUpPoint?.rotation || { x: 0, y: 0, z: 0 })
|
||||
const [objectRotation, setObjectRotation] = useState<
|
||||
{ x: number; y: number; z: number } | undefined
|
||||
>(agvDetail.point?.action?.pickUpPoint?.rotation || { x: 0, y: 0, z: 0 });
|
||||
const [restRotation, setRestingRotation] = useState<boolean>(true);
|
||||
const [currentPath, setCurrentPath] = useState<[number, number, number][]>([]);
|
||||
const [currentPath, setCurrentPath] = useState<[number, number, number][]>(
|
||||
[]
|
||||
);
|
||||
const { scene, controls } = useThree();
|
||||
const { selectedPath } = useSelectedPath();
|
||||
const [isAnyDragging, setIsAnyDragging] = useState<string>("");
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (currentPhase === 'stationed-pickup' && path.length > 0 && selectedPath === "auto") {
|
||||
if (currentPhase === "stationed-pickup" && path.length > 0) {
|
||||
setCurrentPath(path);
|
||||
setObjectRotation(agvDetail.point.action?.pickUpPoint?.rotation)
|
||||
} else if (currentPhase === 'pickup-drop' && path.length > 0) {
|
||||
setObjectRotation(agvDetail.point.action?.unLoadPoint?.rotation)
|
||||
setObjectRotation(agvDetail.point.action?.pickUpPoint?.rotation);
|
||||
} else if (currentPhase === "pickup-drop" && path.length > 0) {
|
||||
setObjectRotation(agvDetail.point.action?.unLoadPoint?.rotation);
|
||||
setCurrentPath(path);
|
||||
} else if (currentPhase === 'drop-pickup' && path.length > 0) {
|
||||
setObjectRotation(agvDetail.point.action?.pickUpPoint?.rotation)
|
||||
} else if (currentPhase === "drop-pickup" && path.length > 0) {
|
||||
setObjectRotation(agvDetail.point.action?.pickUpPoint?.rotation);
|
||||
setCurrentPath(path);
|
||||
}
|
||||
}, [currentPhase, path, objectRotation, selectedPath]);
|
||||
@@ -62,24 +79,32 @@ function VehicleAnimator({ path, handleCallBack, currentPhase, agvUuid, agvDetai
|
||||
progressRef.current = 0;
|
||||
setReset(false);
|
||||
setRestingRotation(true);
|
||||
const object = scene.getObjectByProperty('uuid', agvUuid);
|
||||
const object = scene.getObjectByProperty("uuid", agvUuid);
|
||||
const vehicle = getVehicleById(agvDetail.modelUuid);
|
||||
if (object && vehicle) {
|
||||
object.position.set(vehicle.position[0], vehicle.position[1], vehicle.position[2]);
|
||||
object.rotation.set(vehicle.rotation[0], vehicle.rotation[1], vehicle.rotation[2]);
|
||||
object.position.set(
|
||||
vehicle.position[0],
|
||||
vehicle.position[1],
|
||||
vehicle.position[2]
|
||||
);
|
||||
object.rotation.set(
|
||||
vehicle.rotation[0],
|
||||
vehicle.rotation[1],
|
||||
vehicle.rotation[2]
|
||||
);
|
||||
}
|
||||
}
|
||||
}, [isReset, isPlaying])
|
||||
}, [isReset, isPlaying]);
|
||||
|
||||
const lastTimeRef = useRef(performance.now());
|
||||
|
||||
useFrame(() => {
|
||||
if (!isPlaying) return
|
||||
if (!isPlaying) return;
|
||||
const now = performance.now();
|
||||
const delta = (now - lastTimeRef.current) / 1000;
|
||||
lastTimeRef.current = now;
|
||||
|
||||
const object = scene.getObjectByProperty('uuid', agvUuid);
|
||||
const object = scene.getObjectByProperty("uuid", agvUuid);
|
||||
if (!object || currentPath.length < 2) return;
|
||||
if (isPaused) return;
|
||||
|
||||
@@ -97,7 +122,10 @@ function VehicleAnimator({ path, handleCallBack, currentPhase, agvUuid, agvDetai
|
||||
totalDistance += segmentDistance;
|
||||
}
|
||||
|
||||
while (index < distances.length && progressRef.current > accumulatedDistance + distances[index]) {
|
||||
while (
|
||||
index < distances.length &&
|
||||
progressRef.current > accumulatedDistance + distances[index]
|
||||
) {
|
||||
accumulatedDistance += distances[index];
|
||||
index++;
|
||||
}
|
||||
@@ -107,8 +135,13 @@ function VehicleAnimator({ path, handleCallBack, currentPhase, agvUuid, agvDetai
|
||||
const end = new THREE.Vector3(...currentPath[index + 1]);
|
||||
const segmentDistance = distances[index];
|
||||
|
||||
const targetQuaternion = new THREE.Quaternion().setFromRotationMatrix(new THREE.Matrix4().lookAt(start, end, new THREE.Vector3(0, 1, 0)));
|
||||
const y180 = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI);
|
||||
const targetQuaternion = new THREE.Quaternion().setFromRotationMatrix(
|
||||
new THREE.Matrix4().lookAt(start, end, new THREE.Vector3(0, 1, 0))
|
||||
);
|
||||
const y180 = new THREE.Quaternion().setFromAxisAngle(
|
||||
new THREE.Vector3(0, 1, 0),
|
||||
Math.PI
|
||||
);
|
||||
targetQuaternion.multiply(y180);
|
||||
|
||||
const angle = object.quaternion.angleTo(targetQuaternion);
|
||||
@@ -137,26 +170,23 @@ function VehicleAnimator({ path, handleCallBack, currentPhase, agvUuid, agvDetai
|
||||
|
||||
if (progressRef.current >= totalDistance) {
|
||||
if (restRotation && objectRotation) {
|
||||
const targetEuler = new THREE.Euler(0, objectRotation.y - agvDetail.point.action.steeringAngle, 0);
|
||||
const targetEuler = new THREE.Euler(
|
||||
0,
|
||||
objectRotation.y - agvDetail.point.action.steeringAngle,
|
||||
0
|
||||
);
|
||||
|
||||
const baseQuaternion = new THREE.Quaternion().setFromEuler(targetEuler);
|
||||
const y180 = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI);
|
||||
const y180 = new THREE.Quaternion().setFromAxisAngle(
|
||||
new THREE.Vector3(0, 1, 0),
|
||||
Math.PI
|
||||
);
|
||||
const targetQuaternion = baseQuaternion.multiply(y180);
|
||||
|
||||
const angle = object.quaternion.angleTo(targetQuaternion);
|
||||
if (angle < 0.01) {
|
||||
object.quaternion.copy(targetQuaternion);
|
||||
setRestingRotation(false);
|
||||
setTimeout(() => {
|
||||
setRestingRotation(true);
|
||||
progressRef.current = 0;
|
||||
movingForward.current = !movingForward.current;
|
||||
setCurrentPath([]);
|
||||
handleCallBack();
|
||||
if (currentPhase === 'pickup-drop') {
|
||||
requestAnimationFrame(startUnloadingProcess);
|
||||
}
|
||||
}, 0)
|
||||
} else {
|
||||
const step = rotationSpeed * delta * speed * agvDetail.speed;
|
||||
const angle = object.quaternion.angleTo(targetQuaternion);
|
||||
@@ -177,7 +207,7 @@ function VehicleAnimator({ path, handleCallBack, currentPhase, agvUuid, agvDetai
|
||||
movingForward.current = !movingForward.current;
|
||||
setCurrentPath([]);
|
||||
handleCallBack();
|
||||
if (currentPhase === 'pickup-drop') {
|
||||
if (currentPhase === "pickup-drop") {
|
||||
requestAnimationFrame(startUnloadingProcess);
|
||||
}
|
||||
}
|
||||
@@ -191,7 +221,7 @@ function VehicleAnimator({ path, handleCallBack, currentPhase, agvUuid, agvDetai
|
||||
|
||||
return (
|
||||
<>
|
||||
{selectedPath === "auto" &&
|
||||
{selectedPath === "auto" && (
|
||||
<group visible={false}>
|
||||
{currentPath.map((pos, i) => {
|
||||
if (i < currentPath.length - 1) {
|
||||
@@ -215,9 +245,12 @@ function VehicleAnimator({ path, handleCallBack, currentPhase, agvUuid, agvDetai
|
||||
return null;
|
||||
})}
|
||||
{currentPath.length > 0 && (
|
||||
<group onPointerMissed={() => { if (controls) (controls as any).enabled = true; }}>
|
||||
{currentPath.map((pos, i) =>
|
||||
(
|
||||
<group
|
||||
onPointerMissed={() => {
|
||||
if (controls) (controls as any).enabled = true;
|
||||
}}
|
||||
>
|
||||
{currentPath.map((pos, i) => (
|
||||
<DraggableSphere
|
||||
key={i}
|
||||
index={i}
|
||||
@@ -225,12 +258,12 @@ function VehicleAnimator({ path, handleCallBack, currentPhase, agvUuid, agvDetai
|
||||
onMove={updatePoint}
|
||||
isAnyDragging={isAnyDragging}
|
||||
setIsAnyDragging={setIsAnyDragging}
|
||||
/>)
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</group>
|
||||
)}
|
||||
</group>
|
||||
}
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -256,15 +289,15 @@ function DraggableSphere({
|
||||
const { activeTool } = useActiveTool();
|
||||
|
||||
const onPointerDown = (e: ThreeEvent<PointerEvent>) => {
|
||||
e.stopPropagation()
|
||||
if (activeTool !== 'pen') return;
|
||||
e.stopPropagation();
|
||||
if (activeTool !== "pen") return;
|
||||
setIsAnyDragging("point");
|
||||
gl.domElement.style.cursor = 'grabbing';
|
||||
gl.domElement.style.cursor = "grabbing";
|
||||
if (controls) (controls as any).enabled = false;
|
||||
};
|
||||
|
||||
const onPointerMove = (e: ThreeEvent<PointerEvent>) => {
|
||||
if (isAnyDragging !== "point" || activeTool !== 'pen') return;
|
||||
if (isAnyDragging !== "point" || activeTool !== "pen") return;
|
||||
|
||||
const intersect = new THREE.Vector3();
|
||||
if (raycaster.ray.intersectPlane(plane, intersect)) {
|
||||
@@ -274,17 +307,17 @@ function DraggableSphere({
|
||||
};
|
||||
|
||||
const onPointerUp = () => {
|
||||
if (activeTool !== 'pen') return;
|
||||
if (activeTool !== "pen") return;
|
||||
setIsAnyDragging("");
|
||||
gl.domElement.style.cursor = 'default';
|
||||
gl.domElement.style.cursor = "default";
|
||||
if (controls) (controls as any).enabled = true;
|
||||
};
|
||||
useEffect(() => {
|
||||
gl.domElement.addEventListener("pointerup", onPointerUp);
|
||||
return (() => {
|
||||
return () => {
|
||||
gl.domElement.removeEventListener("pointerup", onPointerUp);
|
||||
})
|
||||
}, [activeTool])
|
||||
};
|
||||
}, [activeTool]);
|
||||
|
||||
return (
|
||||
<mesh
|
||||
@@ -312,7 +345,12 @@ function DraggableLineSegment({
|
||||
index: number;
|
||||
start: THREE.Vector3;
|
||||
end: THREE.Vector3;
|
||||
updatePoints: (i0: number, p0: THREE.Vector3, i1: number, p1: THREE.Vector3) => void;
|
||||
updatePoints: (
|
||||
i0: number,
|
||||
p0: THREE.Vector3,
|
||||
i1: number,
|
||||
p1: THREE.Vector3
|
||||
) => void;
|
||||
isAnyDragging: string;
|
||||
setIsAnyDragging: (val: string) => void;
|
||||
}) {
|
||||
@@ -322,19 +360,22 @@ function DraggableLineSegment({
|
||||
const dragStart = useRef<THREE.Vector3 | null>(null);
|
||||
|
||||
const onPointerDown = () => {
|
||||
if (activeTool !== 'pen' || isAnyDragging) return;
|
||||
if (activeTool !== "pen" || isAnyDragging) return;
|
||||
setIsAnyDragging("line");
|
||||
gl.domElement.style.cursor = 'grabbing';
|
||||
gl.domElement.style.cursor = "grabbing";
|
||||
if (controls) (controls as any).enabled = false;
|
||||
};
|
||||
|
||||
const onPointerMove = (e: ThreeEvent<PointerEvent>) => {
|
||||
if (isAnyDragging !== "line" || activeTool !== 'pen') return;
|
||||
if (isAnyDragging !== "line" || activeTool !== "pen") return;
|
||||
|
||||
const intersect = new THREE.Vector3();
|
||||
if (raycaster.ray.intersectPlane(plane, intersect)) {
|
||||
if (!dragStart.current) dragStart.current = intersect.clone();
|
||||
const offset = new THREE.Vector3().subVectors(intersect, dragStart.current);
|
||||
const offset = new THREE.Vector3().subVectors(
|
||||
intersect,
|
||||
dragStart.current
|
||||
);
|
||||
const newStart = start.clone().add(offset);
|
||||
const newEnd = end.clone().add(offset);
|
||||
updatePoints(index, newStart, index + 1, newEnd);
|
||||
@@ -342,18 +383,18 @@ function DraggableLineSegment({
|
||||
};
|
||||
|
||||
const onPointerUp = () => {
|
||||
if (activeTool !== 'pen') return;
|
||||
if (activeTool !== "pen") return;
|
||||
setIsAnyDragging("");
|
||||
dragStart.current = null;
|
||||
gl.domElement.style.cursor = 'default';
|
||||
gl.domElement.style.cursor = "default";
|
||||
if (controls) (controls as any).enabled = true;
|
||||
};
|
||||
useEffect(() => {
|
||||
gl.domElement.addEventListener("pointerup", onPointerUp);
|
||||
return (() => {
|
||||
return () => {
|
||||
gl.domElement.removeEventListener("pointerup", onPointerUp);
|
||||
})
|
||||
}, [activeTool])
|
||||
};
|
||||
}, [activeTool]);
|
||||
return (
|
||||
<Line
|
||||
points={[start, end]}
|
||||
|
||||
@@ -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 VehicleContentUi from "../../ui3d/VehicleContentUi";
|
||||
import { useSceneContext } from "../../../scene/sceneContext";
|
||||
import { useViewSceneStore } from "../../../../store/builder/store";
|
||||
import PathCreator from "../pathCreator/pathCreator";
|
||||
import VehicleInstance2 from "./instance/vehicleInstance2";
|
||||
|
||||
function VehicleInstances() {
|
||||
const { vehicleStore } = useSceneContext();
|
||||
const { vehicles } = vehicleStore();
|
||||
const { viewSceneLabels } = useViewSceneStore();
|
||||
const [vehiclesData, setVehiclesData] = useState<VehicleStructure[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const updatedVehicles = vehicles.map((val) => ({
|
||||
vehicleId: val.modelUuid,
|
||||
position: val.position,
|
||||
rotation: val.rotation,
|
||||
startPoint: null,
|
||||
endPoint: null,
|
||||
selectedPointId: val.point.uuid,
|
||||
}));
|
||||
setVehiclesData(updatedVehicles);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{vehicles.map((vehicle: VehicleStatus) => (
|
||||
{/* {vehicles.map((vehicle: VehicleStatus) => (
|
||||
<React.Fragment key={vehicle.modelUuid}>
|
||||
<VehicleInstance agvDetail={vehicle} />
|
||||
{viewSceneLabels && <VehicleContentUi vehicle={vehicle} />}
|
||||
</React.Fragment>
|
||||
))} */}
|
||||
{vehiclesData.map((vehicle: VehicleStructure) => (
|
||||
<React.Fragment key={vehicle.vehicleId}>
|
||||
<VehicleInstance2
|
||||
vehicleData={vehicle}
|
||||
vehiclesData={vehiclesData}
|
||||
setVehiclesData={setVehiclesData}
|
||||
/>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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,6 +4,9 @@ import { usePlayButtonStore } from "../../../store/usePlayButtonStore";
|
||||
import VehicleInstances from "./instances/vehicleInstances";
|
||||
import VehicleUI from "../spatialUI/vehicle/vehicleUI";
|
||||
import { useSceneContext } from "../../scene/sceneContext";
|
||||
import PreDefinedPath from "./preDefinedPath/preDefinedPath";
|
||||
import StructuredPath from "./structuredPath/structuredPath";
|
||||
import PathCreator from "./pathCreator/pathCreator";
|
||||
|
||||
function Vehicles() {
|
||||
const { vehicleStore } = useSceneContext();
|
||||
@@ -14,24 +17,24 @@ function Vehicles() {
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedEventSphere) {
|
||||
const selectedVehicle = getVehicleById(selectedEventSphere.userData.modelUuid);
|
||||
const selectedVehicle = getVehicleById(
|
||||
selectedEventSphere.userData.modelUuid
|
||||
);
|
||||
if (selectedVehicle) {
|
||||
setIsVehicleSelected(true);
|
||||
} else {
|
||||
setIsVehicleSelected(false);
|
||||
}
|
||||
}
|
||||
}, [getVehicleById, selectedEventSphere])
|
||||
}, [getVehicleById, selectedEventSphere]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
<PathCreator />
|
||||
{/* <StructuredPath /> */}
|
||||
{/* <PreDefinedPath /> */}
|
||||
<VehicleInstances />
|
||||
|
||||
{isVehicleSelected && selectedEventSphere && !isPlaying &&
|
||||
<VehicleUI />
|
||||
}
|
||||
|
||||
{isVehicleSelected && selectedEventSphere && !isPlaying && <VehicleUI />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -669,6 +669,96 @@ export const useContextActionStore = create<any>((set: any) => ({
|
||||
contextAction: null,
|
||||
setContextAction: (x: any) => set({ contextAction: x }),
|
||||
}));
|
||||
type PointData = {
|
||||
pointId: string;
|
||||
position: [number, number, number];
|
||||
isCurved?: boolean;
|
||||
handleA?: [number, number, number] | null;
|
||||
handleB?: [number, number, number] | null;
|
||||
};
|
||||
|
||||
interface PathDataInterface {
|
||||
pathId: string;
|
||||
isActive?: boolean;
|
||||
isCurved?: boolean;
|
||||
pathPoints: [PointData, PointData];
|
||||
}
|
||||
interface allPaths {
|
||||
paths: string;
|
||||
isAvailable: boolean;
|
||||
vehicleId: string;
|
||||
}
|
||||
|
||||
type PathData = PathDataInterface[];
|
||||
export const useCreatedPaths = create<any>((set: any) => ({
|
||||
paths: [
|
||||
// {
|
||||
// pathId: "276724c5-05a3-4b5e-a127-a60b3533ccce",
|
||||
// pathPoints: [
|
||||
// {
|
||||
// pointId: "19c3f429-f214-4f87-8906-7eaaedd925da",
|
||||
// position: [2.33155763270131, 0, -20.538859668988927],
|
||||
// },
|
||||
// {
|
||||
// pointId: "ea73c7c8-0e26-4aae-9ed8-2349ff2d6718",
|
||||
// position: [17.13371069714903, 0, -22.156135485080462],
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// {
|
||||
// pathId: "2736b20b-a433-443c-a5c9-5ba4348ac682",
|
||||
// pathPoints: [
|
||||
// {
|
||||
// pointId: "ea73c7c8-0e26-4aae-9ed8-2349ff2d6718",
|
||||
// position: [17.13371069714903, 0, -22.156135485080462],
|
||||
// },
|
||||
// {
|
||||
// pointId: "2212bb52-c63c-4289-8b50-5ffd229d13e5",
|
||||
// position: [16.29236816120279, 0, -10.819973445497789],
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// {
|
||||
// pathId: "3144a2df-7aad-483d-bbc7-de7f7b5b3bfc",
|
||||
// pathPoints: [
|
||||
// {
|
||||
// pointId: "2212bb52-c63c-4289-8b50-5ffd229d13e5",
|
||||
// position: [16.29236816120279, 0, -10.819973445497789],
|
||||
// },
|
||||
// {
|
||||
// pointId: "adfd05a7-4e16-403f-81d0-ce99f2e34f5f",
|
||||
// position: [4.677047323894161, 0, -8.279486846767094],
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// {
|
||||
// pathId: "e0a1b5da-27c2-44a0-81db-759b5a5eb416",
|
||||
// pathPoints: [
|
||||
// {
|
||||
// pointId: "adfd05a7-4e16-403f-81d0-ce99f2e34f5f",
|
||||
// position: [4.677047323894161, 0, -8.279486846767094],
|
||||
// },
|
||||
// {
|
||||
// pointId: "19c3f429-f214-4f87-8906-7eaaedd925da",
|
||||
// position: [2.33155763270131, 0, -20.538859668988927],
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
],
|
||||
setPaths: (x: PathData) => set({ paths: x }),
|
||||
allPaths: [],
|
||||
setAllPaths: (x: allPaths) => set({ allPaths: x }),
|
||||
}));
|
||||
|
||||
// route?: {
|
||||
// pathId: string;
|
||||
// pathPoints: {
|
||||
// pointId: string;
|
||||
// position: [number, number, number];
|
||||
// isCurved?: boolean;
|
||||
// handleA?: [number, number, number] | null;
|
||||
// handleB: [number, number, number] | null;
|
||||
// }[];
|
||||
|
||||
|
||||
// Define the store's state and actions type
|
||||
|
||||
Reference in New Issue
Block a user