first commit
This commit is contained in:
132
app/src/modules/scene/camera/camMode.tsx
Normal file
132
app/src/modules/scene/camera/camMode.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
import { useFrame, useThree } from "@react-three/fiber";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import * as CONSTANTS from "../../../types/world/worldConstants";
|
||||
import { useCamMode, useToggleView } from "../../../store/builder/store";
|
||||
import { useKeyboardControls } from "@react-three/drei";
|
||||
import switchToThirdPerson from "./switchToThirdPerson";
|
||||
import switchToFirstPerson from "./switchToFirstPerson";
|
||||
import { detectModifierKeys } from "../../../utils/shortcutkeys/detectModifierKeys";
|
||||
import { firstPersonCamera } from "./firstPersonCamera";
|
||||
|
||||
const CamMode: React.FC = () => {
|
||||
const { camMode, setCamMode } = useCamMode();
|
||||
const [, get] = useKeyboardControls();
|
||||
const [isTransitioning, setIsTransitioning] = useState(false);
|
||||
const state: any = useThree();
|
||||
const { toggleView } = useToggleView();
|
||||
const [isShiftActive, setIsShiftActive] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handlePointerLockChange = async () => {
|
||||
if (document.pointerLockElement && !toggleView) {
|
||||
// Pointer is locked
|
||||
} else if (camMode === "FirstPerson" && !toggleView) {
|
||||
// Pointer is unlocked
|
||||
setCamMode("ThirdPerson");
|
||||
await switchToThirdPerson(state.controls, state.camera);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("pointerlockchange", handlePointerLockChange);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener(
|
||||
"pointerlockchange",
|
||||
handlePointerLockChange
|
||||
);
|
||||
};
|
||||
}, [camMode, toggleView, setCamMode, state.controls, state.camera]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === "Shift") {
|
||||
setIsShiftActive(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyUp = (event: KeyboardEvent) => {
|
||||
if (event.key === "Shift") {
|
||||
setIsShiftActive(false);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
window.addEventListener("keyup", handleKeyUp);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
window.removeEventListener("keyup", handleKeyUp);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyPress = async (event: KeyboardEvent) => {
|
||||
if (!state.controls) return;
|
||||
|
||||
const keyCombination = detectModifierKeys(event);
|
||||
|
||||
if (keyCombination === "/" && !isTransitioning && !toggleView) {
|
||||
firstPersonCamera({
|
||||
setIsTransitioning,
|
||||
state,
|
||||
camMode,
|
||||
setCamMode,
|
||||
switchToFirstPerson,
|
||||
switchToThirdPerson,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyPress);
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyPress);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
camMode,
|
||||
isTransitioning,
|
||||
toggleView,
|
||||
state.controls,
|
||||
state.camera,
|
||||
setCamMode,
|
||||
]);
|
||||
|
||||
useFrame(() => {
|
||||
const { forward, backward, left, right } = get();
|
||||
if (!state.controls) return;
|
||||
if (camMode === "ThirdPerson" || !document.pointerLockElement) return;
|
||||
|
||||
const speedMultiplier = isShiftActive ? 4 : 1;
|
||||
|
||||
if (forward) {
|
||||
state.controls.forward(
|
||||
CONSTANTS.firstPersonControls.forwardSpeed * speedMultiplier,
|
||||
true
|
||||
);
|
||||
}
|
||||
if (backward) {
|
||||
state.controls.forward(
|
||||
CONSTANTS.firstPersonControls.backwardSpeed * speedMultiplier,
|
||||
true
|
||||
);
|
||||
}
|
||||
if (left) {
|
||||
state.controls.truck(
|
||||
CONSTANTS.firstPersonControls.leftSpeed * speedMultiplier,
|
||||
0,
|
||||
true
|
||||
);
|
||||
}
|
||||
if (right) {
|
||||
state.controls.truck(
|
||||
CONSTANTS.firstPersonControls.rightSpeed * speedMultiplier,
|
||||
0,
|
||||
true
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return null; // This component does not render any UI
|
||||
};
|
||||
|
||||
export default CamMode;
|
||||
39
app/src/modules/scene/camera/firstPersonCamera.ts
Normal file
39
app/src/modules/scene/camera/firstPersonCamera.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import * as CONSTANTS from "../../../types/world/worldConstants";
|
||||
|
||||
interface FirstPersonCameraProps {
|
||||
setIsTransitioning?: (value: boolean) => void;
|
||||
state: any;
|
||||
}
|
||||
|
||||
interface FirstPersonCameraParams extends FirstPersonCameraProps {
|
||||
camMode: string;
|
||||
setCamMode: (mode: string) => void;
|
||||
switchToFirstPerson: (controls: any, camera: any) => Promise<void>;
|
||||
switchToThirdPerson: (controls: any, camera: any) => Promise<void>;
|
||||
}
|
||||
|
||||
export async function firstPersonCamera({
|
||||
setIsTransitioning,
|
||||
state,
|
||||
camMode,
|
||||
setCamMode,
|
||||
switchToFirstPerson,
|
||||
switchToThirdPerson
|
||||
}: FirstPersonCameraParams): Promise<void> {
|
||||
setIsTransitioning && setIsTransitioning(true);
|
||||
|
||||
state.controls.mouseButtons.left = CONSTANTS.controlsTransition.leftMouse;
|
||||
state.controls.mouseButtons.right = CONSTANTS.controlsTransition.rightMouse;
|
||||
state.controls.mouseButtons.wheel = CONSTANTS.controlsTransition.wheelMouse;
|
||||
state.controls.mouseButtons.middle = CONSTANTS.controlsTransition.middleMouse;
|
||||
|
||||
if (camMode === "ThirdPerson") {
|
||||
setCamMode("FirstPerson");
|
||||
await switchToFirstPerson(state.controls, state.camera);
|
||||
} else if (camMode === "FirstPerson") {
|
||||
setCamMode("ThirdPerson");
|
||||
await switchToThirdPerson(state.controls, state.camera);
|
||||
}
|
||||
|
||||
setIsTransitioning && setIsTransitioning(false);
|
||||
}
|
||||
25
app/src/modules/scene/camera/switchToFirstPerson.ts
Normal file
25
app/src/modules/scene/camera/switchToFirstPerson.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import * as THREE from 'three';
|
||||
import * as CONSTANTS from '../../../types/world/worldConstants';
|
||||
|
||||
export default async function switchToFirstPerson(
|
||||
controls: any,
|
||||
camera: any
|
||||
) {
|
||||
if (!controls) return;
|
||||
|
||||
const cameraDirection = new THREE.Vector3();
|
||||
camera.getWorldDirection(cameraDirection);
|
||||
cameraDirection.normalize();
|
||||
|
||||
await controls.setPosition(camera.position.x, 2, camera.position.z, true);
|
||||
controls.setTarget(camera.position.x, 2, camera.position.z, true);
|
||||
controls.mouseButtons.left = CONSTANTS.firstPersonControls.leftMouse;
|
||||
controls.lockPointer();
|
||||
|
||||
controls.azimuthRotateSpeed = CONSTANTS.firstPersonControls.azimuthRotateSpeed;
|
||||
controls.polarRotateSpeed = CONSTANTS.firstPersonControls.polarRotateSpeed;
|
||||
controls.truckSpeed = CONSTANTS.firstPersonControls.truckSpeed;
|
||||
controls.minDistance = CONSTANTS.firstPersonControls.minDistance;
|
||||
controls.maxDistance = CONSTANTS.firstPersonControls.maxDistance;
|
||||
controls.maxPolarAngle = CONSTANTS.firstPersonControls.maxPolarAngle;
|
||||
}
|
||||
29
app/src/modules/scene/camera/switchToThirdPerson.ts
Normal file
29
app/src/modules/scene/camera/switchToThirdPerson.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import * as THREE from 'three';
|
||||
import * as CONSTANTS from '../../../types/world/worldConstants';
|
||||
|
||||
export default async function switchToThirdPerson(
|
||||
controls: any,
|
||||
camera: any
|
||||
) {
|
||||
if (!controls) return;
|
||||
controls.mouseButtons.left = CONSTANTS.thirdPersonControls.leftMouse;
|
||||
controls.mouseButtons.right = CONSTANTS.thirdPersonControls.rightMouse;
|
||||
controls.mouseButtons.middle = CONSTANTS.thirdPersonControls.middleMouse;
|
||||
controls.mouseButtons.wheel = CONSTANTS.thirdPersonControls.wheelMouse;
|
||||
controls.unlockPointer();
|
||||
|
||||
const cameraDirection = new THREE.Vector3();
|
||||
camera.getWorldDirection(cameraDirection);
|
||||
const targetOffset = cameraDirection.multiplyScalar(CONSTANTS.thirdPersonControls.targetOffset);
|
||||
const targetPosition = new THREE.Vector3(camera.position.x, camera.position.y, camera.position.z).add(targetOffset);
|
||||
|
||||
controls.setPosition(camera.position.x, CONSTANTS.thirdPersonControls.cameraHeight, camera.position.z, true);
|
||||
controls.setTarget(targetPosition.x, 0, targetPosition.z, true);
|
||||
|
||||
controls.azimuthRotateSpeed = CONSTANTS.thirdPersonControls.azimuthRotateSpeed;
|
||||
controls.polarRotateSpeed = CONSTANTS.thirdPersonControls.polarRotateSpeed;
|
||||
controls.truckSpeed = CONSTANTS.thirdPersonControls.truckSpeed;
|
||||
controls.minDistance = CONSTANTS.threeDimension.minDistance;
|
||||
controls.maxDistance = CONSTANTS.thirdPersonControls.maxDistance;
|
||||
controls.maxPolarAngle = CONSTANTS.thirdPersonControls.maxPolarAngle;
|
||||
}
|
||||
73
app/src/modules/scene/camera/switchView.tsx
Normal file
73
app/src/modules/scene/camera/switchView.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import * as THREE from "three";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useToggleView } from "../../../store/builder/store";
|
||||
import { useThree } from "@react-three/fiber";
|
||||
import { getCamera } from "../../../services/factoryBuilder/camera/getCameraApi";
|
||||
import * as CONSTANTS from '../../../types/world/worldConstants';
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
export default function SwitchView() {
|
||||
const { toggleView } = useToggleView();
|
||||
const state: any = useThree();
|
||||
const { set } = useThree();
|
||||
const perspectiveCamera = useRef<THREE.PerspectiveCamera | null>(null);
|
||||
const orthoCamera = useRef<THREE.OrthographicCamera | null>(null);
|
||||
orthoCamera.current = new THREE.OrthographicCamera(-window.innerWidth / 2, window.innerWidth / 2, window.innerHeight / 2, -window.innerHeight / 2, 0.01, 1000);
|
||||
perspectiveCamera.current = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.01, 1000);
|
||||
const { projectId } = useParams();
|
||||
|
||||
useEffect(() => {
|
||||
if (!perspectiveCamera.current || !orthoCamera.current) return;
|
||||
if (toggleView) {
|
||||
orthoCamera.current.zoom = 10;
|
||||
orthoCamera.current.position.set(...CONSTANTS.twoDimension.defaultPosition);
|
||||
orthoCamera.current.lookAt(new THREE.Vector3(...CONSTANTS.twoDimension.defaultTarget));
|
||||
orthoCamera.current.updateProjectionMatrix();
|
||||
set({ camera: orthoCamera.current });
|
||||
orthoCamera.current.updateProjectionMatrix();
|
||||
} else if (!toggleView) {
|
||||
perspectiveCamera.current.position.set(...CONSTANTS.threeDimension.defaultPosition);
|
||||
perspectiveCamera.current.lookAt(new THREE.Vector3(...CONSTANTS.threeDimension.defaultTarget));
|
||||
set({ camera: perspectiveCamera.current });
|
||||
}
|
||||
}, [toggleView, set]);
|
||||
|
||||
useEffect(() => {
|
||||
if (toggleView && state.controls) {
|
||||
state.controls.mouseButtons.left = CONSTANTS.twoDimension.leftMouse;
|
||||
state.controls.mouseButtons.right = CONSTANTS.twoDimension.rightMouse;
|
||||
} else {
|
||||
try {
|
||||
const email = localStorage.getItem('email');
|
||||
const organization = (email!.split("@")[1]).split(".")[0];
|
||||
getCamera(organization, localStorage.getItem('userId')!,projectId).then((data) => {
|
||||
if (data && data.position && data.target) {
|
||||
state.controls?.setPosition(data.position.x, data.position.y, data.position.z);
|
||||
state.controls?.setTarget(data.target.x, data.target.y, data.target.z);
|
||||
localStorage.setItem("cameraPosition", JSON.stringify(data.position));
|
||||
localStorage.setItem("controlTarget", JSON.stringify(data.target));
|
||||
} else {
|
||||
state.controls?.setPosition(...CONSTANTS.threeDimension.defaultPosition);
|
||||
state.controls?.setTarget(...CONSTANTS.threeDimension.defaultTarget);
|
||||
localStorage.setItem("cameraPosition", JSON.stringify(new THREE.Vector3(...CONSTANTS.threeDimension.defaultPosition)));
|
||||
localStorage.setItem("controlTarget", JSON.stringify(new THREE.Vector3(...CONSTANTS.threeDimension.defaultTarget)));
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
echo.error("Failed to retrieve camera position or target");
|
||||
console.error("Failed to retrieve camera position or target:", error);
|
||||
state.controls?.setPosition(...CONSTANTS.threeDimension.defaultPosition);
|
||||
state.controls?.setTarget(...CONSTANTS.threeDimension.defaultTarget);
|
||||
}
|
||||
|
||||
if (state.controls) {
|
||||
state.controls.mouseButtons.left = CONSTANTS.threeDimension.leftMouse;
|
||||
state.controls.mouseButtons.right = CONSTANTS.threeDimension.rightMouse;
|
||||
}
|
||||
}
|
||||
}, [toggleView, state.controls]);
|
||||
|
||||
return (
|
||||
<></>
|
||||
);
|
||||
}
|
||||
29
app/src/modules/scene/camera/updateCameraPosition.ts
Normal file
29
app/src/modules/scene/camera/updateCameraPosition.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Socket } from "socket.io-client";
|
||||
import * as THREE from 'three';
|
||||
|
||||
export default function updateCamPosition(
|
||||
controls: any,
|
||||
socket: Socket,
|
||||
position: THREE.Vector3,
|
||||
rotation: THREE.Euler,
|
||||
projectId?:string
|
||||
) {
|
||||
if (!controls.current) return;
|
||||
const target = controls.current.getTarget(new THREE.Vector3());
|
||||
const email = localStorage.getItem("email");
|
||||
const organization = email!.split("@")[1].split(".")[0];
|
||||
|
||||
const camData = {
|
||||
organization: organization,
|
||||
userId: localStorage.getItem("userId")!,
|
||||
position: position,
|
||||
target: new THREE.Vector3(target.x, 0, target.z),
|
||||
rotation: new THREE.Vector3(rotation.x, rotation.y, rotation.z),
|
||||
socketId: socket.id,
|
||||
projectId
|
||||
};
|
||||
// console.log('CameracamData: ', camData);
|
||||
socket.emit("v1:Camera:set", camData);
|
||||
localStorage.setItem("cameraPosition", JSON.stringify(position));
|
||||
localStorage.setItem("controlTarget", JSON.stringify(new THREE.Vector3(target.x, 0, target.z)));
|
||||
}
|
||||
90
app/src/modules/scene/clouds/clouds.tsx
Normal file
90
app/src/modules/scene/clouds/clouds.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import * as THREE from 'three';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useFrame } from '@react-three/fiber';
|
||||
import { Clouds, Cloud } from '@react-three/drei';
|
||||
|
||||
interface CloudGroupProps {
|
||||
initialX: number;
|
||||
initialZ: number;
|
||||
speed: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
function CloudGroup({ initialX, initialZ, speed, height }: CloudGroupProps) {
|
||||
const group = useRef<THREE.Group>(null);
|
||||
|
||||
useFrame((_, delta) => {
|
||||
if (group.current) {
|
||||
|
||||
group.current.position.x += delta * speed;
|
||||
group.current.position.z += delta * speed * 0.5;
|
||||
|
||||
if (group.current.position.x > 500) group.current.position.x = -500;
|
||||
if (group.current.position.z > 500) group.current.position.z = -500;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<group ref={group} position={[initialX, height, initialZ]}>
|
||||
<Clouds material={THREE.MeshBasicMaterial} frustumCulled={false} limit={10000}>
|
||||
<Cloud
|
||||
seed={Math.random() * 100}
|
||||
bounds={[100, 15, 100]}
|
||||
volume={50}
|
||||
color="#eeeeee"
|
||||
scale={4}
|
||||
fade={10000}
|
||||
/>
|
||||
<Cloud
|
||||
seed={Math.random() * 100}
|
||||
bounds={[100, 15, 100]}
|
||||
volume={50}
|
||||
scale={6}
|
||||
color="#ffffff"
|
||||
fade={10000}
|
||||
/>
|
||||
<Cloud
|
||||
seed={Math.random() * 100}
|
||||
bounds={[100, 15, 100]}
|
||||
volume={50}
|
||||
scale={4}
|
||||
fade={10000}
|
||||
color="#f0f0f0"
|
||||
/>
|
||||
</Clouds>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
export function MovingClouds() {
|
||||
|
||||
const savedTheme: string | null = localStorage.getItem("theme");
|
||||
const [theme, setTheme] = useState(savedTheme || "light");
|
||||
const cloudGroups = [
|
||||
{ initialX: 0, initialZ: 0, speed: 8, height: 300 },
|
||||
{ initialX: -300, initialZ: 100, speed: 10, height: 300 },
|
||||
{ initialX: 200, initialZ: -150, speed: 4, height: 300 },
|
||||
{ initialX: -400, initialZ: -200, speed: 7, height: 300 },
|
||||
{ initialX: 400, initialZ: 300, speed: 5, height: 300 },
|
||||
{ initialX: -200, initialZ: -300, speed: 7, height: 300 },
|
||||
{ initialX: 300, initialZ: 200, speed: 10, height: 300 },
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{theme === 'light' &&
|
||||
<>
|
||||
{cloudGroups.map((group, index) => (
|
||||
<CloudGroup
|
||||
key={index}
|
||||
initialX={group.initialX}
|
||||
initialZ={group.initialZ}
|
||||
speed={group.speed}
|
||||
height={group.height}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
155
app/src/modules/scene/controls/controls.tsx
Normal file
155
app/src/modules/scene/controls/controls.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
import { CameraControls } from "@react-three/drei";
|
||||
import { useRef, useEffect } from "react";
|
||||
import { useThree } from "@react-three/fiber";
|
||||
import * as THREE from "three";
|
||||
import * as CONSTANTS from '../../../types/world/worldConstants';
|
||||
|
||||
import { useSocketStore, useToggleView, useResetCamera } from "../../../store/builder/store";
|
||||
import { getCamera } from "../../../services/factoryBuilder/camera/getCameraApi";
|
||||
import updateCamPosition from "../camera/updateCameraPosition";
|
||||
import CamMode from "../camera/camMode";
|
||||
import SwitchView from "../camera/switchView";
|
||||
import SelectionControls from "./selectionControls/selectionControls";
|
||||
import TransformControl from "./transformControls/transformControls";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
export default function Controls() {
|
||||
const controlsRef = useRef<CameraControls>(null);
|
||||
|
||||
const { toggleView } = useToggleView();
|
||||
const { resetCamera, setResetCamera } = useResetCamera();
|
||||
const { socket } = useSocketStore();
|
||||
const state = useThree();
|
||||
const { projectId } = useParams();
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (controlsRef.current) {
|
||||
(controlsRef.current as any).mouseButtons.left = CONSTANTS.thirdPersonControls.leftMouse;
|
||||
(controlsRef.current as any).mouseButtons.right = CONSTANTS.thirdPersonControls.rightMouse;
|
||||
}
|
||||
const email = localStorage.getItem("email");
|
||||
const organization = email!.split("@")[1].split(".")[0];
|
||||
const userId = localStorage.getItem("userId")!;
|
||||
|
||||
getCamera(organization, userId, projectId).then((data) => {
|
||||
// console.log('data: ', data);
|
||||
if (data && data.position && data.target) {
|
||||
controlsRef.current?.setPosition(data.position.x, data.position.y, data.position.z);
|
||||
controlsRef.current?.setTarget(data.target.x, data.target.y, data.target.z);
|
||||
} else {
|
||||
controlsRef.current?.setPosition(...CONSTANTS.threeDimension.defaultPosition);
|
||||
controlsRef.current?.setTarget(...CONSTANTS.threeDimension.defaultTarget);
|
||||
}
|
||||
})
|
||||
.catch((error) => console.error("Failed to fetch camera data:", error));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (resetCamera) {
|
||||
controlsRef.current?.setPosition(...CONSTANTS.threeDimension.defaultPosition);
|
||||
controlsRef.current?.setTarget(...CONSTANTS.threeDimension.defaultTarget);
|
||||
controlsRef.current?.rotateAzimuthTo(CONSTANTS.threeDimension.defaultAzimuth);
|
||||
|
||||
localStorage.setItem("cameraPosition", JSON.stringify(new THREE.Vector3(...CONSTANTS.threeDimension.defaultPosition)));
|
||||
localStorage.setItem("controlTarget", JSON.stringify(new THREE.Vector3(...CONSTANTS.threeDimension.defaultTarget)));
|
||||
|
||||
const email = localStorage.getItem('email')
|
||||
const organization = (email!.split("@")[1]).split(".")[0];
|
||||
const userId = localStorage.getItem("userId");
|
||||
|
||||
const camData = {
|
||||
organization: organization,
|
||||
userId: userId,
|
||||
position: new THREE.Vector3(...CONSTANTS.threeDimension.defaultPosition),
|
||||
target: new THREE.Vector3(...CONSTANTS.threeDimension.defaultTarget),
|
||||
rotation: new THREE.Vector3(...CONSTANTS.threeDimension.defaultRotation),
|
||||
socketId: socket.id,
|
||||
projectId
|
||||
};
|
||||
// console.log('CameracamData: ', camData);
|
||||
socket.emit('v1:Camera:set', camData)
|
||||
|
||||
setResetCamera(false);
|
||||
}
|
||||
}, [resetCamera]);
|
||||
|
||||
useEffect(() => {
|
||||
controlsRef.current?.setBoundary(new THREE.Box3(new THREE.Vector3(...CONSTANTS.threeDimension.boundaryBottom), new THREE.Vector3(...CONSTANTS.threeDimension.boundaryTop)));
|
||||
// state.scene.add(new THREE.Box3Helper(new THREE.Box3(new THREE.Vector3(...CONSTANTS.threeDimension.boundaryBottom), new THREE.Vector3(...CONSTANTS.threeDimension.boundaryTop)), 0xffff00));
|
||||
let hasInteracted = false;
|
||||
let intervalId: NodeJS.Timeout | null = null;
|
||||
|
||||
const handleRest = () => {
|
||||
if (hasInteracted && controlsRef.current && state.camera.position && !toggleView) {
|
||||
const position = state.camera.position;
|
||||
if (position.x === 0 && position.y === 0 && position.z === 0) return;
|
||||
updateCamPosition(controlsRef, socket, position, state.camera.rotation, projectId);
|
||||
stopInterval();
|
||||
}
|
||||
};
|
||||
|
||||
const startInterval = () => {
|
||||
hasInteracted = true;
|
||||
if (!intervalId) {
|
||||
intervalId = setInterval(() => {
|
||||
if (controlsRef.current && !toggleView) {
|
||||
handleRest();
|
||||
}
|
||||
}, CONSTANTS.camPositionUpdateInterval);
|
||||
}
|
||||
};
|
||||
|
||||
const stopInterval = () => {
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId);
|
||||
intervalId = null;
|
||||
}
|
||||
};
|
||||
|
||||
const controls = controlsRef.current;
|
||||
if (controls) {
|
||||
controls.addEventListener("sleep", handleRest);
|
||||
controls.addEventListener("control", startInterval);
|
||||
controls.addEventListener("controlend", stopInterval);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (controls) {
|
||||
controls.removeEventListener("sleep", handleRest);
|
||||
controls.removeEventListener("control", startInterval);
|
||||
controls.removeEventListener("controlend", stopInterval);
|
||||
}
|
||||
stopInterval();
|
||||
};
|
||||
}, [toggleView, state, socket]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CameraControls
|
||||
makeDefault
|
||||
ref={controlsRef}
|
||||
minDistance={toggleView ? CONSTANTS.twoDimension.minDistance : CONSTANTS.threeDimension.minDistance}
|
||||
maxDistance={CONSTANTS.thirdPersonControls.maxDistance}
|
||||
minZoom={CONSTANTS.thirdPersonControls.minZoom}
|
||||
maxZoom={CONSTANTS.thirdPersonControls.maxZoom}
|
||||
maxPolarAngle={CONSTANTS.thirdPersonControls.maxPolarAngle}
|
||||
camera={state.camera}
|
||||
verticalDragToForward={true}
|
||||
boundaryEnclosesCamera={true}
|
||||
dollyToCursor={toggleView}
|
||||
>
|
||||
|
||||
<SwitchView />
|
||||
|
||||
<CamMode />
|
||||
|
||||
</CameraControls>
|
||||
|
||||
<SelectionControls />
|
||||
|
||||
<TransformControl />
|
||||
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import { Line } from "@react-three/drei";
|
||||
import { useMemo } from "react";
|
||||
import * as THREE from "three";
|
||||
import { useSelectedAssets } from "../../../../store/builder/store";
|
||||
|
||||
interface BoundingBoxProps {
|
||||
boundingBoxRef?: any;
|
||||
isPerAsset?: boolean;
|
||||
}
|
||||
|
||||
const getBoxLines = (min: THREE.Vector3, max: THREE.Vector3) => [
|
||||
[min.x, min.y, min.z], [max.x, min.y, min.z],
|
||||
[max.x, min.y, min.z], [max.x, max.y, min.z],
|
||||
[max.x, max.y, min.z], [min.x, max.y, min.z],
|
||||
[min.x, max.y, min.z], [min.x, min.y, min.z],
|
||||
|
||||
[min.x, min.y, max.z], [max.x, min.y, max.z],
|
||||
[max.x, min.y, max.z], [max.x, max.y, max.z],
|
||||
[max.x, max.y, max.z], [min.x, max.y, max.z],
|
||||
[min.x, max.y, max.z], [min.x, min.y, max.z],
|
||||
|
||||
[min.x, min.y, min.z], [min.x, min.y, max.z],
|
||||
[max.x, min.y, min.z], [max.x, min.y, max.z],
|
||||
[max.x, max.y, min.z], [max.x, max.y, max.z],
|
||||
[min.x, max.y, min.z], [min.x, max.y, max.z],
|
||||
];
|
||||
|
||||
const BoundingBox = ({ boundingBoxRef, isPerAsset = true }: BoundingBoxProps) => {
|
||||
const { selectedAssets } = useSelectedAssets();
|
||||
const savedTheme: string = localStorage.getItem("theme") || "light";
|
||||
|
||||
const boxes = useMemo(() => {
|
||||
if (selectedAssets.length === 0) return [];
|
||||
|
||||
if (isPerAsset) {
|
||||
return selectedAssets.map((obj: THREE.Object3D) => {
|
||||
const position = obj.position;
|
||||
const rotation = obj.getWorldQuaternion(new THREE.Quaternion());
|
||||
const clone = obj.clone();
|
||||
clone.position.set(0, 0, 0);
|
||||
clone.rotation.set(0, 0, 0);
|
||||
const box = new THREE.Box3().setFromObject(clone);
|
||||
const size = new THREE.Vector3();
|
||||
const center = new THREE.Vector3();
|
||||
box.getSize(size);
|
||||
box.getCenter(center);
|
||||
|
||||
const halfSize = size.clone().multiplyScalar(0.5);
|
||||
const min = center.clone().sub(halfSize);
|
||||
const max = center.clone().add(halfSize);
|
||||
|
||||
return {
|
||||
points: getBoxLines(min, max),
|
||||
position: [position.x, center.y, position.z],
|
||||
rotation: rotation.toArray(),
|
||||
size: size.toArray(),
|
||||
};
|
||||
});
|
||||
} else {
|
||||
const box = new THREE.Box3();
|
||||
selectedAssets.forEach((obj: any) => box.expandByObject(obj.clone()));
|
||||
const size = new THREE.Vector3();
|
||||
const center = new THREE.Vector3();
|
||||
box.getSize(size);
|
||||
box.getCenter(center);
|
||||
|
||||
const halfSize = size.clone().multiplyScalar(0.5);
|
||||
const min = center.clone().sub(halfSize);
|
||||
const max = center.clone().add(halfSize);
|
||||
|
||||
return [
|
||||
{
|
||||
points: getBoxLines(min, max),
|
||||
position: center.toArray(),
|
||||
rotation: [0, 0, 0, 1],
|
||||
size: size.toArray(),
|
||||
},
|
||||
];
|
||||
}
|
||||
}, [selectedAssets, isPerAsset]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{boxes.map((box: any, index: number) => (
|
||||
<group
|
||||
key={index}
|
||||
name="SelectionGroupBoundingBoxLine"
|
||||
>
|
||||
<Line
|
||||
name="SelectionGroupBoundingBox"
|
||||
depthWrite={false}
|
||||
points={box.points}
|
||||
color={savedTheme === "dark" ? "#c4abf1" : "#6f42c1"}
|
||||
lineWidth={2.7}
|
||||
segments
|
||||
position={[box.position[0], 0, box.position[2]]}
|
||||
quaternion={new THREE.Quaternion(...box.rotation)}
|
||||
/>
|
||||
<mesh
|
||||
name="SelectionGroupBoundingLine"
|
||||
ref={index === 0 ? boundingBoxRef : null}
|
||||
visible={false}
|
||||
position={box.position}
|
||||
quaternion={new THREE.Quaternion(...box.rotation)}
|
||||
>
|
||||
<boxGeometry args={box.size} />
|
||||
<meshBasicMaterial />
|
||||
</mesh>
|
||||
</group>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BoundingBox;
|
||||
@@ -0,0 +1,460 @@
|
||||
import * as THREE from "three";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { useFrame, useThree } from "@react-three/fiber";
|
||||
import { useSelectedAssets, useSocketStore, useToggleView } from "../../../../store/builder/store";
|
||||
// import { setFloorItemApi } from '../../../../services/factoryBuilder/assest/floorAsset/setFloorItemApi';
|
||||
import * as Types from "../../../../types/world/worldTypes";
|
||||
import { detectModifierKeys } from "../../../../utils/shortcutkeys/detectModifierKeys";
|
||||
import { useEventsStore } from "../../../../store/simulation/useEventsStore";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useAssetsStore } from "../../../../store/builder/useAssetStore";
|
||||
|
||||
const CopyPasteControls = ({
|
||||
copiedObjects,
|
||||
setCopiedObjects,
|
||||
pastedObjects,
|
||||
setpastedObjects,
|
||||
selectionGroup,
|
||||
setDuplicatedObjects,
|
||||
movedObjects,
|
||||
setMovedObjects,
|
||||
rotatedObjects,
|
||||
setRotatedObjects,
|
||||
boundingBoxRef
|
||||
}: any) => {
|
||||
const { camera, controls, gl, scene, pointer, raycaster } = useThree();
|
||||
const { toggleView } = useToggleView();
|
||||
const { selectedAssets, setSelectedAssets } = useSelectedAssets();
|
||||
const plane = useMemo(() => new THREE.Plane(new THREE.Vector3(0, 1, 0), 0), []);
|
||||
const { socket } = useSocketStore();
|
||||
const { addEvent } = useEventsStore();
|
||||
const { projectId } = useParams();
|
||||
const { assets, addAsset } = useAssetsStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (!camera || !scene || toggleView) return;
|
||||
const canvasElement = gl.domElement;
|
||||
canvasElement.tabIndex = 0;
|
||||
|
||||
let isMoving = false;
|
||||
|
||||
const onPointerDown = () => {
|
||||
isMoving = false;
|
||||
};
|
||||
|
||||
const onPointerMove = () => {
|
||||
isMoving = true;
|
||||
};
|
||||
|
||||
const onPointerUp = (event: PointerEvent) => {
|
||||
if (!isMoving && pastedObjects.length > 0 && event.button === 0 && movedObjects.length === 0 && rotatedObjects.length === 0) {
|
||||
event.preventDefault();
|
||||
addPastedObjects();
|
||||
}
|
||||
};
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
const keyCombination = detectModifierKeys(event);
|
||||
|
||||
if (keyCombination === "Ctrl+C" && movedObjects.length === 0 && rotatedObjects.length === 0) {
|
||||
copySelection();
|
||||
}
|
||||
if (keyCombination === "Ctrl+V" && copiedObjects.length > 0 && pastedObjects.length === 0 && movedObjects.length === 0 && rotatedObjects.length === 0) {
|
||||
pasteCopiedObjects();
|
||||
}
|
||||
};
|
||||
|
||||
if (!toggleView) {
|
||||
canvasElement.addEventListener("pointerdown", onPointerDown);
|
||||
canvasElement.addEventListener("pointermove", onPointerMove);
|
||||
canvasElement.addEventListener("pointerup", onPointerUp);
|
||||
canvasElement.addEventListener("keydown", onKeyDown);
|
||||
}
|
||||
|
||||
return () => {
|
||||
canvasElement.removeEventListener("pointerdown", onPointerDown);
|
||||
canvasElement.removeEventListener("pointermove", onPointerMove);
|
||||
canvasElement.removeEventListener("pointerup", onPointerUp);
|
||||
canvasElement.removeEventListener("keydown", onKeyDown);
|
||||
};
|
||||
|
||||
}, [assets, camera, controls, scene, toggleView, selectedAssets, copiedObjects, pastedObjects, movedObjects, socket, rotatedObjects]);
|
||||
|
||||
useFrame(() => {
|
||||
if (pastedObjects.length > 0) {
|
||||
const intersectionPoint = new THREE.Vector3();
|
||||
raycaster.setFromCamera(pointer, camera);
|
||||
const point = raycaster.ray.intersectPlane(plane, intersectionPoint);
|
||||
if (point) {
|
||||
const position = new THREE.Vector3();
|
||||
if (boundingBoxRef.current) {
|
||||
boundingBoxRef.current?.getWorldPosition(position)
|
||||
selectionGroup.current.position.set(point.x - (position.x - selectionGroup.current.position.x), selectionGroup.current.position.y, point.z - (position.z - selectionGroup.current.position.z));
|
||||
} else {
|
||||
const box = new THREE.Box3();
|
||||
pastedObjects.forEach((obj: THREE.Object3D) => box.expandByObject(obj.clone()));
|
||||
const center = new THREE.Vector3();
|
||||
box.getCenter(center);
|
||||
selectionGroup.current.position.set(point.x - (center.x - selectionGroup.current.position.x), selectionGroup.current.position.y, point.z - (center.z - selectionGroup.current.position.z));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const copySelection = () => {
|
||||
if (selectedAssets.length > 0) {
|
||||
const newClones = selectedAssets.map((asset: any) => {
|
||||
const clone = asset.clone();
|
||||
clone.position.copy(asset.position);
|
||||
return clone;
|
||||
});
|
||||
setCopiedObjects(newClones);
|
||||
echo.info("Objects copied!");
|
||||
}
|
||||
};
|
||||
|
||||
const pasteCopiedObjects = () => {
|
||||
if (copiedObjects.length > 0 && pastedObjects.length === 0) {
|
||||
const newClones = copiedObjects.map((obj: THREE.Object3D) => {
|
||||
const clone = obj.clone();
|
||||
clone.position.copy(obj.position);
|
||||
return clone;
|
||||
});
|
||||
selectionGroup.current.add(...newClones);
|
||||
setpastedObjects([...newClones]);
|
||||
setSelectedAssets([...newClones]);
|
||||
|
||||
const intersectionPoint = new THREE.Vector3();
|
||||
raycaster.setFromCamera(pointer, camera);
|
||||
const point = raycaster.ray.intersectPlane(plane, intersectionPoint);
|
||||
|
||||
if (point) {
|
||||
const position = new THREE.Vector3();
|
||||
if (boundingBoxRef.current) {
|
||||
boundingBoxRef.current?.getWorldPosition(position)
|
||||
selectionGroup.current.position.set(point.x - (position.x - selectionGroup.current.position.x), selectionGroup.current.position.y, point.z - (position.z - selectionGroup.current.position.z));
|
||||
} else {
|
||||
const box = new THREE.Box3();
|
||||
newClones.forEach((obj: THREE.Object3D) => box.expandByObject(obj.clone()));
|
||||
const center = new THREE.Vector3();
|
||||
box.getCenter(center);
|
||||
selectionGroup.current.position.set(point.x - (center.x - selectionGroup.current.position.x), selectionGroup.current.position.y, point.z - (center.z - selectionGroup.current.position.z));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const addPastedObjects = () => {
|
||||
if (pastedObjects.length === 0) return;
|
||||
const email = localStorage.getItem("email");
|
||||
const organization = email ? email.split("@")[1].split(".")[0] : "default";
|
||||
|
||||
pastedObjects.forEach(async (obj: THREE.Object3D) => {
|
||||
if (obj) {
|
||||
const worldPosition = new THREE.Vector3();
|
||||
obj.getWorldPosition(worldPosition);
|
||||
obj.position.copy(worldPosition);
|
||||
|
||||
const newFloorItem: Types.FloorItemType = {
|
||||
modelUuid: THREE.MathUtils.generateUUID(),
|
||||
modelName: obj.userData.modelName,
|
||||
modelfileID: obj.userData.assetId,
|
||||
position: [worldPosition.x, worldPosition.y, worldPosition.z],
|
||||
rotation: { x: obj.rotation.x, y: obj.rotation.y, z: obj.rotation.z, },
|
||||
isLocked: false,
|
||||
isVisible: true,
|
||||
};
|
||||
|
||||
let updatedEventData = null;
|
||||
|
||||
if (obj.userData.eventData) {
|
||||
updatedEventData = JSON.parse(JSON.stringify(obj.userData.eventData));
|
||||
updatedEventData.modelUuid = newFloorItem.modelUuid;
|
||||
|
||||
const eventData: any = {
|
||||
type: obj.userData.eventData.type,
|
||||
};
|
||||
|
||||
if (obj.userData.eventData.type === "Conveyor") {
|
||||
const ConveyorEvent: ConveyorEventSchema = {
|
||||
modelUuid: newFloorItem.modelUuid,
|
||||
modelName: newFloorItem.modelName,
|
||||
position: newFloorItem.position,
|
||||
rotation: [newFloorItem.rotation.x, newFloorItem.rotation.y, newFloorItem.rotation.z],
|
||||
state: "idle",
|
||||
type: 'transfer',
|
||||
speed: 1,
|
||||
points: updatedEventData.points.map((point: any, index: number) => ({
|
||||
uuid: THREE.MathUtils.generateUUID(),
|
||||
position: [point.position[0], point.position[1], point.position[2]],
|
||||
rotation: [point.rotation[0], point.rotation[1], point.rotation[2]],
|
||||
action: {
|
||||
actionUuid: THREE.MathUtils.generateUUID(),
|
||||
actionName: `Action 1`,
|
||||
actionType: 'default',
|
||||
material: 'Default Material',
|
||||
delay: 0,
|
||||
spawnInterval: 5,
|
||||
spawnCount: 1,
|
||||
triggers: []
|
||||
}
|
||||
}))
|
||||
};
|
||||
addEvent(ConveyorEvent);
|
||||
eventData.points = ConveyorEvent.points.map(point => ({
|
||||
uuid: point.uuid,
|
||||
position: point.position,
|
||||
rotation: point.rotation
|
||||
}));
|
||||
|
||||
} else if (obj.userData.eventData.type === "Vehicle") {
|
||||
const vehicleEvent: VehicleEventSchema = {
|
||||
modelUuid: newFloorItem.modelUuid,
|
||||
modelName: newFloorItem.modelName,
|
||||
position: newFloorItem.position,
|
||||
rotation: [newFloorItem.rotation.x, newFloorItem.rotation.y, newFloorItem.rotation.z],
|
||||
state: "idle",
|
||||
type: "vehicle",
|
||||
speed: 1,
|
||||
point: {
|
||||
uuid: THREE.MathUtils.generateUUID(),
|
||||
position: [updatedEventData.point.position[0], updatedEventData.point.position[1], updatedEventData.point.position[2]],
|
||||
rotation: [updatedEventData.point.rotation[0], updatedEventData.point.rotation[1], updatedEventData.point.rotation[2]],
|
||||
action: {
|
||||
actionUuid: THREE.MathUtils.generateUUID(),
|
||||
actionName: "Action 1",
|
||||
actionType: "travel",
|
||||
unLoadDuration: 5,
|
||||
loadCapacity: 1,
|
||||
steeringAngle: 0,
|
||||
pickUpPoint: null,
|
||||
unLoadPoint: null,
|
||||
triggers: []
|
||||
}
|
||||
}
|
||||
};
|
||||
addEvent(vehicleEvent);
|
||||
eventData.point = {
|
||||
uuid: vehicleEvent.point.uuid,
|
||||
position: vehicleEvent.point.position,
|
||||
rotation: vehicleEvent.point.rotation
|
||||
};
|
||||
|
||||
} else if (obj.userData.eventData.type === "ArmBot") {
|
||||
const roboticArmEvent: RoboticArmEventSchema = {
|
||||
modelUuid: newFloorItem.modelUuid,
|
||||
modelName: newFloorItem.modelName,
|
||||
position: newFloorItem.position,
|
||||
rotation: [newFloorItem.rotation.x, newFloorItem.rotation.y, newFloorItem.rotation.z],
|
||||
state: "idle",
|
||||
type: "roboticArm",
|
||||
speed: 1,
|
||||
point: {
|
||||
uuid: THREE.MathUtils.generateUUID(),
|
||||
position: [updatedEventData.point.position[0], updatedEventData.point.position[1], updatedEventData.point.position[2]],
|
||||
rotation: [updatedEventData.point.rotation[0], updatedEventData.point.rotation[1], updatedEventData.point.rotation[2]],
|
||||
actions: [
|
||||
{
|
||||
actionUuid: THREE.MathUtils.generateUUID(),
|
||||
actionName: "Action 1",
|
||||
actionType: "pickAndPlace",
|
||||
process: {
|
||||
startPoint: null,
|
||||
endPoint: null
|
||||
},
|
||||
triggers: []
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
addEvent(roboticArmEvent);
|
||||
eventData.point = {
|
||||
uuid: roboticArmEvent.point.uuid,
|
||||
position: roboticArmEvent.point.position,
|
||||
rotation: roboticArmEvent.point.rotation
|
||||
};
|
||||
|
||||
} else if (obj.userData.eventData.type === "StaticMachine") {
|
||||
const machineEvent: MachineEventSchema = {
|
||||
modelUuid: newFloorItem.modelUuid,
|
||||
modelName: newFloorItem.modelName,
|
||||
position: newFloorItem.position,
|
||||
rotation: [newFloorItem.rotation.x, newFloorItem.rotation.y, newFloorItem.rotation.z],
|
||||
state: "idle",
|
||||
type: "machine",
|
||||
point: {
|
||||
uuid: THREE.MathUtils.generateUUID(),
|
||||
position: [updatedEventData.point.position[0], updatedEventData.point.position[1], updatedEventData.point.position[2]],
|
||||
rotation: [updatedEventData.point.rotation[0], updatedEventData.point.rotation[1], updatedEventData.point.rotation[2]],
|
||||
action: {
|
||||
actionUuid: THREE.MathUtils.generateUUID(),
|
||||
actionName: "Action 1",
|
||||
actionType: "process",
|
||||
processTime: 10,
|
||||
swapMaterial: "Default Material",
|
||||
triggers: []
|
||||
}
|
||||
}
|
||||
};
|
||||
addEvent(machineEvent);
|
||||
eventData.point = {
|
||||
uuid: machineEvent.point.uuid,
|
||||
position: machineEvent.point.position,
|
||||
rotation: machineEvent.point.rotation
|
||||
};
|
||||
} else if (obj.userData.eventData.type === "Storage") {
|
||||
const storageEvent: StorageEventSchema = {
|
||||
modelUuid: newFloorItem.modelUuid,
|
||||
modelName: newFloorItem.modelName,
|
||||
position: newFloorItem.position,
|
||||
rotation: [newFloorItem.rotation.x, newFloorItem.rotation.y, newFloorItem.rotation.z],
|
||||
state: "idle",
|
||||
type: "storageUnit",
|
||||
point: {
|
||||
uuid: THREE.MathUtils.generateUUID(),
|
||||
position: [updatedEventData.point.position[0], updatedEventData.point.position[1], updatedEventData.point.position[2]],
|
||||
rotation: [updatedEventData.point.rotation[0], updatedEventData.point.rotation[1], updatedEventData.point.rotation[2]],
|
||||
action: {
|
||||
actionUuid: THREE.MathUtils.generateUUID(),
|
||||
actionName: "Action 1",
|
||||
actionType: "store",
|
||||
storageCapacity: 10,
|
||||
triggers: []
|
||||
}
|
||||
}
|
||||
}
|
||||
addEvent(storageEvent);
|
||||
eventData.point = {
|
||||
uuid: storageEvent.point.uuid,
|
||||
position: storageEvent.point.position,
|
||||
rotation: storageEvent.point.rotation
|
||||
};
|
||||
}
|
||||
|
||||
newFloorItem.eventData = eventData;
|
||||
|
||||
const userId = localStorage.getItem("userId"); //REST
|
||||
|
||||
// await setFloorItemApi(
|
||||
// organization,
|
||||
// obj.uuid,
|
||||
// obj.userData.name,
|
||||
// [worldPosition.x, worldPosition.y, worldPosition.z],
|
||||
// { "x": obj.rotation.x, "y": obj.rotation.y, "z": obj.rotation.z },
|
||||
// obj.userData.modelId,
|
||||
// false,
|
||||
// true,
|
||||
// );
|
||||
|
||||
//SOCKET
|
||||
|
||||
const data = {
|
||||
organization,
|
||||
modelUuid: newFloorItem.modelUuid,
|
||||
modelName: newFloorItem.modelName,
|
||||
modelfileID: newFloorItem.modelfileID,
|
||||
position: newFloorItem.position,
|
||||
rotation: { x: obj.rotation.x, y: obj.rotation.y, z: obj.rotation.z },
|
||||
isLocked: false,
|
||||
isVisible: true,
|
||||
socketId: socket.id,
|
||||
eventData: eventData,
|
||||
userId,
|
||||
projectId
|
||||
};
|
||||
|
||||
// console.log('data: ', data);
|
||||
socket.emit("v1:model-asset:add", data);
|
||||
|
||||
obj.userData = {
|
||||
name: newFloorItem.modelName,
|
||||
modelId: newFloorItem.modelfileID,
|
||||
modelUuid: newFloorItem.modelUuid,
|
||||
eventData: JSON.parse(JSON.stringify(eventData))
|
||||
};
|
||||
|
||||
const asset: Asset = {
|
||||
modelUuid: data.modelUuid,
|
||||
modelName: data.modelName,
|
||||
assetId: data.modelfileID,
|
||||
position: data.position,
|
||||
rotation: [data.rotation.x, data.rotation.y, data.rotation.z],
|
||||
isLocked: data.isLocked,
|
||||
isCollidable: false,
|
||||
isVisible: data.isVisible,
|
||||
opacity: 1,
|
||||
eventData: data.eventData
|
||||
}
|
||||
|
||||
addAsset(asset);
|
||||
|
||||
} else {
|
||||
|
||||
//REST
|
||||
|
||||
// await setFloorItemApi(
|
||||
// organization,
|
||||
// obj.uuid,
|
||||
// obj.userData.name,
|
||||
// [worldPosition.x, worldPosition.y, worldPosition.z],
|
||||
// { "x": obj.rotation.x, "y": obj.rotation.y, "z": obj.rotation.z },
|
||||
// obj.userData.modelId,
|
||||
// false,
|
||||
// true,
|
||||
// );
|
||||
|
||||
//SOCKET
|
||||
const userId = localStorage.getItem("userId");
|
||||
const data = {
|
||||
organization,
|
||||
modelUuid: newFloorItem.modelUuid,
|
||||
modelName: newFloorItem.modelName,
|
||||
modelfileID: newFloorItem.modelfileID,
|
||||
position: newFloorItem.position,
|
||||
rotation: { x: obj.rotation.x, y: obj.rotation.y, z: obj.rotation.z },
|
||||
isLocked: false,
|
||||
isVisible: true,
|
||||
socketId: socket.id,
|
||||
projectId,
|
||||
userId
|
||||
};
|
||||
|
||||
// console.log('data: ', data);
|
||||
socket.emit("v1:model-asset:add", data);
|
||||
|
||||
const asset: Asset = {
|
||||
modelUuid: data.modelUuid,
|
||||
modelName: data.modelName,
|
||||
assetId: data.modelfileID,
|
||||
position: data.position,
|
||||
rotation: [data.rotation.x, data.rotation.y, data.rotation.z],
|
||||
isLocked: data.isLocked,
|
||||
isCollidable: false,
|
||||
isVisible: data.isVisible,
|
||||
opacity: 1,
|
||||
}
|
||||
|
||||
addAsset(asset);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
echo.success("Object added!");
|
||||
clearSelection();
|
||||
};
|
||||
|
||||
const clearSelection = () => {
|
||||
selectionGroup.current.children = [];
|
||||
selectionGroup.current.position.set(0, 0, 0);
|
||||
selectionGroup.current.rotation.set(0, 0, 0);
|
||||
setMovedObjects([]);
|
||||
setpastedObjects([]);
|
||||
setDuplicatedObjects([]);
|
||||
setRotatedObjects([]);
|
||||
setSelectedAssets([]);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default CopyPasteControls;
|
||||
@@ -0,0 +1,339 @@
|
||||
import React, { useRef, useState } from "react";
|
||||
import {
|
||||
Vector3,
|
||||
Raycaster,
|
||||
BufferGeometry,
|
||||
LineBasicMaterial,
|
||||
Line,
|
||||
Mesh,
|
||||
Group,
|
||||
} from "three";
|
||||
import { useThree, useFrame } from "@react-three/fiber";
|
||||
import { Html } from "@react-three/drei";
|
||||
|
||||
interface DistanceFindingControlsProps {
|
||||
boundingBoxRef: React.RefObject<Mesh>;
|
||||
object: number;
|
||||
}
|
||||
|
||||
const DistanceFindingControls = ({
|
||||
boundingBoxRef,
|
||||
object,
|
||||
}: DistanceFindingControlsProps) => {
|
||||
const { camera, scene } = useThree();
|
||||
const [labelValues, setLabelValues] = useState<{
|
||||
textPosX: any;
|
||||
textNegX: any;
|
||||
textPosZ: any;
|
||||
textNegZ: any;
|
||||
}>({
|
||||
textPosX: "",
|
||||
textNegX: "",
|
||||
textPosZ: "",
|
||||
textNegZ: "",
|
||||
});
|
||||
|
||||
// Refs for measurement lines
|
||||
const line1 = useRef<Line>(null);
|
||||
const line2 = useRef<Line>(null);
|
||||
const line3 = useRef<Line>(null);
|
||||
const line4 = useRef<Line>(null);
|
||||
const line5 = useRef<Line>(null);
|
||||
|
||||
// Refs for measurement text labels
|
||||
const textPosX = useRef<Group>(null);
|
||||
const textNegX = useRef<Group>(null);
|
||||
const textPosZ = useRef<Group>(null);
|
||||
const textNegZ = useRef<Group>(null);
|
||||
const textPosY = useRef<Group>(null);
|
||||
|
||||
// Store line geometries to avoid recreation
|
||||
const lineGeometries = useRef({
|
||||
posX: new BufferGeometry(),
|
||||
negX: new BufferGeometry(),
|
||||
posZ: new BufferGeometry(),
|
||||
negZ: new BufferGeometry(),
|
||||
posY: new BufferGeometry(),
|
||||
});
|
||||
|
||||
useFrame(() => {
|
||||
if (!boundingBoxRef?.current) return;
|
||||
|
||||
boundingBoxRef.current.geometry.computeBoundingBox();
|
||||
const bbox = boundingBoxRef.current.geometry.boundingBox;
|
||||
|
||||
if (!bbox) return;
|
||||
|
||||
const size = {
|
||||
x: bbox.max.x - bbox.min.x,
|
||||
y: bbox.max.y - bbox.min.y,
|
||||
z: bbox.max.z - bbox.min.z,
|
||||
};
|
||||
|
||||
const vec = boundingBoxRef.current?.getWorldPosition(new Vector3()).clone();
|
||||
|
||||
if (!vec) return;
|
||||
updateLine({
|
||||
line: line1.current,
|
||||
geometry: lineGeometries.current.posX,
|
||||
direction: new Vector3(1, 0, 0), // Positive X
|
||||
angle: "pos",
|
||||
mesh: textPosX,
|
||||
vec,
|
||||
size,
|
||||
});
|
||||
updateLine({
|
||||
line: line2.current,
|
||||
geometry: lineGeometries.current.negX,
|
||||
direction: new Vector3(-1, 0, 0), // Negative X
|
||||
angle: "neg",
|
||||
mesh: textNegX,
|
||||
vec,
|
||||
size,
|
||||
});
|
||||
updateLine({
|
||||
line: line3.current,
|
||||
geometry: lineGeometries.current.posZ,
|
||||
direction: new Vector3(0, 0, 1), // Positive Z
|
||||
angle: "pos",
|
||||
mesh: textPosZ,
|
||||
vec,
|
||||
size,
|
||||
});
|
||||
updateLine({
|
||||
line: line4.current,
|
||||
geometry: lineGeometries.current.negZ,
|
||||
direction: new Vector3(0, 0, -1), // Negative Z
|
||||
angle: "neg",
|
||||
mesh: textNegZ,
|
||||
vec,
|
||||
size,
|
||||
});
|
||||
updateLine({
|
||||
line: line5.current,
|
||||
geometry: lineGeometries.current.posY,
|
||||
direction: new Vector3(0, -1, 0), // Down (Y)
|
||||
angle: "posY",
|
||||
mesh: textPosY,
|
||||
vec,
|
||||
size,
|
||||
});
|
||||
});
|
||||
|
||||
const updateLine = ({
|
||||
line,
|
||||
geometry,
|
||||
direction,
|
||||
angle,
|
||||
mesh,
|
||||
vec,
|
||||
size,
|
||||
}: {
|
||||
line: Line | null;
|
||||
geometry: BufferGeometry;
|
||||
direction: Vector3;
|
||||
angle: string;
|
||||
mesh: React.RefObject<Group>;
|
||||
vec: Vector3;
|
||||
size: { x: number; y: number; z: number };
|
||||
}) => {
|
||||
if (!line) return;
|
||||
|
||||
const points = [];
|
||||
|
||||
if (angle === "pos") {
|
||||
points[0] = new Vector3(vec.x, vec.y, vec.z).add(
|
||||
new Vector3((direction.x * size.x) / 2, 0, (direction.z * size.z) / 2)
|
||||
);
|
||||
} else if (angle === "neg") {
|
||||
points[0] = new Vector3(vec.x, vec.y, vec.z).sub(
|
||||
new Vector3((-direction.x * size.x) / 2, 0, (-direction.z * size.z) / 2)
|
||||
);
|
||||
} else if (angle === "posY") {
|
||||
points[0] = new Vector3(vec.x, vec.y, vec.z).sub(
|
||||
new Vector3(0, size.y / 2, 0)
|
||||
);
|
||||
}
|
||||
|
||||
const ray = new Raycaster();
|
||||
if (camera) ray.camera = camera;
|
||||
ray.set(new Vector3(vec.x, vec.y, vec.z), direction);
|
||||
ray.params.Line.threshold = 0.1;
|
||||
|
||||
// Find intersection points
|
||||
const wallsGroup = scene.children.find((val) =>
|
||||
val?.name.includes("Walls")
|
||||
);
|
||||
const intersects = wallsGroup
|
||||
? ray.intersectObjects([wallsGroup], true)
|
||||
: [];
|
||||
|
||||
// Find intersection point
|
||||
if (intersects[0]) {
|
||||
for (const intersect of intersects) {
|
||||
if (intersect.object.name.includes("Wall")) {
|
||||
points[1] =
|
||||
angle !== "posY" ? intersect.point : new Vector3(vec.x, 0, vec.z); // Floor
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (points[1]) {
|
||||
geometry.dispose();
|
||||
geometry.setFromPoints([points[0], points[1]]);
|
||||
line.geometry = geometry;
|
||||
|
||||
// Calculate the distance only once
|
||||
const distance = points[0].distanceTo(points[1]).toFixed(2);
|
||||
|
||||
// Update measurement text
|
||||
if (mesh?.current) {
|
||||
geometry.computeBoundingSphere();
|
||||
const center = geometry.boundingSphere?.center;
|
||||
if (center) {
|
||||
mesh.current.position.copy(center);
|
||||
}
|
||||
|
||||
const label = document.getElementById(mesh.current.name);
|
||||
if (label) {
|
||||
label.innerText = `${distance}m`;
|
||||
|
||||
// Update specific label state based on the label ID
|
||||
switch (label.id) {
|
||||
case "textPosX":
|
||||
setLabelValues((prevState) => ({ ...prevState, textPosX: distance }));
|
||||
break;
|
||||
case "textNegX":
|
||||
setLabelValues((prevState) => ({ ...prevState, textNegX: distance }));
|
||||
break;
|
||||
case "textPosZ":
|
||||
setLabelValues((prevState) => ({ ...prevState, textPosZ: distance }));
|
||||
break;
|
||||
case "textNegZ":
|
||||
setLabelValues((prevState) => ({ ...prevState, textNegZ: distance }));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No intersection found - clear the line
|
||||
geometry.dispose();
|
||||
geometry.setFromPoints([new Vector3(), new Vector3()]);
|
||||
line.geometry = geometry;
|
||||
|
||||
const label = document.getElementById(mesh?.current?.name ?? "");
|
||||
if (label) {
|
||||
label.innerText = "";
|
||||
|
||||
// Clear the corresponding label value in the state
|
||||
switch (label.id) {
|
||||
case "textPosX":
|
||||
setLabelValues((prevState) => ({ ...prevState, textPosX: "" }));
|
||||
break;
|
||||
case "textNegX":
|
||||
setLabelValues((prevState) => ({ ...prevState, textNegX: "" }));
|
||||
break;
|
||||
case "textPosZ":
|
||||
setLabelValues((prevState) => ({ ...prevState, textPosZ: "" }));
|
||||
break;
|
||||
case "textNegZ":
|
||||
setLabelValues((prevState) => ({ ...prevState, textNegZ: "" }));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
const Material = new LineBasicMaterial({ color: "#d2baff" });
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Measurement text labels */}
|
||||
{boundingBoxRef.current && object > 0 && (
|
||||
<>
|
||||
<group name="textPosX" ref={textPosX}>
|
||||
<Html
|
||||
wrapperClass="distance-text-wrapper"
|
||||
className="distance-text"
|
||||
zIndexRange={[1, 0]}
|
||||
style={{
|
||||
pointerEvents: "none",
|
||||
visibility: labelValues.textPosX === "" ? "hidden" : "visible",
|
||||
}}
|
||||
>
|
||||
<div className="distance-label" id="textPosX">{labelValues.textPosX}</div>
|
||||
</Html>
|
||||
</group>
|
||||
|
||||
<group name="textNegX" ref={textNegX}>
|
||||
<Html
|
||||
wrapperClass="distance-text-wrapper"
|
||||
className="distance-text"
|
||||
zIndexRange={[1, 0]}
|
||||
style={{
|
||||
pointerEvents: "none",
|
||||
visibility: labelValues.textNegX === "" ? "hidden" : "visible",
|
||||
}}
|
||||
>
|
||||
<div className="distance-label" id="textNegX">{labelValues.textNegX}</div>
|
||||
</Html>
|
||||
</group>
|
||||
|
||||
<group name="textPosZ" ref={textPosZ}>
|
||||
<Html
|
||||
wrapperClass="distance-text-wrapper"
|
||||
className="distance-text"
|
||||
zIndexRange={[2, 0]}
|
||||
style={{
|
||||
pointerEvents: "none",
|
||||
visibility: labelValues.textPosZ === "" ? "hidden" : "visible",
|
||||
}}
|
||||
>
|
||||
<div className="distance-label" id="textPosZ">{labelValues.textPosZ}</div>
|
||||
</Html>
|
||||
</group>
|
||||
|
||||
<group name="textNegZ" ref={textNegZ}>
|
||||
<Html
|
||||
wrapperClass="distance-text-wrapper"
|
||||
className="distance-text"
|
||||
zIndexRange={[1, 0]}
|
||||
style={{
|
||||
pointerEvents: "none",
|
||||
visibility: labelValues.textNegZ === "" ? "hidden" : "visible",
|
||||
}}
|
||||
>
|
||||
<div className="distance-label" id="textNegZ">{labelValues.textNegZ}</div>
|
||||
</Html>
|
||||
</group>
|
||||
|
||||
{/* Measurement lines */}
|
||||
<primitive
|
||||
object={new Line(new BufferGeometry(), Material)}
|
||||
ref={line1}
|
||||
/>
|
||||
<primitive
|
||||
object={new Line(new BufferGeometry(), Material)}
|
||||
ref={line2}
|
||||
/>
|
||||
<primitive
|
||||
object={new Line(new BufferGeometry(), Material)}
|
||||
ref={line3}
|
||||
/>
|
||||
<primitive
|
||||
object={new Line(new BufferGeometry(), Material)}
|
||||
ref={line4}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DistanceFindingControls;
|
||||
@@ -0,0 +1,429 @@
|
||||
import * as THREE from "three";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { useFrame, useThree } from "@react-three/fiber";
|
||||
import { useSelectedAssets, useSocketStore, useToggleView } from "../../../../store/builder/store";
|
||||
// import { setFloorItemApi } from '../../../../services/factoryBuilder/assest/floorAsset/setFloorItemApi';
|
||||
import * as Types from "../../../../types/world/worldTypes";
|
||||
import { detectModifierKeys } from "../../../../utils/shortcutkeys/detectModifierKeys";
|
||||
import { useEventsStore } from "../../../../store/simulation/useEventsStore";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useAssetsStore } from "../../../../store/builder/useAssetStore";
|
||||
|
||||
const DuplicationControls = ({
|
||||
duplicatedObjects,
|
||||
setDuplicatedObjects,
|
||||
setpastedObjects,
|
||||
selectionGroup,
|
||||
movedObjects,
|
||||
setMovedObjects,
|
||||
rotatedObjects,
|
||||
setRotatedObjects,
|
||||
boundingBoxRef
|
||||
}: any) => {
|
||||
const { camera, controls, gl, scene, pointer, raycaster } = useThree();
|
||||
const { toggleView } = useToggleView();
|
||||
const { selectedAssets, setSelectedAssets } = useSelectedAssets();
|
||||
const plane = useMemo(() => new THREE.Plane(new THREE.Vector3(0, 1, 0), 0), []);
|
||||
const { socket } = useSocketStore();
|
||||
const { addEvent } = useEventsStore();
|
||||
const { projectId } = useParams();
|
||||
const { assets, addAsset } = useAssetsStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (!camera || !scene || toggleView) return;
|
||||
const canvasElement = gl.domElement;
|
||||
canvasElement.tabIndex = 0;
|
||||
|
||||
let isMoving = false;
|
||||
|
||||
const onPointerDown = () => {
|
||||
isMoving = false;
|
||||
};
|
||||
|
||||
const onPointerMove = () => {
|
||||
isMoving = true;
|
||||
};
|
||||
|
||||
const onPointerUp = (event: PointerEvent) => {
|
||||
if (!isMoving && duplicatedObjects.length > 0 && event.button === 0 && movedObjects.length === 0 && rotatedObjects.length === 0) {
|
||||
event.preventDefault();
|
||||
addDuplicatedAssets();
|
||||
}
|
||||
};
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
const keyCombination = detectModifierKeys(event);
|
||||
|
||||
if (keyCombination === "Ctrl+D" && selectedAssets.length > 0 && duplicatedObjects.length === 0 && movedObjects.length === 0 && rotatedObjects.length === 0) {
|
||||
duplicateSelection();
|
||||
}
|
||||
};
|
||||
|
||||
if (!toggleView) {
|
||||
canvasElement.addEventListener("pointerdown", onPointerDown);
|
||||
canvasElement.addEventListener("pointermove", onPointerMove);
|
||||
canvasElement.addEventListener("pointerup", onPointerUp);
|
||||
canvasElement.addEventListener("keydown", onKeyDown);
|
||||
}
|
||||
|
||||
return () => {
|
||||
canvasElement.removeEventListener("pointerdown", onPointerDown);
|
||||
canvasElement.removeEventListener("pointermove", onPointerMove);
|
||||
canvasElement.removeEventListener("pointerup", onPointerUp);
|
||||
canvasElement.removeEventListener("keydown", onKeyDown);
|
||||
};
|
||||
|
||||
}, [assets, camera, controls, scene, toggleView, selectedAssets, duplicatedObjects, movedObjects, socket, rotatedObjects]);
|
||||
|
||||
useFrame(() => {
|
||||
if (duplicatedObjects.length > 0) {
|
||||
const intersectionPoint = new THREE.Vector3();
|
||||
raycaster.setFromCamera(pointer, camera);
|
||||
const point = raycaster.ray.intersectPlane(plane, intersectionPoint);
|
||||
if (point) {
|
||||
const position = new THREE.Vector3();
|
||||
if (boundingBoxRef.current) {
|
||||
boundingBoxRef.current?.getWorldPosition(position)
|
||||
selectionGroup.current.position.set(point.x - (position.x - selectionGroup.current.position.x), selectionGroup.current.position.y, point.z - (position.z - selectionGroup.current.position.z));
|
||||
} else {
|
||||
const box = new THREE.Box3();
|
||||
duplicatedObjects.forEach((obj: THREE.Object3D) => box.expandByObject(obj.clone()));
|
||||
const center = new THREE.Vector3();
|
||||
box.getCenter(center);
|
||||
selectionGroup.current.position.set(point.x - (center.x - selectionGroup.current.position.x), selectionGroup.current.position.y, point.z - (center.z - selectionGroup.current.position.z));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const duplicateSelection = () => {
|
||||
if (selectedAssets.length > 0 && duplicatedObjects.length === 0) {
|
||||
const newClones = selectedAssets.map((asset: any) => {
|
||||
const clone = asset.clone();
|
||||
clone.position.copy(asset.position);
|
||||
return clone;
|
||||
});
|
||||
|
||||
selectionGroup.current.add(...newClones);
|
||||
setDuplicatedObjects(newClones);
|
||||
|
||||
const intersectionPoint = new THREE.Vector3();
|
||||
raycaster.setFromCamera(pointer, camera);
|
||||
const point = raycaster.ray.intersectPlane(plane, intersectionPoint);
|
||||
|
||||
if (point) {
|
||||
const position = new THREE.Vector3();
|
||||
boundingBoxRef.current?.getWorldPosition(position)
|
||||
selectionGroup.current.position.set(point.x - (position.x - selectionGroup.current.position.x), selectionGroup.current.position.y, point.z - (position.z - selectionGroup.current.position.z));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const addDuplicatedAssets = () => {
|
||||
if (duplicatedObjects.length === 0) return;
|
||||
const email = localStorage.getItem("email");
|
||||
const organization = email ? email.split("@")[1].split(".")[0] : "default";
|
||||
|
||||
duplicatedObjects.forEach(async (obj: THREE.Object3D) => {
|
||||
if (obj) {
|
||||
const worldPosition = new THREE.Vector3();
|
||||
obj.getWorldPosition(worldPosition);
|
||||
obj.position.copy(worldPosition);
|
||||
|
||||
const newFloorItem: Types.FloorItemType = {
|
||||
modelUuid: THREE.MathUtils.generateUUID(),
|
||||
modelName: obj.userData.modelName,
|
||||
modelfileID: obj.userData.assetId,
|
||||
position: [worldPosition.x, worldPosition.y, worldPosition.z],
|
||||
rotation: { x: obj.rotation.x, y: obj.rotation.y, z: obj.rotation.z, },
|
||||
isLocked: false,
|
||||
isVisible: true,
|
||||
};
|
||||
|
||||
let updatedEventData = null;
|
||||
if (obj.userData.eventData) {
|
||||
updatedEventData = JSON.parse(JSON.stringify(obj.userData.eventData));
|
||||
updatedEventData.modelUuid = newFloorItem.modelUuid;
|
||||
|
||||
const eventData: any = {
|
||||
type: obj.userData.eventData.type,
|
||||
};
|
||||
|
||||
if (obj.userData.eventData.type === "Conveyor") {
|
||||
const ConveyorEvent: ConveyorEventSchema = {
|
||||
modelUuid: newFloorItem.modelUuid,
|
||||
modelName: newFloorItem.modelName,
|
||||
position: newFloorItem.position,
|
||||
rotation: [newFloorItem.rotation.x, newFloorItem.rotation.y, newFloorItem.rotation.z],
|
||||
state: "idle",
|
||||
type: 'transfer',
|
||||
speed: 1,
|
||||
points: updatedEventData.points.map((point: any, index: number) => ({
|
||||
uuid: THREE.MathUtils.generateUUID(),
|
||||
position: [point.position[0], point.position[1], point.position[2]],
|
||||
rotation: [point.rotation[0], point.rotation[1], point.rotation[2]],
|
||||
action: {
|
||||
actionUuid: THREE.MathUtils.generateUUID(),
|
||||
actionName: `Action 1`,
|
||||
actionType: 'default',
|
||||
material: 'Default Material',
|
||||
delay: 0,
|
||||
spawnInterval: 5,
|
||||
spawnCount: 1,
|
||||
triggers: []
|
||||
}
|
||||
}))
|
||||
};
|
||||
addEvent(ConveyorEvent);
|
||||
eventData.points = ConveyorEvent.points.map(point => ({
|
||||
uuid: point.uuid,
|
||||
position: point.position,
|
||||
rotation: point.rotation
|
||||
}));
|
||||
|
||||
} else if (obj.userData.eventData.type === "Vehicle") {
|
||||
const vehicleEvent: VehicleEventSchema = {
|
||||
modelUuid: newFloorItem.modelUuid,
|
||||
modelName: newFloorItem.modelName,
|
||||
position: newFloorItem.position,
|
||||
rotation: [newFloorItem.rotation.x, newFloorItem.rotation.y, newFloorItem.rotation.z],
|
||||
state: "idle",
|
||||
type: "vehicle",
|
||||
speed: 1,
|
||||
point: {
|
||||
uuid: THREE.MathUtils.generateUUID(),
|
||||
position: [updatedEventData.point.position[0], updatedEventData.point.position[1], updatedEventData.point.position[2]],
|
||||
rotation: [updatedEventData.point.rotation[0], updatedEventData.point.rotation[1], updatedEventData.point.rotation[2]],
|
||||
action: {
|
||||
actionUuid: THREE.MathUtils.generateUUID(),
|
||||
actionName: "Action 1",
|
||||
actionType: "travel",
|
||||
unLoadDuration: 5,
|
||||
loadCapacity: 1,
|
||||
steeringAngle: 0,
|
||||
pickUpPoint: null,
|
||||
unLoadPoint: null,
|
||||
triggers: []
|
||||
}
|
||||
}
|
||||
};
|
||||
addEvent(vehicleEvent);
|
||||
eventData.point = {
|
||||
uuid: vehicleEvent.point.uuid,
|
||||
position: vehicleEvent.point.position,
|
||||
rotation: vehicleEvent.point.rotation
|
||||
};
|
||||
|
||||
} else if (obj.userData.eventData.type === "ArmBot") {
|
||||
const roboticArmEvent: RoboticArmEventSchema = {
|
||||
modelUuid: newFloorItem.modelUuid,
|
||||
modelName: newFloorItem.modelName,
|
||||
position: newFloorItem.position,
|
||||
rotation: [newFloorItem.rotation.x, newFloorItem.rotation.y, newFloorItem.rotation.z],
|
||||
state: "idle",
|
||||
type: "roboticArm",
|
||||
speed: 1,
|
||||
point: {
|
||||
uuid: THREE.MathUtils.generateUUID(),
|
||||
position: [updatedEventData.point.position[0], updatedEventData.point.position[1], updatedEventData.point.position[2]],
|
||||
rotation: [updatedEventData.point.rotation[0], updatedEventData.point.rotation[1], updatedEventData.point.rotation[2]],
|
||||
actions: [
|
||||
{
|
||||
actionUuid: THREE.MathUtils.generateUUID(),
|
||||
actionName: "Action 1",
|
||||
actionType: "pickAndPlace",
|
||||
process: {
|
||||
startPoint: null,
|
||||
endPoint: null
|
||||
},
|
||||
triggers: []
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
addEvent(roboticArmEvent);
|
||||
eventData.point = {
|
||||
uuid: roboticArmEvent.point.uuid,
|
||||
position: roboticArmEvent.point.position,
|
||||
rotation: roboticArmEvent.point.rotation
|
||||
};
|
||||
|
||||
} else if (obj.userData.eventData.type === "StaticMachine") {
|
||||
const machineEvent: MachineEventSchema = {
|
||||
modelUuid: newFloorItem.modelUuid,
|
||||
modelName: newFloorItem.modelName,
|
||||
position: newFloorItem.position,
|
||||
rotation: [newFloorItem.rotation.x, newFloorItem.rotation.y, newFloorItem.rotation.z],
|
||||
state: "idle",
|
||||
type: "machine",
|
||||
point: {
|
||||
uuid: THREE.MathUtils.generateUUID(),
|
||||
position: [updatedEventData.point.position[0], updatedEventData.point.position[1], updatedEventData.point.position[2]],
|
||||
rotation: [updatedEventData.point.rotation[0], updatedEventData.point.rotation[1], updatedEventData.point.rotation[2]],
|
||||
action: {
|
||||
actionUuid: THREE.MathUtils.generateUUID(),
|
||||
actionName: "Action 1",
|
||||
actionType: "process",
|
||||
processTime: 10,
|
||||
swapMaterial: "Default Material",
|
||||
triggers: []
|
||||
}
|
||||
}
|
||||
};
|
||||
addEvent(machineEvent);
|
||||
eventData.point = {
|
||||
uuid: machineEvent.point.uuid,
|
||||
position: machineEvent.point.position,
|
||||
rotation: machineEvent.point.rotation
|
||||
};
|
||||
} else if (obj.userData.eventData.type === "Storage") {
|
||||
const storageEvent: StorageEventSchema = {
|
||||
modelUuid: newFloorItem.modelUuid,
|
||||
modelName: newFloorItem.modelName,
|
||||
position: newFloorItem.position,
|
||||
rotation: [newFloorItem.rotation.x, newFloorItem.rotation.y, newFloorItem.rotation.z],
|
||||
state: "idle",
|
||||
type: "storageUnit",
|
||||
point: {
|
||||
uuid: THREE.MathUtils.generateUUID(),
|
||||
position: [updatedEventData.point.position[0], updatedEventData.point.position[1], updatedEventData.point.position[2]],
|
||||
rotation: [updatedEventData.point.rotation[0], updatedEventData.point.rotation[1], updatedEventData.point.rotation[2]],
|
||||
action: {
|
||||
actionUuid: THREE.MathUtils.generateUUID(),
|
||||
actionName: "Action 1",
|
||||
actionType: "store",
|
||||
storageCapacity: 10,
|
||||
triggers: []
|
||||
}
|
||||
}
|
||||
}
|
||||
addEvent(storageEvent);
|
||||
eventData.point = {
|
||||
uuid: storageEvent.point.uuid,
|
||||
position: storageEvent.point.position,
|
||||
rotation: storageEvent.point.rotation
|
||||
};
|
||||
}
|
||||
|
||||
newFloorItem.eventData = eventData;
|
||||
|
||||
const userId = localStorage.getItem("userId");
|
||||
//REST
|
||||
|
||||
// await setFloorItemApi(
|
||||
// organization,
|
||||
// obj.uuid,
|
||||
// obj.userData.name,
|
||||
// [worldPosition.x, worldPosition.y, worldPosition.z],
|
||||
// { "x": obj.rotation.x, "y": obj.rotation.y, "z": obj.rotation.z },
|
||||
// obj.userData.modelId,
|
||||
// false,
|
||||
// true,
|
||||
// );
|
||||
|
||||
//SOCKET
|
||||
|
||||
const data = {
|
||||
organization,
|
||||
modelUuid: newFloorItem.modelUuid,
|
||||
modelName: newFloorItem.modelName,
|
||||
modelfileID: newFloorItem.modelfileID,
|
||||
position: newFloorItem.position,
|
||||
rotation: { x: obj.rotation.x, y: obj.rotation.y, z: obj.rotation.z },
|
||||
isLocked: false,
|
||||
isVisible: true,
|
||||
socketId: socket.id,
|
||||
eventData: eventData,
|
||||
projectId,
|
||||
userId
|
||||
};
|
||||
|
||||
// console.log('data: ', data);
|
||||
socket.emit("v1:model-asset:add", data);
|
||||
|
||||
const asset: Asset = {
|
||||
modelUuid: data.modelUuid,
|
||||
modelName: data.modelName,
|
||||
assetId: data.modelfileID,
|
||||
position: data.position,
|
||||
rotation: [data.rotation.x, data.rotation.y, data.rotation.z],
|
||||
isLocked: data.isLocked,
|
||||
isCollidable: false,
|
||||
isVisible: data.isVisible,
|
||||
opacity: 1,
|
||||
eventData: data.eventData
|
||||
}
|
||||
|
||||
addAsset(asset);
|
||||
|
||||
} else {
|
||||
|
||||
const userId = localStorage.getItem("userId");
|
||||
//REST
|
||||
|
||||
// await setFloorItemApi(
|
||||
// organization,
|
||||
// obj.uuid,
|
||||
// obj.userData.name,
|
||||
// [worldPosition.x, worldPosition.y, worldPosition.z],
|
||||
// { "x": obj.rotation.x, "y": obj.rotation.y, "z": obj.rotation.z },
|
||||
// obj.userData.modelId,
|
||||
// false,
|
||||
// true,
|
||||
// );
|
||||
|
||||
//SOCKET
|
||||
|
||||
const data = {
|
||||
organization,
|
||||
modelUuid: newFloorItem.modelUuid,
|
||||
modelName: newFloorItem.modelName,
|
||||
modelfileID: newFloorItem.modelfileID,
|
||||
position: newFloorItem.position,
|
||||
rotation: { x: obj.rotation.x, y: obj.rotation.y, z: obj.rotation.z },
|
||||
isLocked: false,
|
||||
isVisible: true,
|
||||
socketId: socket.id,
|
||||
userId,
|
||||
projectId
|
||||
};
|
||||
|
||||
// console.log('data: ', data);/
|
||||
socket.emit("v1:model-asset:add", data);
|
||||
|
||||
const asset: Asset = {
|
||||
modelUuid: data.modelUuid,
|
||||
modelName: data.modelName,
|
||||
assetId: data.modelfileID,
|
||||
position: data.position,
|
||||
rotation: [data.rotation.x, data.rotation.y, data.rotation.z],
|
||||
isLocked: data.isLocked,
|
||||
isCollidable: false,
|
||||
isVisible: data.isVisible,
|
||||
opacity: 1,
|
||||
}
|
||||
|
||||
addAsset(asset);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
echo.success("Object duplicated!");
|
||||
clearSelection();
|
||||
}
|
||||
|
||||
const clearSelection = () => {
|
||||
selectionGroup.current.children = [];
|
||||
selectionGroup.current.position.set(0, 0, 0);
|
||||
selectionGroup.current.rotation.set(0, 0, 0);
|
||||
setMovedObjects([]);
|
||||
setpastedObjects([]);
|
||||
setDuplicatedObjects([]);
|
||||
setRotatedObjects([]);
|
||||
setSelectedAssets([]);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default DuplicationControls;
|
||||
@@ -0,0 +1,349 @@
|
||||
import * as THREE from "three";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useFrame, useThree } from "@react-three/fiber";
|
||||
import { useSelectedAssets, useSocketStore, useToggleView, } from "../../../../store/builder/store";
|
||||
// import { setFloorItemApi } from '../../../../services/factoryBuilder/assest/floorAsset/setFloorItemApi';
|
||||
import * as Types from "../../../../types/world/worldTypes";
|
||||
import { detectModifierKeys } from "../../../../utils/shortcutkeys/detectModifierKeys";
|
||||
import { useEventsStore } from "../../../../store/simulation/useEventsStore";
|
||||
import { useProductStore } from "../../../../store/simulation/useProductStore";
|
||||
import { upsertProductOrEventApi } from "../../../../services/simulation/products/UpsertProductOrEventApi";
|
||||
import { snapControls } from "../../../../utils/handleSnap";
|
||||
import DistanceFindingControls from "./distanceFindingControls";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useAssetsStore } from "../../../../store/builder/useAssetStore";
|
||||
import { useProductContext } from "../../../simulation/products/productContext";
|
||||
|
||||
function MoveControls({
|
||||
movedObjects,
|
||||
setMovedObjects,
|
||||
pastedObjects,
|
||||
setpastedObjects,
|
||||
duplicatedObjects,
|
||||
setDuplicatedObjects,
|
||||
selectionGroup,
|
||||
rotatedObjects,
|
||||
setRotatedObjects,
|
||||
boundingBoxRef,
|
||||
}: any) {
|
||||
const { camera, controls, gl, scene, pointer, raycaster } = useThree();
|
||||
const plane = useMemo(() => new THREE.Plane(new THREE.Vector3(0, 1, 0), 0), []);
|
||||
|
||||
const { toggleView } = useToggleView();
|
||||
const { selectedAssets, setSelectedAssets } = useSelectedAssets();
|
||||
const { selectedProductStore } = useProductContext();
|
||||
const { selectedProduct } = selectedProductStore();
|
||||
const { socket } = useSocketStore();
|
||||
const [keyEvent, setKeyEvent] = useState<"Ctrl" | "Shift" | "Ctrl+Shift" | "">("");
|
||||
const email = localStorage.getItem("email");
|
||||
const organization = email!.split("@")[1].split(".")[0];
|
||||
const userId = localStorage.getItem("userId");
|
||||
const { projectId } = useParams();
|
||||
const { updateAsset } = useAssetsStore();
|
||||
const AssetGroup = useRef<THREE.Group | undefined>(undefined);
|
||||
|
||||
const updateBackend = (
|
||||
productName: string,
|
||||
productUuid: string,
|
||||
projectId: string,
|
||||
eventData: EventsSchema
|
||||
) => {
|
||||
upsertProductOrEventApi({
|
||||
productName: productName,
|
||||
productUuid: productUuid,
|
||||
projectId: projectId,
|
||||
eventDatas: eventData,
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!camera || !scene || toggleView) return;
|
||||
|
||||
const canvasElement = gl.domElement;
|
||||
canvasElement.tabIndex = 0;
|
||||
|
||||
const itemsGroup: any = scene.getObjectByName("Asset Group");
|
||||
AssetGroup.current = itemsGroup;
|
||||
|
||||
if (!AssetGroup.current) {
|
||||
console.error("Asset Group not found in the scene.");
|
||||
return;
|
||||
}
|
||||
|
||||
let isMoving = false;
|
||||
|
||||
const onPointerDown = () => {
|
||||
isMoving = false;
|
||||
};
|
||||
|
||||
const onPointerMove = () => {
|
||||
isMoving = true;
|
||||
};
|
||||
|
||||
const onKeyUp = (event: KeyboardEvent) => {
|
||||
const isModifierKey = event.key === "Control" || event.key === "Shift";
|
||||
|
||||
if (isModifierKey) {
|
||||
setKeyEvent("");
|
||||
}
|
||||
};
|
||||
|
||||
const onPointerUp = (event: PointerEvent) => {
|
||||
if (!isMoving && movedObjects.length > 0 && event.button === 0) {
|
||||
event.preventDefault();
|
||||
placeMovedAssets();
|
||||
}
|
||||
if (!isMoving && movedObjects.length > 0 && event.button === 2) {
|
||||
event.preventDefault();
|
||||
|
||||
clearSelection();
|
||||
movedObjects.forEach((asset: any) => {
|
||||
if (AssetGroup.current) {
|
||||
AssetGroup.current.attach(asset);
|
||||
}
|
||||
});
|
||||
|
||||
setMovedObjects([]);
|
||||
}
|
||||
setKeyEvent("");
|
||||
};
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
const keyCombination = detectModifierKeys(event);
|
||||
|
||||
if (pastedObjects.length > 0 || duplicatedObjects.length > 0 || rotatedObjects.length > 0)
|
||||
return;
|
||||
|
||||
if (keyCombination === "Ctrl" || keyCombination === "Ctrl+Shift" || keyCombination === "Shift") {
|
||||
setKeyEvent(keyCombination);
|
||||
} else {
|
||||
setKeyEvent("");
|
||||
}
|
||||
|
||||
if (keyCombination === "G") {
|
||||
if (selectedAssets.length > 0) {
|
||||
moveAssets();
|
||||
}
|
||||
}
|
||||
|
||||
if (keyCombination === "ESCAPE") {
|
||||
event.preventDefault();
|
||||
|
||||
clearSelection();
|
||||
movedObjects.forEach((asset: any) => {
|
||||
if (AssetGroup.current) {
|
||||
AssetGroup.current.attach(asset);
|
||||
}
|
||||
});
|
||||
|
||||
setMovedObjects([]);
|
||||
}
|
||||
};
|
||||
|
||||
if (!toggleView) {
|
||||
canvasElement.addEventListener("pointerdown", onPointerDown);
|
||||
canvasElement.addEventListener("pointermove", onPointerMove);
|
||||
canvasElement.addEventListener("pointerup", onPointerUp);
|
||||
canvasElement.addEventListener("keydown", onKeyDown);
|
||||
canvasElement?.addEventListener("keyup", onKeyUp);
|
||||
}
|
||||
|
||||
return () => {
|
||||
canvasElement.removeEventListener("pointerdown", onPointerDown);
|
||||
canvasElement.removeEventListener("pointermove", onPointerMove);
|
||||
canvasElement.removeEventListener("pointerup", onPointerUp);
|
||||
canvasElement.removeEventListener("keydown", onKeyDown);
|
||||
canvasElement?.removeEventListener("keyup", onKeyUp);
|
||||
};
|
||||
}, [camera, controls, scene, toggleView, selectedAssets, socket, pastedObjects, duplicatedObjects, movedObjects, rotatedObjects, keyEvent,]);
|
||||
|
||||
let moveSpeed = keyEvent === "Ctrl" || "Ctrl+Shift" ? 1 : 0.25;
|
||||
|
||||
useFrame(() => {
|
||||
if (movedObjects.length > 0) {
|
||||
const intersectionPoint = new THREE.Vector3();
|
||||
raycaster.setFromCamera(pointer, camera);
|
||||
const point = raycaster.ray.intersectPlane(plane, intersectionPoint);
|
||||
|
||||
if (point) {
|
||||
let targetX = point.x;
|
||||
let targetZ = point.z;
|
||||
|
||||
if (keyEvent === "Ctrl") {
|
||||
targetX = snapControls(targetX, "Ctrl");
|
||||
targetZ = snapControls(targetZ, "Ctrl");
|
||||
}
|
||||
|
||||
// else if (keyEvent === "Ctrl+Shift") {
|
||||
// targetX = snapControls(targetX, "Ctrl+Shift");
|
||||
// targetZ = snapControls(targetZ, "Ctrl+Shift");
|
||||
// } else if (keyEvent === "Shift") {
|
||||
// targetX = snapControls(targetX, "Shift");
|
||||
// targetZ = snapControls(targetZ, "Shift");
|
||||
// } else {
|
||||
// }
|
||||
|
||||
const position = new THREE.Vector3();
|
||||
|
||||
if (boundingBoxRef.current) {
|
||||
boundingBoxRef.current.getWorldPosition(position);
|
||||
selectionGroup.current.position.lerp(
|
||||
new THREE.Vector3(
|
||||
targetX - (position.x - selectionGroup.current.position.x),
|
||||
selectionGroup.current.position.y,
|
||||
targetZ - (position.z - selectionGroup.current.position.z)
|
||||
),
|
||||
moveSpeed
|
||||
);
|
||||
} else {
|
||||
const box = new THREE.Box3();
|
||||
movedObjects.forEach((obj: THREE.Object3D) =>
|
||||
box.expandByObject(obj)
|
||||
);
|
||||
const center = new THREE.Vector3();
|
||||
box.getCenter(center);
|
||||
|
||||
selectionGroup.current.position.lerp(
|
||||
new THREE.Vector3(
|
||||
targetX - (center.x - selectionGroup.current.position.x),
|
||||
selectionGroup.current.position.y,
|
||||
targetZ - (center.z - selectionGroup.current.position.z)
|
||||
),
|
||||
moveSpeed
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const moveAssets = () => {
|
||||
setMovedObjects(selectedAssets);
|
||||
selectedAssets.forEach((asset: any) => {
|
||||
selectionGroup.current.attach(asset);
|
||||
});
|
||||
};
|
||||
|
||||
const placeMovedAssets = () => {
|
||||
if (movedObjects.length === 0) return;
|
||||
|
||||
movedObjects.forEach(async (obj: THREE.Object3D) => {
|
||||
if (obj && AssetGroup.current) {
|
||||
const worldPosition = new THREE.Vector3();
|
||||
obj.getWorldPosition(worldPosition);
|
||||
|
||||
selectionGroup.current.remove(obj);
|
||||
obj.position.copy(worldPosition);
|
||||
|
||||
const newFloorItem: Types.FloorItemType = {
|
||||
modelUuid: obj.userData.modelUuid,
|
||||
modelName: obj.userData.modelName,
|
||||
modelfileID: obj.userData.assetId,
|
||||
position: [worldPosition.x, worldPosition.y, worldPosition.z],
|
||||
rotation: { x: obj.rotation.x, y: obj.rotation.y, z: obj.rotation.z },
|
||||
isLocked: false,
|
||||
isVisible: true,
|
||||
};
|
||||
|
||||
if (obj.userData.eventData) {
|
||||
const eventData = useEventsStore.getState().getEventByModelUuid(obj.userData.modelUuid);
|
||||
const productData = useProductStore.getState().getEventByModelUuid(selectedProduct.productUuid, obj.userData.modelUuid);
|
||||
|
||||
if (eventData) {
|
||||
useEventsStore.getState().updateEvent(obj.userData.modelUuid, {
|
||||
position: [worldPosition.x, worldPosition.y, worldPosition.z],
|
||||
rotation: [obj.rotation.x, obj.rotation.y, obj.rotation.z],
|
||||
});
|
||||
}
|
||||
|
||||
if (productData) {
|
||||
const event = useProductStore
|
||||
.getState()
|
||||
.updateEvent(
|
||||
selectedProduct.productUuid,
|
||||
obj.userData.modelUuid,
|
||||
{
|
||||
position: [worldPosition.x, worldPosition.y, worldPosition.z],
|
||||
rotation: [obj.rotation.x, obj.rotation.y, obj.rotation.z],
|
||||
}
|
||||
);
|
||||
|
||||
if (event) {
|
||||
updateBackend(
|
||||
selectedProduct.productName,
|
||||
selectedProduct.productUuid,
|
||||
projectId || '',
|
||||
event
|
||||
);
|
||||
}
|
||||
|
||||
newFloorItem.eventData = eventData;
|
||||
}
|
||||
}
|
||||
|
||||
updateAsset(obj.userData.modelUuid, {
|
||||
position: [worldPosition.x, worldPosition.y, worldPosition.z],
|
||||
rotation: [obj.rotation.x, obj.rotation.y, obj.rotation.z],
|
||||
});
|
||||
|
||||
//REST
|
||||
|
||||
// await setFloorItemApi(
|
||||
// organization,
|
||||
// obj.uuid,
|
||||
// obj.userData.name,
|
||||
// [worldPosition.x, worldPosition.y, worldPosition.z],
|
||||
// { "x": obj.rotation.x, "y": obj.rotation.y, "z": obj.rotation.z },
|
||||
// obj.userData.modelId,
|
||||
// false,
|
||||
// true,
|
||||
// );
|
||||
|
||||
//SOCKET
|
||||
|
||||
const data = {
|
||||
organization,
|
||||
modelUuid: newFloorItem.modelUuid,
|
||||
modelName: newFloorItem.modelName,
|
||||
modelfileID: newFloorItem.modelfileID,
|
||||
position: newFloorItem.position,
|
||||
rotation: { x: obj.rotation.x, y: obj.rotation.y, z: obj.rotation.z },
|
||||
isLocked: false,
|
||||
isVisible: true,
|
||||
socketId: socket.id,
|
||||
projectId,
|
||||
userId
|
||||
};
|
||||
|
||||
// console.log('data: ', data);
|
||||
socket.emit("v1:model-asset:add", data);
|
||||
|
||||
AssetGroup.current.add(obj);
|
||||
}
|
||||
});
|
||||
|
||||
echo.success("Object moved!");
|
||||
|
||||
clearSelection();
|
||||
};
|
||||
|
||||
const clearSelection = () => {
|
||||
selectionGroup.current.children = [];
|
||||
selectionGroup.current.position.set(0, 0, 0);
|
||||
selectionGroup.current.rotation.set(0, 0, 0);
|
||||
setpastedObjects([]);
|
||||
setDuplicatedObjects([]);
|
||||
setMovedObjects([]);
|
||||
setRotatedObjects([]);
|
||||
setSelectedAssets([]);
|
||||
setKeyEvent("");
|
||||
};
|
||||
|
||||
return (
|
||||
<DistanceFindingControls
|
||||
boundingBoxRef={boundingBoxRef}
|
||||
object={movedObjects.length}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default MoveControls;
|
||||
@@ -0,0 +1,301 @@
|
||||
import * as THREE from "three";
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import { useFrame, useThree } from "@react-three/fiber";
|
||||
import { useSelectedAssets, useSocketStore, useToggleView } from "../../../../store/builder/store";
|
||||
// import { setFloorItemApi } from '../../../../services/factoryBuilder/assest/floorAsset/setFloorItemApi';
|
||||
import * as Types from "../../../../types/world/worldTypes";
|
||||
import { useEventsStore } from "../../../../store/simulation/useEventsStore";
|
||||
import { useProductStore } from "../../../../store/simulation/useProductStore";
|
||||
import { upsertProductOrEventApi } from "../../../../services/simulation/products/UpsertProductOrEventApi";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useAssetsStore } from "../../../../store/builder/useAssetStore";
|
||||
import { useProductContext } from "../../../simulation/products/productContext";
|
||||
|
||||
function RotateControls({
|
||||
rotatedObjects,
|
||||
setRotatedObjects,
|
||||
movedObjects,
|
||||
setMovedObjects,
|
||||
pastedObjects,
|
||||
setpastedObjects,
|
||||
duplicatedObjects,
|
||||
setDuplicatedObjects,
|
||||
selectionGroup
|
||||
}: any) {
|
||||
|
||||
const { camera, controls, gl, scene, pointer, raycaster } = useThree();
|
||||
const plane = useMemo(() => new THREE.Plane(new THREE.Vector3(0, 1, 0), 0), []);
|
||||
|
||||
const { toggleView } = useToggleView();
|
||||
const { selectedAssets, setSelectedAssets } = useSelectedAssets();
|
||||
const { selectedProductStore } = useProductContext();
|
||||
const { selectedProduct } = selectedProductStore();
|
||||
const { socket } = useSocketStore();
|
||||
|
||||
const email = localStorage.getItem('email')
|
||||
const organization = (email?.split("@")[1])?.split(".")[0] ?? null;
|
||||
const userId = localStorage.getItem("userId");
|
||||
const { projectId } = useParams();
|
||||
const { updateAsset } = useAssetsStore();
|
||||
const AssetGroup = useRef<THREE.Group | undefined>(undefined);
|
||||
|
||||
const updateBackend = (
|
||||
productName: string,
|
||||
productUuid: string,
|
||||
projectId: string,
|
||||
eventData: EventsSchema
|
||||
) => {
|
||||
upsertProductOrEventApi({
|
||||
productName: productName,
|
||||
productUuid: productUuid,
|
||||
projectId: projectId,
|
||||
eventDatas: eventData
|
||||
})
|
||||
}
|
||||
|
||||
const prevPointerPosition = useRef<THREE.Vector2 | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!camera || !scene || toggleView) return;
|
||||
|
||||
const canvasElement = gl.domElement;
|
||||
canvasElement.tabIndex = 0;
|
||||
|
||||
const itemsGroup: any = scene.getObjectByName("Asset Group");
|
||||
AssetGroup.current = itemsGroup;
|
||||
|
||||
if (!AssetGroup.current) {
|
||||
console.error("Asset Group not found in the scene.");
|
||||
return;
|
||||
}
|
||||
|
||||
let isMoving = false;
|
||||
|
||||
const onPointerDown = () => {
|
||||
isMoving = false;
|
||||
};
|
||||
|
||||
const onPointerMove = () => {
|
||||
isMoving = true;
|
||||
};
|
||||
|
||||
const onPointerUp = (event: PointerEvent) => {
|
||||
if (!isMoving && rotatedObjects.length > 0 && event.button === 0) {
|
||||
event.preventDefault();
|
||||
placeRotatedAssets();
|
||||
}
|
||||
if (!isMoving && rotatedObjects.length > 0 && event.button === 2) {
|
||||
event.preventDefault();
|
||||
|
||||
clearSelection();
|
||||
rotatedObjects.forEach((asset: any) => {
|
||||
if (AssetGroup.current) {
|
||||
AssetGroup.current.attach(asset);
|
||||
}
|
||||
});
|
||||
|
||||
setRotatedObjects([]);
|
||||
}
|
||||
};
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (pastedObjects.length > 0 || duplicatedObjects.length > 0 || movedObjects.length > 0) return;
|
||||
if (event.key.toLowerCase() === "r") {
|
||||
if (selectedAssets.length > 0) {
|
||||
rotateAssets();
|
||||
}
|
||||
}
|
||||
if (event.key.toLowerCase() === "escape") {
|
||||
event.preventDefault();
|
||||
|
||||
clearSelection();
|
||||
rotatedObjects.forEach((asset: any) => {
|
||||
if (AssetGroup.current) {
|
||||
AssetGroup.current.attach(asset);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
setRotatedObjects([]);
|
||||
}
|
||||
};
|
||||
|
||||
if (!toggleView) {
|
||||
canvasElement.addEventListener("pointerdown", onPointerDown);
|
||||
canvasElement.addEventListener("pointermove", onPointerMove);
|
||||
canvasElement.addEventListener("pointerup", onPointerUp);
|
||||
canvasElement.addEventListener("keydown", onKeyDown);
|
||||
}
|
||||
|
||||
return () => {
|
||||
canvasElement.removeEventListener("pointerdown", onPointerDown);
|
||||
canvasElement.removeEventListener("pointermove", onPointerMove);
|
||||
canvasElement.removeEventListener("pointerup", onPointerUp);
|
||||
canvasElement.removeEventListener("keydown", onKeyDown);
|
||||
};
|
||||
}, [camera, controls, scene, toggleView, selectedAssets, socket, pastedObjects, duplicatedObjects, rotatedObjects, movedObjects]);
|
||||
|
||||
useFrame(() => {
|
||||
if (rotatedObjects.length > 0) {
|
||||
const intersectionPoint = new THREE.Vector3();
|
||||
raycaster.setFromCamera(pointer, camera);
|
||||
const point = raycaster.ray.intersectPlane(plane, intersectionPoint);
|
||||
|
||||
if (point && prevPointerPosition.current) {
|
||||
const box = new THREE.Box3();
|
||||
rotatedObjects.forEach((obj: THREE.Object3D) => box.expandByObject(obj));
|
||||
const center = new THREE.Vector3();
|
||||
box.getCenter(center);
|
||||
|
||||
const delta = new THREE.Vector3().subVectors(point, center);
|
||||
const prevPointerPosition3D = new THREE.Vector3(prevPointerPosition.current.x, 0, prevPointerPosition.current.y);
|
||||
|
||||
const angle = Math.atan2(delta.z, delta.x) - Math.atan2(prevPointerPosition3D.z - center.z, prevPointerPosition3D.x - center.x);
|
||||
|
||||
selectionGroup.current.rotation.y += -angle;
|
||||
|
||||
selectionGroup.current.position.sub(center);
|
||||
selectionGroup.current.position.applyAxisAngle(new THREE.Vector3(0, 1, 0), -angle);
|
||||
selectionGroup.current.position.add(center);
|
||||
|
||||
prevPointerPosition.current = new THREE.Vector2(point.x, point.z);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const rotateAssets = () => {
|
||||
const box = new THREE.Box3();
|
||||
selectedAssets.forEach((asset: any) => box.expandByObject(asset));
|
||||
const center = new THREE.Vector3();
|
||||
box.getCenter(center);
|
||||
|
||||
const intersectionPoint = new THREE.Vector3();
|
||||
raycaster.setFromCamera(pointer, camera);
|
||||
const point = raycaster.ray.intersectPlane(plane, intersectionPoint);
|
||||
|
||||
if (point) {
|
||||
prevPointerPosition.current = new THREE.Vector2(point.x, point.z);
|
||||
}
|
||||
|
||||
selectedAssets.forEach((asset: any) => {
|
||||
selectionGroup.current.attach(asset);
|
||||
});
|
||||
|
||||
setRotatedObjects(selectedAssets);
|
||||
};
|
||||
|
||||
const placeRotatedAssets = () => {
|
||||
if (rotatedObjects.length === 0) return;
|
||||
|
||||
rotatedObjects.forEach(async (obj: THREE.Object3D) => {
|
||||
if (obj && AssetGroup.current) {
|
||||
const worldPosition = new THREE.Vector3();
|
||||
const worldQuaternion = new THREE.Quaternion();
|
||||
|
||||
obj.getWorldPosition(worldPosition);
|
||||
obj.getWorldQuaternion(worldQuaternion);
|
||||
|
||||
selectionGroup.current.remove(obj);
|
||||
|
||||
obj.position.copy(worldPosition);
|
||||
obj.quaternion.copy(worldQuaternion);
|
||||
|
||||
const newFloorItem: Types.FloorItemType = {
|
||||
modelUuid: obj.userData.modelUuid,
|
||||
modelName: obj.userData.modelName,
|
||||
modelfileID: obj.userData.assetId,
|
||||
position: [worldPosition.x, worldPosition.y, worldPosition.z],
|
||||
rotation: { x: obj.rotation.x, y: obj.rotation.y, z: obj.rotation.z },
|
||||
isLocked: false,
|
||||
isVisible: true
|
||||
};
|
||||
|
||||
if (obj.userData.eventData) {
|
||||
const eventData = useEventsStore.getState().getEventByModelUuid(obj.userData.modelUuid);
|
||||
const productData = useProductStore.getState().getEventByModelUuid(selectedProductStore.getState().selectedProduct.productUuid, obj.userData.modelUuid);
|
||||
|
||||
if (eventData) {
|
||||
useEventsStore.getState().updateEvent(obj.userData.modelUuid, {
|
||||
position: [worldPosition.x, worldPosition.y, worldPosition.z],
|
||||
rotation: [obj.rotation.x, obj.rotation.y, obj.rotation.z],
|
||||
})
|
||||
}
|
||||
if (productData) {
|
||||
const event = useProductStore.getState().updateEvent(selectedProductStore.getState().selectedProduct.productUuid, obj.userData.modelUuid, {
|
||||
position: [worldPosition.x, worldPosition.y, worldPosition.z],
|
||||
rotation: [obj.rotation.x, obj.rotation.y, obj.rotation.z],
|
||||
})
|
||||
|
||||
if (event && organization) {
|
||||
updateBackend(
|
||||
selectedProduct.productName,
|
||||
selectedProduct.productUuid,
|
||||
projectId || '',
|
||||
event
|
||||
);
|
||||
}
|
||||
|
||||
newFloorItem.eventData = eventData;
|
||||
}
|
||||
}
|
||||
|
||||
updateAsset(obj.userData.modelUuid, {
|
||||
position: [worldPosition.x, worldPosition.y, worldPosition.z],
|
||||
rotation: [obj.rotation.x, obj.rotation.y, obj.rotation.z],
|
||||
});
|
||||
|
||||
//REST
|
||||
|
||||
// await setFloorItemApi(
|
||||
// organization,
|
||||
// obj.uuid,
|
||||
// obj.userData.name,
|
||||
// [worldPosition.x, worldPosition.y, worldPosition.z],
|
||||
// { "x": obj.rotation.x, "y": obj.rotation.y, "z": obj.rotation.z },
|
||||
// obj.userData.modelId,
|
||||
// false,
|
||||
// true,
|
||||
// );
|
||||
|
||||
//SOCKET
|
||||
|
||||
const data = {
|
||||
organization,
|
||||
modelUuid: newFloorItem.modelUuid,
|
||||
modelName: newFloorItem.modelName,
|
||||
modelfileID: newFloorItem.modelfileID,
|
||||
position: newFloorItem.position,
|
||||
rotation: { x: obj.rotation.x, y: obj.rotation.y, z: obj.rotation.z },
|
||||
isLocked: false,
|
||||
isVisible: true,
|
||||
socketId: socket.id,
|
||||
projectId,
|
||||
userId
|
||||
};
|
||||
|
||||
// console.log('data: ', data);
|
||||
socket.emit("v1:model-asset:add", data);
|
||||
|
||||
AssetGroup.current.add(obj);
|
||||
}
|
||||
});
|
||||
echo.success("Object rotated!");
|
||||
|
||||
clearSelection();
|
||||
}
|
||||
|
||||
const clearSelection = () => {
|
||||
selectionGroup.current.children = [];
|
||||
selectionGroup.current.position.set(0, 0, 0);
|
||||
selectionGroup.current.rotation.set(0, 0, 0);
|
||||
setpastedObjects([]);
|
||||
setDuplicatedObjects([]);
|
||||
setMovedObjects([]);
|
||||
setRotatedObjects([]);
|
||||
setSelectedAssets([]);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default RotateControls
|
||||
@@ -0,0 +1,335 @@
|
||||
import * as THREE from "three";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { SelectionBox } from "three/examples/jsm/interactive/SelectionBox";
|
||||
import { SelectionHelper } from "./selectionHelper";
|
||||
import { useFrame, useThree } from "@react-three/fiber";
|
||||
import { useSelectedAssets, useSocketStore, useToggleView, } from "../../../../store/builder/store";
|
||||
import BoundingBox from "./boundingBoxHelper";
|
||||
// import { deleteFloorItem } from '../../../../services/factoryBuilder/assest/floorAsset/deleteFloorItemApi';
|
||||
import * as Types from "../../../../types/world/worldTypes";
|
||||
|
||||
import DuplicationControls from "./duplicationControls";
|
||||
import CopyPasteControls from "./copyPasteControls";
|
||||
import MoveControls from "./moveControls";
|
||||
import RotateControls from "./rotateControls";
|
||||
import useModuleStore from "../../../../store/useModuleStore";
|
||||
import { useEventsStore } from "../../../../store/simulation/useEventsStore";
|
||||
import { useProductStore } from "../../../../store/simulation/useProductStore";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useAssetsStore } from "../../../../store/builder/useAssetStore";
|
||||
|
||||
const SelectionControls: React.FC = () => {
|
||||
const { camera, controls, gl, scene, raycaster, pointer } = useThree();
|
||||
const selectionGroup = useRef() as Types.RefGroup;
|
||||
const { toggleView } = useToggleView();
|
||||
const { selectedAssets, setSelectedAssets } = useSelectedAssets();
|
||||
const [movedObjects, setMovedObjects] = useState<THREE.Object3D[]>([]);
|
||||
const [rotatedObjects, setRotatedObjects] = useState<THREE.Object3D[]>([]);
|
||||
const [copiedObjects, setCopiedObjects] = useState<THREE.Object3D[]>([]);
|
||||
const [pastedObjects, setpastedObjects] = useState<THREE.Object3D[]>([]);
|
||||
const [duplicatedObjects, setDuplicatedObjects] = useState<THREE.Object3D[]>([]);
|
||||
const boundingBoxRef = useRef<THREE.Mesh>();
|
||||
const { activeModule } = useModuleStore();
|
||||
const { socket } = useSocketStore();
|
||||
const { removeAsset } = useAssetsStore();
|
||||
const selectionBox = useMemo(() => new SelectionBox(camera, scene), [camera, scene]);
|
||||
const { projectId } = useParams();
|
||||
|
||||
const isDragging = useRef(false);
|
||||
const isLeftMouseDown = useRef(false);
|
||||
const isSelecting = useRef(false);
|
||||
const isRightClick = useRef(false);
|
||||
const rightClickMoved = useRef(false);
|
||||
const isCtrlSelecting = useRef(false);
|
||||
const isShiftSelecting = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!camera || !scene || toggleView) return;
|
||||
|
||||
const canvasElement = gl.domElement;
|
||||
canvasElement.tabIndex = 0;
|
||||
|
||||
const helper = new SelectionHelper(gl);
|
||||
|
||||
const onPointerDown = (event: PointerEvent) => {
|
||||
if (event.button === 2) {
|
||||
isRightClick.current = true;
|
||||
rightClickMoved.current = false;
|
||||
} else if (event.button === 0) {
|
||||
isSelecting.current = false;
|
||||
isCtrlSelecting.current = event.ctrlKey;
|
||||
isShiftSelecting.current = event.shiftKey;
|
||||
isLeftMouseDown.current = true;
|
||||
isDragging.current = false;
|
||||
if (event.ctrlKey && duplicatedObjects.length === 0) {
|
||||
if (controls) (controls as any).enabled = false;
|
||||
selectionBox.startPoint.set(pointer.x, pointer.y, 0);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onPointerMove = (event: PointerEvent) => {
|
||||
if (isRightClick.current) {
|
||||
rightClickMoved.current = true;
|
||||
}
|
||||
if (isLeftMouseDown.current) {
|
||||
isDragging.current = true;
|
||||
}
|
||||
isSelecting.current = true;
|
||||
if (helper.isDown && event.ctrlKey && duplicatedObjects.length === 0 && isCtrlSelecting.current) {
|
||||
selectionBox.endPoint.set(pointer.x, pointer.y, 0);
|
||||
}
|
||||
};
|
||||
|
||||
const onPointerUp = (event: PointerEvent) => {
|
||||
if (event.button === 2 && !event.ctrlKey && !event.shiftKey) {
|
||||
isRightClick.current = false;
|
||||
if (!rightClickMoved.current) {
|
||||
clearSelection();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSelecting.current && isCtrlSelecting.current) {
|
||||
isCtrlSelecting.current = false;
|
||||
isSelecting.current = false;
|
||||
if (event.ctrlKey && duplicatedObjects.length === 0) {
|
||||
selectAssets();
|
||||
}
|
||||
} else if (!isSelecting.current && selectedAssets.length > 0 && ((!event.ctrlKey && !event.shiftKey && pastedObjects.length === 0 && duplicatedObjects.length === 0 && movedObjects.length === 0 && rotatedObjects.length === 0) || event.button !== 0)) {
|
||||
clearSelection();
|
||||
helper.enabled = true;
|
||||
isCtrlSelecting.current = false;
|
||||
} else if (controls) {
|
||||
(controls as any).enabled = true;
|
||||
}
|
||||
|
||||
if (!isDragging.current && isLeftMouseDown.current && isShiftSelecting.current && event.shiftKey) {
|
||||
isShiftSelecting.current = false;
|
||||
isLeftMouseDown.current = false;
|
||||
isDragging.current = false;
|
||||
|
||||
raycaster.setFromCamera(pointer, camera);
|
||||
const intersects = raycaster.intersectObjects(scene.children, true)
|
||||
.filter(
|
||||
(intersect) =>
|
||||
!intersect.object.name.includes("Roof") &&
|
||||
!intersect.object.name.includes("MeasurementReference") &&
|
||||
!intersect.object.name.includes("agv-collider") &&
|
||||
!intersect.object.name.includes("zonePlane") &&
|
||||
!intersect.object.name.includes("SelectionGroup") &&
|
||||
!intersect.object.name.includes("selectionAssetGroup") &&
|
||||
!intersect.object.name.includes("SelectionGroupBoundingBoxLine") &&
|
||||
!intersect.object.name.includes("SelectionGroupBoundingBox") &&
|
||||
!intersect.object.name.includes("SelectionGroupBoundingLine") &&
|
||||
intersect.object.type !== "GridHelper"
|
||||
);
|
||||
if (intersects.length > 0) {
|
||||
const intersect = intersects[0];
|
||||
const intersectObject = intersect.object;
|
||||
|
||||
let currentObject: THREE.Object3D | null = intersectObject;
|
||||
while (currentObject) {
|
||||
if (currentObject.userData.modelUuid) {
|
||||
break;
|
||||
}
|
||||
currentObject = currentObject.parent || null;
|
||||
}
|
||||
|
||||
if (currentObject) {
|
||||
const updatedSelections = new Set(selectedAssets);
|
||||
|
||||
if (updatedSelections.has(currentObject)) {
|
||||
updatedSelections.delete(currentObject);
|
||||
} else {
|
||||
updatedSelections.add(currentObject);
|
||||
}
|
||||
|
||||
const selected = Array.from(updatedSelections);
|
||||
|
||||
setSelectedAssets(selected);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (movedObjects.length > 0 || rotatedObjects.length > 0) return;
|
||||
if (event.key.toLowerCase() === "escape") {
|
||||
event.preventDefault();
|
||||
clearSelection();
|
||||
}
|
||||
if (event.key.toLowerCase() === "delete") {
|
||||
event.preventDefault();
|
||||
deleteSelection();
|
||||
}
|
||||
};
|
||||
|
||||
const onContextMenu = (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
if (!rightClickMoved.current) {
|
||||
clearSelection();
|
||||
}
|
||||
rightClickMoved.current = false;
|
||||
};
|
||||
|
||||
if (!toggleView && activeModule === "builder") {
|
||||
helper.enabled = true;
|
||||
canvasElement.addEventListener("pointermove", onPointerMove);
|
||||
canvasElement.addEventListener("pointerup", onPointerUp);
|
||||
canvasElement.addEventListener("pointerdown", onPointerDown);
|
||||
canvasElement.addEventListener("contextmenu", onContextMenu);
|
||||
canvasElement.addEventListener("keydown", onKeyDown);
|
||||
} else {
|
||||
helper.enabled = false;
|
||||
helper.dispose();
|
||||
}
|
||||
|
||||
return () => {
|
||||
canvasElement.removeEventListener("pointerdown", onPointerDown);
|
||||
canvasElement.removeEventListener("pointermove", onPointerMove);
|
||||
canvasElement.removeEventListener("contextmenu", onContextMenu);
|
||||
canvasElement.removeEventListener("pointerup", onPointerUp);
|
||||
canvasElement.removeEventListener("keydown", onKeyDown);
|
||||
helper.enabled = false;
|
||||
helper.dispose();
|
||||
};
|
||||
}, [camera, controls, scene, toggleView, selectedAssets, copiedObjects, pastedObjects, duplicatedObjects, movedObjects, socket, rotatedObjects, activeModule,]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeModule !== "builder") {
|
||||
clearSelection();
|
||||
}
|
||||
}, [activeModule]);
|
||||
|
||||
useFrame(() => {
|
||||
if (pastedObjects.length === 0 && duplicatedObjects.length === 0 && movedObjects.length === 0 && rotatedObjects.length === 0) {
|
||||
selectionGroup.current.position.set(0, 0, 0);
|
||||
}
|
||||
});
|
||||
|
||||
const selectAssets = useCallback(() => {
|
||||
selectionBox.endPoint.set(pointer.x, pointer.y, 0);
|
||||
if (controls) (controls as any).enabled = true;
|
||||
|
||||
let selectedObjects = selectionBox.select();
|
||||
let Objects = new Set<THREE.Object3D>();
|
||||
|
||||
selectedObjects.forEach((object) => {
|
||||
let currentObject: THREE.Object3D | null = object;
|
||||
while (currentObject) {
|
||||
if (currentObject.userData.modelUuid) {
|
||||
Objects.add(currentObject);
|
||||
break;
|
||||
}
|
||||
currentObject = currentObject.parent || null;
|
||||
}
|
||||
});
|
||||
|
||||
if (Objects.size === 0) {
|
||||
clearSelection();
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedSelections = new Set(selectedAssets);
|
||||
Objects.forEach((obj) => { updatedSelections.has(obj) ? updatedSelections.delete(obj) : updatedSelections.add(obj); });
|
||||
|
||||
const selected = Array.from(updatedSelections);
|
||||
|
||||
setSelectedAssets(selected);
|
||||
}, [selectionBox, pointer, controls, selectedAssets, setSelectedAssets]);
|
||||
|
||||
const clearSelection = () => {
|
||||
selectionGroup.current.children = [];
|
||||
selectionGroup.current.position.set(0, 0, 0);
|
||||
selectionGroup.current.rotation.set(0, 0, 0);
|
||||
setpastedObjects([]);
|
||||
setDuplicatedObjects([]);
|
||||
setSelectedAssets([]);
|
||||
};
|
||||
|
||||
const deleteSelection = () => {
|
||||
if (selectedAssets.length > 0 && duplicatedObjects.length === 0) {
|
||||
const email = localStorage.getItem("email");
|
||||
const organization = email!.split("@")[1].split(".")[0];
|
||||
const userId = localStorage.getItem("userId");
|
||||
|
||||
const storedItems = JSON.parse(localStorage.getItem("FloorItems") ?? "[]");
|
||||
const selectedUUIDs = selectedAssets.map((mesh: THREE.Object3D) => mesh.uuid);
|
||||
|
||||
const updatedStoredItems = storedItems.filter((item: { modelUuid: string }) => !selectedUUIDs.includes(item.modelUuid));
|
||||
localStorage.setItem("FloorItems", JSON.stringify(updatedStoredItems));
|
||||
|
||||
selectedAssets.forEach((selectedMesh: THREE.Object3D) => {
|
||||
//REST
|
||||
|
||||
// const response = await deleteFloorItem(organization, selectedMesh.userData.modelUuid, selectedMesh.userData.modelName);
|
||||
|
||||
//SOCKET
|
||||
|
||||
const data = {
|
||||
organization: organization,
|
||||
modelUuid: selectedMesh.userData.modelUuid,
|
||||
modelName: selectedMesh.userData.modelName,
|
||||
socketId: socket.id,
|
||||
projectId,
|
||||
userId
|
||||
};
|
||||
|
||||
const response = socket.emit("v1:model-asset:delete", data);
|
||||
|
||||
useEventsStore.getState().removeEvent(selectedMesh.uuid);
|
||||
useProductStore.getState().deleteEvent(selectedMesh.uuid);
|
||||
|
||||
if (response) {
|
||||
|
||||
removeAsset(selectedMesh.uuid);
|
||||
|
||||
echo.success("Model Removed!");
|
||||
}
|
||||
|
||||
selectedMesh.traverse((child: THREE.Object3D) => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
if (child.geometry) child.geometry.dispose();
|
||||
if (Array.isArray(child.material)) {
|
||||
child.material.forEach((material) => {
|
||||
if (material.map) material.map.dispose();
|
||||
material.dispose();
|
||||
});
|
||||
} else if (child.material) {
|
||||
if (child.material.map) child.material.map.dispose();
|
||||
child.material.dispose();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
selectedUUIDs.forEach((uuid: string) => {
|
||||
removeAsset(uuid);
|
||||
});
|
||||
}
|
||||
echo.success("Selected models removed!");
|
||||
clearSelection();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<group name="SelectionGroup">
|
||||
<group ref={selectionGroup} name="selectionAssetGroup">
|
||||
<BoundingBox boundingBoxRef={boundingBoxRef} isPerAsset />
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<MoveControls movedObjects={movedObjects} setMovedObjects={setMovedObjects} pastedObjects={pastedObjects} setpastedObjects={setpastedObjects} duplicatedObjects={duplicatedObjects} setDuplicatedObjects={setDuplicatedObjects} rotatedObjects={rotatedObjects} setRotatedObjects={setRotatedObjects} selectionGroup={selectionGroup} boundingBoxRef={boundingBoxRef} />
|
||||
|
||||
<RotateControls rotatedObjects={rotatedObjects} setRotatedObjects={setRotatedObjects} movedObjects={movedObjects} setMovedObjects={setMovedObjects} pastedObjects={pastedObjects} setpastedObjects={setpastedObjects} duplicatedObjects={duplicatedObjects} setDuplicatedObjects={setDuplicatedObjects} selectionGroup={selectionGroup} />
|
||||
|
||||
<DuplicationControls duplicatedObjects={duplicatedObjects} setDuplicatedObjects={setDuplicatedObjects} setpastedObjects={setpastedObjects} selectionGroup={selectionGroup} movedObjects={movedObjects} setMovedObjects={setMovedObjects} rotatedObjects={rotatedObjects} setRotatedObjects={setRotatedObjects} boundingBoxRef={boundingBoxRef} />
|
||||
|
||||
<CopyPasteControls copiedObjects={copiedObjects} setCopiedObjects={setCopiedObjects} pastedObjects={pastedObjects} setpastedObjects={setpastedObjects} selectionGroup={selectionGroup} setDuplicatedObjects={setDuplicatedObjects} movedObjects={movedObjects} setMovedObjects={setMovedObjects} rotatedObjects={rotatedObjects} setRotatedObjects={setRotatedObjects} boundingBoxRef={boundingBoxRef} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectionControls;
|
||||
@@ -0,0 +1,115 @@
|
||||
import { Vector2, WebGLRenderer } from 'three';
|
||||
|
||||
class SelectionHelper {
|
||||
element: HTMLDivElement;
|
||||
renderer: WebGLRenderer;
|
||||
startPoint: Vector2;
|
||||
pointTopLeft: Vector2;
|
||||
pointBottomRight: Vector2;
|
||||
isDown: boolean;
|
||||
enabled: boolean;
|
||||
|
||||
constructor(renderer: WebGLRenderer) {
|
||||
this.element = document.createElement('div');
|
||||
this.element.style.position = 'fixed';
|
||||
this.element.style.border = '1px solid #55aaff';
|
||||
this.element.style.backgroundColor = 'rgba(75, 160, 255, 0.3)';
|
||||
this.element.style.pointerEvents = 'none';
|
||||
this.element.style.display = 'none';
|
||||
|
||||
this.renderer = renderer;
|
||||
|
||||
this.startPoint = new Vector2();
|
||||
this.pointTopLeft = new Vector2();
|
||||
this.pointBottomRight = new Vector2();
|
||||
|
||||
this.isDown = false;
|
||||
this.enabled = true;
|
||||
|
||||
this.onPointerDown = this.onPointerDown.bind(this);
|
||||
this.onPointerMove = this.onPointerMove.bind(this);
|
||||
this.onPointerUp = this.onPointerUp.bind(this);
|
||||
|
||||
this.renderer.domElement.addEventListener('pointerdown', this.onPointerDown);
|
||||
this.renderer.domElement.addEventListener('pointermove', this.onPointerMove);
|
||||
this.renderer.domElement.addEventListener('pointerup', this.onPointerUp);
|
||||
window.addEventListener("blur", this.cleanup.bind(this));
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.enabled = false;
|
||||
this.isDown = false;
|
||||
this.cleanup();
|
||||
|
||||
this.renderer.domElement.removeEventListener("pointerdown", this.onPointerDown);
|
||||
this.renderer.domElement.removeEventListener("pointermove", this.onPointerMove);
|
||||
this.renderer.domElement.removeEventListener("pointerup", this.onPointerUp);
|
||||
window.removeEventListener("blur", this.cleanup);
|
||||
}
|
||||
|
||||
private cleanup() {
|
||||
this.isDown = false;
|
||||
this.element.style.display = 'none';
|
||||
if (this.element.parentElement) {
|
||||
this.element.parentElement.removeChild(this.element);
|
||||
}
|
||||
}
|
||||
|
||||
onPointerDown(event: PointerEvent) {
|
||||
if (!this.enabled || !event.ctrlKey || event.button !== 0) return;
|
||||
|
||||
this.isDown = true;
|
||||
this.onSelectStart(event);
|
||||
}
|
||||
|
||||
onPointerMove(event: PointerEvent) {
|
||||
if (!this.enabled || !this.isDown || !event.ctrlKey) return;
|
||||
|
||||
this.onSelectMove(event);
|
||||
}
|
||||
|
||||
onPointerUp() {
|
||||
if (!this.enabled) return;
|
||||
|
||||
this.isDown = false;
|
||||
this.onSelectOver();
|
||||
}
|
||||
|
||||
onSelectStart(event: PointerEvent) {
|
||||
this.element.style.display = 'none';
|
||||
this.renderer.domElement.parentElement?.appendChild(this.element);
|
||||
|
||||
this.element.style.left = `${event.clientX}px`;
|
||||
this.element.style.top = `${event.clientY}px`;
|
||||
this.element.style.width = '0px';
|
||||
this.element.style.height = '0px';
|
||||
|
||||
this.startPoint.x = event.clientX;
|
||||
this.startPoint.y = event.clientY;
|
||||
}
|
||||
|
||||
onSelectMove(event: PointerEvent) {
|
||||
if (!this.isDown) return;
|
||||
|
||||
this.element.style.display = 'block';
|
||||
|
||||
this.pointBottomRight.x = Math.max(this.startPoint.x, event.clientX);
|
||||
this.pointBottomRight.y = Math.max(this.startPoint.y, event.clientY);
|
||||
this.pointTopLeft.x = Math.min(this.startPoint.x, event.clientX);
|
||||
this.pointTopLeft.y = Math.min(this.startPoint.y, event.clientY);
|
||||
|
||||
this.element.style.left = `${this.pointTopLeft.x}px`;
|
||||
this.element.style.top = `${this.pointTopLeft.y}px`;
|
||||
this.element.style.width = `${this.pointBottomRight.x - this.pointTopLeft.x}px`;
|
||||
this.element.style.height = `${this.pointBottomRight.y - this.pointTopLeft.y}px`;
|
||||
}
|
||||
|
||||
onSelectOver() {
|
||||
this.element.style.display = 'none';
|
||||
if (this.element.parentElement) {
|
||||
this.element.parentElement.removeChild(this.element);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { SelectionHelper };
|
||||
@@ -0,0 +1,198 @@
|
||||
import { TransformControls } from "@react-three/drei";
|
||||
import * as THREE from "three";
|
||||
import { useSelectedFloorItem, useObjectPosition, useObjectRotation, useActiveTool, useSocketStore } from "../../../../store/builder/store";
|
||||
import { useThree } from "@react-three/fiber";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { detectModifierKeys } from "../../../../utils/shortcutkeys/detectModifierKeys";
|
||||
import { useEventsStore } from "../../../../store/simulation/useEventsStore";
|
||||
import { useProductStore } from "../../../../store/simulation/useProductStore";
|
||||
import { upsertProductOrEventApi } from "../../../../services/simulation/products/UpsertProductOrEventApi";
|
||||
import { setFloorItemApi } from "../../../../services/factoryBuilder/assest/floorAsset/setFloorItemApi";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useAssetsStore } from "../../../../store/builder/useAssetStore";
|
||||
import { useProductContext } from "../../../simulation/products/productContext";
|
||||
|
||||
export default function TransformControl() {
|
||||
const state = useThree();
|
||||
const [transformMode, setTransformMode] = useState<"translate" | "rotate" | null>(null);
|
||||
const { selectedFloorItem, setSelectedFloorItem } = useSelectedFloorItem();
|
||||
const { setObjectPosition } = useObjectPosition();
|
||||
const { setObjectRotation } = useObjectRotation();
|
||||
const { activeTool } = useActiveTool();
|
||||
const { socket } = useSocketStore();
|
||||
const { selectedProductStore } = useProductContext();
|
||||
const { selectedProduct } = selectedProductStore();
|
||||
const { updateAsset, getAssetById } = useAssetsStore();
|
||||
|
||||
const email = localStorage.getItem('email')
|
||||
const organization = (email!.split("@")[1]).split(".")[0];
|
||||
const userId = localStorage.getItem("userId");
|
||||
const { projectId } = useParams();
|
||||
|
||||
const updateBackend = (
|
||||
productName: string,
|
||||
productUuid: string,
|
||||
projectId: string,
|
||||
eventData: EventsSchema
|
||||
) => {
|
||||
upsertProductOrEventApi({
|
||||
productName: productName,
|
||||
productUuid: productUuid,
|
||||
projectId: projectId,
|
||||
eventDatas: eventData,
|
||||
});
|
||||
};
|
||||
|
||||
function handleObjectChange() {
|
||||
if (selectedFloorItem) {
|
||||
setObjectPosition(selectedFloorItem.position);
|
||||
setObjectRotation({
|
||||
x: THREE.MathUtils.radToDeg(selectedFloorItem.rotation.x),
|
||||
y: THREE.MathUtils.radToDeg(selectedFloorItem.rotation.y),
|
||||
z: THREE.MathUtils.radToDeg(selectedFloorItem.rotation.z),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleMouseUp() {
|
||||
if (selectedFloorItem) {
|
||||
setObjectPosition(selectedFloorItem.position);
|
||||
setObjectRotation({
|
||||
x: THREE.MathUtils.radToDeg(selectedFloorItem.rotation.x),
|
||||
y: THREE.MathUtils.radToDeg(selectedFloorItem.rotation.y),
|
||||
z: THREE.MathUtils.radToDeg(selectedFloorItem.rotation.z),
|
||||
});
|
||||
}
|
||||
const asset = getAssetById(selectedFloorItem?.uuid);
|
||||
if (asset) {
|
||||
if (asset.eventData) {
|
||||
const eventData = useEventsStore.getState().getEventByModelUuid(asset.modelUuid);
|
||||
const productData = useProductStore.getState().getEventByModelUuid(selectedProduct.productUuid, asset.modelUuid);
|
||||
|
||||
if (eventData) {
|
||||
useEventsStore.getState().updateEvent(asset.modelUuid, {
|
||||
position: [selectedFloorItem.position.x, 0, selectedFloorItem.position.z] as [number, number, number],
|
||||
rotation: [selectedFloorItem.rotation.x, selectedFloorItem.rotation.y, selectedFloorItem.rotation.z] as [number, number, number],
|
||||
});
|
||||
}
|
||||
|
||||
if (productData) {
|
||||
const event = useProductStore
|
||||
.getState()
|
||||
.updateEvent(
|
||||
selectedProduct.productUuid,
|
||||
asset.modelUuid,
|
||||
{
|
||||
position: [selectedFloorItem.position.x, 0, selectedFloorItem.position.z] as [number, number, number],
|
||||
rotation: [selectedFloorItem.rotation.x, selectedFloorItem.rotation.y, selectedFloorItem.rotation.z] as [number, number, number],
|
||||
}
|
||||
);
|
||||
|
||||
if (event) {
|
||||
updateBackend(
|
||||
selectedProduct.productName,
|
||||
selectedProduct.productUuid,
|
||||
projectId || '',
|
||||
event
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateAsset(asset.modelUuid, {
|
||||
position: [selectedFloorItem.position.x, 0, selectedFloorItem.position.z],
|
||||
rotation: [selectedFloorItem.rotation.x, selectedFloorItem.rotation.y, selectedFloorItem.rotation.z] as [number, number, number],
|
||||
});
|
||||
|
||||
//REST
|
||||
|
||||
// await setFloorItemApi(
|
||||
// organization,
|
||||
// asset.modelUuid,
|
||||
// asset.modelName,
|
||||
// [selectedFloorItem.position.x, 0, selectedFloorItem.position.z,
|
||||
// { "x": selectedFloorItem.rotation.x, "y": selectedFloorItem.rotation.y, "z": selectedFloorItem.rotation.z },
|
||||
// asset.assetId,
|
||||
// false,
|
||||
// true,
|
||||
// );
|
||||
|
||||
//SOCKET
|
||||
|
||||
const data = {
|
||||
organization,
|
||||
modelUuid: asset.modelUuid,
|
||||
modelName: asset.modelName,
|
||||
modelfileID: asset.assetId,
|
||||
position: [selectedFloorItem.position.x, 0, selectedFloorItem.position.z] as [number, number, number],
|
||||
rotation: { x: selectedFloorItem.rotation.x, y: selectedFloorItem.rotation.y, z: selectedFloorItem.rotation.z },
|
||||
isLocked: false,
|
||||
isVisible: true,
|
||||
socketId: socket.id,
|
||||
userId,
|
||||
projectId
|
||||
};
|
||||
|
||||
// console.log('data: ', data);
|
||||
socket.emit("v1:model-asset:add", data);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
const keyCombination = detectModifierKeys(e);
|
||||
if (!selectedFloorItem) return;
|
||||
if (keyCombination === "G") {
|
||||
setTransformMode((prev) => (prev === "translate" ? null : "translate"));
|
||||
}
|
||||
if (keyCombination === "R") {
|
||||
setTransformMode((prev) => (prev === "rotate" ? null : "rotate"));
|
||||
}
|
||||
};
|
||||
|
||||
if (selectedFloorItem) {
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
setObjectPosition(selectedFloorItem.position);
|
||||
setObjectRotation({
|
||||
x: THREE.MathUtils.radToDeg(selectedFloorItem.rotation.x),
|
||||
y: THREE.MathUtils.radToDeg(selectedFloorItem.rotation.y),
|
||||
z: THREE.MathUtils.radToDeg(selectedFloorItem.rotation.z),
|
||||
});
|
||||
} else {
|
||||
setTransformMode(null);
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [selectedFloorItem]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTool === "delete") {
|
||||
if (state.controls) {
|
||||
const target = (state.controls as any).getTarget(new THREE.Vector3());
|
||||
(state.controls as any).setTarget(target.x, 0, target.z, true);
|
||||
}
|
||||
setSelectedFloorItem(null);
|
||||
setObjectPosition({ x: undefined, y: undefined, z: undefined });
|
||||
setObjectRotation({ x: undefined, y: undefined, z: undefined });
|
||||
}
|
||||
}, [activeTool]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{(selectedFloorItem && transformMode) &&
|
||||
<TransformControls
|
||||
showX={transformMode === "translate"}
|
||||
showY={transformMode === "rotate"}
|
||||
showZ={transformMode === "translate"}
|
||||
object={selectedFloorItem}
|
||||
mode={transformMode}
|
||||
onObjectChange={handleObjectChange}
|
||||
onMouseUp={handleMouseUp}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
38
app/src/modules/scene/environment/ground.tsx
Normal file
38
app/src/modules/scene/environment/ground.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { useTileDistance, useToggleView } from "../../../store/builder/store";
|
||||
import * as CONSTANTS from "../../../types/world/worldConstants";
|
||||
|
||||
const Ground = ({ grid, plane }: any) => {
|
||||
const { toggleView } = useToggleView();
|
||||
const { planeValue, gridValue } = useTileDistance();
|
||||
|
||||
return (
|
||||
<mesh name="Ground">
|
||||
<mesh
|
||||
ref={grid}
|
||||
name="Grid"
|
||||
position={!toggleView ? CONSTANTS.gridConfig.position3D : CONSTANTS.gridConfig.position2D}
|
||||
>
|
||||
<gridHelper
|
||||
args={[
|
||||
gridValue.size,
|
||||
gridValue.divisions,
|
||||
CONSTANTS.gridConfig.primaryColor,
|
||||
CONSTANTS.gridConfig.secondaryColor,
|
||||
]}
|
||||
/>
|
||||
</mesh>
|
||||
<mesh
|
||||
ref={plane}
|
||||
rotation-x={CONSTANTS.planeConfig.rotation}
|
||||
position={!toggleView ? CONSTANTS.planeConfig.position3D : CONSTANTS.planeConfig.position2D}
|
||||
name="Plane"
|
||||
receiveShadow
|
||||
>
|
||||
<planeGeometry args={[planeValue.width, planeValue.height]} />
|
||||
<meshBasicMaterial color={CONSTANTS.planeConfig.color} />
|
||||
</mesh>
|
||||
</mesh>
|
||||
);
|
||||
};
|
||||
|
||||
export default Ground;
|
||||
104
app/src/modules/scene/environment/shadow.tsx
Normal file
104
app/src/modules/scene/environment/shadow.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import { useRef, useEffect } from "react";
|
||||
import { useThree } from "@react-three/fiber";
|
||||
import * as THREE from "three";
|
||||
import {
|
||||
useAzimuth,
|
||||
useElevation,
|
||||
useShadows,
|
||||
useSunPosition,
|
||||
useWallItems,
|
||||
useTileDistance,
|
||||
} from "../../../store/builder/store";
|
||||
import * as CONSTANTS from "../../../types/world/worldConstants";
|
||||
const shadowWorker = new Worker(
|
||||
new URL(
|
||||
"../../../services/factoryBuilder/webWorkers/shadowWorker",
|
||||
import.meta.url
|
||||
)
|
||||
);
|
||||
|
||||
export default function Shadows() {
|
||||
const { shadows, setShadows } = useShadows();
|
||||
const { sunPosition, setSunPosition } = useSunPosition();
|
||||
const lightRef = useRef<THREE.DirectionalLight | null>(null);
|
||||
const targetRef = useRef<THREE.Object3D | null>(null);
|
||||
const { controls, gl } = useThree();
|
||||
const { elevation, setElevation } = useElevation();
|
||||
const { azimuth, setAzimuth } = useAzimuth();
|
||||
const { wallItems } = useWallItems();
|
||||
const { planeValue } = useTileDistance();
|
||||
|
||||
useEffect(() => {
|
||||
gl.shadowMap.enabled = true;
|
||||
gl.shadowMap.type = THREE.PCFShadowMap;
|
||||
}, [gl, wallItems]);
|
||||
|
||||
useEffect(() => {
|
||||
if (lightRef.current && targetRef.current) {
|
||||
lightRef.current.target = targetRef.current;
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
shadowWorker.onmessage = (event) => {
|
||||
const { lightPosition, controlsTarget } = event.data;
|
||||
if (lightRef.current && targetRef.current && controls) {
|
||||
lightRef.current.position.copy(lightPosition);
|
||||
targetRef.current.position.copy(controlsTarget);
|
||||
gl.shadowMap.needsUpdate = true;
|
||||
}
|
||||
};
|
||||
}, [shadowWorker, controls]);
|
||||
|
||||
const updateShadows = () => {
|
||||
if (controls && shadowWorker) {
|
||||
const offsetDistance = CONSTANTS.shadowConfig.shadowOffset;
|
||||
const controlsTarget = (controls as any).getTarget();
|
||||
shadowWorker.postMessage({ controlsTarget, sunPosition, offsetDistance });
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (controls && shadows) {
|
||||
updateShadows();
|
||||
(controls as any).addEventListener("update", updateShadows);
|
||||
return () => {
|
||||
(controls as any).removeEventListener("update", updateShadows);
|
||||
};
|
||||
}
|
||||
}, [controls, elevation, azimuth, shadows]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* {(lightRef.current?.shadow) &&
|
||||
<cameraHelper visible={shadows} args={[lightRef.current.shadow.camera]} />
|
||||
} */}
|
||||
<directionalLight
|
||||
ref={lightRef}
|
||||
castShadow={shadows}
|
||||
shadow-mapSize-width={CONSTANTS.shadowConfig.shadowmapSizewidth}
|
||||
shadow-mapSize-height={CONSTANTS.shadowConfig.shadowmapSizeheight}
|
||||
shadow-camera-far={CONSTANTS.shadowConfig.shadowcamerafar}
|
||||
shadow-camera-near={CONSTANTS.shadowConfig.shadowcameranear}
|
||||
shadow-camera-top={CONSTANTS.shadowConfig.shadowcameratop}
|
||||
shadow-camera-bottom={CONSTANTS.shadowConfig.shadowcamerabottom}
|
||||
shadow-camera-left={CONSTANTS.shadowConfig.shadowcameraleft}
|
||||
shadow-camera-right={CONSTANTS.shadowConfig.shadowcameraright}
|
||||
shadow-bias={CONSTANTS.shadowConfig.shadowbias}
|
||||
shadow-normalBias={CONSTANTS.shadowConfig.shadownormalBias}
|
||||
/>
|
||||
<object3D ref={targetRef} />
|
||||
<mesh
|
||||
position={CONSTANTS.shadowConfig.shadowMaterialPosition}
|
||||
rotation={CONSTANTS.shadowConfig.shadowMaterialRotation}
|
||||
receiveShadow
|
||||
>
|
||||
<planeGeometry args={[planeValue.width, planeValue.height]} />
|
||||
<shadowMaterial
|
||||
opacity={CONSTANTS.shadowConfig.shadowMaterialOpacity}
|
||||
transparent
|
||||
/>
|
||||
</mesh>
|
||||
</>
|
||||
);
|
||||
}
|
||||
50
app/src/modules/scene/environment/sky.tsx
Normal file
50
app/src/modules/scene/environment/sky.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import * as THREE from 'three';
|
||||
import { Sky } from "@react-three/drei";
|
||||
import { useAzimuth, useElevation, useSunPosition } from "../../../store/builder/store";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import * as CONSTANTS from '../../../types/world/worldConstants';
|
||||
|
||||
export default function Sun() {
|
||||
const savedTheme: string | null = localStorage.getItem("theme");
|
||||
const { elevation, setElevation } = useElevation();
|
||||
const { sunPosition, setSunPosition } = useSunPosition();
|
||||
const { azimuth, setAzimuth } = useAzimuth();
|
||||
const [turbidity, setTurbidity] = useState(CONSTANTS.skyConfig.defaultTurbidity);
|
||||
const sunPositionRef = useRef(new THREE.Vector3(0, 0, 0));
|
||||
const [_, forceUpdate] = useState(0);
|
||||
const maxTurbidity = CONSTANTS.skyConfig.maxTurbidity;
|
||||
const minTurbidity = CONSTANTS.skyConfig.minTurbidity;
|
||||
|
||||
useEffect(() => {
|
||||
const phi = THREE.MathUtils.degToRad(90 - elevation);
|
||||
const theta = THREE.MathUtils.degToRad(azimuth);
|
||||
|
||||
const computedTurbidity = minTurbidity + ((elevation - 2) / (90 - 2)) * (maxTurbidity - minTurbidity);
|
||||
setTurbidity(computedTurbidity);
|
||||
|
||||
sunPositionRef.current.setFromSphericalCoords(1, phi, theta);
|
||||
setSunPosition(sunPositionRef.current);
|
||||
forceUpdate(prev => prev + 1);
|
||||
}, [elevation, azimuth]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{(azimuth !== undefined && elevation !== undefined && savedTheme !== "dark") && (
|
||||
<>
|
||||
<Sky
|
||||
distance={CONSTANTS.skyConfig.skyDistance}
|
||||
sunPosition={[
|
||||
sunPositionRef.current.x,
|
||||
sunPositionRef.current.y,
|
||||
sunPositionRef.current.z,
|
||||
]}
|
||||
turbidity={turbidity}
|
||||
rayleigh={CONSTANTS.skyConfig.defaultRayleigh}
|
||||
mieCoefficient={CONSTANTS.skyConfig.mieCoefficient}
|
||||
mieDirectionalG={CONSTANTS.skyConfig.mieDirectionalG}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
63
app/src/modules/scene/gizmo/gizmo.tsx
Normal file
63
app/src/modules/scene/gizmo/gizmo.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useThree, useFrame } from '@react-three/fiber'
|
||||
import { ViewportGizmo } from 'three-viewport-gizmo'
|
||||
import { PerspectiveCamera, WebGLRenderer, Scene } from 'three'
|
||||
|
||||
type Controls = {
|
||||
enabled: boolean
|
||||
setPosition: (...args: number[]) => void
|
||||
getTarget: (target: any) => void
|
||||
addEventListener: (type: string, listener: (...args: any[]) => void) => void
|
||||
}
|
||||
|
||||
export const Gizmo = () => {
|
||||
const { camera, gl, scene, controls } = useThree<{
|
||||
camera: PerspectiveCamera
|
||||
gl: WebGLRenderer
|
||||
scene: Scene
|
||||
controls?: Controls
|
||||
}>()
|
||||
|
||||
const gizmoRef = useRef<ViewportGizmo | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const gizmo = new ViewportGizmo(camera, gl, {})
|
||||
gizmoRef.current = gizmo
|
||||
|
||||
const resize = () => {
|
||||
const width = window.innerWidth
|
||||
const height = window.innerHeight - 1
|
||||
camera.aspect = width / height
|
||||
camera.updateProjectionMatrix()
|
||||
gl.setSize(width, height)
|
||||
gizmo.update()
|
||||
}
|
||||
|
||||
if (controls) {
|
||||
gizmo.addEventListener('start', () => { controls.enabled = false })
|
||||
gizmo.addEventListener('end', () => { controls.enabled = true })
|
||||
gizmo.addEventListener('change', () => {
|
||||
controls.setPosition(...camera.position.toArray())
|
||||
})
|
||||
controls.addEventListener('update', () => {
|
||||
controls.getTarget(gizmo.target)
|
||||
gizmo.update()
|
||||
})
|
||||
}
|
||||
|
||||
window.addEventListener('resize', resize)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', resize)
|
||||
}
|
||||
}, [camera, gl, scene, controls])
|
||||
|
||||
useFrame(() => {
|
||||
if (gizmoRef.current) {
|
||||
gl.render(scene, camera)
|
||||
gizmoRef.current.render()
|
||||
}
|
||||
})
|
||||
|
||||
return null
|
||||
}
|
||||
131
app/src/modules/scene/postProcessing/postProcessing.tsx
Normal file
131
app/src/modules/scene/postProcessing/postProcessing.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import { EffectComposer, N8AO, Outline } from "@react-three/postprocessing";
|
||||
import { BlendFunction } from "postprocessing";
|
||||
import {
|
||||
useDeletableFloorItem,
|
||||
useSelectedWallItem,
|
||||
useSelectedFloorItem,
|
||||
} from "../../../store/builder/store";
|
||||
import * as CONSTANTS from "../../../types/world/worldConstants";
|
||||
import { useSelectedEventSphere } from "../../../store/simulation/useSimulationStore";
|
||||
import { useEffect } from "react";
|
||||
import { useBuilderStore } from "../../../store/builder/useBuilderStore";
|
||||
|
||||
export default function PostProcessing() {
|
||||
const { deletableFloorItem } = useDeletableFloorItem();
|
||||
const { selectedWallItem } = useSelectedWallItem();
|
||||
const { selectedFloorItem } = useSelectedFloorItem();
|
||||
const { selectedEventSphere } = useSelectedEventSphere();
|
||||
const { selectedAisle } = useBuilderStore();
|
||||
|
||||
function flattenChildren(children: any[]) {
|
||||
const allChildren: any[] = [];
|
||||
children.forEach((child) => {
|
||||
allChildren.push(child);
|
||||
if (child.children && child.children.length > 0) {
|
||||
allChildren.push(...flattenChildren(child.children));
|
||||
}
|
||||
});
|
||||
return allChildren;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// console.log('selectedFloorItem: ', selectedFloorItem);
|
||||
}, [selectedFloorItem])
|
||||
|
||||
useEffect(() => {
|
||||
// console.log('selectedFloorItem: ', deletableFloorItem);
|
||||
}, [deletableFloorItem])
|
||||
|
||||
useEffect(() => {
|
||||
// console.log('selectedAisle: ', selectedAisle);
|
||||
}, [selectedAisle])
|
||||
|
||||
return (
|
||||
<EffectComposer autoClear={false}>
|
||||
<N8AO
|
||||
color="black"
|
||||
aoRadius={20}
|
||||
intensity={7}
|
||||
distanceFalloff={4}
|
||||
aoSamples={32}
|
||||
denoiseRadius={6}
|
||||
denoiseSamples={16}
|
||||
/>
|
||||
{selectedAisle && (
|
||||
<Outline
|
||||
selection={flattenChildren(selectedAisle.children)}
|
||||
selectionLayer={10}
|
||||
width={2000}
|
||||
blendFunction={BlendFunction.ALPHA}
|
||||
edgeStrength={5}
|
||||
resolutionScale={2}
|
||||
pulseSpeed={0}
|
||||
visibleEdgeColor={CONSTANTS.outlineConfig.assetSelectColor}
|
||||
hiddenEdgeColor={CONSTANTS.outlineConfig.assetSelectColor}
|
||||
blur={true}
|
||||
xRay={false}
|
||||
/>
|
||||
)}
|
||||
{deletableFloorItem && (
|
||||
<Outline
|
||||
selection={flattenChildren(deletableFloorItem.children)}
|
||||
selectionLayer={10}
|
||||
width={3000}
|
||||
blendFunction={BlendFunction.ALPHA}
|
||||
edgeStrength={5}
|
||||
resolutionScale={2}
|
||||
pulseSpeed={0}
|
||||
visibleEdgeColor={CONSTANTS.outlineConfig.assetDeleteColor}
|
||||
hiddenEdgeColor={CONSTANTS.outlineConfig.assetDeleteColor}
|
||||
blur={true}
|
||||
xRay={true}
|
||||
/>
|
||||
)}
|
||||
{selectedWallItem && (
|
||||
<Outline
|
||||
selection={flattenChildren(selectedWallItem.children)}
|
||||
selectionLayer={10}
|
||||
width={3000}
|
||||
blendFunction={BlendFunction.ALPHA}
|
||||
edgeStrength={5}
|
||||
resolutionScale={2}
|
||||
pulseSpeed={0}
|
||||
visibleEdgeColor={CONSTANTS.outlineConfig.assetSelectColor}
|
||||
hiddenEdgeColor={CONSTANTS.outlineConfig.assetSelectColor}
|
||||
blur={true}
|
||||
xRay={true}
|
||||
/>
|
||||
)}
|
||||
{selectedFloorItem && (
|
||||
<Outline
|
||||
selection={flattenChildren(selectedFloorItem.children)}
|
||||
selectionLayer={10}
|
||||
width={3000}
|
||||
blendFunction={BlendFunction.ALPHA}
|
||||
edgeStrength={5}
|
||||
resolutionScale={2}
|
||||
pulseSpeed={0}
|
||||
visibleEdgeColor={CONSTANTS.outlineConfig.assetSelectColor}
|
||||
hiddenEdgeColor={CONSTANTS.outlineConfig.assetSelectColor}
|
||||
blur={true}
|
||||
xRay={true}
|
||||
/>
|
||||
)}
|
||||
{selectedEventSphere && (
|
||||
<Outline
|
||||
selection={[selectedEventSphere]}
|
||||
selectionLayer={10}
|
||||
width={1000}
|
||||
blendFunction={BlendFunction.ALPHA}
|
||||
edgeStrength={10}
|
||||
resolutionScale={2}
|
||||
pulseSpeed={0}
|
||||
visibleEdgeColor={0x6f42c1}
|
||||
hiddenEdgeColor={0x6f42c1}
|
||||
blur={true}
|
||||
xRay={true}
|
||||
/>
|
||||
)}
|
||||
</EffectComposer>
|
||||
);
|
||||
}
|
||||
43
app/src/modules/scene/scene.tsx
Normal file
43
app/src/modules/scene/scene.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { useMemo } from "react";
|
||||
import { Canvas } from "@react-three/fiber";
|
||||
import { Color } from "three";
|
||||
import { KeyboardControls } from "@react-three/drei";
|
||||
import { SceneProvider } from "./sceneContext";
|
||||
|
||||
import Builder from "../builder/builder";
|
||||
import Visualization from "../visualization/visualization";
|
||||
import Setup from "./setup/setup";
|
||||
import Simulation from "../simulation/simulation";
|
||||
import Collaboration from "../collaboration/collaboration";
|
||||
|
||||
export default function Scene({ layout }: { readonly layout: 'Main Layout' | 'Comparison Layout' }) {
|
||||
const map = useMemo(() => [
|
||||
{ name: "forward", keys: ["ArrowUp", "w", "W"] },
|
||||
{ name: "backward", keys: ["ArrowDown", "s", "S"] },
|
||||
{ name: "left", keys: ["ArrowLeft", "a", "A"] },
|
||||
{ name: "right", keys: ["ArrowRight", "d", "D"] },
|
||||
], []);
|
||||
|
||||
return (
|
||||
<SceneProvider layout={layout}>
|
||||
<KeyboardControls map={map}>
|
||||
<Canvas
|
||||
eventPrefix="client"
|
||||
gl={{ powerPreference: "high-performance", antialias: true }}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
onCreated={(e) => {
|
||||
e.scene.background = new Color(0x19191d);
|
||||
}}
|
||||
>
|
||||
<Setup />
|
||||
<Collaboration />
|
||||
<Builder />
|
||||
<Simulation />
|
||||
<Visualization />
|
||||
</Canvas>
|
||||
</KeyboardControls>
|
||||
</SceneProvider>
|
||||
);
|
||||
}
|
||||
61
app/src/modules/scene/sceneContext.tsx
Normal file
61
app/src/modules/scene/sceneContext.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { createContext, useContext, useMemo } from 'react';
|
||||
import { createMaterialStore, MaterialStoreType } from '../../store/simulation/useMaterialStore';
|
||||
import { createArmBotStore, ArmBotStoreType } from '../../store/simulation/useArmBotStore';
|
||||
import { createMachineStore, MachineStoreType } from '../../store/simulation/useMachineStore';
|
||||
import { createConveyorStore, ConveyorStoreType } from '../../store/simulation/useConveyorStore';
|
||||
import { createVehicleStore, VehicleStoreType } from '../../store/simulation/useVehicleStore';
|
||||
import { createStorageUnitStore, StorageUnitStoreType } from '../../store/simulation/useStorageUnitStore';
|
||||
|
||||
type SceneContextValue = {
|
||||
materialStore: MaterialStoreType;
|
||||
armBotStore: ArmBotStoreType;
|
||||
machineStore: MachineStoreType;
|
||||
conveyorStore: ConveyorStoreType;
|
||||
vehicleStore: VehicleStoreType;
|
||||
storageUnitStore: StorageUnitStoreType;
|
||||
layout: 'Main Layout' | 'Comparison Layout';
|
||||
};
|
||||
|
||||
const SceneContext = createContext<SceneContextValue | null>(null);
|
||||
|
||||
export function SceneProvider({
|
||||
children,
|
||||
layout
|
||||
}: {
|
||||
readonly children: React.ReactNode;
|
||||
readonly layout: 'Main Layout' | 'Comparison Layout';
|
||||
}) {
|
||||
const materialStore = useMemo(() => createMaterialStore(), []);
|
||||
const armBotStore = useMemo(() => createArmBotStore(), []);
|
||||
const machineStore = useMemo(() => createMachineStore(), []);
|
||||
const conveyorStore = useMemo(() => createConveyorStore(), []);
|
||||
const vehicleStore = useMemo(() => createVehicleStore(), []);
|
||||
const storageUnitStore = useMemo(() => createStorageUnitStore(), []);
|
||||
|
||||
const contextValue = useMemo(() => (
|
||||
{
|
||||
materialStore,
|
||||
armBotStore,
|
||||
machineStore,
|
||||
conveyorStore,
|
||||
vehicleStore,
|
||||
storageUnitStore,
|
||||
layout
|
||||
}
|
||||
), [materialStore, armBotStore, machineStore, conveyorStore, vehicleStore, storageUnitStore, layout]);
|
||||
|
||||
return (
|
||||
<SceneContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</SceneContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
// Base hook to get the context
|
||||
export function useSceneContext() {
|
||||
const context = useContext(SceneContext);
|
||||
if (!context) {
|
||||
throw new Error('useSceneContext must be used within a SceneProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
27
app/src/modules/scene/setup/setup.tsx
Normal file
27
app/src/modules/scene/setup/setup.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import Sun from '../environment/sky'
|
||||
import Shadows from '../environment/shadow'
|
||||
import PostProcessing from '../postProcessing/postProcessing'
|
||||
import Controls from '../controls/controls';
|
||||
import { Environment } from '@react-three/drei'
|
||||
|
||||
import background from "../../../assets/textures/hdr/mudroadpuresky2k.hdr";
|
||||
|
||||
function Setup() {
|
||||
return (
|
||||
<>
|
||||
<Controls />
|
||||
|
||||
<Sun />
|
||||
|
||||
<Shadows />
|
||||
|
||||
<PostProcessing />
|
||||
|
||||
{/* <MovingClouds /> */}
|
||||
|
||||
<Environment files={background} environmentIntensity={1.5} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Setup
|
||||
241
app/src/modules/scene/tools/measurementTool.tsx
Normal file
241
app/src/modules/scene/tools/measurementTool.tsx
Normal file
@@ -0,0 +1,241 @@
|
||||
import * as THREE from "three";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useThree, useFrame } from "@react-three/fiber";
|
||||
import { useToolMode } from "../../../store/builder/store";
|
||||
import { Html } from "@react-three/drei";
|
||||
|
||||
const MeasurementTool = () => {
|
||||
const { gl, raycaster, pointer, camera, scene } = useThree();
|
||||
const { toolMode } = useToolMode();
|
||||
|
||||
const [points, setPoints] = useState<THREE.Vector3[]>([]);
|
||||
const [tubeGeometry, setTubeGeometry] = useState<THREE.TubeGeometry | null>(
|
||||
null
|
||||
);
|
||||
const groupRef = useRef<THREE.Group>(null);
|
||||
const [startConePosition, setStartConePosition] =
|
||||
useState<THREE.Vector3 | null>(null);
|
||||
const [endConePosition, setEndConePosition] = useState<THREE.Vector3 | null>(
|
||||
null
|
||||
);
|
||||
const [startConeQuaternion, setStartConeQuaternion] = useState(
|
||||
new THREE.Quaternion()
|
||||
);
|
||||
const [endConeQuaternion, setEndConeQuaternion] = useState(
|
||||
new THREE.Quaternion()
|
||||
);
|
||||
const [coneSize, setConeSize] = useState({ radius: 0.2, height: 0.5 });
|
||||
|
||||
const MIN_RADIUS = 0.001, MAX_RADIUS = 0.1;
|
||||
const MIN_CONE_RADIUS = 0.01, MAX_CONE_RADIUS = 0.4;
|
||||
const MIN_CONE_HEIGHT = 0.035, MAX_CONE_HEIGHT = 2.0;
|
||||
|
||||
useEffect(() => {
|
||||
const canvasElement = gl.domElement;
|
||||
let drag = false;
|
||||
let isLeftMouseDown = false;
|
||||
|
||||
const onMouseDown = () => {
|
||||
isLeftMouseDown = true;
|
||||
drag = false;
|
||||
};
|
||||
|
||||
const onMouseUp = (evt: any) => {
|
||||
isLeftMouseDown = false;
|
||||
if (evt.button === 0 && !drag) {
|
||||
raycaster.setFromCamera(pointer, camera);
|
||||
const intersects = raycaster
|
||||
.intersectObjects(scene.children, true)
|
||||
.filter(
|
||||
(intersect) =>
|
||||
!intersect.object.name.includes("Roof") &&
|
||||
!intersect.object.name.includes("MeasurementReference") &&
|
||||
!intersect.object.name.includes("agv-collider") &&
|
||||
!intersect.object.name.includes("zonePlane") &&
|
||||
!intersect.object.name.includes("SelectionGroup") &&
|
||||
!intersect.object.name.includes("selectionAssetGroup") &&
|
||||
!intersect.object.name.includes("SelectionGroupBoundingBoxLine") &&
|
||||
!intersect.object.name.includes("SelectionGroupBoundingBox") &&
|
||||
!intersect.object.name.includes("SelectionGroupBoundingLine") &&
|
||||
intersect.object.type !== "GridHelper"
|
||||
);
|
||||
|
||||
if (intersects.length > 0) {
|
||||
const intersectionPoint = intersects[0].point.clone();
|
||||
if (points.length < 2) {
|
||||
setPoints([...points, intersectionPoint]);
|
||||
} else {
|
||||
setPoints([intersectionPoint]);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseMove = () => {
|
||||
if (isLeftMouseDown) drag = true;
|
||||
};
|
||||
|
||||
const onContextMenu = (evt: any) => {
|
||||
evt.preventDefault();
|
||||
if (!drag) {
|
||||
evt.preventDefault();
|
||||
setPoints([]);
|
||||
setTubeGeometry(null);
|
||||
}
|
||||
};
|
||||
|
||||
if (toolMode === "MeasurementScale") {
|
||||
canvasElement.addEventListener("pointerdown", onMouseDown);
|
||||
canvasElement.addEventListener("pointermove", onMouseMove);
|
||||
canvasElement.addEventListener("pointerup", onMouseUp);
|
||||
canvasElement.addEventListener("contextmenu", onContextMenu);
|
||||
} else {
|
||||
resetMeasurement();
|
||||
setPoints([]);
|
||||
}
|
||||
|
||||
return () => {
|
||||
canvasElement.removeEventListener("pointerdown", onMouseDown);
|
||||
canvasElement.removeEventListener("pointermove", onMouseMove);
|
||||
canvasElement.removeEventListener("pointerup", onMouseUp);
|
||||
canvasElement.removeEventListener("contextmenu", onContextMenu);
|
||||
};
|
||||
}, [toolMode, camera, raycaster, pointer, scene, points]);
|
||||
|
||||
useFrame(() => {
|
||||
if (points.length === 1) {
|
||||
raycaster.setFromCamera(pointer, camera);
|
||||
const intersects = raycaster
|
||||
.intersectObjects(scene.children, true)
|
||||
.filter(
|
||||
(intersect) =>
|
||||
!intersect.object.name.includes("Roof") &&
|
||||
!intersect.object.name.includes("MeasurementReference") &&
|
||||
!intersect.object.name.includes("agv-collider") &&
|
||||
!intersect.object.name.includes("zonePlane") &&
|
||||
!intersect.object.name.includes("SelectionGroup") &&
|
||||
!intersect.object.name.includes("selectionAssetGroup") &&
|
||||
!intersect.object.name.includes("SelectionGroupBoundingBoxLine") &&
|
||||
!intersect.object.name.includes("SelectionGroupBoundingBox") &&
|
||||
!intersect.object.name.includes("SelectionGroupBoundingLine") &&
|
||||
intersect.object.type !== "GridHelper"
|
||||
);
|
||||
|
||||
if (intersects.length > 0) {
|
||||
updateMeasurement(points[0], intersects[0].point);
|
||||
}
|
||||
} else if (points.length === 2) {
|
||||
updateMeasurement(points[0], points[1]);
|
||||
} else {
|
||||
resetMeasurement();
|
||||
}
|
||||
});
|
||||
|
||||
const updateMeasurement = (start: THREE.Vector3, end: THREE.Vector3) => {
|
||||
const distance = start.distanceTo(end);
|
||||
|
||||
const radius = THREE.MathUtils.clamp(distance * 0.02, MIN_RADIUS, MAX_RADIUS);
|
||||
const coneRadius = THREE.MathUtils.clamp(distance * 0.05, MIN_CONE_RADIUS, MAX_CONE_RADIUS);
|
||||
const coneHeight = THREE.MathUtils.clamp(distance * 0.2, MIN_CONE_HEIGHT, MAX_CONE_HEIGHT);
|
||||
|
||||
setConeSize({ radius: coneRadius, height: coneHeight });
|
||||
|
||||
const direction = new THREE.Vector3().subVectors(end, start).normalize();
|
||||
|
||||
const offset = direction.clone().multiplyScalar(coneHeight * 0.5);
|
||||
|
||||
let tubeStart = start.clone().add(offset);
|
||||
let tubeEnd = end.clone().sub(offset);
|
||||
|
||||
tubeStart.y = Math.max(tubeStart.y, 0);
|
||||
tubeEnd.y = Math.max(tubeEnd.y, 0);
|
||||
|
||||
const curve = new THREE.CatmullRomCurve3([tubeStart, tubeEnd]);
|
||||
setTubeGeometry(new THREE.TubeGeometry(curve, 20, radius, 8, false));
|
||||
|
||||
setStartConePosition(tubeStart);
|
||||
setEndConePosition(tubeEnd);
|
||||
setStartConeQuaternion(getArrowOrientation(start, end));
|
||||
setEndConeQuaternion(getArrowOrientation(end, start));
|
||||
};
|
||||
|
||||
const resetMeasurement = () => {
|
||||
setTubeGeometry(null);
|
||||
setStartConePosition(null);
|
||||
setEndConePosition(null);
|
||||
};
|
||||
|
||||
const getArrowOrientation = (start: THREE.Vector3, end: THREE.Vector3) => {
|
||||
const direction = new THREE.Vector3()
|
||||
.subVectors(end, start)
|
||||
.normalize()
|
||||
.negate();
|
||||
const quaternion = new THREE.Quaternion();
|
||||
quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), direction);
|
||||
return quaternion;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (points.length === 2) {
|
||||
// console.log(points[0].distanceTo(points[1]));
|
||||
}
|
||||
}, [points]);
|
||||
|
||||
return (
|
||||
<group ref={groupRef} name="MeasurementGroup">
|
||||
{startConePosition && (
|
||||
<mesh
|
||||
name="MeasurementReference"
|
||||
position={startConePosition}
|
||||
quaternion={startConeQuaternion}
|
||||
>
|
||||
<coneGeometry args={[coneSize.radius, coneSize.height, 16]} />
|
||||
<meshBasicMaterial color="yellow" />
|
||||
</mesh>
|
||||
)}
|
||||
{endConePosition && (
|
||||
<mesh
|
||||
name="MeasurementReference"
|
||||
position={endConePosition}
|
||||
quaternion={endConeQuaternion}
|
||||
>
|
||||
<coneGeometry args={[coneSize.radius, coneSize.height, 16]} />
|
||||
<meshBasicMaterial color="yellow" />
|
||||
</mesh>
|
||||
)}
|
||||
{tubeGeometry && (
|
||||
<mesh name="MeasurementReference" geometry={tubeGeometry}>
|
||||
<meshBasicMaterial color="yellow" />
|
||||
</mesh>
|
||||
)}
|
||||
|
||||
{startConePosition && endConePosition && (
|
||||
<Html
|
||||
scale={THREE.MathUtils.clamp(
|
||||
startConePosition.distanceTo(endConePosition) * 0.25,
|
||||
0,
|
||||
10
|
||||
)}
|
||||
position={[
|
||||
(startConePosition.x + endConePosition.x) / 2,
|
||||
(startConePosition.y + endConePosition.y) / 2,
|
||||
(startConePosition.z + endConePosition.z) / 2,
|
||||
]}
|
||||
// class
|
||||
wrapperClass="distance-text-wrapper"
|
||||
className="distance-text"
|
||||
// other
|
||||
zIndexRange={[1, 0]}
|
||||
prepend
|
||||
sprite
|
||||
>
|
||||
<div>
|
||||
{(startConePosition.distanceTo(endConePosition) + (coneSize.height)).toFixed(2)} m
|
||||
</div>
|
||||
</Html>
|
||||
)}
|
||||
</group>
|
||||
);
|
||||
};
|
||||
|
||||
export default MeasurementTool;
|
||||
Reference in New Issue
Block a user