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
>
-
+
-
-
+