diff --git a/app/src/modules/scene/physics/colliders/colliderCreator.tsx b/app/src/modules/scene/physics/colliders/colliderCreator.tsx new file mode 100644 index 0000000..e4aa3e5 --- /dev/null +++ b/app/src/modules/scene/physics/colliders/colliderCreator.tsx @@ -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 }) => ( + + ))} + + ); +} + +export default ColliderCreator; \ No newline at end of file diff --git a/app/src/modules/scene/physics/colliders/colliderInstance/colliderInstance.tsx b/app/src/modules/scene/physics/colliders/colliderInstance/colliderInstance.tsx new file mode 100644 index 0000000..bcf1165 --- /dev/null +++ b/app/src/modules/scene/physics/colliders/colliderInstance/colliderInstance.tsx @@ -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>; + position: [number, number, number]; +}) { + const { camera, gl, pointer, controls } = useThree(); + const [draggedId, setDraggedId] = useState(null); + const dragOffset = useRef(new THREE.Vector3()); + const initialDepth = useRef(0); + const ref = useRef(null); + const [color, setColor] = useState(new THREE.Color('white')); + const [objectsOnCollider, setObjectsOnCollider] = useState>(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 ( + + { + 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 + ) + ); + }} + + > + + + + + ); +} + +export default ColliderInstance; diff --git a/app/src/modules/scene/physics/materialSpawner.tsx b/app/src/modules/scene/physics/materialSpawner.tsx index 95df602..80b524d 100644 --- a/app/src/modules/scene/physics/materialSpawner.tsx +++ b/app/src/modules/scene/physics/materialSpawner.tsx @@ -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 }[]>([]); + const [spawned, setSpawned] = useState<{ + id: string; + position: [number, number, number]; + ref: React.RefObject; + materialType: string; + }[]>([]); + const [spawningPaused, setSpawningPaused] = useState(false); 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']; 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(), + 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 }) => ( + + + + + + {spawned.map(({ id, position, materialType, ref }) => ( handleSleep(id)} + userData={{ materialType, materialUuid: id }} + // onSleep={() => handleSleep(id)} > { e.stopPropagation(); handlePointerDown(id); diff --git a/app/src/modules/scene/physics/physicsSimulator.tsx b/app/src/modules/scene/physics/physicsSimulator.tsx new file mode 100644 index 0000000..f6dddc0 --- /dev/null +++ b/app/src/modules/scene/physics/physicsSimulator.tsx @@ -0,0 +1,28 @@ +import React from 'react' +import MaterialSpawner from './materialSpawner' +import ColliderCreator from './colliders/colliderCreator' + +function PhysicsSimulator() { + return ( + <> + + + + + + ) +} + +export default PhysicsSimulator diff --git a/app/src/modules/scene/scene.tsx b/app/src/modules/scene/scene.tsx index 55aefb5..2ac0377 100644 --- a/app/src/modules/scene/scene.tsx +++ b/app/src/modules/scene/scene.tsx @@ -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 > - + - - +