feat: Implement collider functionality with drag-and-drop arrows and conditions

- Added Collider and ColliderCondition types to manage collider properties.
- Created a new useColliderStore for managing colliders and their arrows.
- Implemented ColliderArrow component for visual representation and interaction of arrows.
- Enhanced ColliderProperties UI for managing directions and conditions.
- Updated PhysicsSimulator and Scene components to integrate collider features.
- Refactored existing code for better organization and clarity.
This commit is contained in:
2025-08-21 17:57:45 +05:30
parent 57dffa0961
commit d1c78495ea
14 changed files with 965 additions and 118 deletions

View File

@@ -3,28 +3,28 @@ 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';
import { useSceneContext } from '../../../sceneContext';
import { ColliderArrow } from './colliderArrow';
function ColliderInstance({ id, colliders, setColliders, position, rotation }: {
id: string;
colliders: { id: string; position: [number, number, number]; rotation: [number, number, number]; colliderType: 'Default material' | 'Material 1' | 'Material 2' | 'Material 3' }[];
setColliders: React.Dispatch<React.SetStateAction<{ id: string; position: [number, number, number]; rotation: [number, number, number]; colliderType: 'Default material' | 'Material 1' | 'Material 2' | 'Material 3' }[]>>;
position: [number, number, number];
rotation: [number, number, number];
function ColliderInstance({ collider }: {
collider: Collider
}) {
const { camera, gl, pointer, controls } = useThree();
const { colliderStore } = useSceneContext();
const { colliders, selectedCollider, setSelectedCollider, updateCollider, getArrowByArrowId } = colliderStore();
const [draggedId, setDraggedId] = useState<string | null>(null);
const dragOffset = useRef(new THREE.Vector3());
const initialDepth = useRef(0);
const ref = useRef<RapierRigidBody>(null);
const [color, setColor] = useState(new THREE.Color('white'));
const [objectsOnCollider, setObjectsOnCollider] = useState<Set<RapierRigidBody>>(new Set());
const isSelected = selectedCollider?.id === collider.id;
const getColorByType = (type: string) => {
switch (type) {
case 'Material 1':
return new THREE.Color('blue');
case 'Material 2':
return new THREE.Color('green');
return new THREE.Color('purple');
case 'Material 3':
return new THREE.Color('yellow');
default:
@@ -32,14 +32,6 @@ function ColliderInstance({ id, colliders, setColliders, position, rotation }: {
}
};
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;
@@ -60,11 +52,19 @@ function ColliderInstance({ id, colliders, setColliders, position, rotation }: {
ref.current.setAngularDamping(10);
};
const handlePointerMove = () => {
const handlePointerMove = (e: PointerEvent) => {
if (!draggedId) return;
const collider = colliders.find(c => c.id === draggedId);
if (!collider || !ref.current) return;
if (e.altKey) {
const rotation = ref.current.rotation();
const currentQuaternion = new THREE.Quaternion(rotation.x, rotation.y, rotation.z, rotation.w);
const deltaQuaternion = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), 0.01);
const newQuaternion = currentQuaternion.multiply(deltaQuaternion);
ref.current.setRotation({ x: newQuaternion.x, y: newQuaternion.y, z: newQuaternion.z, w: newQuaternion.w }, true);
return;
};
const screenTarget = new THREE.Vector3(
pointer.x + dragOffset.current.x,
@@ -73,6 +73,7 @@ function ColliderInstance({ id, colliders, setColliders, position, rotation }: {
);
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));
@@ -80,7 +81,7 @@ function ColliderInstance({ id, colliders, setColliders, position, rotation }: {
{ x: finalPos.x, y: finalPos.y, z: finalPos.z },
true
);
};
}
const handlePointerUp = () => {
if (controls) {
@@ -89,9 +90,20 @@ function ColliderInstance({ id, colliders, setColliders, position, rotation }: {
if (!draggedId) return;
if (ref.current) {
// restore physics
ref.current.setGravityScale(1, true);
ref.current.setLinearDamping(0.5);
ref.current.setAngularDamping(0.5);
// get final transform
const pos = ref.current.translation();
const rot = ref.current.rotation();
// update Zustand store
updateCollider(draggedId, {
position: [pos.x, pos.y, pos.z],
rotation: [rot.x, rot.y, rot.z], // quaternion
});
}
setDraggedId(null);
@@ -109,24 +121,17 @@ function ColliderInstance({ id, colliders, setColliders, position, rotation }: {
}, [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;
});
}
if (body && (body.userData as any)?.materialType) {
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 => {
@@ -140,20 +145,48 @@ function ColliderInstance({ id, colliders, setColliders, position, rotation }: {
useFrame(() => {
objectsOnCollider.forEach(rigidBody => {
if (!rigidBody) return;
// if (collider.colliderCondition.conditionType === 'material') {
// if (collider.colliderCondition.materialType === (rigidBody as any).userData.materialType) {
// console.log('(rigidBody as any).userData.materialType: ', (rigidBody as any).userData.materialType);
// collider.colliderCondition.arrowsOrder.forEach((arrowId) => {
// console.log('arrow: ', arrowId);
// let arrowDetails = getArrowByArrowId(arrowId);
// if (arrowDetails?.position) {
// const arrowPos = new THREE.Vector3(...arrowDetails?.position); // arrow position
// const colliderPos = new THREE.Vector3(...(selectedCollider?.position || [0, 0, 0])); // collider position
// const direction = new THREE.Vector3();
// direction.subVectors(arrowPos, colliderPos).normalize();
// console.log('Direction vector:', direction);
// rigidBody.setLinvel({ x: direction.x, y: direction.y, z: direction.z }, true);
// }
rigidBody.setLinvel({ x: 0, y: 0, z: 2 }, true);
rigidBody.setAngvel({ x: 0, y: 0, z: 0 }, true);
// // rigidBody.setAngvel({ x: 0, y: 0, z: 0 }, true);
// // Direction vector from collider to arrow
// });
// } else {
// }
// } else if (collider.colliderCondition.conditionType === 'count') {
// }
});
});
return (
<RigidBody
name='Sensor-Collider'
key={id}
key={collider.id}
ref={ref}
type="fixed"
sensor
position={position}
position={collider.position}
rotation={collider.rotation}
colliders="cuboid"
includeInvisible
gravityScale={0}
@@ -163,34 +196,28 @@ function ColliderInstance({ id, colliders, setColliders, position, rotation }: {
<mesh
onPointerDown={(e) => {
e.stopPropagation();
handlePointerDown(id);
handlePointerDown(collider.id);
}}
onDoubleClick={(e) => {
onClick={(e) => {
e.stopPropagation();
setSelectedCollider(collider);
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
)
);
}}
>
<boxGeometry args={[0.1, 1, 1]} />
<meshStandardMaterial color={color} transparent opacity={0.3} />
<meshStandardMaterial color={isSelected ? 'green' : 'white'} transparent opacity={0.3} />
</mesh>
{isSelected && collider.arrows.map((arrow) => (
<ColliderArrow
key={arrow.arrowId}
startPosition={collider.position}
endPosition={arrow.position}
colliderRotation={collider.rotation}
colliderId={collider.id}
arrowId={arrow.arrowId}
/>
))}
</RigidBody>
);
}