feat: Implement ColliderCreator and ColliderInstance components, enhance PhysicsSimulator with multiple MaterialSpawner instances
This commit is contained in:
58
app/src/modules/scene/physics/colliders/colliderCreator.tsx
Normal file
58
app/src/modules/scene/physics/colliders/colliderCreator.tsx
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
|
||||
28
app/src/modules/scene/physics/physicsSimulator.tsx
Normal file
28
app/src/modules/scene/physics/physicsSimulator.tsx
Normal 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
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user