diff --git a/app/package-lock.json b/app/package-lock.json index 5da4733..b12c166 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -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" } diff --git a/app/package.json b/app/package.json index a6683aa..eb7152a 100644 --- a/app/package.json +++ b/app/package.json @@ -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", diff --git a/app/src/modules/builder/asset/functions/assetBoundingBox.tsx b/app/src/modules/builder/asset/functions/assetBoundingBox.tsx index d74f37d..9dea36a 100644 --- a/app/src/modules/builder/asset/functions/assetBoundingBox.tsx +++ b/app/src/modules/builder/asset/functions/assetBoundingBox.tsx @@ -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 ( - - + + - + + + + + + ); -}; \ No newline at end of file +}; diff --git a/app/src/modules/builder/asset/models/model/conveyorCollider.tsx b/app/src/modules/builder/asset/models/model/conveyorCollider.tsx new file mode 100644 index 0000000..e95ac73 --- /dev/null +++ b/app/src/modules/builder/asset/models/model/conveyorCollider.tsx @@ -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(null); + const [objectsOnConveyor, setObjectsOnConveyor] = useState>(new Set()); + const conveyorDirection = useRef(new THREE.Vector3()); + const conveyorSpeed = 2; + const reached = useRef>(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 && ( + + + + + + + )} + + ); +} + +export default ConveyorCollider; \ No newline at end of file diff --git a/app/src/modules/builder/asset/models/model/model.tsx b/app/src/modules/builder/asset/models/model/model.tsx index 7baa4b7..4cc4ad5 100644 --- a/app/src/modules/builder/asset/models/model/model.tsx +++ b/app/src/modules/builder/asset/models/model/model.tsx @@ -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(null); const [boundingBox, setBoundingBox] = useState(null); + const [conveyorPlaneSize, setConveyorPlaneSize] = useState<[number, number] | null>(null); const groupRef = useRef(null); + const rigidBodyRef = useRef(null); const mixerRef = useRef(); const actions = useRef<{ [name: string]: THREE.AnimationAction }>({}); const [previousAnimation, setPreviousAnimation] = useState(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 ? ( - - ) : ( - - ) + <> + {isRendered ? ( + <> + + + + + + ) : ( + <> + {boundingBox && + + } + + )} + { + // 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" + }} /> + )} ); diff --git a/app/src/modules/scene/controls/selectionControls/boundingBoxHelper.tsx b/app/src/modules/scene/controls/selectionControls/boundingBoxHelper.tsx index b30b03e..43e044c 100644 --- a/app/src/modules/scene/controls/selectionControls/boundingBoxHelper.tsx +++ b/app/src/modules/scene/controls/selectionControls/boundingBoxHelper.tsx @@ -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); diff --git a/app/src/modules/scene/environment/ground.tsx b/app/src/modules/scene/environment/ground.tsx index f6baeef..3d3538d 100644 --- a/app/src/modules/scene/environment/ground.tsx +++ b/app/src/modules/scene/environment/ground.tsx @@ -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 ( - + <> { ]} /> - - - - - + + + + + + ); }; diff --git a/app/src/modules/scene/physics/colliders.tsx b/app/src/modules/scene/physics/colliders.tsx new file mode 100644 index 0000000..1dbcd4a --- /dev/null +++ b/app/src/modules/scene/physics/colliders.tsx @@ -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(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 && ( +// +// +// +// )} +// +// ) +// } + +// 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([]); + + const spawnNewMaterial = () => { + setMaterials(prev => [...prev, { id: generateUniqueId() }]); + }; + + useEffect(() => { + if (loadingProgress === 0 && materials.length === 0) { + spawnNewMaterial(); // spawn one initially + } + }, [loadingProgress]); + + return ( + <> + {materials.map((mat) => ( + + ))} + + ); +} +export default Colliders; + +function SingleMaterial({ onReachEnd }: { onReachEnd: () => void }) { + const rigidBodyRef = useRef(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 ( + + + + ); +} diff --git a/app/src/modules/scene/scene.tsx b/app/src/modules/scene/scene.tsx index 499aef5..b8acb64 100644 --- a/app/src/modules/scene/scene.tsx +++ b/app/src/modules/scene/scene.tsx @@ -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 > - - + + + + + + diff --git a/app/src/modules/scene/setup/setup.tsx b/app/src/modules/scene/setup/setup.tsx index 184bc56..59d5cb5 100644 --- a/app/src/modules/scene/setup/setup.tsx +++ b/app/src/modules/scene/setup/setup.tsx @@ -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() { {/* */} + ) } diff --git a/app/src/modules/secondaryCamera/secondaryCamera.tsx b/app/src/modules/secondaryCamera/secondaryCamera.tsx new file mode 100644 index 0000000..570a772 --- /dev/null +++ b/app/src/modules/secondaryCamera/secondaryCamera.tsx @@ -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(null); + const helperRef = useRef(null); + const rendererRef = useRef(null); + const dummyMeshRef = useRef(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