feat: Refactor conveyor collider logic, enhance material spawning, and remove unused colliders component

This commit is contained in:
2025-07-17 17:15:46 +05:30
parent 660e21edcc
commit 1e314bc290
7 changed files with 277 additions and 174 deletions

View File

@@ -13,7 +13,6 @@ function ConveyorCollider({ boundingBox, asset, conveyorPlaneSize, onReachEnd }:
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;
@@ -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 (
<>

View File

@@ -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;

View File

@@ -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;

View File

@@ -25,18 +25,26 @@ const Ground = ({ plane }: any) => {
<RigidBody
type="fixed"
colliders='cuboid'
includeInvisible
>
<mesh
ref={plane}
rotation-x={CONSTANTS.planeConfig.rotation}
position={!toggleView ? CONSTANTS.planeConfig.position3D : CONSTANTS.planeConfig.position2D}
name="Plane"
receiveShadow
position={[0, 0, 0]}
visible={false}
>
<planeGeometry args={[planeValue.width, planeValue.height]} />
<meshBasicMaterial color={CONSTANTS.planeConfig.color} />
</mesh>
</RigidBody>
<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>
</>
);
};

View File

@@ -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<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>
);
}

View File

@@ -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<RapierRigidBody> }[]>([]);
const spawnedCount = useRef(0);
const { gl, camera, pointer, controls } = useThree();
const [draggedId, setDraggedId] = useState<string | null>(null);
const dragOffset = useRef<THREE.Vector3>(new THREE.Vector3());
const initialDepth = useRef<number>(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<RapierRigidBody>(),
}
];
});
}, 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 }) => (
<RigidBody
key={id}
ref={ref}
position={position}
colliders="cuboid"
angularDamping={0.5}
linearDamping={0.5}
restitution={0.1}
// onSleep={() => handleSleep(id)}
>
<MaterialModel
materialId={id}
materialType="Default material"
onPointerDown={(e) => {
e.stopPropagation();
handlePointerDown(id);
}}
/>
</RigidBody>
))}
</>
);
}
export default MaterialSpawner;

View File

@@ -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
>
<Setup />
<Collaboration />
<Physics gravity={[0, -9.81, 0]} debug >
<Physics gravity={[0, -9.81, 0]} allowedLinearError={20} numSolverIterations={20} debug >
<Builder />
<Simulation />
<Colliders />
<MaterialSpawner
position={[0, 3, 0]}
spawnInterval={1000}
spawnCount={50}
/>
<MaterialSpawner
position={[-21, 3, -8]}
spawnInterval={1000}
spawnCount={50}
/>
</Physics>
<Visualization />
</Canvas>