diff --git a/app/src/modules/builder/asset/models/model/conveyorCollider.tsx b/app/src/modules/builder/asset/models/model/conveyorCollider.tsx index e95ac73..077de1f 100644 --- a/app/src/modules/builder/asset/models/model/conveyorCollider.tsx +++ b/app/src/modules/builder/asset/models/model/conveyorCollider.tsx @@ -13,7 +13,6 @@ function ConveyorCollider({ boundingBox, asset, conveyorPlaneSize, onReachEnd }: 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; @@ -49,72 +48,67 @@ function ConveyorCollider({ boundingBox, asset, conveyorPlaneSize, onReachEnd }: } }; - // useFrame(() => { - // const forward = conveyorDirection.current.clone().normalize(); - // const side = new THREE.Vector3().crossVectors(forward, new THREE.Vector3(0, 1, 0)).normalize(); // perpendicular vector + useFrame(() => { + 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); + + 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) { - // const position = rigidBody.translation(); + // if (!rigidBody) return; - // const centerLine = conveyorRef.current.translation(); - // const relative = new THREE.Vector3().subVectors(position, centerLine); - // const sideOffset = relative.dot(side); + // 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; - // const centeringStrength = 10; + // 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); // } // }); // }); - 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 ( <> diff --git a/app/src/modules/scene/controls/selectionControls/copyPasteControls.tsx b/app/src/modules/scene/controls/selectionControls/copyPasteControls.tsx index e8c8886..6a48465 100644 --- a/app/src/modules/scene/controls/selectionControls/copyPasteControls.tsx +++ b/app/src/modules/scene/controls/selectionControls/copyPasteControls.tsx @@ -111,6 +111,11 @@ const CopyPasteControls = ({ const copySelection = () => { if (selectedAssets.length > 0) { const newClones = selectedAssets.map((asset: any) => { + if (asset.userData.rigidBodyRef) { + let userData = { ...asset.userData }; + delete userData.rigidBodyRef; + asset.userData = userData; + } const clone = SkeletonUtils.clone(asset); clone.position.copy(asset.position); return clone; diff --git a/app/src/modules/scene/controls/selectionControls/duplicationControls.tsx b/app/src/modules/scene/controls/selectionControls/duplicationControls.tsx index 3b52cdf..38c62d1 100644 --- a/app/src/modules/scene/controls/selectionControls/duplicationControls.tsx +++ b/app/src/modules/scene/controls/selectionControls/duplicationControls.tsx @@ -106,6 +106,11 @@ const DuplicationControls = ({ const duplicateSelection = () => { if (selectedAssets.length > 0 && duplicatedObjects.length === 0) { const newClones = selectedAssets.map((asset: any) => { + if (asset.userData.rigidBodyRef) { + let userData = { ...asset.userData }; + delete userData.rigidBodyRef; + asset.userData = userData; + } const clone = SkeletonUtils.clone(asset); clone.position.copy(asset.position); return clone; diff --git a/app/src/modules/scene/environment/ground.tsx b/app/src/modules/scene/environment/ground.tsx index 3d3538d..16ce19d 100644 --- a/app/src/modules/scene/environment/ground.tsx +++ b/app/src/modules/scene/environment/ground.tsx @@ -25,18 +25,26 @@ const Ground = ({ plane }: any) => { - + + + + ); }; diff --git a/app/src/modules/scene/physics/colliders.tsx b/app/src/modules/scene/physics/colliders.tsx deleted file mode 100644 index 1dbcd4a..0000000 --- a/app/src/modules/scene/physics/colliders.tsx +++ /dev/null @@ -1,112 +0,0 @@ -// 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/physics/materialSpawner.tsx b/app/src/modules/scene/physics/materialSpawner.tsx new file mode 100644 index 0000000..18f5fdb --- /dev/null +++ b/app/src/modules/scene/physics/materialSpawner.tsx @@ -0,0 +1,194 @@ +import { RigidBody, RapierRigidBody } from '@react-three/rapier'; +import React, { useEffect, useRef, useState } from 'react'; +import { useLoadingProgress } from '../../../store/builder/store'; +import { MaterialModel } from '../../simulation/materials/instances/material/materialModel'; +import { useThree } from '@react-three/fiber'; +import * as THREE from 'three'; +import { CameraControls } from '@react-three/drei'; + +type MaterialSpawnerProps = { + position: [number, number, number]; + spawnInterval: number; + spawnCount?: number; +}; + +function MaterialSpawner({ position, spawnInterval, spawnCount }: MaterialSpawnerProps) { + const { loadingProgress } = useLoadingProgress(); + const [spawned, setSpawned] = useState<{ id: string; position: [number, number, number]; ref: React.RefObject }[]>([]); + const spawnedCount = useRef(0); + const { gl, camera, pointer, controls } = useThree(); + const [draggedId, setDraggedId] = useState(null); + const dragOffset = useRef(new THREE.Vector3()); + const initialDepth = useRef(0); + + useEffect(() => { + if (loadingProgress !== 0) return; + + let interval: NodeJS.Timeout | null = null; + + const startSpawning = () => { + if (!interval) { + interval = setInterval(() => { + setSpawned(prev => { + if (spawnCount !== undefined && spawnedCount.current >= spawnCount) { + clearInterval(interval!); + interval = null; + return prev; + } + spawnedCount.current++; + return [ + ...prev, + { + id: crypto.randomUUID(), + position, + ref: React.createRef(), + } + ]; + }); + }, spawnInterval); + } + }; + + const stopSpawning = () => { + if (interval) { + clearInterval(interval); + interval = null; + } + }; + + const handleVisibility = () => { + if (document.visibilityState === 'visible') { + startSpawning(); + } else { + stopSpawning(); + } + }; + + document.addEventListener('visibilitychange', handleVisibility); + handleVisibility(); + + return () => { + stopSpawning(); + document.removeEventListener('visibilitychange', handleVisibility); + }; + }, [loadingProgress, spawnInterval, spawnCount, position]); + + const handleSleep = (id: string) => { + setSpawned(prev => prev.filter(obj => obj.id !== id)); + }; + + const handlePointerDown = (id: string) => { + if (controls) { + (controls as CameraControls).enabled = false; + } + setDraggedId(id); + + const obj = spawned.find(o => o.id === id); + if (!obj || !obj.ref.current) return; + + const currentPosition = obj.ref.current.translation(); + + const screenPosition = new THREE.Vector3(currentPosition.x, currentPosition.y, currentPosition.z).project(camera); + + dragOffset.current = new THREE.Vector3( + screenPosition.x - pointer.x, + 0, + screenPosition.y - pointer.y + ); + + initialDepth.current = new THREE.Vector3(currentPosition.x, currentPosition.y, currentPosition.z) + .sub(camera.position) + .length(); + + obj.ref.current.setGravityScale(0, true); + obj.ref.current.setLinearDamping(10); + obj.ref.current.setAngularDamping(10); + }; + + const handlePointerMove = () => { + if (!draggedId) return; + + const obj = spawned.find(o => o.id === draggedId); + if (!obj || !obj.ref.current) return; + + const targetScreenPos = new THREE.Vector3( + pointer.x + dragOffset.current.x, + 0, + pointer.y + dragOffset.current.z + ); + + const targetWorldPos = new THREE.Vector3( + targetScreenPos.x, + targetScreenPos.z, + 0.5 + ).unproject(camera); + + const direction = targetWorldPos.sub(camera.position).normalize(); + const finalPosition = camera.position.clone().add(direction.multiplyScalar(initialDepth.current)); + + const currentPosition = obj.ref.current.translation(); + + const moveDirection = new THREE.Vector3().subVectors(finalPosition, currentPosition); + + obj.ref.current.setLinvel({ + x: moveDirection.x * 20, + y: moveDirection.y * 20, + z: moveDirection.z * 20 + }, true); + }; + + const handlePointerUp = () => { + if (controls) { + (controls as CameraControls).enabled = true; + } + if (!draggedId) return; + + const obj = spawned.find(o => o.id === draggedId); + if (obj?.ref.current) { + obj.ref.current.setGravityScale(1, true); + obj.ref.current.setLinearDamping(0.5); + obj.ref.current.setAngularDamping(0.5); + } + + setDraggedId(null); + }; + + useEffect(() => { + const canvasElement = gl.domElement; + canvasElement.addEventListener('pointermove', handlePointerMove); + canvasElement.addEventListener('pointerup', handlePointerUp); + + return () => { + canvasElement.removeEventListener('pointermove', handlePointerMove); + canvasElement.removeEventListener('pointerup', handlePointerUp); + }; + }, [draggedId, spawned, controls, camera, gl]); + + return ( + <> + {spawned.map(({ id, position, ref }) => ( + handleSleep(id)} + > + { + e.stopPropagation(); + handlePointerDown(id); + }} + /> + + ))} + + ); +} + +export default MaterialSpawner; \ No newline at end of file diff --git a/app/src/modules/scene/scene.tsx b/app/src/modules/scene/scene.tsx index b8acb64..1fc8547 100644 --- a/app/src/modules/scene/scene.tsx +++ b/app/src/modules/scene/scene.tsx @@ -15,7 +15,7 @@ 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"; +import MaterialSpawner from "./physics/materialSpawner"; export default function Scene({ layout }: { readonly layout: 'Main Layout' | 'Comparison Layout' }) { const map = useMemo(() => [ @@ -73,11 +73,20 @@ export default function Scene({ layout }: { readonly layout: 'Main Layout' | 'Co > - + - + +