feat: Enhance vehicle simulation UI with draggable points and asset details

- Updated VehicleInstances component to include asset details card for each vehicle.
- Refactored Vehicles component to remove unused vehicle state logging.
- Added new styles for asset details card in simulation.scss.
- Removed unnecessary styles from realTimeViz.scss.
- Implemented PickDropPoints component for draggable pick and drop locations.
- Created ArmBotUI component to manage robotic arm interactions and actions.
- Developed useDraggableGLTF hook for handling drag-and-drop functionality for GLTF models.
- Introduced VehicleUI component to visualize vehicle start and end points with rotation controls.
- Updated backend integration for vehicle actions and positions.
This commit is contained in:
2025-05-08 18:26:14 +05:30
parent 520cd5f581
commit fef6da5ab7
15 changed files with 846 additions and 429 deletions

View File

@@ -1,4 +1,4 @@
export function AnalysisIcon({ isActive }: { isActive: boolean }) {
export function AnalysisIcon({ isActive }: Readonly<{ isActive: boolean }>) {
return (
<svg
width="20"
@@ -27,7 +27,7 @@ export function AnalysisIcon({ isActive }: { isActive: boolean }) {
);
}
export function MechanicsIcon({ isActive }: { isActive: boolean }) {
export function MechanicsIcon({ isActive }: Readonly<{ isActive: boolean }>) {
return (
<svg
width="20"
@@ -52,7 +52,7 @@ export function MechanicsIcon({ isActive }: { isActive: boolean }) {
);
}
export function PropertiesIcon({ isActive }: { isActive: boolean }) {
export function PropertiesIcon({ isActive }: Readonly<{ isActive: boolean }>) {
return (
<svg
width="20"
@@ -83,7 +83,7 @@ export function PropertiesIcon({ isActive }: { isActive: boolean }) {
);
}
export function SimulationIcon({ isActive }: { isActive: boolean }) {
export function SimulationIcon({ isActive }: Readonly<{ isActive: boolean }>) {
return (
<svg
width="20"
@@ -222,3 +222,111 @@ export function MoveArrowLeft() {
</svg>
);
}
// simulation card icons
export function ExpandIcon({
color = "#6F42C1",
}: Readonly<{ color?: string }>) {
return (
<svg
width="30"
height="30"
viewBox="0 0 30 30"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="15" cy="15" r="15" fill={color} />
<path
d="M20.4993 11.0263C20.5138 10.7505 20.302 10.5152 20.0263 10.5007L15.5325 10.2642C15.2567 10.2497 15.0214 10.4614 15.0069 10.7372C14.9924 11.013 15.2042 11.2483 15.4799 11.2628L19.4744 11.473L19.2642 15.4675C19.2497 15.7433 19.4614 15.9786 19.7372 15.9931C20.013 16.0076 20.2483 15.7958 20.2628 15.5201L20.4993 11.0263ZM10 20L10.3345 20.3716L20.3345 11.3716L20 11L19.6655 10.6284L9.66552 19.6284L10 20Z"
fill="white"
/>
</svg>
);
}
export function SimulationStatusIcon({
color = "#21FF59",
}: Readonly<{ color?: string }>) {
return (
<svg
width="14"
height="14"
viewBox="0 0 14 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M7.46647 12.6024C10.3014 12.6024 12.5997 10.3041 12.5997 7.46916C12.5997 4.63416 10.3014 2.33594 7.46647 2.33594C4.63147 2.33594 2.33325 4.63416 2.33325 7.46916C2.33325 10.3041 4.63147 12.6024 7.46647 12.6024Z"
fill={color}
fillOpacity="0.3"
/>
<path
d="M8.79738 8.04925L6.83063 9.26848C6.51405 9.46474 6.125 9.20928 6.125 8.80516V6.3667C6.125 5.96258 6.51405 5.70714 6.83063 5.9034L8.79738 7.12263C9.12309 7.32458 9.12309 7.8473 8.79738 8.04925Z"
fill={color}
/>
</svg>
);
}
export function IndicationArrow() {
return (
<svg
width="15"
height="6"
viewBox="0 0 15 6"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.09697 5.03095L14.5 0L0 0L6.88778 5.05104C7.24969 5.31644 7.74408 5.30822 8.09697 5.03095Z"
fill="white"
/>
</svg>
);
}
export function CartBagIcon() {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M2 3L2.26491 3.0883C3.58495 3.52832 4.24497 3.74832 4.62248 4.2721C5 4.79587 5 5.49159 5 6.88304V9.5C5 12.3284 5 13.7426 5.87868 14.6213C6.75736 15.5 8.17157 15.5 11 15.5H19"
stroke="#FFFFFF"
strokeWidth="1.5"
strokeLinecap="round"
/>
<path
opacity="0.5"
d="M7.5 18C8.32843 18 9 18.6716 9 19.5C9 20.3284 8.32843 21 7.5 21C6.67157 21 6 20.3284 6 19.5C6 18.6716 6.67157 18 7.5 18Z"
stroke="#FFFFFF"
strokeWidth="1.5"
/>
<path
opacity="0.5"
d="M16.5 18C17.3284 18 18 18.6715 18 19.5C18 20.3284 17.3284 21 16.5 21C15.6716 21 15 20.3284 15 19.5C15 18.6715 15.6716 18 16.5 18Z"
stroke="#FFFFFF"
strokeWidth="1.5"
/>
<path
opacity="0.5"
d="M11 9H8"
stroke="#FFFFFF"
strokeWidth="1.5"
strokeLinecap="round"
/>
<path
d="M5 6H16.4504C18.5054 6 19.5328 6 19.9775 6.67426C20.4221 7.34853 20.0173 8.29294 19.2078 10.1818L18.7792 11.1818C18.4013 12.0636 18.2123 12.5045 17.8366 12.7523C17.4609 13 16.9812 13 16.0218 13H5"
stroke="#FFFFFF"
strokeWidth="1.5"
/>
</svg>
);
}

View File

@@ -1,11 +1,114 @@
import React from 'react'
import React, { useState } from "react";
import {
CartBagIcon,
ExpandIcon,
IndicationArrow,
SimulationStatusIcon,
} from "../../icons/SimulationIcons";
import { usePlayButtonStore } from "../../../store/usePlayButtonStore";
const AssetDetailsCard:React.FC = () => {
return (
<div className="asset-details-card-wrapper">
</div>
)
interface AssetDetailsCardInterface {
name: string;
status: string;
count?: number;
totalCapacity?: number;
assetDetails?: {
assetName: string;
const: string;
performance: string;
};
}
export default AssetDetailsCard
const GetStatus = (status: string) => {
// "idle" | "running" | "stopped" | "disabled" | "error"
switch (status) {
case "idle":
return (
<div className="status">
<div className="icon">
<SimulationStatusIcon color="#FFD321" />
</div>
<div className="value">Idle</div>
</div>
);
case "running":
return (
<div className="status">
<div className="icon">
<SimulationStatusIcon color="#21FF59" />
</div>
<div className="value">Running</div>
</div>
);
case "stopped":
return (
<div className="status">
<div className="icon">
<SimulationStatusIcon color="#FF1010" />
</div>
<div className="value">Stopped</div>
</div>
);
}
};
const AssetDetailsCard: React.FC<AssetDetailsCardInterface> = ({
name,
status,
count,
totalCapacity,
assetDetails,
}) => {
const [moreDetails, setMoreDetails] = useState(false);
// hooks
const { isPlaying } = usePlayButtonStore();
return (
<div
className="asset-details-card-wrapper"
style={{ display: isPlaying ? "" : "none" }}
>
<div className="asset-details-card-container">
<div className="asset-details-header">
<div className="content">
<div className="name">{name}</div>
<div className="status-container">{GetStatus(status)}</div>
</div>
<button
className="expand-button"
onClick={() => {
setMoreDetails(!moreDetails);
}}
>
<ExpandIcon />
</button>
</div>
{totalCapacity && (
<div className="count-ui-wrapper">
<div className="count-ui-container">
<div className="icon">
<CartBagIcon />
</div>
<div className="value">{count?.toString()}</div>
</div>
</div>
)}
{status === "running" && (
<div className={`process-running-container ${status}`}>
<div
className="process-running"
style={{ "--process-color": "#21FF59" } as React.CSSProperties}
></div>
</div>
)}
<div className="indication-arrow">
<IndicationArrow />
</div>
</div>
</div>
);
};
export default AssetDetailsCard;

View File

@@ -1,16 +1,40 @@
import RoboticArmInstance from './armInstance/roboticArmInstance';
import { useArmBotStore } from '../../../../store/simulation/useArmBotStore';
import RoboticArmInstance from "./armInstance/roboticArmInstance";
import { useArmBotStore } from "../../../../store/simulation/useArmBotStore";
import { Html } from "@react-three/drei";
import AssetDetailsCard from "../../../../components/ui/simulation/AssetDetailsCard";
import { Vector3 } from "three";
function RoboticArmInstances() {
const { armBots } = useArmBotStore();
const { armBots } = useArmBotStore();
return (
return (
<>
{armBots?.map((robot: ArmBotStatus) => (
<>
{armBots?.map((robot: ArmBotStatus) => (
<RoboticArmInstance key={robot.modelUuid} armBot={robot} />
))}
<RoboticArmInstance key={robot.modelUuid} armBot={robot} />
<Html
// data
position={
new Vector3(
robot.position[0],
robot.point.position[1],
robot.position[2]
)
}
// class none
// other
zIndexRange={[1, 0]}
prepend
sprite
center
distanceFactor={20}
>
<AssetDetailsCard name={robot.modelName} status={robot.state} />
</Html>
</>
)
))}
</>
);
}
export default RoboticArmInstances;

View File

@@ -2,19 +2,15 @@ import { useEffect, useState } from "react";
import { useArmBotStore } from "../../../store/simulation/useArmBotStore";
import { useSelectedEventSphere } from "../../../store/simulation/useSimulationStore";
import { usePlayButtonStore } from "../../../store/usePlayButtonStore";
import ArmBotUI from "../ui/arm/armBotUI";
import RoboticArmInstances from "./instances/roboticArmInstances";
import ArmBotUI from "../spatialUI/arm/armBotUI";
function RoboticArm() {
const { armBots, getArmBotById } = useArmBotStore();
const { getArmBotById } = useArmBotStore();
const { selectedEventSphere } = useSelectedEventSphere();
const { isPlaying } = usePlayButtonStore();
const [isArmBotSelected, setIsArmBotSelected] = useState(false);
useEffect(() => {
// console.log('armBots: ', armBots);
}, [armBots])
useEffect(() => {
if (selectedEventSphere) {
const selectedArmBot = getArmBotById(selectedEventSphere.userData.modelUuid);
@@ -24,7 +20,7 @@ function RoboticArm() {
setIsArmBotSelected(false);
}
}
}, [selectedEventSphere])
}, [getArmBotById, selectedEventSphere])
return (
<>

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import { useSelectedAction, useSelectedEventData, useSelectedEventSphere, useSelectedProduct } from '../../../../store/simulation/useSimulationStore';
import { useSelectedAction, useSelectedEventSphere, useSelectedProduct } from '../../../../store/simulation/useSimulationStore';
import { useGLTF } from '@react-three/drei';
import { useThree } from '@react-three/fiber';
import { useProductStore } from '../../../../store/simulation/useProductStore';

View File

@@ -0,0 +1,409 @@
import { useEffect, useRef, useState } from "react";
import * as Types from "../../../../types/world/worldTypes";
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 { useFrame, useThree } from "@react-three/fiber";
import {
useSelectedEventSphere,
useIsDragging,
useSelectedProduct,
useIsRotating,
} from "../../../../store/simulation/useSimulationStore";
import { useVehicleStore } from "../../../../store/simulation/useVehicleStore";
import { useProductStore } from "../../../../store/simulation/useProductStore";
import { upsertProductOrEventApi } from "../../../../services/simulation/UpsertProductOrEventApi";
import { DoubleSide, Group, Plane, Vector3 } from "three";
const VehicleUI = () => {
const { scene: startScene } = useGLTF(startPoint) as any;
const { scene: endScene } = useGLTF(startEnd) as any;
const startMarker = useRef<Group>(null);
const endMarker = useRef<Group>(null);
const prevMousePos = useRef({ x: 0, y: 0 });
const { selectedEventSphere } = useSelectedEventSphere();
const { selectedProduct } = useSelectedProduct();
const { getVehicleById } = useVehicleStore();
const { updateEvent } = useProductStore();
const [startPosition, setStartPosition] = useState<[number, number, number]>([
0, 1, 0,
]);
const [endPosition, setEndPosition] = useState<[number, number, number]>([
0, 1, 0,
]);
const [startRotation, setStartRotation] = useState<[number, number, number]>([
0, 0, 0,
]);
const [endRotation, setEndRotation] = useState<[number, number, number]>([
0, 0, 0,
]);
const [steeringRotation, setSteeringRotation] = useState<
[number, number, number]
>([0, 0, 0]);
const { isDragging, setIsDragging } = useIsDragging();
const { isRotating, setIsRotating } = useIsRotating();
const { raycaster } = useThree();
const [point, setPoint] = useState<[number, number, number]>([0, 0, 0]);
const plane = useRef(new Plane(new Vector3(0, 1, 0), 0));
const [tubeRotation, setTubeRotation] = useState<boolean>(false);
const tubeRef = useRef<Group>(null);
const outerGroup = useRef<Group>(null);
const state: Types.ThreeState = useThree();
const controls: any = state.controls;
const [selectedVehicleData, setSelectedVechicleData] = useState<{
position: [number, number, number];
rotation: [number, number, number];
}>({ position: [0, 0, 0], rotation: [0, 0, 0] });
const CIRCLE_RADIUS = 0.8;
const email = localStorage.getItem("email");
const organization = email!.split("@")[1].split(".")[0];
const updateBackend = (
productName: string,
productId: string,
organization: string,
eventData: EventsSchema
) => {
upsertProductOrEventApi({
productName: productName,
productId: productId,
organization: organization,
eventDatas: eventData,
});
};
useEffect(() => {
if (!selectedEventSphere) return;
const selectedVehicle = getVehicleById(
selectedEventSphere.userData.modelUuid
);
if (selectedVehicle) {
setSelectedVechicleData({
position: selectedVehicle.position,
rotation: selectedVehicle.rotation,
});
setPoint(selectedVehicle.point.position);
}
setTimeout(() => {
if (selectedVehicle?.point?.action) {
const { pickUpPoint, unLoadPoint, steeringAngle } =
selectedVehicle.point.action;
if (pickUpPoint && outerGroup.current) {
const worldPos = new Vector3(
pickUpPoint.position.x,
pickUpPoint.position.y,
pickUpPoint.position.z
);
const localPosition = outerGroup.current.worldToLocal(
worldPos.clone()
);
setStartPosition([
localPosition.x,
selectedVehicle.point.position[1],
localPosition.z,
]);
setStartRotation([
pickUpPoint.rotation.x,
pickUpPoint.rotation.y,
pickUpPoint.rotation.z,
]);
} else {
setStartPosition([0, selectedVehicle.point.position[1] + 0.1, 1.5]);
setStartRotation([0, 0, 0]);
}
// end point
if (unLoadPoint && outerGroup.current) {
const worldPos = new Vector3(
unLoadPoint.position.x,
unLoadPoint.position.y,
unLoadPoint.position.z
);
const localPosition = outerGroup.current.worldToLocal(worldPos);
setEndPosition([
localPosition.x,
selectedVehicle.point.position[1],
localPosition.z,
]);
setEndRotation([
unLoadPoint.rotation.x,
unLoadPoint.rotation.y,
unLoadPoint.rotation.z,
]);
} else {
setEndPosition([0, selectedVehicle.point.position[1] + 0.1, -1.5]);
setEndRotation([0, 0, 0]);
}
setSteeringRotation([0, steeringAngle, 0]);
}
}, 10);
}, [selectedEventSphere, outerGroup.current]);
const handlePointerDown = (
e: any,
state: "start" | "end",
rotation: "start" | "end"
) => {
if (e.object.name === "handle") {
const normalizedX = (e.clientX / window.innerWidth) * 2 - 1;
const normalizedY = -(e.clientY / window.innerHeight) * 2 + 1;
prevMousePos.current = { x: normalizedX, y: normalizedY };
setIsRotating(rotation);
if (controls) controls.enabled = false;
setIsDragging(null);
} else {
setIsDragging(state);
setIsRotating(null);
if (controls) controls.enabled = false;
}
};
const handlePointerUp = () => {
controls.enabled = true;
setIsDragging(null);
setIsRotating(null);
if (selectedEventSphere?.userData.modelUuid) {
const updatedVehicle = getVehicleById(
selectedEventSphere.userData.modelUuid
);
let globalStartPosition = null;
let globalEndPosition = null;
if (outerGroup.current && startMarker.current && endMarker.current) {
const worldPosStart = new Vector3(...startPosition);
globalStartPosition = outerGroup.current.localToWorld(
worldPosStart.clone()
);
const worldPosEnd = new Vector3(...endPosition);
globalEndPosition = outerGroup.current.localToWorld(
worldPosEnd.clone()
);
}
if (updatedVehicle && globalEndPosition && globalStartPosition) {
const event = updateEvent(
selectedProduct.productId,
selectedEventSphere.userData.modelUuid,
{
point: {
...updatedVehicle.point,
action: {
...updatedVehicle.point?.action,
pickUpPoint: {
position: {
x: globalStartPosition.x,
y: 0,
z: globalStartPosition.z,
},
rotation: { x: 0, y: startRotation[1], z: 0 },
},
unLoadPoint: {
position: {
x: globalEndPosition.x,
y: 0,
z: globalEndPosition.z,
},
rotation: { x: 0, y: endRotation[1], z: 0 },
},
steeringAngle: steeringRotation[1],
},
},
}
);
if (event) {
updateBackend(
selectedProduct.productName,
selectedProduct.productId,
organization,
event
);
}
}
}
};
useFrame(() => {
if (!isDragging || !plane.current || !raycaster || !outerGroup.current)
return;
const intersectPoint = new Vector3();
const intersects = raycaster.ray.intersectPlane(
plane.current,
intersectPoint
);
if (!intersects) return;
const localPoint = outerGroup?.current.worldToLocal(intersectPoint.clone());
if (isDragging === "start") {
setStartPosition([localPoint.x, point[1], localPoint.z]);
} else if (isDragging === "end") {
setEndPosition([localPoint.x, point[1], localPoint.z]);
}
});
useEffect(() => {
const handleGlobalPointerUp = () => {
setIsDragging(null);
setIsRotating(null);
setTubeRotation(false);
if (controls) controls.enabled = true;
handlePointerUp();
};
if (isDragging || isRotating || tubeRotation) {
window.addEventListener("pointerup", handleGlobalPointerUp);
}
return () => {
window.removeEventListener("pointerup", handleGlobalPointerUp);
};
}, [
isDragging,
isRotating,
startPosition,
startRotation,
endPosition,
endRotation,
tubeRotation,
steeringRotation,
outerGroup.current,
tubeRef.current,
]);
const prevSteeringY = useRef(0);
useFrame((state) => {
if (tubeRotation) {
const currentPointerX = state.pointer.x;
const deltaX = currentPointerX - prevMousePos.current.x;
prevMousePos.current.x = currentPointerX;
const marker = tubeRef.current;
if (marker) {
const rotationSpeed = 2;
marker.rotation.y += deltaX * rotationSpeed;
setSteeringRotation([
marker.rotation.x,
marker.rotation.y,
marker.rotation.z,
]);
}
} else {
prevSteeringY.current = 0;
}
});
useFrame((state) => {
if (!isRotating) return;
const currentPointerX = state.pointer.x;
const deltaX = currentPointerX - prevMousePos.current.x;
prevMousePos.current.x = currentPointerX;
const marker =
isRotating === "start" ? startMarker.current : endMarker.current;
if (marker) {
const rotationSpeed = 10;
marker.rotation.y += deltaX * rotationSpeed;
if (isRotating === "start") {
setStartRotation([
marker.rotation.x,
marker.rotation.y,
marker.rotation.z,
]);
} else {
setEndRotation([
marker.rotation.x,
marker.rotation.y,
marker.rotation.z,
]);
}
}
});
return selectedVehicleData ? (
<group
position={selectedVehicleData.position}
rotation={selectedVehicleData.rotation}
ref={outerGroup}
>
<group
position={[0, 0, 0]}
ref={tubeRef}
rotation={steeringRotation}
onPointerDown={(e) => {
e.stopPropagation();
setTubeRotation(true);
prevMousePos.current.x = e.pointer.x;
controls.enabled = false;
}}
onPointerMissed={() => {
controls.enabled = true;
setTubeRotation(false);
}}
onPointerUp={() => {
controls.enabled = true;
setTubeRotation(false);
}}
>
(
<mesh
position={[0, point[1], 0]}
rotation={[-Math.PI / 2, 0, 0]}
name="steering"
>
<ringGeometry args={[CIRCLE_RADIUS, CIRCLE_RADIUS + 0.2, 36]} />
<meshBasicMaterial color="yellow" side={DoubleSide} />
</mesh>
<mesh
position={[0, point[1], CIRCLE_RADIUS + 0.24]}
rotation={[Math.PI / 2, 0, 0]}
>
<coneGeometry args={[0.1, 0.3, 12]} />
<meshBasicMaterial color="yellow" side={DoubleSide} />
</mesh>
)
</group>
{/* Start Marker */}
<primitive
name="startMarker"
object={startScene}
ref={startMarker}
position={startPosition}
rotation={startRotation}
onPointerDown={(e: any) => {
e.stopPropagation();
handlePointerDown(e, "start", "start");
}}
onPointerMissed={() => {
controls.enabled = true;
setIsDragging(null);
setIsRotating(null);
}}
/>
{/* End Marker */}
<primitive
name="endMarker"
object={endScene}
ref={endMarker}
position={endPosition}
rotation={endRotation}
onPointerDown={(e: any) => {
e.stopPropagation();
handlePointerDown(e, "end", "end");
}}
onPointerMissed={() => {
controls.enabled = true;
setIsDragging(null);
setIsRotating(null);
}}
/>
</group>
) : null;
};
export default VehicleUI;

View File

@@ -1,372 +0,0 @@
import React, { useEffect, useRef, useState } from "react";
import * as Types from "../../../../types/world/worldTypes";
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 { useFrame, useThree } from "@react-three/fiber";
import {
useSelectedEventSphere,
useIsDragging,
useIsRotating,
} from "../../../../store/simulation/useSimulationStore";
import { useVehicleStore } from "../../../../store/simulation/useVehicleStore";
import { useProductStore } from "../../../../store/simulation/useProductStore";
import { useSelectedProduct } from "../../../../store/simulation/useSimulationStore";
import { upsertProductOrEventApi } from "../../../../services/simulation/UpsertProductOrEventApi";
import { Box3, DoubleSide, Euler, Group, Mesh, Plane, Quaternion, Vector3 } from "three";
import { position } from "html2canvas/dist/types/css/property-descriptors/position";
const VehicleUI = () => {
const { scene: startScene } = useGLTF(startPoint) as any;
const { scene: endScene } = useGLTF(startEnd) as any;
const startMarker = useRef<Group>(null);
const endMarker = useRef<Group>(null);
const prevMousePos = useRef({ x: 0, y: 0 });
const { selectedEventSphere } = useSelectedEventSphere();
const { selectedProduct } = useSelectedProduct();
const { getVehicleById } = useVehicleStore();
const { updateEvent } = useProductStore();
const [startPosition, setStartPosition] = useState<[number, number, number]>([
0, 1, 0,
]);
const [endPosition, setEndPosition] = useState<[number, number, number]>([
0, 1, 0,
]);
const [startRotation, setStartRotation] = useState<[number, number, number]>([
0, 0, 0,
]);
const [endRotation, setEndRotation] = useState<[number, number, number]>([
0, 0, 0,
]);
const [steeringRotation, setSteeringRotation] = useState<
[number, number, number]
>([0, 0, 0]);
const { isDragging, setIsDragging } = useIsDragging();
const { isRotating, setIsRotating } = useIsRotating();
const { raycaster } = useThree();
const [point, setPoint] = useState<
[number, number, number]
>([0, 0, 0]);
const plane = useRef(new Plane(new Vector3(0, 1, 0), 0));
const [tubeRotation, setTubeRotation] = useState<boolean>(false);
const tubeRef = useRef<Group>(null);
const outerGroup = useRef<Group>(null);
const state: Types.ThreeState = useThree();
const controls: any = state.controls;
const [selectedVehicleData, setSelectedVechicleData] = useState<
{ position: [number, number, number], rotation: [number, number, number], }
>({ position: [0, 0, 0], rotation: [0, 0, 0], });
const CIRCLE_RADIUS = 0.8;
const email = localStorage.getItem("email");
const organization = email!.split("@")[1].split(".")[0];
const updateBackend = (
productName: string,
productId: string,
organization: string,
eventData: EventsSchema
) => {
upsertProductOrEventApi({
productName: productName,
productId: productId,
organization: organization,
eventDatas: eventData,
});
};
useEffect(() => {
if (!selectedEventSphere) return;
const selectedVehicle = getVehicleById(
selectedEventSphere.userData.modelUuid
);
if (selectedVehicle) {
setSelectedVechicleData({ position: selectedVehicle.position, rotation: selectedVehicle.rotation });
setPoint(selectedVehicle.point.position)
}
setTimeout(() => {
if (selectedVehicle?.point?.action) {
const { pickUpPoint, unLoadPoint, steeringAngle } = selectedVehicle.point.action;
if (pickUpPoint && outerGroup.current) {
const worldPos = new Vector3(
pickUpPoint.position.x,
pickUpPoint.position.y,
pickUpPoint.position.z
);
const localPosition = outerGroup.current.worldToLocal(worldPos.clone());
setStartPosition([localPosition.x, selectedVehicle.point.position[1], localPosition.z,
]);
setStartRotation([pickUpPoint.rotation.x, pickUpPoint.rotation.y, pickUpPoint.rotation.z,])
} else {
setStartPosition([0, selectedVehicle.point.position[1] + 0.1, 1.5]);
setStartRotation([0, 0, 0]);
}
// end point
if (unLoadPoint && outerGroup.current) {
const worldPos = new Vector3(unLoadPoint.position.x, unLoadPoint.position.y, unLoadPoint.position.z);
const localPosition = outerGroup.current.worldToLocal(worldPos);
setEndPosition([localPosition.x, selectedVehicle.point.position[1], localPosition.z]);
setEndRotation([unLoadPoint.rotation.x, unLoadPoint.rotation.y, unLoadPoint.rotation.z,
]);
} else {
setEndPosition([0, selectedVehicle.point.position[1] + 0.1, -1.5]);
setEndRotation([0, 0, 0]);
}
setSteeringRotation([0, steeringAngle, 0])
}
}, 10);
}, [selectedEventSphere, outerGroup.current]);
const handlePointerDown = (
e: any,
state: "start" | "end",
rotation: "start" | "end"
) => {
if (e.object.name === "handle") {
const normalizedX = (e.clientX / window.innerWidth) * 2 - 1;
const normalizedY = -(e.clientY / window.innerHeight) * 2 + 1;
prevMousePos.current = { x: normalizedX, y: normalizedY };
setIsRotating(rotation);
if (controls) controls.enabled = false;
setIsDragging(null);
} else {
setIsDragging(state);
setIsRotating(null);
if (controls) controls.enabled = false;
}
};
const handlePointerUp = () => {
controls.enabled = true;
setIsDragging(null);
setIsRotating(null);
if (selectedEventSphere?.userData.modelUuid) {
const updatedVehicle = getVehicleById(
selectedEventSphere.userData.modelUuid
);
let globalStartPosition = null;
let globalEndPosition = null;
if (outerGroup.current && startMarker.current && endMarker.current) {
const worldPosStart = new Vector3(...startPosition);
globalStartPosition = outerGroup.current.localToWorld(worldPosStart.clone());
const worldPosEnd = new Vector3(...endPosition);
globalEndPosition = outerGroup.current.localToWorld(worldPosEnd.clone());
}
if (updatedVehicle && globalEndPosition && globalStartPosition) {
const event = updateEvent(
selectedProduct.productId,
selectedEventSphere.userData.modelUuid,
{
point: {
...updatedVehicle.point,
action: {
...updatedVehicle.point?.action,
pickUpPoint: {
position: {
x: globalStartPosition.x,
y: 0,
z: globalStartPosition.z,
},
rotation: { x: 0, y: startRotation[1], z: 0 },
},
unLoadPoint: {
position: {
x: globalEndPosition.x,
y: 0,
z: globalEndPosition.z,
},
rotation: { x: 0, y: endRotation[1], z: 0 },
},
steeringAngle: steeringRotation[1]
},
},
}
);
if (event) {
updateBackend(
selectedProduct.productName,
selectedProduct.productId,
organization,
event
);
}
}
}
};
useFrame(() => {
if (!isDragging || !plane.current || !raycaster || !outerGroup.current) return;
const intersectPoint = new Vector3();
const intersects = raycaster.ray.intersectPlane(plane.current, intersectPoint);
if (!intersects) return;
const localPoint = outerGroup?.current.worldToLocal(intersectPoint.clone());
if (isDragging === "start") {
if (startMarker.current) {
}
setStartPosition([localPoint.x, point[1], localPoint.z]);
} else if (isDragging === "end") {
setEndPosition([localPoint.x, point[1], localPoint.z]);
}
});
useEffect(() => {
const handleGlobalPointerUp = () => {
setIsDragging(null);
setIsRotating(null);
setTubeRotation(false);
if (controls) controls.enabled = true;
handlePointerUp();
};
if (isDragging || isRotating || tubeRotation) {
window.addEventListener("pointerup", handleGlobalPointerUp);
}
return () => {
window.removeEventListener("pointerup", handleGlobalPointerUp);
};
}, [
isDragging, isRotating, startPosition, startRotation, endPosition, endRotation, tubeRotation, steeringRotation, outerGroup.current, tubeRef.current
]);
const prevSteeringY = useRef(0);
useFrame((state) => {
if (tubeRotation) {
const currentPointerX = state.pointer.x;
const deltaX = currentPointerX - prevMousePos.current.x;
prevMousePos.current.x = currentPointerX;
const marker = tubeRef.current;
if (marker) {
const rotationSpeed = 2;
marker.rotation.y += deltaX * rotationSpeed;
setSteeringRotation([marker.rotation.x, marker.rotation.y, marker.rotation.z]);
}
} else {
prevSteeringY.current = 0;
}
});
useFrame((state) => {
if (!isRotating) return;
const currentPointerX = state.pointer.x;
const deltaX = currentPointerX - prevMousePos.current.x;
prevMousePos.current.x = currentPointerX;
const marker =
isRotating === "start" ? startMarker.current : endMarker.current;
if (marker) {
const rotationSpeed = 10;
marker.rotation.y += deltaX * rotationSpeed;
if (isRotating === "start") {
setStartRotation([
marker.rotation.x,
marker.rotation.y,
marker.rotation.z,
]);
} else {
setEndRotation([
marker.rotation.x,
marker.rotation.y,
marker.rotation.z,
]);
}
}
});
return selectedVehicleData ? (
<group position={selectedVehicleData.position} rotation={selectedVehicleData.rotation} ref={outerGroup}>
<group
position={[0, 0, 0]}
ref={tubeRef}
rotation={steeringRotation}
onPointerDown={(e) => {
e.stopPropagation();
setTubeRotation(true);
prevMousePos.current.x = e.pointer.x;
controls.enabled = false;
}}
onPointerMissed={() => {
controls.enabled = true;
setTubeRotation(false);
}}
onPointerUp={() => {
controls.enabled = true;
setTubeRotation(false);
}}
>
(
<mesh
position={[0, point[1], 0]}
rotation={[-Math.PI / 2, 0, 0]}
name="steering"
>
<ringGeometry args={[CIRCLE_RADIUS, CIRCLE_RADIUS + 0.2, 36]} />
<meshBasicMaterial color="yellow" side={DoubleSide} />
</mesh>
<mesh position={[0, point[1], CIRCLE_RADIUS + 0.24]} rotation={[Math.PI / 2, 0, 0]}>
<coneGeometry args={[0.1, 0.3, 12]} />
<meshBasicMaterial color="yellow" side={DoubleSide} />
</mesh>
)
</group>
{/* Start Marker */}
<primitive
name="startMarker"
object={startScene}
ref={startMarker}
position={startPosition}
rotation={startRotation}
onPointerDown={(e: any) => {
e.stopPropagation();
handlePointerDown(e, "start", "start");
}}
onPointerMissed={() => {
controls.enabled = true;
setIsDragging(null);
setIsRotating(null);
}}
/>
{/* End Marker */}
<primitive
name="endMarker"
object={endScene}
ref={endMarker}
position={endPosition}
rotation={endRotation}
onPointerDown={(e: any) => {
e.stopPropagation();
handlePointerDown(e, "end", "end");
}}
onPointerMissed={() => {
controls.enabled = true;
setIsDragging(null);
setIsRotating(null);
}}
/>
</group>
) : null;
};
export default VehicleUI;

View File

@@ -1,20 +1,46 @@
import React from 'react'
import VehicleInstance from './instance/vehicleInstance'
import { useVehicleStore } from '../../../../store/simulation/useVehicleStore'
import React from "react";
import VehicleInstance from "./instance/vehicleInstance";
import { useVehicleStore } from "../../../../store/simulation/useVehicleStore";
import { Html } from "@react-three/drei";
import { Vector3 } from "three";
import AssetDetailsCard from "../../../../components/ui/simulation/AssetDetailsCard";
function VehicleInstances() {
const { vehicles } = useVehicleStore();
const { vehicles } = useVehicleStore();
return (
return (
<>
{vehicles.map((vehicle: VehicleStatus) => (
<>
{vehicles.map((vehicle: VehicleStatus) =>
<VehicleInstance agvDetail={vehicle} key={vehicle.modelUuid} />
)}
<VehicleInstance agvDetail={vehicle} key={vehicle.modelUuid} />
<Html
// data
position={
new Vector3(
vehicle.position[0],
vehicle.point.position[1],
vehicle.position[2]
)
}
// class none
// other
zIndexRange={[1, 0]}
prepend
sprite
center
distanceFactor={20}
>
<AssetDetailsCard
name={vehicle.modelName}
status={vehicle.state}
count={vehicle.currentLoad}
totalCapacity={vehicle.point.action.loadCapacity}
/>
</Html>
</>
)
))}
</>
);
}
export default VehicleInstances
export default VehicleInstances;

View File

@@ -2,19 +2,15 @@ import { useEffect, useState } from "react";
import { useVehicleStore } from "../../../store/simulation/useVehicleStore";
import { useSelectedEventSphere } from "../../../store/simulation/useSimulationStore";
import { usePlayButtonStore } from "../../../store/usePlayButtonStore";
import VehicleUI from "../ui/vehicle/vehicleUI";
import VehicleInstances from "./instances/vehicleInstances";
import VehicleUI from "../spatialUI/vehicle/vehicleUI";
function Vehicles() {
const { vehicles, getVehicleById } = useVehicleStore();
const { getVehicleById } = useVehicleStore();
const { selectedEventSphere } = useSelectedEventSphere();
const { isPlaying } = usePlayButtonStore();
const [isVehicleSelected, setIsVehicleSelected] = useState(false);
useEffect(() => {
// console.log('vehicles: ', vehicles);
}, [vehicles])
useEffect(() => {
if (selectedEventSphere) {
const selectedVehicle = getVehicleById(selectedEventSphere.userData.modelUuid);
@@ -24,7 +20,7 @@ function Vehicles() {
setIsVehicleSelected(false);
}
}
}, [selectedEventSphere])
}, [getVehicleById, selectedEventSphere])
return (
<>

View File

@@ -315,7 +315,6 @@
}
.open {
.start-displayer,
.end-displayer {
display: none;
@@ -402,3 +401,137 @@
}
}
}
.asset-details-card-wrapper {
pointer-events: none;
.asset-details-card-container {
position: relative;
padding: 8px;
background: var(--background-color);
backdrop-filter: blur(6px);
border-radius: #{$border-radius-large};
transform: translate(0, -100%);
z-index: 0;
box-shadow: inset 0px 10px 50px #80808075;
min-width: 124px;
&::before {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
background: linear-gradient(135deg, var(--accent-color), #ff00f000);
background-size: 400% 400%;
animation: borderAnimation 5s linear infinite;
-webkit-mask: linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
z-index: -1;
padding: 1px;
}
.asset-details-header {
@include flex-space-between;
gap: 12px;
.content {
.name {
text-wrap: nowrap;
max-width: 140px;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 4px;
text-transform: capitalize;
}
.status-container {
.status {
display: flex;
align-items: center;
gap: 6px;
.icon {
@include flex-center;
}
}
}
}
}
.process-running-container {
width: 100%;
height: 8px;
background: var(--background-color-solid);
margin-top: 12px;
border-radius: #{$border-radius-small};
overflow: hidden;
position: relative;
.process-running {
height: 100%;
width: 35%;
border-radius: #{$border-radius-small};
background: var(--process-color);
animation: playing-process 1s ease-in-out infinite;
}
}
.indication-arrow {
position: absolute;
left: 50%;
bottom: 0;
transform: translate(-50%, 10px);
filter: drop-shadow(0px 0px 4px #ffffff);
}
.count-ui-wrapper {
position: absolute;
right: -42px;
top: 5px;
padding: 4px;
padding-right: 8px;
.count-ui-container {
@include flex-center;
gap: 6px;
.icon{
@include flex-center;
padding: 3px;
border-radius: #{$border-radius-circle};
background: var(--background-color-accent);
svg {
scale: 0.6;
}
}
.value{
position: absolute;
width: 48px;
background: var(--background-color-solid-gradient);
border-radius: #{$border-radius-large};
outline: 1px solid var(--border-color);
padding: 4px 10px;
padding-left: 16px;
transform: translateX(28px);
z-index: -1;
}
}
}
}
}
@keyframes playing-process {
from {
transform: translateX(-100%);
}
to {
transform: translateX(300%);
}
}
@keyframes borderAnimation {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}

View File

@@ -962,8 +962,6 @@
}
}
.widget-placeholder {
background-color: gray;
border-radius: 6px;
@@ -971,7 +969,3 @@
justify-content: center;
align-items: center;
}
.dragging {
display: none;
}