Files
Dwinzo_Demo/app/src/modules/scene/physics/materialSpawner.tsx

287 lines
10 KiB
TypeScript

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';
import { generateUniqueId } from '../../../functions/generateUniqueId';
type MaterialSpawnerProps = {
position: [number, number, number];
spawnInterval: number;
spawnCount: number;
};
function MaterialSpawner({ position: initialPos, spawnInterval, spawnCount }: MaterialSpawnerProps) {
const { loadingProgress } = useLoadingProgress();
const [spawned, setSpawned] = useState<{
id: string;
position: [number, number, number];
ref: React.RefObject<RapierRigidBody>;
materialType: string;
}[]>([]);
const [spawningPaused, setSpawningPaused] = useState(true);
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'];
const spawnerRef = useRef<THREE.Mesh>(null!);
const [isDraggingSpawner, setIsDraggingSpawner] = useState(false);
const spawnerDragOffset = useRef<THREE.Vector3>(new THREE.Vector3());
const spawnerInitialDepth = useRef<number>(0);
const [boxPosition, setBoxPosition] = useState<[number, number, number]>(initialPos);
useEffect(() => {
if (loadingProgress !== 0) return;
let interval: NodeJS.Timeout | null = null;
const startSpawning = () => {
if (!interval) {
interval = setInterval(() => {
if (spawningPaused) return;
setSpawned(prev => {
if (spawnCount !== undefined && spawnedCount.current >= spawnCount) {
clearInterval(interval!);
interval = null;
return prev;
}
spawnedCount.current++;
const randomMaterialType = materialTypes[Math.floor(Math.random() * materialTypes.length)];
return [
...prev,
{
id: generateUniqueId(),
position: [...boxPosition] as [number, number, number], // use latest position state
ref: React.createRef<RapierRigidBody>(),
materialType: randomMaterialType,
}
];
});
}, 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, spawningPaused, boxPosition]);
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]);
const handleSpawnerPointerDown = (e: any) => {
if (e.button !== 2) return; // right click only
e.stopPropagation();
if (controls) (controls as CameraControls).enabled = false;
setIsDraggingSpawner(true);
const worldPos = spawnerRef.current.getWorldPosition(new THREE.Vector3());
const screenPos = worldPos.clone().project(camera);
spawnerDragOffset.current.set(
screenPos.x - pointer.x,
0,
screenPos.y - pointer.y
);
spawnerInitialDepth.current = worldPos.clone().sub(camera.position).length();
};
const handleSpawnerPointerMove = () => {
if (!isDraggingSpawner) return;
const targetScreenPos = new THREE.Vector3(
pointer.x + spawnerDragOffset.current.x,
0,
pointer.y + spawnerDragOffset.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(spawnerInitialDepth.current));
spawnerRef.current.position.copy(finalPosition);
};
const handleSpawnerPointerUp = () => {
if (controls) (controls as CameraControls).enabled = true;
setIsDraggingSpawner(false);
if (spawnerRef.current) {
const worldPos = spawnerRef.current.getWorldPosition(new THREE.Vector3());
setBoxPosition([worldPos.x, worldPos.y, worldPos.z] as [number, number, number]);
}
};
useEffect(() => {
const canvas = gl.domElement;
canvas.addEventListener("pointermove", handleSpawnerPointerMove);
canvas.addEventListener("pointerup", handleSpawnerPointerUp);
return () => {
canvas.removeEventListener("pointermove", handleSpawnerPointerMove);
canvas.removeEventListener("pointerup", handleSpawnerPointerUp);
};
}, [isDraggingSpawner, camera, gl, controls]);
const handleBoxClick = () => {
setSpawningPaused(prev => !prev);
};
return (
<>
<mesh
name='Spawner Box'
ref={spawnerRef}
position={boxPosition}
onClick={handleBoxClick}
onPointerDown={handleSpawnerPointerDown}
>
<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}
position={position}
colliders="hull"
angularDamping={0.5}
linearDamping={0.5}
restitution={0.1}
userData={{ materialType, materialUuid: id }}
onSleep={() => handleSleep(id)}
>
<MaterialModel
materialId={id}
materialType={materialType}
onPointerDown={(e) => {
e.stopPropagation();
handlePointerDown(id);
}}
/>
</RigidBody>
))}
</>
);
}
export default MaterialSpawner;