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:
@@ -0,0 +1,115 @@
|
||||
import { useRef, useMemo, useState, useCallback } from "react";
|
||||
import { useThree, useFrame } from "@react-three/fiber";
|
||||
import * as THREE from "three";
|
||||
import { useSceneContext } from "../../../sceneContext";
|
||||
|
||||
type ColliderArrowProps = {
|
||||
arrowId: string;
|
||||
colliderId: string;
|
||||
startPosition: [number, number, number];
|
||||
endPosition: [number, number, number];
|
||||
colliderRotation: [number, number, number];
|
||||
thickness?: number;
|
||||
depth?: number;
|
||||
color?: string;
|
||||
};
|
||||
|
||||
export function ColliderArrow({
|
||||
arrowId,
|
||||
colliderId,
|
||||
startPosition,
|
||||
endPosition,
|
||||
colliderRotation,
|
||||
thickness = 0.05,
|
||||
depth = 0.01,
|
||||
color = "green",
|
||||
}: ColliderArrowProps) {
|
||||
const groupRef = useRef<THREE.Group>(null);
|
||||
const { raycaster, pointer, controls } = useThree();
|
||||
const { colliderStore } = useSceneContext();
|
||||
const { updateArrow, selectedArrowId, clearSelectedArrow } = colliderStore();
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const { dir, length } = useMemo(() => {
|
||||
const start = new THREE.Vector3(...startPosition);
|
||||
const end = new THREE.Vector3(...endPosition);
|
||||
const d = new THREE.Vector3().subVectors(end, start);
|
||||
return { dir: d.clone().normalize(), length: d.length() };
|
||||
}, [startPosition, endPosition]);
|
||||
|
||||
const arrowShape = useMemo(() => {
|
||||
const shaftWidth = thickness;
|
||||
const headLength = Math.min(length * 0.3, 0.4);
|
||||
const headWidth = thickness * 3;
|
||||
|
||||
const shape = new THREE.Shape();
|
||||
shape.moveTo(0, -shaftWidth / 2);
|
||||
shape.lineTo(length - headLength, -shaftWidth / 2);
|
||||
shape.lineTo(length - headLength, -headWidth / 2);
|
||||
shape.lineTo(length, 0);
|
||||
shape.lineTo(length - headLength, headWidth / 2);
|
||||
shape.lineTo(length - headLength, shaftWidth / 2);
|
||||
shape.lineTo(0, shaftWidth / 2);
|
||||
shape.closePath();
|
||||
|
||||
return shape;
|
||||
}, [length, thickness]);
|
||||
|
||||
const extrudeSettings = useMemo(() => ({ depth, bevelEnabled: false, }), [depth]);
|
||||
const geometry = useMemo(() => new THREE.ExtrudeGeometry(arrowShape, extrudeSettings), [arrowShape, extrudeSettings]);
|
||||
|
||||
const quaternion = useMemo(() => {
|
||||
const q = new THREE.Quaternion();
|
||||
q.setFromUnitVectors(new THREE.Vector3(1, 0, 0), dir);
|
||||
const colliderQuat = new THREE.Quaternion().setFromEuler(
|
||||
new THREE.Euler().fromArray(colliderRotation)
|
||||
);
|
||||
|
||||
return colliderQuat.multiply(q);
|
||||
}, [dir, colliderRotation]);
|
||||
|
||||
const handlePointerDown = useCallback((e: any) => {
|
||||
e.stopPropagation();
|
||||
setDragging(true);
|
||||
(controls as any).enabled = false; // Disable controls while dragging
|
||||
}, []);
|
||||
|
||||
const handlePointerUp = useCallback((e: any) => {
|
||||
e.stopPropagation();
|
||||
clearSelectedArrow()
|
||||
setDragging(false);
|
||||
(controls as any).enabled = true;
|
||||
}, []);
|
||||
|
||||
useFrame(({ camera }) => {
|
||||
if (dragging) {
|
||||
if (selectedArrowId) {
|
||||
const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), -startPosition[1]);
|
||||
raycaster.setFromCamera(pointer, camera);
|
||||
const hit = new THREE.Vector3();
|
||||
if (raycaster.ray.intersectPlane(plane, hit)) {
|
||||
updateArrow(colliderId, arrowId, {
|
||||
position: [hit.x, startPosition[1], hit.z],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<group ref={groupRef} quaternion={quaternion}>
|
||||
|
||||
<mesh geometry={geometry} rotation={[Math.PI / 2, 0, 0]}>
|
||||
<meshStandardMaterial color={color} />
|
||||
</mesh>
|
||||
|
||||
<mesh
|
||||
position={[length, 0, 0]}
|
||||
onPointerDown={handlePointerDown}
|
||||
onPointerUp={handlePointerUp}
|
||||
>
|
||||
<sphereGeometry args={[0.15, 16, 16]} />
|
||||
<meshBasicMaterial transparent opacity={0.2} color="yellow" />
|
||||
</mesh>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user