first commit
This commit is contained in:
61
app/src/modules/simulation/spatialUI/arm/PickDropPoints.tsx
Normal file
61
app/src/modules/simulation/spatialUI/arm/PickDropPoints.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import React, { useRef } from "react";
|
||||
import * as THREE from "three";
|
||||
import { ThreeEvent } from "@react-three/fiber";
|
||||
|
||||
interface PickDropProps {
|
||||
position: number[];
|
||||
modelUuid: string;
|
||||
pointUuid: string;
|
||||
actionType: "pick" | "drop";
|
||||
actionUuid: string;
|
||||
gltfScene: THREE.Group;
|
||||
|
||||
handlePointerDown: (e: ThreeEvent<PointerEvent>) => void;
|
||||
isSelected: boolean;
|
||||
}
|
||||
|
||||
const PickDropPoints: React.FC<PickDropProps> = ({
|
||||
position,
|
||||
modelUuid,
|
||||
pointUuid,
|
||||
actionType,
|
||||
actionUuid,
|
||||
gltfScene,
|
||||
handlePointerDown,
|
||||
isSelected,
|
||||
}) => {
|
||||
const groupRef = useRef<THREE.Group>(null);
|
||||
return (
|
||||
<group
|
||||
ref={groupRef}
|
||||
position={
|
||||
Array.isArray(position) && position.length === 3
|
||||
? new THREE.Vector3(...position)
|
||||
: new THREE.Vector3(0, 0, 0)
|
||||
}
|
||||
onPointerDown={(e) => {
|
||||
|
||||
e.stopPropagation(); // Prevent event bubbling
|
||||
if (!isSelected) return;
|
||||
handlePointerDown(e);
|
||||
}}
|
||||
userData={{ modelUuid, pointUuid, actionType, actionUuid }}
|
||||
>
|
||||
<primitive
|
||||
object={(() => {
|
||||
const cloned = gltfScene.clone();
|
||||
cloned.traverse((child: any) => {
|
||||
if (child.isMesh) {
|
||||
child.userData = { modelUuid, pointUuid, actionType, actionUuid };
|
||||
}
|
||||
});
|
||||
return cloned;
|
||||
})()}
|
||||
position={[0, 0, 0]}
|
||||
scale={[0.5, 0.5, 0.5]}
|
||||
/>
|
||||
</group>
|
||||
);
|
||||
};
|
||||
|
||||
export default PickDropPoints;
|
||||
222
app/src/modules/simulation/spatialUI/arm/armBotUI.tsx
Normal file
222
app/src/modules/simulation/spatialUI/arm/armBotUI.tsx
Normal file
@@ -0,0 +1,222 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useSelectedAction, useSelectedEventSphere } from '../../../../store/simulation/useSimulationStore';
|
||||
import { useGLTF } from '@react-three/drei';
|
||||
import { useThree } from '@react-three/fiber';
|
||||
import { useProductStore } from '../../../../store/simulation/useProductStore';
|
||||
import PickDropPoints from './PickDropPoints';
|
||||
import useDraggableGLTF from './useDraggableGLTF';
|
||||
import * as THREE from 'three';
|
||||
|
||||
import armPick from "../../../../assets/gltf-glb/ui/arm_ui_pick.glb";
|
||||
import armDrop from "../../../../assets/gltf-glb/ui/arm_ui_drop.glb";
|
||||
import { upsertProductOrEventApi } from '../../../../services/simulation/products/UpsertProductOrEventApi';
|
||||
import { useSceneContext } from '../../../scene/sceneContext';
|
||||
import { useProductContext } from '../../products/productContext';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
type Positions = {
|
||||
pick: [number, number, number];
|
||||
drop: [number, number, number];
|
||||
default: [number, number, number];
|
||||
};
|
||||
|
||||
const ArmBotUI = () => {
|
||||
const { getEventByModelUuid, updateAction, getActionByUuid } = useProductStore();
|
||||
const { selectedEventSphere } = useSelectedEventSphere();
|
||||
const { selectedProductStore } = useProductContext();
|
||||
const { selectedProduct } = selectedProductStore();
|
||||
const { scene } = useThree();
|
||||
const { selectedAction } = useSelectedAction();
|
||||
const { armBotStore } = useSceneContext();
|
||||
const { armBots } = armBotStore();
|
||||
const { projectId } = useParams();
|
||||
|
||||
const armUiPick = useGLTF(armPick) as any;
|
||||
const armUiDrop = useGLTF(armDrop) as any;
|
||||
|
||||
const [startPosition, setStartPosition] = useState<[number, number, number] | null>([0, 0, 0]);
|
||||
const [endPosition, setEndPosition] = useState<[number, number, number] | null>([0, 0, 0]);
|
||||
const [selectedArmBotData, setSelectedArmBotData] = useState<any>(null);
|
||||
|
||||
const updateBackend = (
|
||||
productName: string,
|
||||
productUuid: string,
|
||||
projectId: string,
|
||||
eventData: EventsSchema
|
||||
) => {
|
||||
upsertProductOrEventApi({
|
||||
productName: productName,
|
||||
productUuid: productUuid,
|
||||
projectId: projectId,
|
||||
eventDatas: eventData
|
||||
})
|
||||
}
|
||||
|
||||
// Fetch and setup selected ArmBot data
|
||||
useEffect(() => {
|
||||
if (selectedEventSphere) {
|
||||
const selectedArmBot = getEventByModelUuid(selectedProduct.productUuid, selectedEventSphere.userData.modelUuid);
|
||||
|
||||
if (selectedArmBot?.type === "roboticArm" && selectedAction.actionId) {
|
||||
setSelectedArmBotData(selectedArmBot);
|
||||
const defaultPositions = getDefaultPositions(selectedArmBot.modelUuid);
|
||||
const matchingAction = getActionByUuid(selectedProduct.productUuid, selectedAction.actionId);
|
||||
if (matchingAction && (matchingAction as RoboticArmPointSchema["actions"][0]).process) {
|
||||
const startPoint = (matchingAction as RoboticArmPointSchema["actions"][0]).process.startPoint;
|
||||
const pickPosition = (!startPoint || (Array.isArray(startPoint) && startPoint.every(v => v === 0)))
|
||||
? defaultPositions.pick
|
||||
: startPoint;
|
||||
|
||||
const endPoint = (matchingAction as RoboticArmPointSchema["actions"][0]).process.endPoint;
|
||||
const dropPosition = (!endPoint || (Array.isArray(endPoint) && endPoint.every(v => v === 0)))
|
||||
? defaultPositions.drop
|
||||
: endPoint;
|
||||
|
||||
setStartPosition(pickPosition);
|
||||
setEndPosition(dropPosition);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [armBots, selectedEventSphere, selectedProduct, getEventByModelUuid, selectedAction]);
|
||||
|
||||
function getDefaultPositions(modelUuid: string): Positions {
|
||||
const modelData = getEventByModelUuid(selectedProduct.productUuid, modelUuid);
|
||||
|
||||
if (modelData?.type === "roboticArm") {
|
||||
const baseX = modelData.point.position?.[0] || 0;
|
||||
const baseY = modelData.point.position?.[1] || 0;;
|
||||
const baseZ = modelData.point.position?.[2] || 0;
|
||||
return {
|
||||
pick: [baseX, baseY, baseZ + 0.5],
|
||||
drop: [baseX, baseY, baseZ - 0.5],
|
||||
default: [baseX, baseY, baseZ],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
pick: [0.5, 1.5, 0],
|
||||
drop: [-0.5, 1.5, 0],
|
||||
default: [0, 1.5, 0],
|
||||
};
|
||||
}
|
||||
|
||||
function getLocalPosition(parentUuid: string, worldPosArray: [number, number, number] | null): [number, number, number] | null {
|
||||
if (worldPosArray) {
|
||||
const worldPos = new THREE.Vector3(...worldPosArray);
|
||||
const parentObject = scene.getObjectByProperty('uuid', parentUuid);
|
||||
|
||||
if (parentObject) {
|
||||
const localPos = worldPos.clone();
|
||||
parentObject.worldToLocal(localPos);
|
||||
return [localPos.x, localPos.y, localPos.z];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const updatePointToState = (obj: THREE.Object3D) => {
|
||||
const { modelUuid, actionType, actionUuid } = obj.userData;
|
||||
const newPosition = new THREE.Vector3();
|
||||
obj.getWorldPosition(newPosition);
|
||||
const worldPositionArray = newPosition.toArray() as [number, number, number];
|
||||
|
||||
if (selectedEventSphere) {
|
||||
const selectedArmBot = getEventByModelUuid(selectedProduct.productUuid, selectedEventSphere.userData.modelUuid);
|
||||
|
||||
const armBot = selectedArmBot?.modelUuid === modelUuid ? selectedArmBot : null;
|
||||
if (!armBot) return;
|
||||
|
||||
if (armBot.type === "roboticArm") {
|
||||
armBot?.point?.actions?.map((action) => {
|
||||
if (action.actionUuid === actionUuid) {
|
||||
const updatedProcess = { ...action.process };
|
||||
|
||||
if (actionType === "pick") {
|
||||
updatedProcess.startPoint = getLocalPosition(modelUuid, worldPositionArray);
|
||||
setStartPosition(updatedProcess.startPoint)
|
||||
|
||||
} else if (actionType === "drop") {
|
||||
updatedProcess.endPoint = getLocalPosition(modelUuid, worldPositionArray);
|
||||
setEndPosition(updatedProcess.endPoint)
|
||||
}
|
||||
|
||||
const event = updateAction(selectedProduct.productUuid,
|
||||
actionUuid,
|
||||
{
|
||||
actionUuid: action.actionUuid,
|
||||
process: updatedProcess,
|
||||
}
|
||||
)
|
||||
|
||||
if (event) {
|
||||
updateBackend(
|
||||
selectedProduct.productName,
|
||||
selectedProduct.productUuid,
|
||||
projectId || '',
|
||||
event
|
||||
);
|
||||
}
|
||||
return {
|
||||
...action,
|
||||
process: updatedProcess,
|
||||
};
|
||||
}
|
||||
return action;
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { handlePointerDown } = useDraggableGLTF(updatePointToState);
|
||||
|
||||
if (!selectedArmBotData || !Array.isArray(selectedArmBotData.point?.actions)) {
|
||||
return null; // avoid rendering if no data yet
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{selectedArmBotData.point.actions.map((action: any) => {
|
||||
if (action.actionUuid === selectedAction.actionId) {
|
||||
return (
|
||||
<React.Fragment key={action.actionUuid}>
|
||||
<group
|
||||
position={new THREE.Vector3(...selectedArmBotData.position)}
|
||||
rotation={new THREE.Euler(...selectedArmBotData.rotation)}
|
||||
>
|
||||
{startPosition && endPosition && (
|
||||
<>
|
||||
<PickDropPoints
|
||||
position={startPosition}
|
||||
modelUuid={selectedArmBotData.modelUuid}
|
||||
pointUuid={selectedArmBotData.point.uuid}
|
||||
actionType="pick"
|
||||
actionUuid={action.actionUuid}
|
||||
gltfScene={armUiPick.scene}
|
||||
handlePointerDown={handlePointerDown}
|
||||
isSelected={true}
|
||||
/>
|
||||
<PickDropPoints
|
||||
position={endPosition}
|
||||
modelUuid={selectedArmBotData.modelUuid}
|
||||
pointUuid={selectedArmBotData.point.uuid}
|
||||
actionType="drop"
|
||||
actionUuid={action.actionUuid}
|
||||
gltfScene={armUiDrop.scene}
|
||||
handlePointerDown={handlePointerDown}
|
||||
isSelected={true}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</group>
|
||||
</React.Fragment>
|
||||
);
|
||||
} else {
|
||||
return null; // important! must return something
|
||||
}
|
||||
})}
|
||||
</>
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
export default ArmBotUI;
|
||||
200
app/src/modules/simulation/spatialUI/arm/useDraggableGLTF.ts
Normal file
200
app/src/modules/simulation/spatialUI/arm/useDraggableGLTF.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
import { useRef, useState } from "react";
|
||||
import * as THREE from "three";
|
||||
import { ThreeEvent, useThree } from "@react-three/fiber";
|
||||
import { useProductStore } from "../../../../store/simulation/useProductStore";
|
||||
import {
|
||||
useSelectedEventData,
|
||||
} from "../../../../store/simulation/useSimulationStore";
|
||||
import { useProductContext } from "../../products/productContext";
|
||||
|
||||
type OnUpdateCallback = (object: THREE.Object3D) => void;
|
||||
|
||||
export default function useDraggableGLTF(onUpdate: OnUpdateCallback) {
|
||||
const { getEventByModelUuid } = useProductStore();
|
||||
const { selectedEventData } = useSelectedEventData();
|
||||
const { selectedProductStore } = useProductContext();
|
||||
const { selectedProduct } = selectedProductStore();
|
||||
const { camera, gl, controls } = useThree();
|
||||
const activeObjRef = useRef<THREE.Object3D | null>(null);
|
||||
const planeRef = useRef<THREE.Plane>(new THREE.Plane(new THREE.Vector3(0, 1, 0), 0));
|
||||
const offsetRef = useRef<THREE.Vector3>(new THREE.Vector3());
|
||||
const initialPositionRef = useRef<THREE.Vector3>(new THREE.Vector3());
|
||||
|
||||
const raycaster = new THREE.Raycaster();
|
||||
const pointer = new THREE.Vector2();
|
||||
const [objectWorldPos, setObjectWorldPos] = useState(new THREE.Vector3());
|
||||
|
||||
const handlePointerDown = (e: ThreeEvent<PointerEvent>) => {
|
||||
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
|
||||
setObjectWorldPos(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;
|
||||
if (selectedEventData?.data.type === "roboticArm") {
|
||||
const selectedArmBot = getEventByModelUuid(
|
||||
selectedProduct.productUuid,
|
||||
selectedEventData.data.modelUuid
|
||||
);
|
||||
if (!selectedArmBot) 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);
|
||||
|
||||
// Get the parent's world matrix if exists
|
||||
const parent = activeObjRef.current.parent;
|
||||
const targetPosition = new THREE.Vector3();
|
||||
|
||||
// OnPointerDown
|
||||
initialPositionRef.current.copy(objectWorldPos);
|
||||
|
||||
// OnPointerMove
|
||||
if (isShiftKeyPressed) {
|
||||
const { x: initialX, y: initialY } = initialPositionRef.current;
|
||||
const { x: objectX, z: objectZ } = objectWorldPos;
|
||||
|
||||
const deltaX = intersection.x - initialX;
|
||||
|
||||
targetPosition.set(objectX, initialY + deltaX, objectZ);
|
||||
} else {
|
||||
// For free movement
|
||||
targetPosition.copy(intersection);
|
||||
}
|
||||
|
||||
// CONSTRAIN MOVEMENT HERE:
|
||||
const centerX = selectedArmBot.position[0];
|
||||
const centerZ = selectedArmBot.position[2];
|
||||
const minDistance = 1.2;
|
||||
const maxDistance = 2;
|
||||
|
||||
const delta = new THREE.Vector3(targetPosition.x - centerX, 0, targetPosition.z - centerZ);
|
||||
|
||||
// Create quaternion from rotation
|
||||
const robotEuler = new THREE.Euler(selectedArmBot.rotation[0], selectedArmBot.rotation[1], selectedArmBot.rotation[2]);
|
||||
const robotQuaternion = new THREE.Quaternion().setFromEuler(robotEuler);
|
||||
|
||||
// Inverse rotate
|
||||
const inverseQuaternion = robotQuaternion.clone().invert();
|
||||
delta.applyQuaternion(inverseQuaternion);
|
||||
|
||||
// Angle in robot local space
|
||||
let relativeAngle = Math.atan2(delta.z, delta.x);
|
||||
let angleDeg = (relativeAngle * 180) / Math.PI;
|
||||
if (angleDeg < 0) {
|
||||
angleDeg += 360;
|
||||
}
|
||||
|
||||
// Clamp angle
|
||||
if (angleDeg < 0 || angleDeg > 270) {
|
||||
const distanceTo90 = Math.abs(angleDeg - 0);
|
||||
const distanceTo270 = Math.abs(angleDeg - 270);
|
||||
if (distanceTo90 < distanceTo270) {
|
||||
angleDeg = 0;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
relativeAngle = (angleDeg * Math.PI) / 180;
|
||||
}
|
||||
|
||||
// Distance clamp
|
||||
const distance = delta.length();
|
||||
const clampedDistance = Math.min(Math.max(distance, minDistance), maxDistance);
|
||||
|
||||
// Calculate local target
|
||||
const finalLocal = new THREE.Vector3(
|
||||
Math.cos(relativeAngle) * clampedDistance,
|
||||
0,
|
||||
Math.sin(relativeAngle) * clampedDistance
|
||||
);
|
||||
|
||||
// Rotate back to world space
|
||||
finalLocal.applyQuaternion(robotQuaternion);
|
||||
|
||||
targetPosition.x = centerX + finalLocal.x;
|
||||
targetPosition.z = centerZ + finalLocal.z;
|
||||
|
||||
|
||||
// Clamp Y axis if needed
|
||||
targetPosition.y = Math.min(Math.max(targetPosition.y, 0.6), 1.9);
|
||||
|
||||
// Convert to local if parent exists
|
||||
if (parent) {
|
||||
parent.worldToLocal(targetPosition);
|
||||
}
|
||||
|
||||
// Update the 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 };
|
||||
}
|
||||
0
app/src/modules/simulation/spatialUI/temp.md
Normal file
0
app/src/modules/simulation/spatialUI/temp.md
Normal file
131
app/src/modules/simulation/spatialUI/vehicle/useDraggableGLTF.ts
Normal file
131
app/src/modules/simulation/spatialUI/vehicle/useDraggableGLTF.ts
Normal file
@@ -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<THREE.Object3D | null>(null);
|
||||
const planeRef = useRef<THREE.Plane>(
|
||||
new THREE.Plane(new THREE.Vector3(0, 1, 0), 0)
|
||||
);
|
||||
const offsetRef = useRef<THREE.Vector3>(new THREE.Vector3());
|
||||
const initialPositionRef = useRef<THREE.Vector3>(new THREE.Vector3());
|
||||
|
||||
const raycaster = new THREE.Raycaster();
|
||||
const pointer = new THREE.Vector2();
|
||||
|
||||
const handlePointerDown = (e: ThreeEvent<PointerEvent>) => {
|
||||
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 };
|
||||
}
|
||||
|
||||
|
||||
|
||||
380
app/src/modules/simulation/spatialUI/vehicle/vehicleUI.tsx
Normal file
380
app/src/modules/simulation/spatialUI/vehicle/vehicleUI.tsx
Normal file
@@ -0,0 +1,380 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import * as Types from "../../../../types/world/worldTypes";
|
||||
import { useGLTF } from "@react-three/drei";
|
||||
import { useFrame, useThree } from "@react-three/fiber";
|
||||
import { useSelectedEventSphere, useIsDragging, useIsRotating, } from "../../../../store/simulation/useSimulationStore";
|
||||
import { useProductStore } from "../../../../store/simulation/useProductStore";
|
||||
import { upsertProductOrEventApi } from "../../../../services/simulation/products/UpsertProductOrEventApi";
|
||||
import { DoubleSide, Group, Plane, Vector3 } from "three";
|
||||
|
||||
import startPoint from "../../../../assets/gltf-glb/ui/arrow_green.glb";
|
||||
import startEnd from "../../../../assets/gltf-glb/ui/arrow_red.glb";
|
||||
import { useSceneContext } from "../../../scene/sceneContext";
|
||||
import { useProductContext } from "../../products/productContext";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
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 { selectedProductStore } = useProductContext();
|
||||
const { vehicleStore } = useSceneContext();
|
||||
const { selectedProduct } = selectedProductStore();
|
||||
const { vehicles, getVehicleById } = vehicleStore();
|
||||
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 { projectId } = useParams();
|
||||
|
||||
const updateBackend = (
|
||||
productName: string,
|
||||
productUuid: string,
|
||||
projectId: string,
|
||||
eventData: EventsSchema
|
||||
) => {
|
||||
upsertProductOrEventApi({
|
||||
productName: productName,
|
||||
productUuid: productUuid,
|
||||
projectId: projectId,
|
||||
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, vehicles]);
|
||||
|
||||
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.productUuid,
|
||||
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.productUuid,
|
||||
projectId || '',
|
||||
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;
|
||||
Reference in New Issue
Block a user