141 lines
3.6 KiB
TypeScript
141 lines
3.6 KiB
TypeScript
import { useRef } from "react";
|
|
import { useFrame, useThree } from "@react-three/fiber";
|
|
import { RigidBody } from "@react-three/rapier";
|
|
import type { RapierRigidBody } from "@react-three/rapier";
|
|
import * as THREE from "three";
|
|
import { InteractableObject } from "@/components/3d/InteractableObject";
|
|
import {
|
|
GRAB_DEFAULT_COLLIDERS,
|
|
GRAB_DEFAULT_LABEL,
|
|
GRAB_HOLD_DISTANCE_DEFAULT,
|
|
GRAB_HOLD_DISTANCE_MAX,
|
|
GRAB_HOLD_DISTANCE_MIN,
|
|
GRAB_HOLD_DISTANCE_STEP,
|
|
GRAB_STIFFNESS_DEFAULT,
|
|
GRAB_STIFFNESS_MAX,
|
|
GRAB_STIFFNESS_MIN,
|
|
GRAB_STIFFNESS_STEP,
|
|
GRAB_THROW_BOOST_DEFAULT,
|
|
GRAB_THROW_BOOST_MAX,
|
|
GRAB_THROW_BOOST_MIN,
|
|
GRAB_THROW_BOOST_STEP,
|
|
} from "@/data/grabConfig";
|
|
import { useDebugFolder } from "@/hooks/debug/useDebugFolder";
|
|
import type { ColliderShape, Vector3Tuple } from "@/types/3d";
|
|
|
|
interface GrabbableObjectProps {
|
|
position: Vector3Tuple;
|
|
children: React.ReactNode;
|
|
colliders?: ColliderShape;
|
|
label?: string;
|
|
}
|
|
|
|
// Shared params let one debug folder drive every instance.
|
|
const params = {
|
|
stiffness: GRAB_STIFFNESS_DEFAULT,
|
|
throwBoost: GRAB_THROW_BOOST_DEFAULT,
|
|
holdDistance: GRAB_HOLD_DISTANCE_DEFAULT,
|
|
};
|
|
|
|
const ZERO_ANGULAR_VELOCITY = { x: 0, y: 0, z: 0 };
|
|
|
|
const _holdTarget = new THREE.Vector3();
|
|
const _currentPos = new THREE.Vector3();
|
|
const _velocity = new THREE.Vector3();
|
|
|
|
export function GrabbableObject({
|
|
position,
|
|
children,
|
|
colliders = GRAB_DEFAULT_COLLIDERS,
|
|
label = GRAB_DEFAULT_LABEL,
|
|
}: GrabbableObjectProps): React.JSX.Element {
|
|
const camera = useThree((state) => state.camera);
|
|
const rbRef = useRef<RapierRigidBody>(null);
|
|
const isHolding = useRef(false);
|
|
|
|
useDebugFolder("GrabbableObject", (folder) => {
|
|
folder
|
|
.add(
|
|
params,
|
|
"stiffness",
|
|
GRAB_STIFFNESS_MIN,
|
|
GRAB_STIFFNESS_MAX,
|
|
GRAB_STIFFNESS_STEP,
|
|
)
|
|
.name("Hold stiffness");
|
|
folder
|
|
.add(
|
|
params,
|
|
"throwBoost",
|
|
GRAB_THROW_BOOST_MIN,
|
|
GRAB_THROW_BOOST_MAX,
|
|
GRAB_THROW_BOOST_STEP,
|
|
)
|
|
.name("Throw boost");
|
|
folder
|
|
.add(
|
|
params,
|
|
"holdDistance",
|
|
GRAB_HOLD_DISTANCE_MIN,
|
|
GRAB_HOLD_DISTANCE_MAX,
|
|
GRAB_HOLD_DISTANCE_STEP,
|
|
)
|
|
.name("Hold distance");
|
|
});
|
|
|
|
useFrame(() => {
|
|
if (!isHolding.current || !rbRef.current) return;
|
|
|
|
camera.getWorldDirection(_holdTarget);
|
|
_holdTarget.multiplyScalar(params.holdDistance).add(camera.position);
|
|
|
|
const t = rbRef.current.translation();
|
|
_currentPos.set(t.x, t.y, t.z);
|
|
|
|
_velocity
|
|
.subVectors(_holdTarget, _currentPos)
|
|
.multiplyScalar(params.stiffness);
|
|
|
|
rbRef.current.setLinvel(
|
|
{ x: _velocity.x, y: _velocity.y, z: _velocity.z },
|
|
true,
|
|
);
|
|
rbRef.current.setAngvel(ZERO_ANGULAR_VELOCITY, true);
|
|
});
|
|
|
|
return (
|
|
<RigidBody
|
|
ref={rbRef}
|
|
type="dynamic"
|
|
colliders={colliders}
|
|
position={position}
|
|
>
|
|
<InteractableObject
|
|
kind="grab"
|
|
label={label}
|
|
position={position}
|
|
bodyRef={rbRef}
|
|
onPress={() => {
|
|
isHolding.current = true;
|
|
}}
|
|
onRelease={() => {
|
|
isHolding.current = false;
|
|
if (!rbRef.current || params.throwBoost === GRAB_THROW_BOOST_DEFAULT)
|
|
return;
|
|
const v = rbRef.current.linvel();
|
|
rbRef.current.setLinvel(
|
|
{
|
|
x: v.x * params.throwBoost,
|
|
y: v.y * params.throwBoost,
|
|
z: v.z * params.throwBoost,
|
|
},
|
|
true,
|
|
);
|
|
}}
|
|
>
|
|
{children}
|
|
</InteractableObject>
|
|
</RigidBody>
|
|
);
|
|
}
|