From 5a0978560c73f9530215a7627e81bd669f604407 Mon Sep 17 00:00:00 2001 From: Poovizhi99 Date: Sat, 5 Jul 2025 11:16:31 +0530 Subject: [PATCH] feat: Enhance vehicle simulation with draggable path points and interactive controls --- app/package-lock.json | 47 +- app/src/components/Dashboard/SidePannel.tsx | 7 +- .../eventProperties/EventProperties.tsx | 1 - .../mechanics/vehicleMechanics.tsx | 29 +- app/src/modules/builder/builder.tsx | 5 +- .../instances/animator/interactivePoint.tsx | 420 +++++++++++++ .../instances/animator/vehicleAnimator.tsx | 576 +++++++++++++++--- .../instances/instance/vehicleInstance.tsx | 18 +- app/src/pages/Dashboard.tsx | 3 +- app/src/pages/Project.tsx | 3 +- app/src/store/builder/store.ts | 21 +- 11 files changed, 996 insertions(+), 134 deletions(-) create mode 100644 app/src/modules/simulation/vehicle/instances/animator/interactivePoint.tsx diff --git a/app/package-lock.json b/app/package-lock.json index 5da4733..7bb6a97 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -2026,7 +2026,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==", - "dev": true, + "devOptional": true, "dependencies": { "@jridgewell/trace-mapping": "0.3.9" }, @@ -2038,7 +2038,7 @@ "version": "0.3.9", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, + "devOptional": true, "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" @@ -4180,6 +4180,25 @@ "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==", + "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", @@ -4291,25 +4310,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==", - "dev": true + "devOptional": 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==", - "dev": true + "devOptional": 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==", - "dev": true + "devOptional": 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==", - "dev": true + "devOptional": true }, "node_modules/@turf/along": { "version": "7.2.0", @@ -9063,7 +9082,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==", - "dev": true + "devOptional": true }, "node_modules/cross-env": { "version": "7.0.3", @@ -9940,7 +9959,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true, + "devOptional": true, "engines": { "node": ">=0.3.1" } @@ -15324,7 +15343,7 @@ "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true + "devOptional": true }, "node_modules/makeerror": { "version": "1.0.12", @@ -20801,7 +20820,7 @@ "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "dev": true, + "devOptional": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -20844,7 +20863,7 @@ "version": "8.3.4", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "dev": true, + "devOptional": true, "dependencies": { "acorn": "^8.11.0" }, @@ -20856,7 +20875,7 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true + "devOptional": true }, "node_modules/tsconfig-paths": { "version": "3.15.0", @@ -21352,7 +21371,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==", - "dev": true + "devOptional": true }, "node_modules/v8-to-istanbul": { "version": "8.1.1", @@ -22411,7 +22430,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, + "devOptional": true, "engines": { "node": ">=6" } diff --git a/app/src/components/Dashboard/SidePannel.tsx b/app/src/components/Dashboard/SidePannel.tsx index 5cfaeb0..89ce5ea 100644 --- a/app/src/components/Dashboard/SidePannel.tsx +++ b/app/src/components/Dashboard/SidePannel.tsx @@ -38,9 +38,11 @@ const SidePannel: React.FC = ({ setActiveTab, activeTab }) => { const handleCreateNewProject = async () => { const token = localStorage.getItem("token"); + const refreshToken = localStorage.getItem("refreshToken") + console.log('refreshToken: ', refreshToken); try { const projectId = generateProjectId(); - useSocketStore.getState().initializeSocket(email, organization, token); + useSocketStore.getState().initializeSocket(email, organization, token, refreshToken); //API for creating new Project // const project = await createProject( @@ -56,7 +58,8 @@ const SidePannel: React.FC = ({ setActiveTab, activeTab }) => { organization: organization, projectUuid: projectId, }; - + + console.log('projectSocket: ', projectSocket); if (projectSocket) { const handleResponse = (data: any) => { if (data.message === "Project created successfully") { diff --git a/app/src/components/layout/sidebarRight/properties/eventProperties/EventProperties.tsx b/app/src/components/layout/sidebarRight/properties/eventProperties/EventProperties.tsx index ed799e5..eee19b1 100644 --- a/app/src/components/layout/sidebarRight/properties/eventProperties/EventProperties.tsx +++ b/app/src/components/layout/sidebarRight/properties/eventProperties/EventProperties.tsx @@ -27,7 +27,6 @@ const EventProperties: React.FC = () => { const { selectedVersionStore } = useVersionContext(); const { selectedVersion } = selectedVersionStore(); const { projectId } = useParams(); - useEffect(() => { const event = getCurrentEventData(); setCurrentEventData(event); diff --git a/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/vehicleMechanics.tsx b/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/vehicleMechanics.tsx index 78583d8..85a539c 100644 --- a/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/vehicleMechanics.tsx +++ b/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/vehicleMechanics.tsx @@ -14,6 +14,7 @@ import { useProductContext } from "../../../../../../modules/simulation/products import { useParams } from "react-router-dom"; import { useVersionContext } from "../../../../../../modules/builder/version/versionContext"; import { useSceneContext } from "../../../../../../modules/scene/sceneContext"; +import { useSelectedPath } from "../../../../../../store/builder/store"; function VehicleMechanics() { const [activeOption, setActiveOption] = useState<"default" | "travel">("default"); @@ -27,6 +28,7 @@ function VehicleMechanics() { const { selectedVersionStore } = useVersionContext(); const { selectedVersion } = selectedVersionStore(); const { projectId } = useParams(); + const { selectedPath, setSelectedPath } = useSelectedPath(); useEffect(() => { if (selectedEventData && selectedEventData.data.type === "vehicle") { @@ -290,9 +292,34 @@ function VehicleMechanics() { type={"Vehicle"} /> +
+ + +
- )} + ) + } ); } diff --git a/app/src/modules/builder/builder.tsx b/app/src/modules/builder/builder.tsx index ab9434b..a98805b 100644 --- a/app/src/modules/builder/builder.tsx +++ b/app/src/modules/builder/builder.tsx @@ -15,6 +15,7 @@ import { useToolMode, useRenderDistance, useLimitDistance, + useLoadingProgress, } from "../../store/builder/store"; ////////// 3D Function Imports ////////// @@ -56,6 +57,7 @@ export default function Builder() { const { projectId } = useParams(); const { setHoveredPoint, setHoveredLine } = useBuilderStore(); const { userId, organization } = getUserData(); + const { loadingProgress } = useLoadingProgress(); useEffect(() => { if (!toggleView) { @@ -115,8 +117,7 @@ export default function Builder() { - - + {loadingProgress == 0 && } diff --git a/app/src/modules/simulation/vehicle/instances/animator/interactivePoint.tsx b/app/src/modules/simulation/vehicle/instances/animator/interactivePoint.tsx new file mode 100644 index 0000000..d6174e3 --- /dev/null +++ b/app/src/modules/simulation/vehicle/instances/animator/interactivePoint.tsx @@ -0,0 +1,420 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import * as THREE from 'three'; +import { Canvas, useThree, useFrame, ThreeEvent } from '@react-three/fiber'; +import { Line, OrbitControls } from '@react-three/drei'; +import { useAnimationPlaySpeed, usePauseButtonStore, usePlayButtonStore } from '../../../../../store/usePlayButtonStore'; +import { useActiveTool, useSelectedPath } from '../../../../../store/builder/store'; + +interface InteractivePointsProps { + agvUuid: string; +} + + +export default function InteractivePoints({ agvUuid }: InteractivePointsProps) { + const { gl, scene, raycaster } = useThree(); + const [points, setPoints] = useState<[number, number, number][]>([]); + const { isPaused } = usePauseButtonStore(); + const { isPlaying } = usePlayButtonStore(); + const { speed } = useAnimationPlaySpeed(); + // const raycaster = useRef(new THREE.Raycaster()); + const plane = useRef(new THREE.Plane(new THREE.Vector3(0, 1, 0), 0)); // XZ plane + const progressRef = useRef(0); + const { selectedPath } = useSelectedPath(); + const lastTimeRef = useRef(performance.now()); + const [isAnyDragging, setIsAnyDragging] = useState(""); + const { activeTool } = useActiveTool(); + const hasClicked = useRef(false); + + useFrame(() => { + if (!isPlaying) return + const now = performance.now(); + const delta = (now - lastTimeRef.current) / 1000; + lastTimeRef.current = now; + + const object = scene.getObjectByProperty('uuid', agvUuid); + if (!object || points.length < 2) return; + if (isPaused) return; + + let totalDistance = 0; + const distances = []; + let accumulatedDistance = 0; + let index = 0; + const rotationSpeed = 1; + + for (let i = 0; i < points.length - 1; i++) { + const start = new THREE.Vector3(...points[i]); + const end = new THREE.Vector3(...points[i + 1]); + const segmentDistance = start.distanceTo(end); + distances.push(segmentDistance); + totalDistance += segmentDistance; + } + + while (index < distances.length && progressRef.current > accumulatedDistance + distances[index]) { + accumulatedDistance += distances[index]; + index++; + } + + if (index < distances.length) { + const start = new THREE.Vector3(...points[index]); + const end = new THREE.Vector3(...points[index + 1]); + const segmentDistance = distances[index]; + + const currentDirection = new THREE.Vector3().subVectors(end, start).normalize(); + const targetAngle = Math.atan2(currentDirection.x, currentDirection.z); + const currentAngle = object.rotation.y; + + let angleDifference = targetAngle - currentAngle; + if (angleDifference > Math.PI) angleDifference -= 2 * Math.PI; + if (angleDifference < -Math.PI) angleDifference += 2 * Math.PI; + + const maxRotationStep = (rotationSpeed * speed * 2) * delta; + object.rotation.y += Math.sign(angleDifference) * Math.min(Math.abs(angleDifference), maxRotationStep); + const isAligned = Math.abs(angleDifference) < 0.01; + + if (isAligned) { + progressRef.current += delta * (speed * 2); + const t = (progressRef.current - accumulatedDistance) / segmentDistance; + const position = start.clone().lerp(end, t); + object.position.copy(position); + } + } + }); + + const downPosition = useRef<{ x: number; y: number } | null>(null); + + // const handleMouseDown = (e: MouseEvent) => { + // downPosition.current = { x: e.clientX, y: e.clientY }; + // }; + + // const handleClick = useCallback((e: MouseEvent) => { + // 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) && activeTool !== "pen") { + // const pointArray = intersection.toArray() as [number, number, number]; + // console.log('pointArray: ', pointArray); + // setPoints((prev) => [...prev, pointArray]); + // console.log("points created"); + // } + // }, [activeTool, raycaster]); + + const handleMouseDown = useCallback((e: MouseEvent) => { + hasClicked.current = false; + downPosition.current = { x: e.clientX, y: e.clientY }; + }, []); + + const handleClick = useCallback((e: MouseEvent) => { + 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) && activeTool !== "pen") { + const pointArray = intersection.toArray() as [number, number, number]; + + // ✅ Check if this point already exists + const alreadyExists = points.some((p) => + Math.abs(p[0] - pointArray[0]) < 0.01 && + Math.abs(p[1] - pointArray[1]) < 0.01 && + Math.abs(p[2] - pointArray[2]) < 0.01 + ); + + if (!alreadyExists) { + console.log("pointArray: ", pointArray); + setPoints((prev) => [...prev, pointArray]); + console.log("points created"); + } + } + }, [activeTool, raycaster, points]); + + + + // const handleClick = useCallback((e: MouseEvent) => { + // 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) && activeTool !== "pen") { + // const pointArray = intersection.toArray() as [number, number, number]; + // console.log('pointArray: ', pointArray); + // setPoints((prev) => [...prev, pointArray]); + // console.log("points created"); + // } + // }, [activeTool, raycaster]); + + useEffect(() => { + if (isPlaying) return; + const domElement = gl.domElement; + + domElement.addEventListener('mousedown', handleMouseDown); + domElement.addEventListener('mouseup', handleClick); + + + return () => { + domElement.removeEventListener('mousedown', handleMouseDown); + domElement.removeEventListener('mouseup', handleClick); + ; + }; + }, [isPlaying, handleClick, handleMouseDown]); + + + const updatePoint = (index: number, pos: THREE.Vector3) => { + const updated = [...points]; + updated[index] = pos.toArray() as [number, number, number]; + setPoints(updated); + }; + + + return ( + <> + {selectedPath === "manual" && + + {points.length > 0 && ( + + {points.map((pos, i) => + ( + + + ) + + + )} + + ) + } + {points && ( + points.map((pos, i) => { + if (i < points.length - 1) { + return ( + { + const updated = [...points]; + updated[i0] = p0.toArray() as [number, number, number]; + updated[i1] = p1.toArray() as [number, number, number]; + setPoints(updated); + }} + isAnyDragging={isAnyDragging} + setIsAnyDragging={setIsAnyDragging} + /> + ); + } + return null; + }) + )} + + } + + ); +} + + + + +// function DraggableSphere({ +// index, +// position, +// onMove, +// }: { +// index: number; +// position: THREE.Vector3; +// onMove: (index: number, pos: THREE.Vector3) => void; +// }) { +// const meshRef = useRef(null); +// const { gl, controls, raycaster } = useThree(); +// const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0); // XZ plane +// const isDragging = useRef(false); +// const { activeTool } = useActiveTool(); + +// const onPointerDown = (e: ThreeEvent) => { +// if (activeTool !== 'pen') return; + +// isDragging.current = true; +// gl.domElement.style.cursor = 'grabbing'; + +// if (controls) { +// (controls as any).enabled = false; +// } +// }; + +// const onPointerMove = (e: ThreeEvent) => { +// if (!isDragging.current || activeTool !== 'pen') return; + +// const intersect = new THREE.Vector3(); +// if (raycaster.ray.intersectPlane(plane, intersect)) { +// meshRef.current!.position.copy(intersect); +// onMove(index, intersect); +// } +// }; + +// const onPointerUp = () => { +// if (activeTool !== 'pen') return; + +// isDragging.current = false; +// gl.domElement.style.cursor = 'default'; +// if (controls) { +// (controls as any).enabled = true; +// } +// }; + +// return ( +// +// +// +// +// ); +// } + +function DraggableSphere({ + index, + position, + onMove, + isAnyDragging, + setIsAnyDragging, +}: { + index: number; + position: THREE.Vector3; + onMove: (index: number, pos: THREE.Vector3) => void; + isAnyDragging: string; + setIsAnyDragging: (val: string) => void; +}) { + const meshRef = useRef(null); + const { gl, controls, raycaster } = useThree(); + const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0); + const { activeTool } = useActiveTool(); + + const onPointerDown = (e: ThreeEvent) => { + e.stopPropagation() + if (activeTool !== 'pen') return; + setIsAnyDragging("point"); + gl.domElement.style.cursor = 'grabbing'; + if (controls) (controls as any).enabled = false; + }; + + const onPointerMove = (e: ThreeEvent) => { + if (isAnyDragging !== "point" || activeTool !== 'pen') return; + + const intersect = new THREE.Vector3(); + if (raycaster.ray.intersectPlane(plane, intersect)) { + meshRef.current!.position.copy(intersect); + onMove(index, intersect); + } + }; + + const onPointerUp = () => { + if (activeTool !== 'pen') return; + setIsAnyDragging(""); + gl.domElement.style.cursor = 'default'; + if (controls) (controls as any).enabled = true; + }; + + return ( + + + + + ); +} + +function DraggableLineSegment({ + index, + start, + end, + updatePoints, + isAnyDragging, + setIsAnyDragging, +}: { + index: number; + start: THREE.Vector3; + end: THREE.Vector3; + updatePoints: (i0: number, p0: THREE.Vector3, i1: number, p1: THREE.Vector3) => void; + isAnyDragging: string; + setIsAnyDragging: (val: string) => void; +}) { + const { gl, raycaster, controls } = useThree(); + const { activeTool } = useActiveTool(); + const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0); + const dragStart = useRef(null); + + const onPointerDown = () => { + if (activeTool !== 'pen' || isAnyDragging) return; + setIsAnyDragging("line"); + gl.domElement.style.cursor = 'grabbing'; + if (controls) (controls as any).enabled = false; + }; + + const onPointerMove = (e: ThreeEvent) => { + if (isAnyDragging !== "line" || activeTool !== 'pen') return; + + const intersect = new THREE.Vector3(); + if (raycaster.ray.intersectPlane(plane, intersect)) { + if (!dragStart.current) dragStart.current = intersect.clone(); + const offset = new THREE.Vector3().subVectors(intersect, dragStart.current); + const newStart = start.clone().add(offset); + const newEnd = end.clone().add(offset); + updatePoints(index, newStart, index + 1, newEnd); + } + }; + + const onPointerUp = () => { + if (activeTool !== 'pen') return; + setIsAnyDragging(""); + dragStart.current = null; + gl.domElement.style.cursor = 'default'; + if (controls) (controls as any).enabled = true; + }; + + return ( + + ); +} \ No newline at end of file diff --git a/app/src/modules/simulation/vehicle/instances/animator/vehicleAnimator.tsx b/app/src/modules/simulation/vehicle/instances/animator/vehicleAnimator.tsx index 9d45fd8..2f37503 100644 --- a/app/src/modules/simulation/vehicle/instances/animator/vehicleAnimator.tsx +++ b/app/src/modules/simulation/vehicle/instances/animator/vehicleAnimator.tsx @@ -1,9 +1,12 @@ -import { useEffect, useRef, useState } from 'react' -import { useFrame, useThree } from '@react-three/fiber'; + +import React, { useEffect, useMemo, useRef, useState } from 'react' +import { useFrame, useThree, ThreeEvent } from '@react-three/fiber'; import * as THREE from 'three'; -import { Line } from '@react-three/drei'; +import { Line, TransformControls } 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][]; @@ -28,11 +31,13 @@ function VehicleAnimator({ path, handleCallBack, currentPhase, agvUuid, agvDetai 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(true); const [currentPath, setCurrentPath] = useState<[number, number, number][]>([]); - const { scene } = useThree(); + const { scene, controls } = useThree(); + const { selectedPath } = useSelectedPath(); + const [isAnyDragging, setIsAnyDragging] = useState(""); + useEffect(() => { - if (currentPhase === 'stationed-pickup' && path.length > 0) { - // console.log('path: ', path); + if (currentPhase === 'stationed-pickup' && path.length > 0 && selectedPath === "auto") { setCurrentPath(path); setObjectRotation(agvDetail.point.action?.pickUpPoint?.rotation) } else if (currentPhase === 'pickup-drop' && path.length > 0) { @@ -42,7 +47,7 @@ function VehicleAnimator({ path, handleCallBack, currentPhase, agvUuid, agvDetai setObjectRotation(agvDetail.point.action?.pickUpPoint?.rotation) setCurrentPath(path); } - }, [currentPhase, path, objectRotation]); + }, [currentPhase, path, objectRotation, selectedPath]); useEffect(() => { completedRef.current = false; @@ -66,87 +71,11 @@ function VehicleAnimator({ path, handleCallBack, currentPhase, agvUuid, agvDetai } }, [isReset, isPlaying]) - // useFrame((_, delta) => { - // const object = scene.getObjectByProperty('uuid', agvUuid); - // if (!object || currentPath.length < 2) return; - // if (isPaused) return; - // let totalDistance = 0; - // const distances = []; - // let accumulatedDistance = 0; - // let index = 0; - // const rotationSpeed = 1; - - // for (let i = 0; i < currentPath.length - 1; i++) { - // const start = new THREE.Vector3(...currentPath[i]); - // const end = new THREE.Vector3(...currentPath[i + 1]); - // const segmentDistance = start.distanceTo(end); - // distances.push(segmentDistance); - // totalDistance += segmentDistance; - // } - - // while (index < distances.length && progressRef.current > accumulatedDistance + distances[index]) { - // accumulatedDistance += distances[index]; - // index++; - // } - - // if (index < distances.length) { - // const start = new THREE.Vector3(...currentPath[index]); - // const end = new THREE.Vector3(...currentPath[index + 1]); - // const segmentDistance = distances[index]; - - // const currentDirection = new THREE.Vector3().subVectors(end, start).normalize(); - // const targetAngle = Math.atan2(currentDirection.x, currentDirection.z); - - // const currentAngle = object.rotation.y; - - // let angleDifference = targetAngle - currentAngle; - // if (angleDifference > Math.PI) angleDifference -= 2 * Math.PI; - // if (angleDifference < -Math.PI) angleDifference += 2 * Math.PI; - - // const maxRotationStep = (rotationSpeed * speed * agvDetail.speed) * delta; - // object.rotation.y += Math.sign(angleDifference) * Math.min(Math.abs(angleDifference), maxRotationStep); - // const isAligned = Math.abs(angleDifference) < 0.01; - - // if (isAligned) { - // progressRef.current += delta * (speed * agvDetail.speed); - // const t = (progressRef.current - accumulatedDistance) / segmentDistance; - // const position = start.clone().lerp(end, t); - // object.position.copy(position); - // } - // } - - // if (progressRef.current >= totalDistance) { - // if (restRotation && objectRotation) { - // const targetEuler = new THREE.Euler( - // objectRotation.x, - // objectRotation.y - (agvDetail.point.action.steeringAngle), - // objectRotation.z - // ); - // const targetQuaternion = new THREE.Quaternion().setFromEuler(targetEuler); - // object.quaternion.slerp(targetQuaternion, delta * (rotationSpeed * speed * agvDetail.speed)); - // if (object.quaternion.angleTo(targetQuaternion) < 0.01) { - // object.quaternion.copy(targetQuaternion); - // object.rotation.copy(targetEuler); - // setRestingRotation(false); - // } - // return; - // } - // } - // if (progressRef.current >= totalDistance) { - // setRestingRotation(true); - // progressRef.current = 0; - // movingForward.current = !movingForward.current; - // setCurrentPath([]); - // handleCallBack(); - // if (currentPhase === 'pickup-drop') { - // requestAnimationFrame(startUnloadingProcess); - // } - // } - // }); const lastTimeRef = useRef(performance.now()); useFrame(() => { + if (!isPlaying) return const now = performance.now(); const delta = (now - lastTimeRef.current) / 1000; lastTimeRef.current = now; @@ -225,26 +154,479 @@ function VehicleAnimator({ path, handleCallBack, currentPhase, agvUuid, agvDetai handleCallBack(); if (currentPhase === 'pickup-drop') { requestAnimationFrame(startUnloadingProcess); + } } }); + const updatePoint = (index: number, pos: THREE.Vector3) => { + const updated = [...currentPath]; + updated[index] = pos.toArray() as [number, number, number]; + setCurrentPath(updated); + }; + + return ( <> - {currentPath.length > 0 && ( - // helper - - - {currentPath.map((point, index) => ( - - - - - ))} - - )} + {selectedPath === "auto" && + {currentPath.map((pos, i) => { + if (i < currentPath.length - 1) { + return ( + { + const updated = [...currentPath]; + updated[i0] = p0.toArray() as [number, number, number]; + updated[i1] = p1.toArray() as [number, number, number]; + setCurrentPath(updated); + }} + isAnyDragging={isAnyDragging} + setIsAnyDragging={setIsAnyDragging} + /> + ); + } + return null; + })} + {currentPath.length > 0 && ( + { if (controls) (controls as any).enabled = true; }}> + {currentPath.map((pos, i) => + ( + ) + )} + + ) + } + + } ); } -export default VehicleAnimator; \ No newline at end of file + + +export default VehicleAnimator; + + + + +function DraggableSphere({ + index, + position, + onMove, + isAnyDragging, + setIsAnyDragging, +}: { + index: number; + position: THREE.Vector3; + onMove: (index: number, pos: THREE.Vector3) => void; + isAnyDragging: string; + setIsAnyDragging: (val: string) => void; +}) { + const meshRef = useRef(null); + const { gl, controls, raycaster } = useThree(); + const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0); + const { activeTool } = useActiveTool(); + + const onPointerDown = (e: ThreeEvent) => { + e.stopPropagation() + if (activeTool !== 'pen') return; + setIsAnyDragging("point"); + gl.domElement.style.cursor = 'grabbing'; + if (controls) (controls as any).enabled = false; + }; + + const onPointerMove = (e: ThreeEvent) => { + if (isAnyDragging !== "point" || activeTool !== 'pen') return; + + const intersect = new THREE.Vector3(); + if (raycaster.ray.intersectPlane(plane, intersect)) { + meshRef.current!.position.copy(intersect); + onMove(index, intersect); + } + }; + + const onPointerUp = () => { + if (activeTool !== 'pen') return; + setIsAnyDragging(""); + gl.domElement.style.cursor = 'default'; + if (controls) (controls as any).enabled = true; + }; + useEffect(() => { + gl.domElement.addEventListener("pointerup", onPointerUp); + return (() => { + gl.domElement.removeEventListener("pointerup", onPointerUp); + }) + }, []) + + return ( + + + + + ); +} + +// function DraggableLineSegment({ +// index, +// start, +// end, +// updatePoints, +// isAnyDragging, +// setIsAnyDragging, +// }: { +// index: number; +// start: THREE.Vector3; +// end: THREE.Vector3; +// updatePoints: (i0: number, p0: THREE.Vector3, i1: number, p1: THREE.Vector3) => void; +// isAnyDragging: string; +// setIsAnyDragging: (val: string) => void; +// }) { +// const meshRef = useRef(null); +// const { gl, raycaster, controls } = useThree(); +// const { activeTool } = useActiveTool(); +// const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0); +// const dragStart = useRef(null); + + +// const onPointerDown = () => { +// if (activeTool !== 'pen' || isAnyDragging) return; // <-- Skip if dragging sphere +// setIsAnyDragging("line"); +// gl.domElement.style.cursor = 'grabbing'; +// if (controls) (controls as any).enabled = false; +// }; + +// const onPointerMove = (e: ThreeEvent) => { +// if (isAnyDragging !== "line" || activeTool !== 'pen') return; + +// const intersect = new THREE.Vector3(); +// if (raycaster.ray.intersectPlane(plane, intersect)) { +// if (!dragStart.current) dragStart.current = intersect.clone(); + +// const offset = new THREE.Vector3().subVectors(intersect, dragStart.current); +// const newStart = start.clone().add(offset); +// const newEnd = end.clone().add(offset); +// updatePoints(index, newStart, index + 1, newEnd); +// } +// }; + +// const onPointerUp = () => { +// if (activeTool !== 'pen') return; +// setIsAnyDragging(""); +// dragStart.current = null; +// gl.domElement.style.cursor = 'default'; +// if (controls) (controls as any).enabled = true; +// }; +// const noopRaycast = (raycaster: THREE.Raycaster, intersects: THREE.Intersection[]) => { }; + +// return ( +// +// +// +// ); + + +// // return ( +// // null : undefined} +// // raycast={isAnyDragging === "point" ? () => { } : undefined} +// // // pointerEvents={isAnyDragging === "point" ? "none" : "auto"} // ✅ the correct way +// // > +// // +// // +// // ); +// } + +function DraggableLineSegment({ + index, + start, + end, + updatePoints, + isAnyDragging, + setIsAnyDragging, +}: { + index: number; + start: THREE.Vector3; + end: THREE.Vector3; + updatePoints: (i0: number, p0: THREE.Vector3, i1: number, p1: THREE.Vector3) => void; + isAnyDragging: string; + setIsAnyDragging: (val: string) => void; +}) { + const { gl, raycaster, controls } = useThree(); + const { activeTool } = useActiveTool(); + const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0); + const dragStart = useRef(null); + + const onPointerDown = () => { + if (activeTool !== 'pen' || isAnyDragging) return; + setIsAnyDragging("line"); + gl.domElement.style.cursor = 'grabbing'; + if (controls) (controls as any).enabled = false; + }; + + const onPointerMove = (e: ThreeEvent) => { + console.log('isAnyDragging: ', isAnyDragging); + if (isAnyDragging !== "line" || activeTool !== 'pen') return; + + const intersect = new THREE.Vector3(); + if (raycaster.ray.intersectPlane(plane, intersect)) { + if (!dragStart.current) dragStart.current = intersect.clone(); + const offset = new THREE.Vector3().subVectors(intersect, dragStart.current); + const newStart = start.clone().add(offset); + const newEnd = end.clone().add(offset); + updatePoints(index, newStart, index + 1, newEnd); + } + }; + + const onPointerUp = () => { + if (activeTool !== 'pen') return; + setIsAnyDragging(""); + dragStart.current = null; + gl.domElement.style.cursor = 'default'; + if (controls) (controls as any).enabled = true; + }; + + return ( + + ); +} + +// These are recently edited files. Do not suggest code that has been deleted. +// function DraggableSphere({ +// index, +// position, +// onMove, +// isAnyDragging, +// setIsAnyDragging, +// }: { +// index: number; +// position: THREE.Vector3; +// onMove: (index: number, pos: THREE.Vector3) => void; +// isAnyDragging: boolean; +// setIsAnyDragging: (val: boolean) => void; +// }) { +// const meshRef = useRef(null); +// const { gl, controls, raycaster } = useThree(); +// const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0); // XZ plane +// const isDragging = useRef(false); +// const { activeTool } = useActiveTool(); + +// const onPointerDown = (e: ThreeEvent) => { +// if (activeTool !== 'pen') return; + +// isDragging.current = true; +// gl.domElement.style.cursor = 'grabbing'; + +// if (controls) { +// (controls as any).enabled = false; +// } +// }; + +// const onPointerMove = (e: ThreeEvent) => { +// if (!isDragging.current || activeTool !== 'pen') return; + +// const intersect = new THREE.Vector3(); +// if (raycaster.ray.intersectPlane(plane, intersect)) { +// meshRef.current!.position.copy(intersect); +// onMove(index, intersect); +// } +// }; + +// const onPointerUp = () => { +// if (activeTool !== 'pen') return; + +// isDragging.current = false; +// gl.domElement.style.cursor = 'default'; +// if (controls) { +// (controls as any).enabled = true; +// } +// }; + +// return ( +// +// +// +// +// ); +// } + +// function DraggableLineSegment({ +// index, +// start, +// end, +// updatePoints, +// isAnyDragging, +// setIsAnyDragging, +// }: { +// index: number; +// start: THREE.Vector3; +// end: THREE.Vector3; +// updatePoints: (i0: number, p0: THREE.Vector3, i1: number, p1: THREE.Vector3) => void; +// isAnyDragging: boolean; +// setIsAnyDragging: (val: boolean) => void; +// }) { +// const meshRef = useRef(null); +// const { gl, camera, controls, raycaster } = useThree(); +// const { activeTool } = useActiveTool(); // 👈 Get active tool +// const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0); +// const isDragging = useRef(false); +// const dragStart = useRef(null); + +// const onPointerDown = () => { +// if (activeTool !== 'pen') return; // 👈 Only allow when tool is 'pen' + +// isDragging.current = true; +// gl.domElement.style.cursor = 'grabbing'; +// if (controls) (controls as any).enabled = false; +// }; + +// const onPointerMove = (e: ThreeEvent) => { +// if (!isDragging.current || activeTool !== 'pen') return; + +// const intersect = new THREE.Vector3(); +// if (raycaster.ray.intersectPlane(plane, intersect)) { +// if (!dragStart.current) { +// dragStart.current = intersect.clone(); +// } + +// const offset = new THREE.Vector3().subVectors(intersect, dragStart.current); +// const newStart = start.clone().add(offset); +// const newEnd = end.clone().add(offset); +// updatePoints(index, newStart, index + 1, newEnd); +// } +// }; + +// const onPointerUp = () => { +// if (activeTool !== 'pen') return; + +// isDragging.current = false; +// dragStart.current = null; +// gl.domElement.style.cursor = 'default'; +// if (controls) (controls as any).enabled = true; +// }; + +// return ( +// +// +// +// ); +// } + + + + + + + + + + + +// return ( +// <> +// {selectedPath === "auto" && +// {/* {currentPath.map((pos, i) => { +// if (i < currentPath.length - 1) { +// return ( +// { +// const updated = [...currentPath]; +// updated[i0] = p0.toArray() as [number, number, number]; +// updated[i1] = p1.toArray() as [number, number, number]; +// setCurrentPath(updated); +// }} +// /> +// ); +// } +// return null; +// })} */} +// {currentPath.length > 0 && ( +// { if (controls) (controls as any).enabled = true; }}> +// +// {currentPath.map((pos, i) => { +// if (i < currentPath.length - 1) { +// return ( +// +// +// {/* { +// const updated = [...currentPath]; +// updated[i0] = p0.toArray() as [number, number, number]; +// updated[i1] = p1.toArray() as [number, number, number]; +// setCurrentPath(updated); +// }} +// /> */} +// +// ) +// } +// } +// )} +// +// ) +// } +// +// } +// +// ); \ No newline at end of file diff --git a/app/src/modules/simulation/vehicle/instances/instance/vehicleInstance.tsx b/app/src/modules/simulation/vehicle/instances/instance/vehicleInstance.tsx index 0709109..83a803e 100644 --- a/app/src/modules/simulation/vehicle/instances/instance/vehicleInstance.tsx +++ b/app/src/modules/simulation/vehicle/instances/instance/vehicleInstance.tsx @@ -2,12 +2,13 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import VehicleAnimator from '../animator/vehicleAnimator'; import * as THREE from 'three'; import { NavMeshQuery } from '@recast-navigation/core'; -import { useNavMesh } from '../../../../../store/builder/store'; +import { useNavMesh, useSelectedPath } from '../../../../../store/builder/store'; import { useAnimationPlaySpeed, usePauseButtonStore, usePlayButtonStore } from '../../../../../store/usePlayButtonStore'; import { useTriggerHandler } from '../../../triggers/triggerHandler/useTriggerHandler'; import MaterialAnimator from '../animator/materialAnimator'; import { useSceneContext } from '../../../../scene/sceneContext'; import { useProductContext } from '../../../products/productContext'; +import InteractivePoints from '../animator/interactivePoint'; function VehicleInstance({ agvDetail }: Readonly<{ agvDetail: VehicleStatus }>) { const { navMesh } = useNavMesh(); @@ -22,7 +23,6 @@ function VehicleInstance({ agvDetail }: Readonly<{ agvDetail: VehicleStatus }>) const { selectedProductStore } = useProductContext(); const { selectedProduct } = selectedProductStore(); const { vehicles, setVehicleActive, setVehicleState, setVehiclePicking, clearCurrentMaterials, setVehicleLoad, decrementVehicleLoad, removeLastMaterial, getLastMaterial, incrementIdleTime, incrementActiveTime, resetTime } = vehicleStore(); - const [currentPhase, setCurrentPhase] = useState('stationed'); const [path, setPath] = useState<[number, number, number][]>([]); const pauseTimeRef = useRef(null); @@ -36,6 +36,7 @@ function VehicleInstance({ agvDetail }: Readonly<{ agvDetail: VehicleStatus }>) const { isPaused } = usePauseButtonStore(); const previousTimeRef = useRef(null); const animationFrameIdRef = useRef(null); + const { selectedPath, setSelectedPath } = useSelectedPath(); useEffect(() => { isPausedRef.current = isPaused; @@ -55,12 +56,14 @@ function VehicleInstance({ agvDetail }: Readonly<{ agvDetail: VehicleStatus }>) Math.round(segmentPath[segmentPath.length - 1].x) == Math.round(end.x) && Math.round(segmentPath[segmentPath.length - 1].z) == Math.round(end.z) ) { + console.log('segmentPath: ', segmentPath); return segmentPath?.map(({ x, y, z }) => [x, 0, z] as [number, number, number]) || []; } else { console.log("There is no path here...Choose valid path") const { path: segmentPaths } = navMeshQuery.computePath(start, start); return segmentPaths.map(({ x, y, z }) => [x, 0, z] as [number, number, number]) || []; } + } catch { console.error("Failed to compute path"); return []; @@ -95,7 +98,7 @@ function VehicleInstance({ agvDetail }: Readonly<{ agvDetail: VehicleStatus }>) } useEffect(() => { - if (isPlaying) { + if (isPlaying || selectedPath === "auto") { if (!agvDetail.point.action.unLoadPoint || !agvDetail.point.action.pickUpPoint) return; if (!agvDetail.isActive && agvDetail.state === 'idle' && currentPhase === 'stationed') { @@ -103,10 +106,7 @@ function VehicleInstance({ agvDetail }: Readonly<{ agvDetail: VehicleStatus }>) new THREE.Vector3(agvDetail?.position[0], agvDetail?.position[1], agvDetail?.position[2]), agvDetail?.point?.action?.pickUpPoint?.position ); - // const toPickupPath = computePath( - // new THREE.Vector3(agvDetail?.position[0], agvDetail?.position[1], agvDetail?.position[2]), - // new THREE.Vector3(agvDetail?.position[0], agvDetail?.position[1], agvDetail?.position[2]) - // ); + setPath(toPickupPath); setCurrentPhase('stationed-pickup'); setVehicleState(agvDetail.modelUuid, 'running'); @@ -147,8 +147,7 @@ function VehicleInstance({ agvDetail }: Readonly<{ agvDetail: VehicleStatus }>) else { reset() } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [vehicles, currentPhase, path, isPlaying]); + }, [vehicles, currentPhase, path, isPlaying, selectedPath]); function animate(currentTime: number) { @@ -522,6 +521,7 @@ function VehicleInstance({ agvDetail }: Readonly<{ agvDetail: VehicleStatus }>) reset={reset} startUnloadingProcess={startUnloadingProcess} /> + {selectedPath === "manual" && ()} ); diff --git a/app/src/pages/Dashboard.tsx b/app/src/pages/Dashboard.tsx index 15959c1..606765c 100644 --- a/app/src/pages/Dashboard.tsx +++ b/app/src/pages/Dashboard.tsx @@ -16,8 +16,9 @@ const Dashboard: React.FC = () => { useEffect(() => { const token = localStorage.getItem("token"); + const refreshToken = localStorage.getItem("refreshToken") if (token) { - useSocketStore.getState().initializeSocket(email, organization, token); + useSocketStore.getState().initializeSocket(email, organization, token, refreshToken); } else { } diff --git a/app/src/pages/Project.tsx b/app/src/pages/Project.tsx index bf01b3a..0802fcd 100644 --- a/app/src/pages/Project.tsx +++ b/app/src/pages/Project.tsx @@ -114,8 +114,9 @@ const Project: React.FC = () => { setActiveModule("builder"); if (email) { const token = localStorage.getItem("token"); + const refreshToken = localStorage.getItem("refreshToken") if (token) { - useSocketStore.getState().initializeSocket(email, organization, token); + useSocketStore.getState().initializeSocket(email, organization, token, refreshToken); } if (organization && userName) { setOrganization(organization); diff --git a/app/src/store/builder/store.ts b/app/src/store/builder/store.ts index 06cb1fb..529e42f 100644 --- a/app/src/store/builder/store.ts +++ b/app/src/store/builder/store.ts @@ -5,7 +5,12 @@ import * as CONSTANTS from "../../types/world/worldConstants"; export const useSocketStore = create((set: any, get: any) => ({ socket: null, - initializeSocket: (email?: string, organization?: string, token?: string) => { + initializeSocket: ( + email?: string, + organization?: string, + token?: string, + refreshToken?: string + ) => { const existingSocket = get().socket; if (existingSocket) { return; @@ -15,7 +20,7 @@ export const useSocketStore = create((set: any, get: any) => ({ `http://${process.env.REACT_APP_SERVER_SOCKET_API_BASE_URL}/Builder_v1`, { reconnection: true, - auth: { token }, + auth: { token, refreshToken }, } ); @@ -23,7 +28,7 @@ export const useSocketStore = create((set: any, get: any) => ({ `http://${process.env.REACT_APP_SERVER_SOCKET_API_BASE_URL}/Visualization_v1`, { reconnection: true, - auth: { token }, + auth: { token, refreshToken }, } ); @@ -31,21 +36,21 @@ export const useSocketStore = create((set: any, get: any) => ({ `http://${process.env.REACT_APP_SERVER_SOCKET_API_BASE_URL}/dashboard`, { reconnection: true, - auth: { token }, + auth: { token, refreshToken }, } ); const projectSocket = io( `http://${process.env.REACT_APP_SERVER_SOCKET_API_BASE_URL}/project`, { reconnection: true, - auth: { token }, + auth: { token, refreshToken }, } ); const threadSocket = io( `http://${process.env.REACT_APP_SERVER_SOCKET_API_BASE_URL}/thread`, { reconnection: true, - auth: { token }, + auth: { token, refreshToken }, } ); @@ -724,3 +729,7 @@ export const useSelectedComment = create((set: any) => ({ commentPositionState: null, setCommentPositionState: (x: any) => set({ commentPositionState: x }), })); +export const useSelectedPath = create((set: any) => ({ + selectedPath: "", + setSelectedPath: (x: any) => set({ selectedPath: x }), +}));