(null);
+ const { scene, size, controls } = useThree();
+ const { secondaryCameraData, setSecondaryCameraData, updateSecondaryCameraData } = useSecondaryCameraData();
+ const { selectedSecondaryCamera, setSelectedSecondaryCamera } = useSecondaryCameraState();
+ const { secondaryCameraEdit } = useSecondaryCameraEdit();
+
+ // Sync main controls to selected camera
+ useEffect(() => {
+ if (selectedSecondaryCamera) {
+ (controls as CameraControls).setLookAt(
+ ...selectedSecondaryCamera.position,
+ ...selectedSecondaryCamera.target
+ );
+ }
+ }, [selectedSecondaryCamera?.id]);
+
+ // Setup WebGLRenderer
+ useEffect(() => {
+ const secondaryCanvas = document.getElementById('secondary-canvas');
+ if (!secondaryCanvas) return;
+
+ const customRenderer = new THREE.WebGLRenderer({
+ canvas: secondaryCanvas,
+ antialias: true,
+ alpha: true,
+ });
+
+ customRenderer.setSize(
+ secondaryCanvas.getBoundingClientRect().width,
+ secondaryCanvas.getBoundingClientRect().height
+ );
+ customRenderer.setPixelRatio(window.devicePixelRatio);
+ rendererRef.current = customRenderer;
+
+ return () => {
+ customRenderer.dispose();
+ };
+ }, [size]);
+
+ // Camera helper
+ useEffect(() => {
+ if (selectedSecondaryCamera && cameraRef.current) {
+ const helper = new THREE.CameraHelper(cameraRef.current);
+ helperRef.current = helper;
+ scene.add(helper);
+
+ return () => {
+ scene.remove(helper);
+ helper.geometry.dispose();
+ (helper.material as any).dispose?.();
+ };
+ }
+ }, [scene, selectedSecondaryCamera?.id]);
+
+ // Render to secondary canvas
+ useFrame(() => {
+ if (rendererRef.current && cameraRef.current && selectedSecondaryCamera) {
+ rendererRef.current.render(scene, cameraRef.current);
+ }
+ });
+
+ // Live update camera if editing enabled
+ useFrame(() => {
+ if (secondaryCameraEdit && selectedSecondaryCamera && cameraRef.current) {
+ const target = (controls as CameraControls).getTarget(new THREE.Vector3());
+ const position = (controls as CameraControls).getPosition(new THREE.Vector3());
+
+ updateSecondaryCameraData(selectedSecondaryCamera.id, {
+ position: position.toArray(),
+ target: target.toArray(),
+ });
+
+ setSelectedSecondaryCamera({
+ id: selectedSecondaryCamera.id,
+ position: position.toArray(),
+ target: target.toArray(),
+ });
+ }
+ });
+
+ function handleCamera() {
+ const target = (controls as CameraControls).getTarget(new THREE.Vector3());
+ const position = (controls as CameraControls).getPosition(new THREE.Vector3());
+
+ const newCameraData: CameraData = {
+ id: secondaryCameraData.length + 1,
+ name: `Camera ${secondaryCameraData.length + 1}`,
+ position: position.toArray(),
+ target: target.toArray(),
+ };
+
+ setSecondaryCameraData([...secondaryCameraData, newCameraData]);
+ }
+
+ return (
+ <>
+
+
+
+
+ {selectedSecondaryCamera && (
+ self.lookAt(...selectedSecondaryCamera.target)}
+ >
+ {
+ if (dummyMeshRef.current) {
+ const worldPos = new THREE.Vector3();
+ dummyMeshRef.current.getWorldPosition(worldPos);
+
+ const updatedPos: [number, number, number] = worldPos.toArray() as [number, number, number];
+
+ updateSecondaryCameraData(selectedSecondaryCamera.id, {
+ position: updatedPos,
+ });
+
+ setSelectedSecondaryCamera({
+ ...selectedSecondaryCamera,
+ position: updatedPos,
+ });
+ }
+ }}
+ >
+
+
+
+
+
+
+ )}
+ >
+ );
+}
+
+export default SecondaryCameraView;
diff --git a/app/src/modules/secondaryCamera/secondaryCanvas.tsx b/app/src/modules/secondaryCamera/secondaryCanvas.tsx
new file mode 100644
index 0000000..daad491
--- /dev/null
+++ b/app/src/modules/secondaryCamera/secondaryCanvas.tsx
@@ -0,0 +1,46 @@
+import React, { useEffect } from 'react'
+import { useSecondaryCameraData, useSecondaryCameraEdit, useSecondaryCameraState } from '../../store/builder/store'
+
+type CameraData = {
+ id: any
+ name: string
+ position: [number, number, number]
+ target: [number, number, number]
+}
+
+function SecondaryCanvas() {
+ const { secondaryCameraData } = useSecondaryCameraData()
+ const { selectedSecondaryCamera, setSelectedSecondaryCamera } = useSecondaryCameraState();
+ const { setSecondaryCameraEdit, secondaryCameraEdit } = useSecondaryCameraEdit()
+
+ function handleSelectCamera(camera: any) {
+ const selectedCamera = camera
+ if (!selectedCamera || (selectedSecondaryCamera && selectedCamera.id === selectedSecondaryCamera.id)) {
+ setSelectedSecondaryCamera(null);
+ } else {
+ setSelectedSecondaryCamera({
+ id: selectedCamera.id,
+ position: selectedCamera.position,
+ target: selectedCamera.target
+ });
+ }
+ }
+
+ return (
+ <>
+ {secondaryCameraData.map((camera: CameraData) => (
+ handleSelectCamera(camera)} style={{ zIndex: 10, position: 'absolute', bottom: 300 - (camera.id * 30), right: 0, border: '1px solid black', backgroundColor: 'black' }}>
+
{camera.name}
+
+ ))}
+ { setSecondaryCameraEdit(!secondaryCameraEdit) }} style={{ zIndex: 10, position: 'absolute', bottom: 500, right: 0, border: '1px solid black', backgroundColor: secondaryCameraEdit ? 'black' : "white", color: !secondaryCameraEdit ? "black" : "white", padding: '10px' }}>
+ Edit
+
+
+
+
+ >
+ )
+}
+
+export default SecondaryCanvas
diff --git a/app/src/modules/simulation/human/instances/animator/humanAnimator.tsx b/app/src/modules/simulation/human/instances/animator/humanAnimator.tsx
index bb944f6..2b89bc2 100644
--- a/app/src/modules/simulation/human/instances/animator/humanAnimator.tsx
+++ b/app/src/modules/simulation/human/instances/animator/humanAnimator.tsx
@@ -1,5 +1,6 @@
import { useEffect, useRef, useState } from 'react'
import { useFrame, useThree } from '@react-three/fiber';
+import { RapierRigidBody } from '@react-three/rapier';
import * as THREE from 'three';
import { Line } from '@react-three/drei';
import { useAnimationPlaySpeed, usePauseButtonStore, usePlayButtonStore, useResetButtonStore } from '../../../../../store/usePlayButtonStore';
@@ -28,6 +29,7 @@ function HumanAnimator({ path, handleCallBack, currentPhase, human, reset, start
const [objectRotation, setObjectRotation] = useState<[number, number, number] | null>(human.point?.action?.pickUpPoint?.rotation || [0, 0, 0])
const [restRotation, setRestingRotation] = useState(true);
const [currentPath, setCurrentPath] = useState<[number, number, number][]>([]);
+ const rigidBodyRef = useRef(null);
const { scene } = useThree();
useEffect(() => {
diff --git a/app/src/modules/simulation/materials/instances/material/materialModel.tsx b/app/src/modules/simulation/materials/instances/material/materialModel.tsx
index 5ffb486..9f550af 100644
--- a/app/src/modules/simulation/materials/instances/material/materialModel.tsx
+++ b/app/src/modules/simulation/materials/instances/material/materialModel.tsx
@@ -19,7 +19,7 @@ type ModelType = keyof typeof modelPaths;
interface ModelProps extends React.ComponentProps<'group'> {
materialId: string;
materialType: ModelType;
- matRef: React.Ref>
+ matRef?: React.Ref>
}
export function MaterialModel({ materialId, materialType, matRef, ...props }: Readonly) {
diff --git a/app/src/modules/simulation/vehicle/instances/animator/vehicleAnimator.tsx b/app/src/modules/simulation/vehicle/instances/animator/vehicleAnimator.tsx
index be10213..d76c0b3 100644
--- a/app/src/modules/simulation/vehicle/instances/animator/vehicleAnimator.tsx
+++ b/app/src/modules/simulation/vehicle/instances/animator/vehicleAnimator.tsx
@@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from 'react'
import { useFrame, useThree } from '@react-three/fiber';
import * as THREE from 'three';
import { Line } from '@react-three/drei';
+import { RapierRigidBody } from '@react-three/rapier';
import { useAnimationPlaySpeed, usePauseButtonStore, usePlayButtonStore, useResetButtonStore } from '../../../../../store/usePlayButtonStore';
import { useSceneContext } from '../../../../scene/sceneContext';
@@ -28,8 +29,15 @@ function VehicleAnimator({ path, handleCallBack, currentPhase, agvUuid, agvDetai
const [objectRotation, setObjectRotation] = useState<{ x: number; y: number; z: number } | undefined>(agvDetail.point?.action?.pickUpPoint?.rotation || { x: 0, y: 0, z: 0 })
const [restRotation, setRestingRotation] = useState(true);
const [currentPath, setCurrentPath] = useState<[number, number, number][]>([]);
+ const rigidBodyRef = useRef(null);
const { scene } = useThree();
+ const object = scene.getObjectByProperty('uuid', agvUuid);
+
+ if (object?.userData.rigidBodyRef) {
+ (rigidBodyRef as React.MutableRefObject).current = object.userData.rigidBodyRef;
+ }
+
useEffect(() => {
if (currentPhase === 'stationed-pickup' && path.length > 0) {
setCurrentPath(path);
@@ -56,11 +64,17 @@ function VehicleAnimator({ path, handleCallBack, currentPhase, agvUuid, agvDetai
progressRef.current = 0;
setReset(false);
setRestingRotation(true);
- const object = scene.getObjectByProperty('uuid', agvUuid);
+
const vehicle = getVehicleById(agvDetail.modelUuid);
- if (object && vehicle) {
- object.position.set(vehicle.position[0], vehicle.position[1], vehicle.position[2]);
- object.rotation.set(vehicle.rotation[0], vehicle.rotation[1], vehicle.rotation[2]);
+ if (rigidBodyRef.current && vehicle) {
+ rigidBodyRef.current.setTranslation(
+ { x: vehicle.position[0], y: vehicle.position[1], z: vehicle.position[2] },
+ true
+ );
+ rigidBodyRef.current.setRotation(
+ { x: vehicle.rotation[0], y: vehicle.rotation[1], z: vehicle.rotation[2], w: 1 },
+ true
+ );
}
}
}, [isReset, isPlaying])
@@ -72,8 +86,7 @@ function VehicleAnimator({ path, handleCallBack, currentPhase, agvUuid, agvDetai
const delta = (now - lastTimeRef.current) / 1000;
lastTimeRef.current = now;
- const object = scene.getObjectByProperty('uuid', agvUuid);
- if (!object || currentPath.length < 2) return;
+ if (!rigidBodyRef.current || currentPath.length < 2) return;
if (isPaused) return;
let totalDistance = 0;
@@ -100,21 +113,42 @@ function VehicleAnimator({ path, handleCallBack, currentPhase, agvUuid, agvDetai
const end = new THREE.Vector3(...currentPath[index + 1]);
const segmentDistance = distances[index];
- const targetQuaternion = new THREE.Quaternion().setFromRotationMatrix(new THREE.Matrix4().lookAt(start, end, new THREE.Vector3(0, 1, 0)));
+ const targetQuaternion = new THREE.Quaternion().setFromRotationMatrix(
+ new THREE.Matrix4().lookAt(start, end, new THREE.Vector3(0, 1, 0))
+ );
const y180 = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI);
targetQuaternion.multiply(y180);
- const angle = object.quaternion.angleTo(targetQuaternion);
+ const currentRotation = rigidBodyRef.current.rotation();
+ const currentQuaternion = new THREE.Quaternion(
+ currentRotation.x,
+ currentRotation.y,
+ currentRotation.z,
+ currentRotation.w
+ );
+
+ const angle = currentQuaternion.angleTo(targetQuaternion);
+
if (angle < 0.01) {
- object.quaternion.copy(targetQuaternion);
+ rigidBodyRef.current.setRotation(
+ { x: targetQuaternion.x, y: targetQuaternion.y, z: targetQuaternion.z, w: targetQuaternion.w },
+ true
+ );
} else {
const step = rotationSpeed * delta * speed * agvDetail.speed;
- const angle = object.quaternion.angleTo(targetQuaternion);
+ const angle = currentQuaternion.angleTo(targetQuaternion);
if (angle < step) {
- object.quaternion.copy(targetQuaternion);
+ rigidBodyRef.current.setRotation(
+ { x: targetQuaternion.x, y: targetQuaternion.y, z: targetQuaternion.z, w: targetQuaternion.w },
+ true
+ );
} else {
- object.quaternion.rotateTowards(targetQuaternion, step);
+ const newQuaternion = currentQuaternion.clone().rotateTowards(targetQuaternion, step);
+ rigidBodyRef.current.setRotation(
+ { x: newQuaternion.x, y: newQuaternion.y, z: newQuaternion.z, w: newQuaternion.w },
+ true
+ );
}
}
@@ -124,30 +158,51 @@ function VehicleAnimator({ path, handleCallBack, currentPhase, agvUuid, agvDetai
progressRef.current += delta * (speed * agvDetail.speed);
const t = (progressRef.current - accumulatedDistance) / segmentDistance;
const position = start.clone().lerp(end, t);
- object.position.copy(position);
+
+ rigidBodyRef.current.setTranslation(
+ { x: position.x, y: position.y, z: position.z },
+ true
+ );
}
}
if (progressRef.current >= totalDistance) {
if (restRotation && objectRotation) {
const targetEuler = new THREE.Euler(0, objectRotation.y - agvDetail.point.action.steeringAngle, 0);
-
const baseQuaternion = new THREE.Quaternion().setFromEuler(targetEuler);
const y180 = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI);
const targetQuaternion = baseQuaternion.multiply(y180);
- const angle = object.quaternion.angleTo(targetQuaternion);
+ const currentRotation = rigidBodyRef.current.rotation();
+ const currentQuaternion = new THREE.Quaternion(
+ currentRotation.x,
+ currentRotation.y,
+ currentRotation.z,
+ currentRotation.w
+ );
+
+ const angle = currentQuaternion.angleTo(targetQuaternion);
if (angle < 0.01) {
- object.quaternion.copy(targetQuaternion);
+ rigidBodyRef.current.setRotation(
+ { x: targetQuaternion.x, y: targetQuaternion.y, z: targetQuaternion.z, w: targetQuaternion.w },
+ true
+ );
setRestingRotation(false);
} else {
const step = rotationSpeed * delta * speed * agvDetail.speed;
- const angle = object.quaternion.angleTo(targetQuaternion);
+ const angle = currentQuaternion.angleTo(targetQuaternion);
if (angle < step) {
- object.quaternion.copy(targetQuaternion);
+ rigidBodyRef.current.setRotation(
+ { x: targetQuaternion.x, y: targetQuaternion.y, z: targetQuaternion.z, w: targetQuaternion.w },
+ true
+ );
} else {
- object.quaternion.rotateTowards(targetQuaternion, step);
+ const newQuaternion = currentQuaternion.clone().rotateTowards(targetQuaternion, step);
+ rigidBodyRef.current.setRotation(
+ { x: newQuaternion.x, y: newQuaternion.y, z: newQuaternion.z, w: newQuaternion.w },
+ true
+ );
}
}
return;
diff --git a/app/src/pages/Project.tsx b/app/src/pages/Project.tsx
index bf01b3a..4ff57df 100644
--- a/app/src/pages/Project.tsx
+++ b/app/src/pages/Project.tsx
@@ -30,6 +30,7 @@ import { getVersionHistoryApi } from "../services/factoryBuilder/versionControl/
import { useVersionHistoryStore } from "../store/builder/useVersionHistoryStore";
import { VersionProvider } from "../modules/builder/version/versionContext";
import { sharedWithMeProjects } from "../services/dashboard/sharedWithMeProject";
+import SecondaryCanvas from "../modules/secondaryCamera/secondaryCanvas";
const Project: React.FC = () => {
let navigate = useNavigate();
@@ -140,6 +141,9 @@ const Project: React.FC = () => {
+
+
+
{selectedUser && }
{isLogListVisible && (
diff --git a/app/src/store/builder/store.ts b/app/src/store/builder/store.ts
index 06cb1fb..ba4a007 100644
--- a/app/src/store/builder/store.ts
+++ b/app/src/store/builder/store.ts
@@ -724,3 +724,49 @@ export const useSelectedComment = create((set: any) => ({
commentPositionState: null,
setCommentPositionState: (x: any) => set({ commentPositionState: x }),
}));
+
+
+type CameraData = {
+ id: any;
+ name: string;
+ position: [number, number, number];
+ target: [number, number, number];
+};
+
+type SecondaryCameraStore = {
+ secondaryCameraData: CameraData[];
+ setSecondaryCameraData: (data: CameraData[]) => void;
+ updateSecondaryCameraData: (id: any, updatedData: Partial) => void;
+};
+
+export const useSecondaryCameraData = create((set) => ({
+ secondaryCameraData: [],
+ setSecondaryCameraData: (data) => set({ secondaryCameraData: data }),
+ updateSecondaryCameraData: (id, updatedData) =>
+ set((state) => ({
+ secondaryCameraData: state.secondaryCameraData.map((camera) =>
+ camera.id === id ? { ...camera, ...updatedData } : camera
+ ),
+ })),
+}));
+
+type SecondaryCamera = {
+ id: number;
+ position: [number, number, number];
+ target: [number, number, number];
+};
+
+type SecondaryCameraState = {
+ selectedSecondaryCamera: SecondaryCamera | null;
+ setSelectedSecondaryCamera: (camera: SecondaryCamera | null) => void;
+};
+
+export const useSecondaryCameraState = create((set) => ({
+ selectedSecondaryCamera: null,
+ setSelectedSecondaryCamera: (camera) => set({ selectedSecondaryCamera: camera }),
+}));
+
+export const useSecondaryCameraEdit = create((set) => ({
+ secondaryCameraEdit: false,
+ setSecondaryCameraEdit: (x: boolean) => set({ secondaryCameraEdit: x }),
+}));
\ No newline at end of file