feat: Implement ColliderCreator and ColliderInstance components, enhance PhysicsSimulator with multiple MaterialSpawner instances

This commit is contained in:
2025-07-23 14:03:00 +05:30
parent d4d66d9d32
commit ec4a4247b2
5 changed files with 319 additions and 24 deletions

View File

@@ -0,0 +1,58 @@
import { useEffect, useState } from 'react';
import { useThree } from '@react-three/fiber';
import ColliderInstance from './colliderInstance/colliderInstance';
function ColliderCreator() {
const { camera, gl, scene, raycaster, pointer } = useThree();
const [colliders, setColliders] = useState<
{ id: string; position: [number, number, number]; colliderType: 'Default material' | 'Material 1' | 'Material 2' | 'Material 3' }[]
>([]);
const handleCtrlClick = (e: MouseEvent) => {
if (!e.ctrlKey) return;
raycaster.setFromCamera(pointer, camera);
const intersects = raycaster.intersectObjects(scene.children, true);
if (intersects.length > 0) {
const point = intersects[0].point;
const spawnPosition: [number, number, number] = [point.x, point.y + 0.5, point.z];
setColliders(prev => [
...prev,
{
id: Date.now().toString(),
position: spawnPosition,
colliderType: 'Default material',
}
]);
}
};
useEffect(() => {
const canvas = gl.domElement;
canvas.addEventListener('click', handleCtrlClick);
return () => {
canvas.removeEventListener('click', handleCtrlClick);
};
}, [colliders, camera]);
return (
<>
{colliders.map(({ id, position }) => (
<ColliderInstance
key={id}
id={id}
colliders={colliders}
setColliders={setColliders}
position={position}
/>
))}
</>
);
}
export default ColliderCreator;

View File

@@ -0,0 +1,197 @@
import { CameraControls } from '@react-three/drei';
import { useThree, useFrame } from '@react-three/fiber';
import { CollisionPayload, RapierRigidBody, RigidBody } from '@react-three/rapier';
import { useEffect, useRef, useState } from 'react'
import * as THREE from 'three';
function ColliderInstance({ id, colliders, setColliders, position }: {
id: string;
colliders: { id: string; position: [number, number, number]; colliderType: 'Default material' | 'Material 1' | 'Material 2' | 'Material 3' }[];
setColliders: React.Dispatch<React.SetStateAction<{ id: string; position: [number, number, number]; colliderType: 'Default material' | 'Material 1' | 'Material 2' | 'Material 3' }[]>>;
position: [number, number, number];
}) {
const { camera, gl, pointer, controls } = useThree();
const [draggedId, setDraggedId] = useState<string | null>(null);
const dragOffset = useRef(new THREE.Vector3());
const initialDepth = useRef(0);
const ref = useRef<RapierRigidBody>(null);
const [color, setColor] = useState(new THREE.Color('white'));
const [objectsOnCollider, setObjectsOnCollider] = useState<Set<RapierRigidBody>>(new Set());
const getColorByType = (type: string) => {
switch (type) {
case 'Material 1':
return new THREE.Color('blue');
case 'Material 2':
return new THREE.Color('green');
case 'Material 3':
return new THREE.Color('yellow');
default:
return new THREE.Color('white');
}
};
useEffect(() => {
const current = colliders.find(c => c.id === id);
if (current) {
setColor(getColorByType(current.colliderType));
}
}, [colliders, id]);
const handlePointerDown = (id: string) => {
if (controls) {
(controls as CameraControls).enabled = false;
}
setDraggedId(id);
const collider = colliders.find(c => c.id === id);
if (!collider || !ref.current) return;
const pos = ref.current.translation();
const screenPos = new THREE.Vector3(pos.x, pos.y, pos.z).project(camera);
dragOffset.current = new THREE.Vector3(screenPos.x - pointer.x, 0, screenPos.y - pointer.y);
initialDepth.current = new THREE.Vector3(pos.x, pos.y, pos.z).sub(camera.position).length();
ref.current.setGravityScale(0, true);
ref.current.setLinearDamping(10);
ref.current.setAngularDamping(10);
};
const handlePointerMove = () => {
if (!draggedId) return;
const collider = colliders.find(c => c.id === draggedId);
if (!collider || !ref.current) return;
const screenTarget = new THREE.Vector3(
pointer.x + dragOffset.current.x,
0,
pointer.y + dragOffset.current.z
);
const worldTarget = new THREE.Vector3(screenTarget.x, screenTarget.z, 0.5).unproject(camera);
const dir = worldTarget.clone().sub(camera.position).normalize();
const finalPos = camera.position.clone().add(dir.multiplyScalar(initialDepth.current));
ref.current.setTranslation(
{ x: finalPos.x, y: finalPos.y, z: finalPos.z },
true
);
};
const handlePointerUp = () => {
if (controls) {
(controls as CameraControls).enabled = true;
}
if (!draggedId) return;
if (ref.current) {
ref.current.setGravityScale(1, true);
ref.current.setLinearDamping(0.5);
ref.current.setAngularDamping(0.5);
}
setDraggedId(null);
};
useEffect(() => {
const canvas = gl.domElement;
canvas.addEventListener('pointermove', handlePointerMove);
canvas.addEventListener('pointerup', handlePointerUp);
return () => {
canvas.removeEventListener('pointermove', handlePointerMove);
canvas.removeEventListener('pointerup', handlePointerUp);
};
}, [colliders, draggedId, controls]);
const handleMaterialEnter = (e: CollisionPayload) => {
setColor(new THREE.Color('pink'));
const body = e.other.rigidBody;
const current = colliders.find(c => c.id === id);
if (current) {
if (body && (body.userData as any)?.materialType === current.colliderType) {
setObjectsOnCollider(prev => {
const newSet = new Set(prev);
newSet.add(body);
return newSet;
});
}
}
};
const handleMaterialExit = (e: CollisionPayload) => {
const current = colliders.find(c => c.id === id);
if (current)
setColor(getColorByType(current.colliderType));
const body = e.other.rigidBody;
if (body) {
setObjectsOnCollider(prev => {
const newSet = new Set(prev);
newSet.delete(body);
return newSet;
});
}
};
useFrame(() => {
objectsOnCollider.forEach(rigidBody => {
if (!rigidBody) return;
rigidBody.setLinvel({ x: 0, y: 0, z: 2 }, true);
rigidBody.setAngvel({ x: 0, y: 0, z: 0 }, true);
});
});
return (
<RigidBody
name='Sensor-Collider'
key={id}
ref={ref}
type="fixed"
sensor
position={position}
colliders="cuboid"
includeInvisible
gravityScale={0}
onIntersectionEnter={handleMaterialEnter}
onIntersectionExit={handleMaterialExit}
>
<mesh
onPointerDown={(e) => {
e.stopPropagation();
handlePointerDown(id);
}}
onDoubleClick={(e) => {
e.stopPropagation();
setColliders(prev =>
prev.map(c =>
c.id === id
? {
...c,
colliderType:
c.colliderType === 'Default material'
? 'Material 1'
: c.colliderType === 'Material 1'
? 'Material 2'
: c.colliderType === 'Material 2'
? 'Material 3'
: 'Default material',
}
: c
)
);
}}
>
<boxGeometry args={[0.1, 1, 1]} />
<meshStandardMaterial color={color} transparent opacity={0.3} />
</mesh>
</RigidBody>
);
}
export default ColliderInstance;

View File

@@ -15,12 +15,19 @@ type MaterialSpawnerProps = {
function MaterialSpawner({ position, spawnInterval, spawnCount }: MaterialSpawnerProps) {
const { loadingProgress } = useLoadingProgress();
const [spawned, setSpawned] = useState<{ id: string; position: [number, number, number]; ref: React.RefObject<RapierRigidBody> }[]>([]);
const [spawned, setSpawned] = useState<{
id: string;
position: [number, number, number];
ref: React.RefObject<RapierRigidBody>;
materialType: string;
}[]>([]);
const [spawningPaused, setSpawningPaused] = useState(false);
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);
const materialTypes = ['Default material', 'Material 1', 'Material 2', 'Material 3'];
useEffect(() => {
if (loadingProgress !== 0) return;
@@ -30,6 +37,7 @@ function MaterialSpawner({ position, spawnInterval, spawnCount }: MaterialSpawne
const startSpawning = () => {
if (!interval) {
interval = setInterval(() => {
if (spawningPaused) return;
setSpawned(prev => {
if (spawnCount !== undefined && spawnedCount.current >= spawnCount) {
clearInterval(interval!);
@@ -37,15 +45,20 @@ function MaterialSpawner({ position, spawnInterval, spawnCount }: MaterialSpawne
return prev;
}
spawnedCount.current++;
const randomMaterialType = materialTypes[Math.floor(Math.random() * materialTypes.length)];
return [
...prev,
{
id: generateUniqueId(),
position,
ref: React.createRef<RapierRigidBody>(),
materialType: randomMaterialType,
}
];
});
}, spawnInterval);
}
};
@@ -72,16 +85,14 @@ function MaterialSpawner({ position, spawnInterval, spawnCount }: MaterialSpawne
stopSpawning();
document.removeEventListener('visibilitychange', handleVisibility);
};
}, [loadingProgress, spawnInterval, spawnCount, position]);
}, [loadingProgress, spawnInterval, spawnCount, position, spawningPaused]);
const handleSleep = (id: string) => {
setSpawned(prev => prev.filter(obj => obj.id !== id));
};
const handlePointerDown = (id: string) => {
if (controls) {
(controls as CameraControls).enabled = false;
}
if (controls) (controls as CameraControls).enabled = false;
setDraggedId(id);
const obj = spawned.find(o => o.id === id);
@@ -128,7 +139,6 @@ function MaterialSpawner({ position, spawnInterval, spawnCount }: MaterialSpawne
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({
@@ -139,9 +149,7 @@ function MaterialSpawner({ position, spawnInterval, spawnCount }: MaterialSpawne
};
const handlePointerUp = () => {
if (controls) {
(controls as CameraControls).enabled = true;
}
if (controls) (controls as CameraControls).enabled = true;
if (!draggedId) return;
const obj = spawned.find(o => o.id === draggedId);
@@ -165,9 +173,21 @@ function MaterialSpawner({ position, spawnInterval, spawnCount }: MaterialSpawne
};
}, [draggedId, spawned, controls, camera, gl]);
const handleBoxClick = () => {
setSpawningPaused(prev => !prev);
};
return (
<>
{spawned.map(({ id, position, ref }) => (
<mesh
position={position}
onClick={handleBoxClick}
>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color={spawningPaused ? "red" : "white"} transparent opacity={0.2} />
</mesh>
{spawned.map(({ id, position, materialType, ref }) => (
<RigidBody
key={id}
ref={ref}
@@ -176,11 +196,12 @@ function MaterialSpawner({ position, spawnInterval, spawnCount }: MaterialSpawne
angularDamping={0.5}
linearDamping={0.5}
restitution={0.1}
// onSleep={() => handleSleep(id)}
userData={{ materialType, materialUuid: id }}
// onSleep={() => handleSleep(id)}
>
<MaterialModel
materialId={id}
materialType="Default material"
materialType={materialType}
onPointerDown={(e) => {
e.stopPropagation();
handlePointerDown(id);

View File

@@ -0,0 +1,28 @@
import React from 'react'
import MaterialSpawner from './materialSpawner'
import ColliderCreator from './colliders/colliderCreator'
function PhysicsSimulator() {
return (
<>
<MaterialSpawner
position={[0, 3, 0]}
spawnInterval={1000}
spawnCount={15}
/>
<MaterialSpawner
position={[-21, 3, -8]}
spawnInterval={1000}
spawnCount={5}
/>
<MaterialSpawner
position={[-17, 3, 6]}
spawnInterval={1000}
spawnCount={50}
/>
<ColliderCreator />
</>
)
}
export default PhysicsSimulator

View File

@@ -7,6 +7,7 @@ import Builder from "../builder/builder";
import Visualization from "../visualization/visualization";
import Setup from "./setup/setup";
import Simulation from "../simulation/simulation";
import PhysicsSimulator from "./physics/physicsSimulator";
import Collaboration from "../collaboration/collaboration";
import useModuleStore from "../../store/useModuleStore";
import { useParams } from "react-router-dom";
@@ -15,7 +16,6 @@ import { getUserData } from "../../functions/getUserData";
import { useLoadingProgress, useSocketStore } from "../../store/builder/store";
import { Color } from "three";
import { Physics } from "@react-three/rapier";
import MaterialSpawner from "./physics/materialSpawner";
export default function Scene({ layout }: { readonly layout: 'Main Layout' | 'Comparison Layout' }) {
const map = useMemo(() => [
@@ -73,20 +73,11 @@ export default function Scene({ layout }: { readonly layout: 'Main Layout' | 'Co
>
<Setup />
<Collaboration />
<Physics gravity={[0, -9.81, 0]} allowedLinearError={20} numSolverIterations={20} debug >
<Physics gravity={[0, -9.81, 0]} allowedLinearError={50} numSolverIterations={50} debug >
<Builder />
<Simulation />
<MaterialSpawner
position={[0, 3, 0]}
spawnInterval={1000}
spawnCount={50}
/>
<MaterialSpawner
position={[-21, 3, -8]}
spawnInterval={1000}
spawnCount={10}
/>
<PhysicsSimulator />
</Physics>
<Visualization />
</Canvas>