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; materialType: string; }[]>([]); const [spawningPaused, setSpawningPaused] = useState(true); const spawnedCount = useRef(0); const { gl, camera, pointer, controls } = useThree(); const [draggedId, setDraggedId] = useState(null); const dragOffset = useRef(new THREE.Vector3()); const initialDepth = useRef(0); const materialTypes = ['Default material', 'Material 1', 'Material 2', 'Material 3']; const spawnerRef = useRef(null!); const [isDraggingSpawner, setIsDraggingSpawner] = useState(false); const spawnerDragOffset = useRef(new THREE.Vector3()); const spawnerInitialDepth = useRef(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(), 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 ( <> {spawned.map(({ id, position, materialType, ref }) => ( handleSleep(id)} > { e.stopPropagation(); handlePointerDown(id); }} /> ))} ); } export default MaterialSpawner;