diff --git a/app/package-lock.json b/app/package-lock.json
index fa04c9f..3d5aea4 100644
--- a/app/package-lock.json
+++ b/app/package-lock.json
@@ -30,7 +30,6 @@
"glob": "^11.0.0",
"gsap": "^3.12.5",
"html2canvas": "^1.4.1",
- "immer": "^10.1.1",
"leva": "^0.10.0",
"mqtt": "^5.10.4",
"postprocessing": "^6.36.4",
@@ -2022,7 +2021,7 @@
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
- "devOptional": true,
+ "dev": true,
"dependencies": {
"@jridgewell/trace-mapping": "0.3.9"
},
@@ -2034,7 +2033,7 @@
"version": "0.3.9",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
- "devOptional": true,
+ "dev": true,
"dependencies": {
"@jridgewell/resolve-uri": "^3.0.3",
"@jridgewell/sourcemap-codec": "^1.4.10"
@@ -4137,26 +4136,6 @@
"url": "https://github.com/sponsors/gregberge"
}
},
- "node_modules/@testing-library/dom": {
- "version": "10.4.0",
- "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
- "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@babel/code-frame": "^7.10.4",
- "@babel/runtime": "^7.12.5",
- "@types/aria-query": "^5.0.1",
- "aria-query": "5.3.0",
- "chalk": "^4.1.0",
- "dom-accessibility-api": "^0.5.9",
- "lz-string": "^1.5.0",
- "pretty-format": "^27.0.2"
- },
- "engines": {
- "node": ">=18"
- }
- },
"node_modules/@testing-library/jest-dom": {
"version": "5.17.0",
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.17.0.tgz",
@@ -4268,25 +4247,25 @@
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
"integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==",
- "devOptional": true
+ "dev": true
},
"node_modules/@tsconfig/node12": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
- "devOptional": true
+ "dev": true
},
"node_modules/@tsconfig/node14": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
- "devOptional": true
+ "dev": true
},
"node_modules/@tsconfig/node16": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
- "devOptional": true
+ "dev": true
},
"node_modules/@turf/along": {
"version": "7.2.0",
@@ -9040,7 +9019,7 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
- "devOptional": true
+ "dev": true
},
"node_modules/cross-env": {
"version": "7.0.3",
@@ -9917,7 +9896,7 @@
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
- "devOptional": true,
+ "dev": true,
"engines": {
"node": ">=0.3.1"
}
@@ -12747,10 +12726,9 @@
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
},
"node_modules/immer": {
- "version": "10.1.1",
- "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz",
- "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
- "license": "MIT",
+ "version": "9.0.21",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
+ "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
@@ -15281,7 +15259,7 @@
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
- "devOptional": true
+ "dev": true
},
"node_modules/makeerror": {
"version": "1.0.12",
@@ -18012,16 +17990,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/react-dev-utils/node_modules/immer": {
- "version": "9.0.21",
- "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
- "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==",
- "license": "MIT",
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/immer"
- }
- },
"node_modules/react-dev-utils/node_modules/loader-utils": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz",
@@ -20759,7 +20727,7 @@
"version": "10.9.2",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
- "devOptional": true,
+ "dev": true,
"dependencies": {
"@cspotcode/source-map-support": "^0.8.0",
"@tsconfig/node10": "^1.0.7",
@@ -20802,7 +20770,7 @@
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
"integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
- "devOptional": true,
+ "dev": true,
"dependencies": {
"acorn": "^8.11.0"
},
@@ -20814,7 +20782,7 @@
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
- "devOptional": true
+ "dev": true
},
"node_modules/tsconfig-paths": {
"version": "3.15.0",
@@ -21310,7 +21278,7 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
- "devOptional": true
+ "dev": true
},
"node_modules/v8-to-istanbul": {
"version": "8.1.1",
@@ -22369,7 +22337,7 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
- "devOptional": true,
+ "dev": true,
"engines": {
"node": ">=6"
}
diff --git a/app/src/assets/gltf-glb/arrow_green.glb b/app/src/assets/gltf-glb/arrow_green.glb
new file mode 100644
index 0000000..8e02527
Binary files /dev/null and b/app/src/assets/gltf-glb/arrow_green.glb differ
diff --git a/app/src/assets/gltf-glb/arrow_red.glb b/app/src/assets/gltf-glb/arrow_red.glb
new file mode 100644
index 0000000..02bfea4
Binary files /dev/null and b/app/src/assets/gltf-glb/arrow_red.glb differ
diff --git a/app/src/modules/scene/scene.tsx b/app/src/modules/scene/scene.tsx
index 6564032..0e81cf6 100644
--- a/app/src/modules/scene/scene.tsx
+++ b/app/src/modules/scene/scene.tsx
@@ -9,28 +9,35 @@ import Simulation from "../simulation/simulation";
import Collaboration from "../collaboration/collaboration";
export default function Scene() {
- const map = useMemo(() => [
- { name: "forward", keys: ["ArrowUp", "w", "W"] },
- { name: "backward", keys: ["ArrowDown", "s", "S"] },
- { name: "left", keys: ["ArrowLeft", "a", "A"] },
- { name: "right", keys: ["ArrowRight", "d", "D"] },],
- []);
+ const map = useMemo(
+ () => [
+ { name: "forward", keys: ["ArrowUp", "w", "W"] },
+ { name: "backward", keys: ["ArrowDown", "s", "S"] },
+ { name: "left", keys: ["ArrowLeft", "a", "A"] },
+ { name: "right", keys: ["ArrowRight", "d", "D"] },
+ ],
+ []
+ );
- return (
-
-
+
+ );
}
diff --git a/app/src/modules/simulation/events/points/creator/pointsCreator.tsx b/app/src/modules/simulation/events/points/creator/pointsCreator.tsx
index bc2f4db..e503419 100644
--- a/app/src/modules/simulation/events/points/creator/pointsCreator.tsx
+++ b/app/src/modules/simulation/events/points/creator/pointsCreator.tsx
@@ -1,173 +1,237 @@
-import React, { useEffect, useRef, useState } from 'react';
-import * as THREE from 'three';
-import { useEventsStore } from '../../../../../store/simulation/useEventsStore';
-import useModuleStore from '../../../../../store/useModuleStore';
-import { TransformControls } from '@react-three/drei';
-import { detectModifierKeys } from '../../../../../utils/shortcutkeys/detectModifierKeys';
-import { useSelectedEventSphere, useSelectedEventData } from '../../../../../store/simulation/useSimulationStore';
+import React, { useEffect, useRef, useState } from "react";
+import * as THREE from "three";
+import { useEventsStore } from "../../../../../store/simulation/useEventsStore";
+import useModuleStore from "../../../../../store/useModuleStore";
+import { TransformControls } from "@react-three/drei";
+import { detectModifierKeys } from "../../../../../utils/shortcutkeys/detectModifierKeys";
+import {
+ useSelectedEventSphere,
+ useSelectedEventData,
+} from "../../../../../store/simulation/useSimulationStore";
function PointsCreator() {
- const { events, updatePoint, getPointByUuid, getEventByModelUuid } = useEventsStore();
- const { activeModule } = useModuleStore();
- const transformRef = useRef(null);
- const [transformMode, setTransformMode] = useState<"translate" | "rotate" | null>(null);
- const sphereRefs = useRef<{ [key: string]: THREE.Mesh }>({});
- const { selectedEventSphere, setSelectedEventSphere, clearSelectedEventSphere } = useSelectedEventSphere();
- const { setSelectedEventData, clearSelectedEventData } = useSelectedEventData();
+ const { events, updatePoint, getPointByUuid, getEventByModelUuid } =
+ useEventsStore();
+ const { activeModule } = useModuleStore();
+ const transformRef = useRef(null);
+ const [transformMode, setTransformMode] = useState<
+ "translate" | "rotate" | null
+ >(null);
+ const sphereRefs = useRef<{ [key: string]: THREE.Mesh }>({});
+ const {
+ selectedEventSphere,
+ setSelectedEventSphere,
+ clearSelectedEventSphere,
+ } = useSelectedEventSphere();
+ const { setSelectedEventData, clearSelectedEventData } =
+ useSelectedEventData();
- useEffect(() => {
- if (selectedEventSphere) {
- const eventData = getEventByModelUuid(selectedEventSphere.userData.modelUuid);
- if (eventData) {
- setSelectedEventData(
- eventData,
- selectedEventSphere.userData.pointUuid
- );
- } else {
- clearSelectedEventData();
- }
- } else {
- clearSelectedEventData();
- }
- }, [selectedEventSphere]);
-
- useEffect(() => {
- const handleKeyDown = (e: KeyboardEvent) => {
- const keyCombination = detectModifierKeys(e);
- if (!selectedEventSphere) return;
- if (keyCombination === "G") {
- setTransformMode((prev) => (prev === "translate" ? null : "translate"));
- }
- if (keyCombination === "R") {
- setTransformMode((prev) => (prev === "rotate" ? null : "rotate"));
- }
- };
-
- window.addEventListener("keydown", handleKeyDown);
- return () => window.removeEventListener("keydown", handleKeyDown);
- }, [selectedEventSphere]);
-
- const updatePointToState = (selectedEventSphere: THREE.Mesh) => {
- let point = JSON.parse(JSON.stringify(getPointByUuid(selectedEventSphere.userData.modelUuid, selectedEventSphere.userData.pointUuid)));
- if (point) {
- point.position = [selectedEventSphere.position.x, selectedEventSphere.position.y, selectedEventSphere.position.z];
- updatePoint(selectedEventSphere.userData.modelUuid, selectedEventSphere.userData.pointUuid, point)
- }
+ useEffect(() => {
+ if (selectedEventSphere) {
+ const eventData = getEventByModelUuid(
+ selectedEventSphere.userData.modelUuid
+ );
+ if (eventData) {
+ setSelectedEventData(eventData, selectedEventSphere.userData.pointUuid);
+ } else {
+ clearSelectedEventData();
+ }
+ } else {
+ clearSelectedEventData();
}
+ }, [selectedEventSphere]);
- return (
- <>
- {activeModule === 'simulation' &&
- <>
-
- {events.map((event, i) => {
- if (event.type === 'transfer') {
- return (
-
- {event.points.map((point, j) => (
- (sphereRefs.current[point.uuid] = el!)}
- onClick={(e) => {
- e.stopPropagation();
- setSelectedEventSphere(sphereRefs.current[point.uuid]);
- }}
- onPointerMissed={() => {
- clearSelectedEventSphere();
- setTransformMode(null);
- }}
- key={`${i}-${j}`}
- position={new THREE.Vector3(...point.position)}
- userData={{ modelUuid: event.modelUuid, pointUuid: point.uuid }}
- >
-
-
-
- ))}
-
- );
- } else if (event.type === 'vehicle') {
- return (
-
- (sphereRefs.current[event.point.uuid] = el!)}
- onClick={(e) => {
- e.stopPropagation();
- setSelectedEventSphere(sphereRefs.current[event.point.uuid]);
- }}
- onPointerMissed={() => {
- clearSelectedEventSphere();
- setTransformMode(null);
- }}
- position={new THREE.Vector3(...event.point.position)}
- userData={{ modelUuid: event.modelUuid, pointUuid: event.point.uuid }}
- >
-
-
-
-
- );
- } else if (event.type === 'roboticArm') {
- return (
-
- (sphereRefs.current[event.point.uuid] = el!)}
- onClick={(e) => {
- e.stopPropagation();
- setSelectedEventSphere(sphereRefs.current[event.point.uuid]);
- }}
- onPointerMissed={() => {
- clearSelectedEventSphere();
- setTransformMode(null);
- }}
- position={new THREE.Vector3(...event.point.position)}
- userData={{ modelUuid: event.modelUuid, pointUuid: event.point.uuid }}
- >
-
-
-
-
- );
- } else if (event.type === 'machine') {
- return (
-
- (sphereRefs.current[event.point.uuid] = el!)}
- onClick={(e) => {
- e.stopPropagation();
- setSelectedEventSphere(sphereRefs.current[event.point.uuid]);
- }}
- onPointerMissed={() => {
- clearSelectedEventSphere();
- setTransformMode(null);
- }}
- position={new THREE.Vector3(...event.point.position)}
- userData={{ modelUuid: event.modelUuid, pointUuid: event.point.uuid }}
- >
-
-
-
-
- );
- } else {
- return null;
- }
- })}
-
- {(selectedEventSphere && transformMode) &&
- { updatePointToState(selectedEventSphere) }} />
- }
- >
- }
- >
+ useEffect(() => {
+ const handleKeyDown = (e: KeyboardEvent) => {
+ const keyCombination = detectModifierKeys(e);
+ if (!selectedEventSphere) return;
+ if (keyCombination === "G") {
+ setTransformMode((prev) => (prev === "translate" ? null : "translate"));
+ }
+ if (keyCombination === "R") {
+ setTransformMode((prev) => (prev === "rotate" ? null : "rotate"));
+ }
+ };
+
+ window.addEventListener("keydown", handleKeyDown);
+ return () => window.removeEventListener("keydown", handleKeyDown);
+ }, [selectedEventSphere]);
+
+ const updatePointToState = (selectedEventSphere: THREE.Mesh) => {
+ let point = JSON.parse(
+ JSON.stringify(
+ getPointByUuid(
+ selectedEventSphere.userData.modelUuid,
+ selectedEventSphere.userData.pointUuid
+ )
+ )
);
+ if (point) {
+ point.position = [
+ selectedEventSphere.position.x,
+ selectedEventSphere.position.y,
+ selectedEventSphere.position.z,
+ ];
+ updatePoint(
+ selectedEventSphere.userData.modelUuid,
+ selectedEventSphere.userData.pointUuid,
+ point
+ );
+ }
+ };
+
+ return (
+ <>
+ {activeModule === "simulation" && (
+ <>
+
+ {events.map((event, i) => {
+ if (event.type === "transfer") {
+ return (
+
+ {event.points.map((point, j) => (
+ (sphereRefs.current[point.uuid] = el!)}
+ onClick={(e) => {
+ e.stopPropagation();
+ setSelectedEventSphere(
+ sphereRefs.current[point.uuid]
+ );
+ }}
+ onPointerMissed={() => {
+ // clearSelectedEventSphere();
+ setTransformMode(null);
+ }}
+ key={`${i}-${j}`}
+ position={new THREE.Vector3(...point.position)}
+ userData={{
+ modelUuid: event.modelUuid,
+ pointUuid: point.uuid,
+ }}
+ >
+
+
+
+ ))}
+
+ );
+ } else if (event.type === "vehicle") {
+ return (
+
+ (sphereRefs.current[event.point.uuid] = el!)}
+ onClick={(e) => {
+ e.stopPropagation();
+ setSelectedEventSphere(
+ sphereRefs.current[event.point.uuid]
+ );
+ }}
+ onPointerMissed={() => {
+ // clearSelectedEventSphere();
+ setTransformMode(null);
+ }}
+ position={new THREE.Vector3(...event.point.position)}
+ userData={{
+ modelUuid: event.modelUuid,
+ pointUuid: event.point.uuid,
+ }}
+ >
+
+
+
+
+ );
+ } else if (event.type === "roboticArm") {
+ return (
+
+ (sphereRefs.current[event.point.uuid] = el!)}
+ onClick={(e) => {
+ e.stopPropagation();
+ setSelectedEventSphere(
+ sphereRefs.current[event.point.uuid]
+ );
+ }}
+ onPointerMissed={() => {
+ // clearSelectedEventSphere();
+ setTransformMode(null);
+ }}
+ position={new THREE.Vector3(...event.point.position)}
+ userData={{
+ modelUuid: event.modelUuid,
+ pointUuid: event.point.uuid,
+ }}
+ >
+
+
+
+
+ );
+ } else if (event.type === "machine") {
+ return (
+
+ (sphereRefs.current[event.point.uuid] = el!)}
+ onClick={(e) => {
+ e.stopPropagation();
+ setSelectedEventSphere(
+ sphereRefs.current[event.point.uuid]
+ );
+ }}
+ onPointerMissed={() => {
+ // clearSelectedEventSphere();
+ setTransformMode(null);
+ }}
+ position={new THREE.Vector3(...event.point.position)}
+ userData={{
+ modelUuid: event.modelUuid,
+ pointUuid: event.point.uuid,
+ }}
+ >
+
+
+
+
+ );
+ } else {
+ return null;
+ }
+ })}
+
+ {selectedEventSphere && transformMode && (
+ {
+ updatePointToState(selectedEventSphere);
+ }}
+ />
+ )}
+ >
+ )}
+ >
+ );
}
export default PointsCreator;
diff --git a/app/src/modules/simulation/ui/vehicle/useDraggableGLTF.ts b/app/src/modules/simulation/ui/vehicle/useDraggableGLTF.ts
new file mode 100644
index 0000000..b7e9272
--- /dev/null
+++ b/app/src/modules/simulation/ui/vehicle/useDraggableGLTF.ts
@@ -0,0 +1,131 @@
+import { useRef } from "react";
+import * as THREE from "three";
+import { ThreeEvent, useThree } from "@react-three/fiber";
+
+type OnUpdateCallback = (object: THREE.Object3D) => void;
+
+export default function useDraggableGLTF(onUpdate: OnUpdateCallback) {
+ const { camera, gl, controls, scene } = useThree();
+ const activeObjRef = useRef(null);
+ const planeRef = useRef(
+ new THREE.Plane(new THREE.Vector3(0, 1, 0), 0)
+ );
+ const offsetRef = useRef(new THREE.Vector3());
+ const initialPositionRef = useRef(new THREE.Vector3());
+
+ const raycaster = new THREE.Raycaster();
+ const pointer = new THREE.Vector2();
+
+ const handlePointerDown = (e: ThreeEvent) => {
+ e.stopPropagation();
+
+ let obj: THREE.Object3D | null = e.object;
+
+ // Traverse up until we find modelUuid in userData
+ while (obj && !obj.userData?.modelUuid) {
+ obj = obj.parent;
+ }
+
+ if (!obj) return;
+
+ // Disable orbit controls while dragging
+ if (controls) (controls as any).enabled = false;
+
+ activeObjRef.current = obj;
+ initialPositionRef.current.copy(obj.position);
+
+ // Get world position
+ const objectWorldPos = new THREE.Vector3();
+ obj.getWorldPosition(objectWorldPos);
+
+ // Set plane at the object's Y level
+ planeRef.current.set(new THREE.Vector3(0, 1, 0), -objectWorldPos.y);
+
+ // Convert pointer to NDC
+ const rect = gl.domElement.getBoundingClientRect();
+ pointer.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
+ pointer.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
+
+ // Raycast to intersection
+ raycaster.setFromCamera(pointer, camera);
+ const intersection = new THREE.Vector3();
+ raycaster.ray.intersectPlane(planeRef.current, intersection);
+
+ // Calculate offset
+ offsetRef.current.copy(objectWorldPos).sub(intersection);
+
+ // Start listening for drag
+ gl.domElement.addEventListener("pointermove", handlePointerMove);
+ gl.domElement.addEventListener("pointerup", handlePointerUp);
+ };
+
+ const handlePointerMove = (e: PointerEvent) => {
+ if (!activeObjRef.current) return;
+
+ // Check if Shift key is pressed
+ const isShiftKeyPressed = e.shiftKey;
+
+ // Get the mouse position relative to the canvas
+ const rect = gl.domElement.getBoundingClientRect();
+ pointer.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
+ pointer.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
+
+ // Update raycaster to point to the mouse position
+ raycaster.setFromCamera(pointer, camera);
+
+ // Create a vector to store intersection point
+ const intersection = new THREE.Vector3();
+ const intersects = raycaster.ray.intersectPlane(planeRef.current, intersection);
+ if (!intersects) return;
+
+ // Add offset for dragging
+ intersection.add(offsetRef.current);
+ console.log('intersection: ', intersection);
+
+ // Get the parent's world matrix if exists
+ const parent = activeObjRef.current.parent;
+ const targetPosition = new THREE.Vector3();
+
+ if (isShiftKeyPressed) {
+ console.log('isShiftKeyPressed: ', isShiftKeyPressed);
+ // For Y-axis only movement, maintain original X and Z
+ console.log('initialPositionRef: ', initialPositionRef);
+ console.log('intersection.y: ', intersection);
+ targetPosition.set(
+ initialPositionRef.current.x,
+ intersection.y,
+ initialPositionRef.current.z
+ );
+ } else {
+ // For free movement
+ targetPosition.copy(intersection);
+ }
+
+ // Convert world position to local if object is nested inside a parent
+ if (parent) {
+ parent.worldToLocal(targetPosition);
+ }
+
+ // Update object position
+ activeObjRef.current.position.copy(targetPosition);
+ };
+
+ const handlePointerUp = () => {
+ if (controls) (controls as any).enabled = true;
+
+ if (activeObjRef.current) {
+ // Pass the updated position to the onUpdate callback to persist it
+ onUpdate(activeObjRef.current);
+ }
+
+ gl.domElement.removeEventListener("pointermove", handlePointerMove);
+ gl.domElement.removeEventListener("pointerup", handlePointerUp);
+
+ activeObjRef.current = null;
+ };
+
+ return { handlePointerDown };
+}
+
+
+
diff --git a/app/src/modules/simulation/ui/vehicle/vehicleUI.tsx b/app/src/modules/simulation/ui/vehicle/vehicleUI.tsx
new file mode 100644
index 0000000..f927c33
--- /dev/null
+++ b/app/src/modules/simulation/ui/vehicle/vehicleUI.tsx
@@ -0,0 +1,283 @@
+import React, { useRef, useEffect, useState } from "react";
+import startPoint from "../../../../assets/gltf-glb/arrow_green.glb";
+import startEnd from "../../../../assets/gltf-glb/arrow_red.glb";
+import { useGLTF } from "@react-three/drei";
+import { useSelectedEventSphere } from "../../../../store/simulation/useSimulationStore";
+import * as THREE from "three";
+import { useThree } from "@react-three/fiber";
+
+type VehicleUIProps = {
+ vehicleStatusSample: VehicleEventSchema[];
+ setVehicleStatusSample: React.Dispatch<
+ React.SetStateAction
+ >;
+};
+
+const VehicleUI: React.FC = ({
+ vehicleStatusSample,
+ setVehicleStatusSample,
+}) => {
+ const { scene: startScene } = useGLTF(startPoint) as any;
+ const { scene: endScene } = useGLTF(startEnd) as any;
+ const { camera, gl, controls } = useThree();
+ const { selectedEventSphere } = useSelectedEventSphere();
+
+ const startMarker = useRef(null);
+ const endMarker = useRef(null);
+ const hasInitialized = useRef(false);
+
+ const [draggedMarker, setDraggedMarker] = useState<"start" | "end" | null>(
+ null
+ );
+ const [dragOffset, setDragOffset] = useState(null);
+ const [isRotating, setIsRotating] = useState(false);
+
+ const raycaster = useRef(new THREE.Raycaster());
+ const plane = useRef(new THREE.Plane(new THREE.Vector3(0, 1, 0), 0)); // Y = 0 plane
+ const mouse = useRef(new THREE.Vector2());
+ const prevMousePos = useRef({ x: 0, y: 0 });
+
+ // Initialize start/end markers
+ useEffect(() => {
+ if (
+ selectedEventSphere &&
+ startMarker.current &&
+ endMarker.current &&
+ !hasInitialized.current
+ ) {
+ startMarker.current.clear();
+ endMarker.current.clear();
+
+ const startClone = startScene.clone();
+ const endClone = endScene.clone();
+
+ startClone.name = "start-marker";
+ endClone.name = "end-marker";
+
+ startClone.traverse((child: any) => {
+ if (child.isMesh && child.name.toLowerCase().includes("handle")) {
+ child.name = "handle";
+ }
+ });
+ endClone.traverse((child: any) => {
+ if (child.isMesh && child.name.toLowerCase().includes("handle")) {
+ child.name = "handle";
+ }
+ });
+
+ startMarker.current.add(startClone);
+ endMarker.current.add(endClone);
+
+ hasInitialized.current = true;
+ }
+ }, [selectedEventSphere, startScene, endScene]);
+
+ // Position start/end markers
+ useEffect(() => {
+ if (!selectedEventSphere || !startMarker.current || !endMarker.current)
+ return;
+
+ const selectedVehicle = vehicleStatusSample.find(
+ (vehicle) => vehicle.modelUuid === selectedEventSphere.userData.modelUuid
+ );
+
+ if (selectedVehicle?.point?.action) {
+ const { pickUpPoint, unLoadPoint } = selectedVehicle.point.action;
+
+ // Update start marker position
+ if (pickUpPoint) {
+ const localPos = new THREE.Vector3(
+ pickUpPoint.x,
+ pickUpPoint.y,
+ pickUpPoint.z
+ );
+ const worldPos = selectedEventSphere.localToWorld(localPos);
+ worldPos.y = 0; // Force y to 0
+ startMarker.current.position.copy(worldPos);
+ } else {
+ const defaultLocal = new THREE.Vector3(0, 0, 1.5);
+ const defaultWorld = selectedEventSphere.localToWorld(defaultLocal);
+ defaultWorld.y = 0; // Force y to 0
+ startMarker.current.position.copy(defaultWorld);
+ }
+
+ // Update end marker position
+ if (unLoadPoint) {
+ const localPos = new THREE.Vector3(
+ unLoadPoint.x,
+ unLoadPoint.y,
+ unLoadPoint.z
+ );
+ const worldPos = selectedEventSphere.localToWorld(localPos);
+ worldPos.y = 0; // Force y to 0
+ endMarker.current.position.copy(worldPos);
+ } else {
+ const defaultLocal = new THREE.Vector3(0, 0, -1.5);
+ const defaultWorld = selectedEventSphere.localToWorld(defaultLocal);
+ defaultWorld.y = 0; // Force y to 0
+ endMarker.current.position.copy(defaultWorld);
+ }
+ }
+ }, [selectedEventSphere, vehicleStatusSample]);
+
+ // Handle dragging and rotation
+ const handlePointerDown = (e: any, markerType: "start" | "end") => {
+ if (!selectedEventSphere) return;
+
+ if (e.object.name === "handle") {
+ setIsRotating(true);
+ prevMousePos.current = { x: e.clientX, y: e.clientY };
+ if (controls) (controls as any).enabled = false;
+ e.stopPropagation();
+ setDraggedMarker(markerType);
+ return;
+ }
+
+ setDraggedMarker(markerType);
+ if (controls) (controls as any).enabled = false;
+
+ const marker =
+ markerType === "start" ? startMarker.current : endMarker.current;
+ if (!marker) return;
+
+ mouse.current.x = (e.clientX / gl.domElement.clientWidth) * 2 - 1;
+ mouse.current.y = -(e.clientY / gl.domElement.clientHeight) * 2 + 1;
+
+ raycaster.current.setFromCamera(mouse.current, camera);
+
+ const intersectPoint = new THREE.Vector3();
+ raycaster.current.ray.intersectPlane(plane.current, intersectPoint);
+
+ const offset = new THREE.Vector3().subVectors(
+ marker.position,
+ intersectPoint
+ );
+ setDragOffset(offset);
+ };
+
+ const handlePointerMove = (e: PointerEvent) => {
+ if (!selectedEventSphere) return;
+
+ if (isRotating) {
+ const deltaX = e.clientX - prevMousePos.current.x;
+ prevMousePos.current = { x: e.clientX, y: e.clientY };
+
+ const rotationSpeed = 0.01;
+ const marker =
+ draggedMarker === "start" ? startMarker.current : endMarker.current;
+
+ if (marker) {
+ marker.rotation.y -= deltaX * rotationSpeed;
+ }
+ return;
+ }
+
+ if (!draggedMarker || !dragOffset) return;
+
+ mouse.current.x = (e.clientX / gl.domElement.clientWidth) * 2 - 1;
+ mouse.current.y = -(e.clientY / gl.domElement.clientHeight) * 2 + 1;
+
+ raycaster.current.setFromCamera(mouse.current, camera);
+
+ const intersectPoint = new THREE.Vector3();
+ raycaster.current.ray.intersectPlane(plane.current, intersectPoint);
+
+ if (!intersectPoint) return;
+
+ const newPos = {
+ x: intersectPoint.x + dragOffset.x,
+ y: 0,
+ z: intersectPoint.z + dragOffset.z,
+ };
+
+ if (draggedMarker === "start" && startMarker.current) {
+ startMarker.current.position.set(newPos.x, newPos.y, newPos.z);
+ } else if (draggedMarker === "end" && endMarker.current) {
+ endMarker.current.position.set(newPos.x, newPos.y, newPos.z);
+ }
+ };
+
+ const handlePointerUp = () => {
+ if (isRotating) {
+ setIsRotating(false);
+ if (controls) (controls as any).enabled = true;
+ return;
+ }
+
+ if (!selectedEventSphere || !draggedMarker || !dragOffset) {
+ if (controls) (controls as any).enabled = true;
+ return;
+ }
+
+ if (controls) (controls as any).enabled = true;
+
+ const marker =
+ draggedMarker === "start" ? startMarker.current : endMarker.current;
+ if (!marker) return;
+
+ const worldPos = marker.position.clone();
+ const localPos = selectedEventSphere.worldToLocal(worldPos);
+
+ // Direct update (no snapping, ground level forced at y = 0)
+ const updatedLocalPos = { x: localPos.x, y: 0, z: localPos.z };
+
+ setVehicleStatusSample((prev) =>
+ prev.map((vehicle) => {
+ if (
+ vehicle.modelUuid === selectedEventSphere.userData.modelUuid &&
+ selectedEventSphere
+ ) {
+ const updatedVehicle = {
+ ...vehicle,
+ point: {
+ ...vehicle.point,
+ action: {
+ ...vehicle.point?.action,
+ ...(draggedMarker === "start"
+ ? { pickUpPoint: updatedLocalPos }
+ : { unLoadPoint: updatedLocalPos }),
+ },
+ },
+ };
+ return updatedVehicle;
+ }
+ return vehicle;
+ })
+ );
+
+ setDraggedMarker(null);
+ setDragOffset(null);
+ };
+
+ useEffect(() => {
+ window.addEventListener("pointermove", handlePointerMove);
+ window.addEventListener("pointerup", handlePointerUp);
+
+ return () => {
+ window.removeEventListener("pointermove", handlePointerMove);
+ window.removeEventListener("pointerup", handlePointerUp);
+ };
+ }, [draggedMarker, dragOffset, isRotating]);
+
+ if (!selectedEventSphere) {
+ hasInitialized.current = false;
+ return null;
+ }
+
+ return (
+
+ handlePointerDown(e, "start")}
+ />
+ handlePointerDown(e, "end")}
+ />
+
+ );
+};
+
+export default VehicleUI;
diff --git a/app/src/modules/simulation/vehicle/vehicles.tsx b/app/src/modules/simulation/vehicle/vehicles.tsx
index f0b4d17..65376f2 100644
--- a/app/src/modules/simulation/vehicle/vehicles.tsx
+++ b/app/src/modules/simulation/vehicle/vehicles.tsx
@@ -1,162 +1,140 @@
-import React, { useEffect } from 'react'
-import VehicleInstances from './instances/vehicleInstances';
-import { useVehicleStore } from '../../../store/simulation/useVehicleStore';
-import { useFloorItems } from '../../../store/store';
-
+import React, { useEffect, useState } from "react";
+import VehicleInstances from "./instances/vehicleInstances";
+import { useVehicleStore } from "../../../store/simulation/useVehicleStore";
+import { useFloorItems } from "../../../store/store";
+import { useSelectedEventSphere } from "../../../store/simulation/useSimulationStore";
+import VehicleUI from "../ui/vehicle/vehicleUI";
function Vehicles() {
+ const { vehicles, addVehicle } = useVehicleStore();
+ const { selectedEventSphere } = useSelectedEventSphere();
- const { vehicles, addVehicle } = useVehicleStore();
+ const { floorItems } = useFloorItems();
- const { floorItems } = useFloorItems();
-
- const vehicleStatusSample: VehicleEventSchema[] = [
- {
- modelUuid: "9356f710-4727-4b50-bdb2-9c1e747ecc74",
- modelName: "AGV",
- position: [97.9252965204558, 0, 37.96138815638661],
- rotation: [0, 0, 0],
- state: "idle",
- type: "vehicle",
- speed: 2.5,
- point: {
- uuid: "point-789",
- position: [0, 1, 0],
- rotation: [0, 0, 0],
- action: {
- actionUuid: "action-456",
- actionName: "Deliver to Zone A",
- actionType: "travel",
- unLoadDuration: 10,
- loadCapacity: 2,
- pickUpPoint: { x: 98.71483985219794, y: 0, z: 28.66321267938962 },
- unLoadPoint: { x: 105.71483985219794, y: 0, z: 28.66321267938962 },
- triggers: [
- {
- triggerUuid: "trig-001",
- triggerName: "Start Travel",
- triggerType: "onComplete",
- delay: 0,
- triggeredAsset: {
- triggeredModel: { modelName: "ArmBot-X", modelUuid: "arm-001" },
- triggeredPoint: { pointName: "Pickup Arm Point", pointUuid: "arm-point-01" },
- triggeredAction: { actionName: "Grab Widget", actionUuid: "grab-001" }
- }
- },
- {
- triggerUuid: "trig-002",
- triggerName: "Complete Travel",
- triggerType: "onComplete",
- delay: 2,
- triggeredAsset: null
- }
- ]
- }
- }
+ const [vehicleStatusSample, setVehicleStatusSample] = useState<
+ VehicleEventSchema[]
+ >([
+
+ {
+ modelUuid: "68f8dc55-7802-47fe-aa1c-eade54b4320a",
+ modelName: "AGV",
+ position: [89.61609306554463, 0, 33.634136622267356],
+ rotation: [0, 0, 0],
+ state: "idle",
+ type: "vehicle",
+ speed: 2.5,
+ point: {
+ uuid: "point-789",
+ position: [0, 1, 0],
+ rotation: [0, 0, 0],
+ action: {
+ actionUuid: "action-456",
+ actionName: "Deliver to Zone A",
+ actionType: "travel",
+ unLoadDuration: 10,
+ loadCapacity: 2,
+ pickUpPoint: null,
+ unLoadPoint: null,
+ triggers: [
+ {
+ triggerUuid: "trig-001",
+ triggerName: "Start Travel",
+ triggerType: "onStart",
+ delay: 0,
+ triggeredAsset: {
+ triggeredModel: { modelName: "ArmBot-X", modelUuid: "arm-001" },
+ triggeredPoint: {
+ pointName: "Pickup Arm Point",
+ pointUuid: "arm-point-01",
+ },
+ triggeredAction: {
+ actionName: "Grab Widget",
+ actionUuid: "grab-001",
+ },
+ },
+ },
+ {
+ triggerUuid: "trig-002",
+ triggerName: "Complete Travel",
+ triggerType: "onComplete",
+ delay: 2,
+ triggeredAsset: null,
+ },
+ ],
},
- {
- modelUuid: "b06960bb-3d2e-41f7-a646-335f389c68b4",
- modelName: "AGV",
- position: [89.61609306554463, 0, 33.634136622267356],
- rotation: [0, 0, 0],
- state: "idle",
- type: "vehicle",
- speed: 2.5,
- point: {
- uuid: "point-789",
- position: [0, 1, 0],
- rotation: [0, 0, 0],
- action: {
- actionUuid: "action-456",
- actionName: "Deliver to Zone A",
- actionType: "travel",
- unLoadDuration: 10,
- loadCapacity: 2,
- pickUpPoint: { x: 90, y: 0, z: 28 },
- unLoadPoint: { x: 20, y: 0, z: 10 },
- triggers: [
- {
- triggerUuid: "trig-001",
- triggerName: "Start Travel",
- triggerType: "onStart",
- delay: 0,
- triggeredAsset: {
- triggeredModel: { modelName: "ArmBot-X", modelUuid: "arm-001" },
- triggeredPoint: { pointName: "Pickup Arm Point", pointUuid: "arm-point-01" },
- triggeredAction: { actionName: "Grab Widget", actionUuid: "grab-001" }
- }
- },
- {
- triggerUuid: "trig-002",
- triggerName: "Complete Travel",
- triggerType: "onComplete",
- delay: 2,
- triggeredAsset: null
- }
- ]
- }
- }
- }, {
- modelUuid: "e729a4f1-11d2-4778-8d6a-468f1b4f6b79",
- modelName: "forklift",
- position: [98.85729337188162, 0, 38.36616546567653],
- rotation: [0, 0, 0],
- state: "idle",
- type: "vehicle",
- speed: 2.5,
- point: {
- uuid: "point-789",
- position: [0, 1, 0],
- rotation: [0, 0, 0],
- action: {
- actionUuid: "action-456",
- actionName: "Deliver to Zone A",
- actionType: "travel",
- unLoadDuration: 15,
- loadCapacity: 5,
- pickUpPoint: { x: 98.71483985219794, y: 0, z: 28.66321267938962 },
- unLoadPoint: { x: 20, y: 0, z: 10 },
- triggers: [
- {
- triggerUuid: "trig-001",
- triggerName: "Start Travel",
- triggerType: "onStart",
- delay: 0,
- triggeredAsset: {
- triggeredModel: { modelName: "ArmBot-X", modelUuid: "arm-001" },
- triggeredPoint: { pointName: "Pickup Arm Point", pointUuid: "arm-point-01" },
- triggeredAction: { actionName: "Grab Widget", actionUuid: "grab-001" }
- }
- },
- {
- triggerUuid: "trig-002",
- triggerName: "Complete Travel",
- triggerType: "onComplete",
- delay: 2,
- triggeredAsset: null
- }
- ]
- }
- }
- }
- ];
+ },
+ },
+ {
+ modelUuid: "3a8f6da6-da57-4ef5-91e3-b8daf89e5753",
+ modelName: "forklift",
+ position: [98.85729337188162, 0, 38.36616546567653],
+ rotation: [0, 0, 0],
+ state: "idle",
+ type: "vehicle",
+ speed: 2.5,
+ point: {
+ uuid: "point-789",
+ position: [0, 1, 0],
+ rotation: [0, 0, 0],
+ action: {
+ actionUuid: "action-456",
+ actionName: "Deliver to Zone A",
+ actionType: "travel",
+ unLoadDuration: 15,
+ loadCapacity: 5,
+ pickUpPoint: null,
+ unLoadPoint: null,
+ triggers: [
+ {
+ triggerUuid: "trig-001",
+ triggerName: "Start Travel",
+ triggerType: "onStart",
+ delay: 0,
+ triggeredAsset: {
+ triggeredModel: { modelName: "ArmBot-X", modelUuid: "arm-001" },
+ triggeredPoint: {
+ pointName: "Pickup Arm Point",
+ pointUuid: "arm-point-01",
+ },
+ triggeredAction: {
+ actionName: "Grab Widget",
+ actionUuid: "grab-001",
+ },
+ },
+ },
+ {
+ triggerUuid: "trig-002",
+ triggerName: "Complete Travel",
+ triggerType: "onComplete",
+ delay: 2,
+ triggeredAsset: null,
+ },
+ ],
+ },
+ },
+ },
+ ]);
+ // useEffect(())
+ console.log("vehicleStatusSample", vehicleStatusSample);
+ useEffect(() => {
+ addVehicle("123", vehicleStatusSample[0]);
+ // addVehicle('123', vehicleStatusSample[1]);
+ // addVehicle('123', vehicleStatusSample[2]);
+ }, []);
+ useEffect(() => {}, [vehicles]);
- useEffect(() => {
- addVehicle('123', vehicleStatusSample[0]);
- // addVehicle('123', vehicleStatusSample[1]);
- // addVehicle('123', vehicleStatusSample[2]);
- }, [])
-
- useEffect(() => {
- console.log('vehicles: ', vehicles);
- }, [vehicles])
-
-
- return (
- <>
-
- >
- )
+ return (
+ <>
+
+
+ >
+ );
}
-export default Vehicles;
\ No newline at end of file
+export default Vehicles;
+
+
+