287 lines
10 KiB
TypeScript
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; |