feat: Integrate @react-three/rapier for physics simulation and add conveyor collider functionality
- Added @react-three/rapier to package.json for physics support. - Refactored AssetBoundingBox to utilize RigidBody for collision detection. - Implemented ConveyorCollider to manage object movement on conveyor belts. - Enhanced Model component to include rigid body references and bounding box calculations. - Updated Ground component to use RigidBody for ground physics. - Introduced Colliders component to manage material instances with physics interactions. - Created SecondaryCamera for enhanced camera management and editing capabilities. - Added secondary canvas for rendering secondary camera views. - Updated selection controls to utilize bounding boxes for asset selection.
This commit is contained in:
70
app/package-lock.json
generated
70
app/package-lock.json
generated
@@ -16,6 +16,7 @@
|
||||
"@react-three/drei": "^9.113.0",
|
||||
"@react-three/fiber": "^8.17.7",
|
||||
"@react-three/postprocessing": "^2.16.3",
|
||||
"@react-three/rapier": "^1.5.0",
|
||||
"@recast-navigation/core": "^0.39.0",
|
||||
"@recast-navigation/three": "^0.39.0",
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
@@ -2026,7 +2027,7 @@
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "0.3.9"
|
||||
},
|
||||
@@ -2038,7 +2039,7 @@
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
|
||||
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.0.3",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
@@ -2371,6 +2372,12 @@
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@dimforge/rapier3d-compat": {
|
||||
"version": "0.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.14.0.tgz",
|
||||
"integrity": "sha512-/uHrUzS+CRQ+NQrrJCEDUkhwHlNsAAexbNXgbN9sHY+GwR+SFFAFrxRr8Llf5/AJZzqiLANdQIfJ63Cw4gJVqw==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@dnd-kit/accessibility": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
|
||||
@@ -3804,6 +3811,21 @@
|
||||
"three": ">=0.144.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-three/rapier": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-three/rapier/-/rapier-1.5.0.tgz",
|
||||
"integrity": "sha512-gylk2KyCer9EoymFyTyc+g2IqyAq4mTbZgaHoSJi6gHoXlJsC2LVeN4jedvegvjUsXPExdE60wHjCPa+DS4iXw==",
|
||||
"dependencies": {
|
||||
"@dimforge/rapier3d-compat": "0.14.0",
|
||||
"suspend-react": "^0.1.3",
|
||||
"three-stdlib": "^2.29.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@react-three/fiber": ">=8.9.0",
|
||||
"react": ">=18.0.0",
|
||||
"three": ">=0.139.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@recast-navigation/core": {
|
||||
"version": "0.39.0",
|
||||
"resolved": "https://registry.npmjs.org/@recast-navigation/core/-/core-0.39.0.tgz",
|
||||
@@ -4180,6 +4202,26 @@
|
||||
"url": "https://github.com/sponsors/gregberge"
|
||||
}
|
||||
},
|
||||
"node_modules/@testing-library/dom": {
|
||||
"version": "10.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
|
||||
"integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.10.4",
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@types/aria-query": "^5.0.1",
|
||||
"aria-query": "5.3.0",
|
||||
"chalk": "^4.1.0",
|
||||
"dom-accessibility-api": "^0.5.9",
|
||||
"lz-string": "^1.5.0",
|
||||
"pretty-format": "^27.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@testing-library/jest-dom": {
|
||||
"version": "5.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.17.0.tgz",
|
||||
@@ -4291,25 +4333,25 @@
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
|
||||
"integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==",
|
||||
"dev": true
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/@tsconfig/node12": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
|
||||
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
|
||||
"dev": true
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/@tsconfig/node14": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
|
||||
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
|
||||
"dev": true
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/@tsconfig/node16": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
|
||||
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
|
||||
"dev": true
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/@turf/along": {
|
||||
"version": "7.2.0",
|
||||
@@ -9063,7 +9105,7 @@
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
|
||||
"dev": true
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/cross-env": {
|
||||
"version": "7.0.3",
|
||||
@@ -9940,7 +9982,7 @@
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"engines": {
|
||||
"node": ">=0.3.1"
|
||||
}
|
||||
@@ -15324,7 +15366,7 @@
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||
"dev": true
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/makeerror": {
|
||||
"version": "1.0.12",
|
||||
@@ -20801,7 +20843,7 @@
|
||||
"version": "10.9.2",
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
||||
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"@cspotcode/source-map-support": "^0.8.0",
|
||||
"@tsconfig/node10": "^1.0.7",
|
||||
@@ -20844,7 +20886,7 @@
|
||||
"version": "8.3.4",
|
||||
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
|
||||
"integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"acorn": "^8.11.0"
|
||||
},
|
||||
@@ -20856,7 +20898,7 @@
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
||||
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
|
||||
"dev": true
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/tsconfig-paths": {
|
||||
"version": "3.15.0",
|
||||
@@ -21352,7 +21394,7 @@
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
|
||||
"dev": true
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/v8-to-istanbul": {
|
||||
"version": "8.1.1",
|
||||
@@ -22411,7 +22453,7 @@
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"@react-three/csg": "^3.2.0",
|
||||
"@react-three/drei": "^9.113.0",
|
||||
"@react-three/fiber": "^8.17.7",
|
||||
"@react-three/rapier": "^1.5.0",
|
||||
"@react-three/postprocessing": "^2.16.3",
|
||||
"@recast-navigation/core": "^0.39.0",
|
||||
"@recast-navigation/three": "^0.39.0",
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
import { Box3, BoxGeometry, EdgesGeometry, Vector3 } from "three";
|
||||
import { RigidBody } from "@react-three/rapier";
|
||||
import { useMemo } from "react";
|
||||
|
||||
export const AssetBoundingBox = ({ boundingBox }: { boundingBox: Box3 | null }) => {
|
||||
if (!boundingBox) return null;
|
||||
export const AssetBoundingBox = ({ boundingBox }: { boundingBox: Box3 }) => {
|
||||
|
||||
const size = boundingBox.getSize(new Vector3());
|
||||
const center = boundingBox.getCenter(new Vector3());
|
||||
|
||||
const boxGeometry = new BoxGeometry(size.x, size.y, size.z);
|
||||
const edges = new EdgesGeometry(boxGeometry);
|
||||
const boxGeometry = useMemo(() => new BoxGeometry(size.x, size.y, size.z), [size]);
|
||||
const edges = useMemo(() => new EdgesGeometry(boxGeometry), [boxGeometry]);
|
||||
|
||||
return (
|
||||
<group name='Asset FallBack'>
|
||||
<lineSegments position={center}>
|
||||
<RigidBody includeInvisible type="fixed" colliders="cuboid" position={center.toArray()} rotation={[0, 0, 0]}>
|
||||
<lineSegments>
|
||||
<bufferGeometry attach="geometry" {...edges} />
|
||||
<lineBasicMaterial depthWrite={false} attach="material" color="gray" linewidth={1} />
|
||||
</lineSegments>
|
||||
</group>
|
||||
|
||||
<mesh visible={false}>
|
||||
<boxGeometry args={[size.x, size.y, size.z]} />
|
||||
<meshStandardMaterial transparent opacity={0} />
|
||||
</mesh>
|
||||
</RigidBody>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
148
app/src/modules/builder/asset/models/model/conveyorCollider.tsx
Normal file
148
app/src/modules/builder/asset/models/model/conveyorCollider.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
import * as THREE from 'three';
|
||||
import { CollisionPayload, RapierRigidBody, RigidBody } from '@react-three/rapier';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useFrame } from '@react-three/fiber';
|
||||
|
||||
function ConveyorCollider({ boundingBox, asset, conveyorPlaneSize, onReachEnd }: {
|
||||
boundingBox: THREE.Box3 | null,
|
||||
asset: Asset,
|
||||
conveyorPlaneSize: [number, number] | null,
|
||||
onReachEnd?: (rigidBody: RapierRigidBody) => void
|
||||
}) {
|
||||
const conveyorRef = useRef<any>(null);
|
||||
const [objectsOnConveyor, setObjectsOnConveyor] = useState<Set<any>>(new Set());
|
||||
const conveyorDirection = useRef<THREE.Vector3>(new THREE.Vector3());
|
||||
const conveyorSpeed = 2;
|
||||
const reached = useRef<Set<RapierRigidBody>>(new Set());
|
||||
|
||||
useEffect(() => {
|
||||
if (!boundingBox || !conveyorPlaneSize) return;
|
||||
|
||||
const [width, depth] = conveyorPlaneSize;
|
||||
if (width < depth) {
|
||||
conveyorDirection.current.set(0, 0, 1);
|
||||
} else {
|
||||
conveyorDirection.current.set(1, 0, 0);
|
||||
}
|
||||
|
||||
const rotation = new THREE.Euler().fromArray(asset.rotation || [0, 0, 0]);
|
||||
conveyorDirection.current.applyEuler(rotation);
|
||||
}, [boundingBox, conveyorPlaneSize, asset.rotation]);
|
||||
|
||||
const handleMaterialEnter = (e: CollisionPayload) => {
|
||||
if (e.other.rigidBody) {
|
||||
setObjectsOnConveyor(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.add(e.other.rigidBody);
|
||||
return newSet;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleMaterialExit = (e: CollisionPayload) => {
|
||||
if (e.other.rigidBody) {
|
||||
setObjectsOnConveyor(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(e.other.rigidBody);
|
||||
return newSet;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// useFrame(() => {
|
||||
// const forward = conveyorDirection.current.clone().normalize();
|
||||
// const side = new THREE.Vector3().crossVectors(forward, new THREE.Vector3(0, 1, 0)).normalize(); // perpendicular vector
|
||||
|
||||
// const force = forward.clone().multiplyScalar(conveyorSpeed);
|
||||
|
||||
// objectsOnConveyor.forEach(rigidBody => {
|
||||
// if (rigidBody) {
|
||||
// const position = rigidBody.translation();
|
||||
|
||||
// const centerLine = conveyorRef.current.translation();
|
||||
// const relative = new THREE.Vector3().subVectors(position, centerLine);
|
||||
// const sideOffset = relative.dot(side);
|
||||
|
||||
// const centeringStrength = 10;
|
||||
// const centeringForce = side.clone().multiplyScalar(-sideOffset * centeringStrength);
|
||||
|
||||
// const totalForce = force.clone().add(centeringForce);
|
||||
|
||||
// rigidBody.setAngvel(new THREE.Vector3(0, 0, 0), true);
|
||||
// rigidBody.setLinvel(totalForce, true);
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
useFrame(() => {
|
||||
if (
|
||||
!boundingBox ||
|
||||
!conveyorPlaneSize ||
|
||||
!conveyorRef.current ||
|
||||
typeof conveyorRef.current.translation !== 'function'
|
||||
) return;
|
||||
|
||||
const forward = conveyorDirection.current.clone().normalize();
|
||||
const side = new THREE.Vector3().crossVectors(forward, new THREE.Vector3(0, 1, 0)).normalize();
|
||||
const force = forward.clone().multiplyScalar(conveyorSpeed);
|
||||
const start = conveyorRef.current.translation();
|
||||
const conveyorLength = Math.max(conveyorPlaneSize[0], conveyorPlaneSize[1]);
|
||||
const halfLength = conveyorLength / 2;
|
||||
|
||||
objectsOnConveyor.forEach(rigidBody => {
|
||||
if (!rigidBody) return;
|
||||
|
||||
const position = rigidBody.translation();
|
||||
const relative = new THREE.Vector3().subVectors(position, start);
|
||||
const forwardOffset = relative.dot(forward);
|
||||
const sideOffset = relative.dot(side);
|
||||
const atEnd = forwardOffset >= halfLength - 0.5;
|
||||
|
||||
if (!atEnd) {
|
||||
const centeringStrength = 10;
|
||||
const centeringForce = side.clone().multiplyScalar(-sideOffset * centeringStrength);
|
||||
const totalForce = force.clone().add(centeringForce);
|
||||
rigidBody.setAngvel(new THREE.Vector3(0, 0, 0), true);
|
||||
rigidBody.setLinvel(totalForce, true);
|
||||
} else {
|
||||
rigidBody.setLinvel(new THREE.Vector3(0, 0, 0), true);
|
||||
rigidBody.setAngvel(new THREE.Vector3(0, 0, 0), true);
|
||||
|
||||
if (!reached.current.has(rigidBody)) {
|
||||
reached.current.add(rigidBody);
|
||||
console.log("✅ Triggering spawn from conveyor");
|
||||
onReachEnd?.(rigidBody); // ✅ trigger here
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{asset.eventData?.type === 'Conveyor' && conveyorPlaneSize && (
|
||||
<RigidBody
|
||||
ref={conveyorRef}
|
||||
type="fixed"
|
||||
position={[0, (boundingBox?.max.y ?? 0) + 0.1, 0]}
|
||||
rotation={[Math.PI / 2, 0, 0]}
|
||||
sensor
|
||||
userData={{ isConveyor: true }}
|
||||
onIntersectionEnter={handleMaterialEnter}
|
||||
onIntersectionExit={handleMaterialExit}
|
||||
colliders="cuboid"
|
||||
>
|
||||
<mesh>
|
||||
<planeGeometry args={conveyorPlaneSize} />
|
||||
<meshBasicMaterial
|
||||
color="green"
|
||||
transparent
|
||||
opacity={0.3}
|
||||
visible={false}
|
||||
/>
|
||||
</mesh>
|
||||
</RigidBody>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ConveyorCollider;
|
||||
@@ -1,6 +1,8 @@
|
||||
import * as THREE from 'three';
|
||||
import { SkeletonUtils } from 'three-stdlib';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { retrieveGLTF, storeGLTF } from '../../../../../utils/indexDB/idbUtils';
|
||||
import { RapierRigidBody, RigidBody } from '@react-three/rapier';
|
||||
import { GLTF, GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
|
||||
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader';
|
||||
import { ThreeEvent, useFrame, useThree } from '@react-three/fiber';
|
||||
@@ -15,14 +17,14 @@ import { useParams } from 'react-router-dom';
|
||||
import { getUserData } from '../../../../../functions/getUserData';
|
||||
import { useSceneContext } from '../../../../scene/sceneContext';
|
||||
import { useVersionContext } from '../../../version/versionContext';
|
||||
import { SkeletonUtils } from 'three-stdlib';
|
||||
import { useAnimationPlaySpeed } from '../../../../../store/usePlayButtonStore';
|
||||
import { upsertProductOrEventApi } from '../../../../../services/simulation/products/UpsertProductOrEventApi';
|
||||
import { getAssetIksApi } from '../../../../../services/simulation/ik/getAssetIKs';
|
||||
import ConveyorCollider from './conveyorCollider';
|
||||
|
||||
function Model({ asset }: { readonly asset: Asset }) {
|
||||
const url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_MARKETPLACE_URL}`;
|
||||
const { camera, controls, gl } = useThree();
|
||||
const { camera, controls, gl, scene } = useThree();
|
||||
const { activeTool } = useActiveTool();
|
||||
const { toolMode } = useToolMode();
|
||||
const { toggleView } = useToggleView();
|
||||
@@ -50,7 +52,9 @@ function Model({ asset }: { readonly asset: Asset }) {
|
||||
const [isRendered, setIsRendered] = useState(false);
|
||||
const [gltfScene, setGltfScene] = useState<GLTF["scene"] | null>(null);
|
||||
const [boundingBox, setBoundingBox] = useState<THREE.Box3 | null>(null);
|
||||
const [conveyorPlaneSize, setConveyorPlaneSize] = useState<[number, number] | null>(null);
|
||||
const groupRef = useRef<THREE.Group>(null);
|
||||
const rigidBodyRef = useRef<RapierRigidBody>(null);
|
||||
const mixerRef = useRef<THREE.AnimationMixer>();
|
||||
const actions = useRef<{ [name: string]: THREE.AnimationAction }>({});
|
||||
const [previousAnimation, setPreviousAnimation] = useState<string | null>(null);
|
||||
@@ -181,6 +185,11 @@ function Model({ asset }: { readonly asset: Asset }) {
|
||||
const calculateBoundingBox = (scene: THREE.Object3D) => {
|
||||
const box = new THREE.Box3().setFromObject(scene);
|
||||
setBoundingBox(box);
|
||||
|
||||
if (asset.eventData?.type === 'Conveyor') {
|
||||
const size = box.getSize(new THREE.Vector3());
|
||||
setConveyorPlaneSize([size.x, size.z]);
|
||||
}
|
||||
};
|
||||
|
||||
loadModel();
|
||||
@@ -464,7 +473,7 @@ function Model({ asset }: { readonly asset: Asset }) {
|
||||
position={asset.position}
|
||||
rotation={asset.rotation}
|
||||
visible={asset.isVisible}
|
||||
userData={{ ...asset, iks: ikData }}
|
||||
userData={{ ...asset, iks: ikData, rigidBodyRef: rigidBodyRef.current, boundingBox: boundingBox }}
|
||||
onDoubleClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!toggleView) {
|
||||
@@ -495,11 +504,38 @@ function Model({ asset }: { readonly asset: Asset }) {
|
||||
}}
|
||||
>
|
||||
{gltfScene && (
|
||||
isRendered ? (
|
||||
<primitive object={gltfScene} />
|
||||
) : (
|
||||
<AssetBoundingBox boundingBox={boundingBox} />
|
||||
)
|
||||
<>
|
||||
{isRendered ? (
|
||||
<>
|
||||
<RigidBody
|
||||
type="fixed"
|
||||
colliders='cuboid'
|
||||
ref={rigidBodyRef}
|
||||
>
|
||||
<primitive object={gltfScene} />
|
||||
</ RigidBody>
|
||||
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{boundingBox &&
|
||||
<AssetBoundingBox boundingBox={boundingBox} />
|
||||
}
|
||||
</>
|
||||
)}
|
||||
<ConveyorCollider boundingBox={boundingBox}
|
||||
asset={asset}
|
||||
conveyorPlaneSize={conveyorPlaneSize}
|
||||
onReachEnd={(rigidBody) => {
|
||||
// Option A: Reset the same object
|
||||
rigidBody.setTranslation({ x: 0, y: 10, z: 0 }, true);
|
||||
rigidBody.setLinvel({ x: 0, y: 0, z: 0 }, true);
|
||||
rigidBody.setAngvel({ x: 0, y: 0, z: 0 }, true);
|
||||
rigidBody.wakeUp();
|
||||
|
||||
// Option B: You can also call a function that sets a state to "add a new rigid body"
|
||||
}} />
|
||||
</>
|
||||
)}
|
||||
</group>
|
||||
);
|
||||
|
||||
@@ -36,10 +36,8 @@ const BoundingBox = ({ boundingBoxRef, isPerAsset = true }: BoundingBoxProps) =>
|
||||
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 box: THREE.Box3 = obj.userData.boundingBox ?? new THREE.Box3().setFromObject(obj);
|
||||
const size = new THREE.Vector3();
|
||||
const center = new THREE.Vector3();
|
||||
box.getSize(size);
|
||||
@@ -57,12 +55,17 @@ const BoundingBox = ({ boundingBoxRef, isPerAsset = true }: BoundingBoxProps) =>
|
||||
};
|
||||
});
|
||||
} else {
|
||||
const box = new THREE.Box3();
|
||||
selectedAssets.forEach((obj: any) => box.expandByObject(obj.clone()));
|
||||
const unionBox = new THREE.Box3();
|
||||
|
||||
selectedAssets.forEach((obj: any) => {
|
||||
const localBox: THREE.Box3 = obj.userData.boundingBox ?? new THREE.Box3().setFromObject(obj);
|
||||
unionBox.union(localBox);
|
||||
});
|
||||
|
||||
const size = new THREE.Vector3();
|
||||
const center = new THREE.Vector3();
|
||||
box.getSize(size);
|
||||
box.getCenter(center);
|
||||
unionBox.getSize(size);
|
||||
unionBox.getCenter(center);
|
||||
|
||||
const halfSize = size.clone().multiplyScalar(0.5);
|
||||
const min = center.clone().sub(halfSize);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { RigidBody } from "@react-three/rapier";
|
||||
import { useTileDistance, useToggleView } from "../../../store/builder/store";
|
||||
import * as CONSTANTS from "../../../types/world/worldConstants";
|
||||
|
||||
@@ -6,7 +7,7 @@ const Ground = ({ plane }: any) => {
|
||||
const { planeValue, gridValue } = useTileDistance();
|
||||
|
||||
return (
|
||||
<mesh name="Ground">
|
||||
<>
|
||||
<mesh
|
||||
name="Grid"
|
||||
position={!toggleView ? CONSTANTS.gridConfig.position3D : CONSTANTS.gridConfig.position2D}
|
||||
@@ -20,17 +21,23 @@ const Ground = ({ plane }: any) => {
|
||||
]}
|
||||
/>
|
||||
</mesh>
|
||||
<mesh
|
||||
ref={plane}
|
||||
rotation-x={CONSTANTS.planeConfig.rotation}
|
||||
position={!toggleView ? CONSTANTS.planeConfig.position3D : CONSTANTS.planeConfig.position2D}
|
||||
name="Plane"
|
||||
receiveShadow
|
||||
|
||||
<RigidBody
|
||||
type="fixed"
|
||||
colliders='cuboid'
|
||||
>
|
||||
<planeGeometry args={[planeValue.width, planeValue.height]} />
|
||||
<meshBasicMaterial color={CONSTANTS.planeConfig.color} />
|
||||
</mesh>
|
||||
</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>
|
||||
</RigidBody>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
112
app/src/modules/scene/physics/colliders.tsx
Normal file
112
app/src/modules/scene/physics/colliders.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
// import { RigidBody, RapierRigidBody } from '@react-three/rapier'
|
||||
// import { useRef } from 'react';
|
||||
// import { useLoadingProgress } from '../../../store/builder/store'
|
||||
// import { MaterialModel } from '../../simulation/materials/instances/material/materialModel';
|
||||
|
||||
// function Colliders() {
|
||||
// const { loadingProgress } = useLoadingProgress();
|
||||
// const rigidBodyRef = useRef<RapierRigidBody>(null);
|
||||
|
||||
// const handleSleep = () => {
|
||||
// console.log("slept");
|
||||
// const body = rigidBodyRef.current;
|
||||
// if (body) {
|
||||
// body.setTranslation({ x: 0, y: 10, z: 0 }, true);
|
||||
// body.setLinvel({ x: 0, y: 0, z: 0 }, true);
|
||||
// body.setAngvel({ x: 0, y: 0, z: 0 }, true);
|
||||
// body.wakeUp();
|
||||
// }
|
||||
// };
|
||||
|
||||
// return (
|
||||
// <>
|
||||
// {loadingProgress === 0 && (
|
||||
// <RigidBody
|
||||
// ref={rigidBodyRef}
|
||||
// position={[0, 10, 0]}
|
||||
// colliders="cuboid"
|
||||
// angularDamping={5}
|
||||
// linearDamping={1}
|
||||
// restitution={0.1}
|
||||
// onSleep={handleSleep}
|
||||
// >
|
||||
// <MaterialModel materialId='123' materialType={"Default material"} />
|
||||
// </RigidBody>
|
||||
// )}
|
||||
// </>
|
||||
// )
|
||||
// }
|
||||
|
||||
// export default Colliders;
|
||||
|
||||
|
||||
import { RigidBody, RapierRigidBody } from '@react-three/rapier';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useLoadingProgress } from '../../../store/builder/store';
|
||||
import { MaterialModel } from '../../simulation/materials/instances/material/materialModel';
|
||||
import { generateUniqueId } from '../../../functions/generateUniqueId';
|
||||
import { useFrame } from '@react-three/fiber';
|
||||
|
||||
type MaterialInstance = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
function Colliders() {
|
||||
const { loadingProgress } = useLoadingProgress();
|
||||
const [materials, setMaterials] = useState<MaterialInstance[]>([]);
|
||||
|
||||
const spawnNewMaterial = () => {
|
||||
setMaterials(prev => [...prev, { id: generateUniqueId() }]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (loadingProgress === 0 && materials.length === 0) {
|
||||
spawnNewMaterial(); // spawn one initially
|
||||
}
|
||||
}, [loadingProgress]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{materials.map((mat) => (
|
||||
<SingleMaterial
|
||||
key={mat.id}
|
||||
onReachEnd={spawnNewMaterial}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
export default Colliders;
|
||||
|
||||
function SingleMaterial({ onReachEnd }: { onReachEnd: () => void }) {
|
||||
const rigidBodyRef = useRef<RapierRigidBody>(null);
|
||||
const hasReachedEnd = useRef(false);
|
||||
|
||||
useFrame(() => {
|
||||
const body = rigidBodyRef.current;
|
||||
if (!body || hasReachedEnd.current) return;
|
||||
|
||||
const position = body.translation();
|
||||
if (position && position.z > 5) {
|
||||
console.log('✅ Reached end — triggering spawn');
|
||||
hasReachedEnd.current = true;
|
||||
body.setLinvel({ x: 0, y: 0, z: 0 }, true);
|
||||
body.setAngvel({ x: 0, y: 0, z: 0 }, true);
|
||||
onReachEnd();
|
||||
body.setTranslation({ x: 0, y: -100, z: 0 }, true);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<RigidBody
|
||||
ref={rigidBodyRef}
|
||||
position={[0, 10, 0]}
|
||||
colliders="cuboid"
|
||||
angularDamping={5}
|
||||
linearDamping={1}
|
||||
restitution={0.1}
|
||||
>
|
||||
<MaterialModel materialId='123' materialType='Default material' />
|
||||
</RigidBody>
|
||||
);
|
||||
}
|
||||
@@ -14,6 +14,8 @@ import { getAllProjects } from "../../services/dashboard/getAllProjects";
|
||||
import { getUserData } from "../../functions/getUserData";
|
||||
import { useLoadingProgress, useSocketStore } from "../../store/builder/store";
|
||||
import { Color } from "three";
|
||||
import { Physics } from "@react-three/rapier";
|
||||
import Colliders from "./physics/colliders";
|
||||
|
||||
export default function Scene({ layout }: { readonly layout: 'Main Layout' | 'Comparison Layout' }) {
|
||||
const map = useMemo(() => [
|
||||
@@ -71,8 +73,12 @@ export default function Scene({ layout }: { readonly layout: 'Main Layout' | 'Co
|
||||
>
|
||||
<Setup />
|
||||
<Collaboration />
|
||||
<Builder />
|
||||
<Simulation />
|
||||
<Physics gravity={[0, -9.81, 0]} debug >
|
||||
<Builder />
|
||||
<Simulation />
|
||||
|
||||
<Colliders />
|
||||
</Physics>
|
||||
<Visualization />
|
||||
</Canvas>
|
||||
</KeyboardControls>
|
||||
|
||||
@@ -5,6 +5,7 @@ import Controls from '../controls/controls';
|
||||
import { Environment } from '@react-three/drei'
|
||||
|
||||
import background from "../../../assets/textures/hdr/mudroadpuresky2k.hdr";
|
||||
import SecondaryCamera from '../../secondaryCamera/secondaryCamera';
|
||||
|
||||
function Setup() {
|
||||
return (
|
||||
@@ -20,6 +21,7 @@ function Setup() {
|
||||
{/* <MovingClouds /> */}
|
||||
|
||||
<Environment files={background} environmentIntensity={1.5} />
|
||||
<SecondaryCamera/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
169
app/src/modules/secondaryCamera/secondaryCamera.tsx
Normal file
169
app/src/modules/secondaryCamera/secondaryCamera.tsx
Normal file
@@ -0,0 +1,169 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useThree, useFrame } from '@react-three/fiber';
|
||||
import { CameraControls, Html, PerspectiveCamera, PivotControls } from '@react-three/drei';
|
||||
import * as THREE from 'three';
|
||||
import {
|
||||
useSecondaryCameraData,
|
||||
useSecondaryCameraEdit,
|
||||
useSecondaryCameraState,
|
||||
} from '../../store/builder/store';
|
||||
|
||||
type CameraData = {
|
||||
id: any;
|
||||
name: string;
|
||||
position: [number, number, number];
|
||||
target: [number, number, number];
|
||||
};
|
||||
|
||||
function SecondaryCameraView() {
|
||||
const cameraRef = useRef<THREE.PerspectiveCamera | null>(null);
|
||||
const helperRef = useRef<THREE.CameraHelper | null>(null);
|
||||
const rendererRef = useRef<THREE.WebGLRenderer | null>(null);
|
||||
const dummyMeshRef = useRef<THREE.Mesh | null>(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 (
|
||||
<>
|
||||
<Html
|
||||
className='secondary-camera-controls'
|
||||
style={{
|
||||
zIndex: 10,
|
||||
position: 'absolute',
|
||||
bottom: 350,
|
||||
right: 0,
|
||||
border: '1px solid black',
|
||||
backgroundColor: 'black',
|
||||
}}
|
||||
>
|
||||
<button onClick={handleCamera}>+Add Camera</button>
|
||||
</Html>
|
||||
|
||||
{selectedSecondaryCamera && (
|
||||
<PerspectiveCamera
|
||||
ref={cameraRef}
|
||||
position={selectedSecondaryCamera.position}
|
||||
onUpdate={(self) => self.lookAt(...selectedSecondaryCamera.target)}
|
||||
>
|
||||
<PivotControls
|
||||
anchor={[0, 0, 0]}
|
||||
onDrag={() => {
|
||||
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,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<mesh ref={dummyMeshRef}>
|
||||
<sphereGeometry args={[0.1, 32, 32]} />
|
||||
<meshBasicMaterial color="yellow" />
|
||||
</mesh>
|
||||
</PivotControls>
|
||||
</PerspectiveCamera>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default SecondaryCameraView;
|
||||
46
app/src/modules/secondaryCamera/secondaryCanvas.tsx
Normal file
46
app/src/modules/secondaryCamera/secondaryCanvas.tsx
Normal file
@@ -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) => (
|
||||
<div key={camera.id} className='secondary-camera-item' onClick={() => handleSelectCamera(camera)} style={{ zIndex: 10, position: 'absolute', bottom: 300 - (camera.id * 30), right: 0, border: '1px solid black', backgroundColor: 'black' }}>
|
||||
<p>{camera.name}</p>
|
||||
</div>
|
||||
))}
|
||||
<div onClick={() => { 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
|
||||
</div>
|
||||
<div className='secondary-canvas-container' style={{ zIndex: 10, position: 'absolute', bottom: 0, right: 0, width: '300px', height: '200px' }}>
|
||||
<canvas id='secondary-canvas' className='secondary-canvas' style={{ height: "100%", width: "100%", background: "gray" }}></canvas>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SecondaryCanvas
|
||||
@@ -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<boolean>(true);
|
||||
const [currentPath, setCurrentPath] = useState<[number, number, number][]>([]);
|
||||
const rigidBodyRef = useRef<RapierRigidBody>(null);
|
||||
const { scene } = useThree();
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -19,7 +19,7 @@ type ModelType = keyof typeof modelPaths;
|
||||
interface ModelProps extends React.ComponentProps<'group'> {
|
||||
materialId: string;
|
||||
materialType: ModelType;
|
||||
matRef: React.Ref<THREE.Group<THREE.Object3DEventMap>>
|
||||
matRef?: React.Ref<THREE.Group<THREE.Object3DEventMap>>
|
||||
}
|
||||
|
||||
export function MaterialModel({ materialId, materialType, matRef, ...props }: Readonly<ModelProps>) {
|
||||
|
||||
@@ -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<boolean>(true);
|
||||
const [currentPath, setCurrentPath] = useState<[number, number, number][]>([]);
|
||||
const rigidBodyRef = useRef<RapierRigidBody>(null);
|
||||
const { scene } = useThree();
|
||||
|
||||
const object = scene.getObjectByProperty('uuid', agvUuid);
|
||||
|
||||
if (object?.userData.rigidBodyRef) {
|
||||
(rigidBodyRef as React.MutableRefObject<RapierRigidBody | null>).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;
|
||||
|
||||
@@ -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 = () => {
|
||||
<ComparisonSceneProvider />
|
||||
</VersionProvider>
|
||||
</SceneProvider>
|
||||
|
||||
<SecondaryCanvas />
|
||||
|
||||
{selectedUser && <FollowPerson />}
|
||||
{isLogListVisible && (
|
||||
<RenderOverlay>
|
||||
|
||||
@@ -724,3 +724,49 @@ export const useSelectedComment = create<any>((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<CameraData>) => void;
|
||||
};
|
||||
|
||||
export const useSecondaryCameraData = create<SecondaryCameraStore>((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<SecondaryCameraState>((set) => ({
|
||||
selectedSecondaryCamera: null,
|
||||
setSelectedSecondaryCamera: (camera) => set({ selectedSecondaryCamera: camera }),
|
||||
}));
|
||||
|
||||
export const useSecondaryCameraEdit = create<any>((set) => ({
|
||||
secondaryCameraEdit: false,
|
||||
setSecondaryCameraEdit: (x: boolean) => set({ secondaryCameraEdit: x }),
|
||||
}));
|
||||
Reference in New Issue
Block a user