fix repair game interaction coordinate spaces

This commit is contained in:
Tom Boullay
2026-05-09 01:19:16 +01:00
parent 254311bddf
commit e0eae67ace
6 changed files with 134 additions and 88 deletions
@@ -81,6 +81,7 @@ export function RepairCaseModel({
const initialOpen = useRef(open);
const openedRotationZ = useRef(0);
const parsedScale = toVector3Scale(scale);
const placeholderNodes = useRef<THREE.Object3D[]>([]);
const placeholderSignature = useRef("__initial__");
const placeholderPosition = useRef(new THREE.Vector3());
const placeholderLocalPosition = useRef(new THREE.Vector3());
@@ -138,6 +139,15 @@ export function RepairCaseModel({
const lid = model.getObjectByName(REPAIR_CASE_LID_NODE_NAME);
lidRef.current = lid ?? null;
openedRotationZ.current = lid?.rotation.z ?? 0;
placeholderNodes.current = [];
model.traverse((child) => {
if (
child.name.toLowerCase().startsWith(REPAIR_CASE_PLACEHOLDER_NAME_PREFIX)
) {
placeholderNodes.current.push(child);
}
});
if (lid) {
lid.rotation.z =
@@ -195,16 +205,9 @@ export function RepairCaseModel({
parsedScale[2] * pop.current.scale,
);
if (placeholderNodes.current.length > 0) {
const placeholders: RepairCasePlaceholder[] = [];
model.traverse((child) => {
if (
!child.name
.toLowerCase()
.startsWith(REPAIR_CASE_PLACEHOLDER_NAME_PREFIX)
) {
return;
}
placeholderNodes.current.forEach((child) => {
child.getWorldPosition(placeholderPosition.current);
placeholderLocalPosition.current.copy(placeholderPosition.current);
group.parent?.worldToLocal(placeholderLocalPosition.current);
@@ -231,6 +234,7 @@ export function RepairCaseModel({
placeholderSignature.current = nextSignature;
onPlaceholdersChangeRef.current?.(placeholders);
}
}
animationActiveRef.current = isNear;
@@ -1,4 +1,4 @@
import { useState } from "react";
import { useRef, useState } from "react";
import * as THREE from "three";
import type { RepairCasePlaceholder } from "@/components/three/gameplay/RepairCaseModel";
import { RepairObjectModel } from "@/components/three/gameplay/RepairObjectModel";
@@ -56,6 +56,8 @@ export function RepairRepairingStep({
placeholders,
onRepair,
}: RepairRepairingStepProps): React.JSX.Element {
const groupRef = useRef<THREE.Group>(null);
const localPosition = useRef(new THREE.Vector3());
const [placedPartIds, setPlacedPartIds] = useState<Record<string, boolean>>(
{},
);
@@ -97,16 +99,19 @@ export function RepairRepairingStep({
const installLabel = isReadyToInstall
? `Installer ${requiredReplacementLabel}`
: hasWrongPartPlaced
? `Mauvaise piece`
? `Mauvaise pièce`
: hasCorrectPartPlaced
? `Ranger piece cassee`
? `Ranger pièce cassée`
: `Approcher ${requiredReplacementLabel}`;
function handleReplacementPosition(
partId: string,
position: THREE.Vector3,
): void {
const isPlaced = isNearPlaceholder(position, placeholderPositions);
const isPlaced = isNearPlaceholder(
getStepLocalPosition(position, groupRef.current, localPosition.current),
placeholderPositions,
);
setPlacedPartIds((current) => {
if (!current[partId] || isPlaced) return current;
@@ -127,7 +132,10 @@ export function RepairRepairingStep({
position: THREE.Vector3,
targets: readonly Vector3Tuple[],
): void {
const isDeposited = isNearPlaceholder(position, targets);
const isDeposited = isNearPlaceholder(
getStepLocalPosition(position, groupRef.current, localPosition.current),
targets,
);
setDepositedBrokenPartIds((current) => {
if (!current[partId] || isDeposited) return current;
@@ -144,7 +152,7 @@ export function RepairRepairingStep({
}
return (
<group>
<group ref={groupRef}>
<RepairInstallTarget
fillColor={installFillColor}
isReadyToInstall={isReadyToInstall}
@@ -333,6 +341,17 @@ function isNearPlaceholder(
);
}
function getStepLocalPosition(
worldPosition: THREE.Vector3,
group: THREE.Group | null,
target: THREE.Vector3,
): THREE.Vector3 {
target.copy(worldPosition);
group?.worldToLocal(target);
return target;
}
function getReplacementParts(
config: RepairMissionConfig,
): readonly RepairMissionPartConfig[] {
@@ -62,6 +62,7 @@ const _handHitDirection = new THREE.Vector3();
const _cameraPos = new THREE.Vector3();
const _objectPos = new THREE.Vector3();
const _snapPosition = new THREE.Vector3();
const _snapTargetWorldPosition = new THREE.Vector3();
const _handRaycaster = new THREE.Raycaster();
const HAND_GRAB_SCREEN_RADIUS = 0.04;
@@ -138,6 +139,7 @@ export function GrabbableObject({
}: GrabbableObjectProps): React.JSX.Element {
const camera = useThree((state) => state.camera);
const { hands } = useHandTrackingSnapshot();
const spaceRef = useRef<THREE.Group>(null);
const groupRef = useRef<THREE.Group>(null);
const rbRef = useRef<RapierRigidBody>(null);
const isHolding = useRef(false);
@@ -160,17 +162,24 @@ export function GrabbableObject({
_currentPos.set(translation.x, translation.y, translation.z);
let nearestTarget: Vector3Tuple | null = null;
let nearestTargetWorld: Vector3Tuple | null = null;
let nearestDistance = snapRadius;
snapTargets.forEach((target) => {
_snapPosition.set(target[0], target[1], target[2]);
const distance = _currentPos.distanceTo(_snapPosition);
_snapTargetWorldPosition.set(target[0], target[1], target[2]);
spaceRef.current?.localToWorld(_snapTargetWorldPosition);
const distance = _currentPos.distanceTo(_snapTargetWorldPosition);
if (distance <= nearestDistance) {
nearestDistance = distance;
nearestTarget = target;
nearestTargetWorld = [
_snapTargetWorldPosition.x,
_snapTargetWorldPosition.y,
_snapTargetWorldPosition.z,
];
}
});
if (!nearestTarget) return;
if (!nearestTarget || !nearestTargetWorld) return;
snapTween.current?.kill();
const animatedPosition = {
@@ -182,9 +191,9 @@ export function GrabbableObject({
body.setLinvel({ x: 0, y: 0, z: 0 }, true);
body.setAngvel(ZERO_ANGULAR_VELOCITY, true);
snapTween.current = gsap.to(animatedPosition, {
x: nearestTarget[0],
y: nearestTarget[1],
z: nearestTarget[2],
x: nearestTargetWorld[0],
y: nearestTargetWorld[1],
z: nearestTargetWorld[2],
duration: snapDuration,
ease: "power2.out",
onUpdate: () => {
@@ -311,6 +320,7 @@ export function GrabbableObject({
});
return (
<group ref={spaceRef}>
<RigidBody
ref={rbRef}
type="dynamic"
@@ -349,5 +359,6 @@ export function GrabbableObject({
</InteractableObject>
</group>
</RigidBody>
</group>
);
}
@@ -148,6 +148,8 @@ export function InteractableObject(
if (bodyRef?.current) {
const t = bodyRef.current.translation();
_objectPos.set(t.x, t.y, t.z);
} else if (group) {
group.getWorldPosition(_objectPos);
} else {
_objectPos.set(...position);
}
@@ -1,5 +1,6 @@
import { useState } from "react";
import { useRef, useState } from "react";
import { RigidBody } from "@react-three/rapier";
import type { RapierRigidBody } from "@react-three/rapier";
import { InteractableObject } from "@/components/three/interaction/InteractableObject";
import { useClonedObject } from "@/hooks/three/useClonedObject";
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
@@ -59,14 +60,21 @@ export function TriggerObject({
onTrigger,
}: TriggerObjectProps): React.JSX.Element {
const [spawned, setSpawned] = useState<SpawnedModel[]>([]);
const rbRef = useRef<RapierRigidBody>(null);
return (
<>
<RigidBody type="fixed" colliders={colliders} position={position}>
<RigidBody
ref={rbRef}
type="fixed"
colliders={colliders}
position={position}
>
<InteractableObject
kind="trigger"
label={label}
position={position}
bodyRef={rbRef}
onPress={() => {
if (soundPath) {
AudioManager.getInstance().playSound(soundPath, soundVolume);
+4 -2
View File
@@ -142,9 +142,11 @@ export function TestMap({ onOctreeReady }: TestMapProps): React.JSX.Element {
</TriggerObject>
{TEST_SCENE_REPAIR_ZONES.map((zone) => (
<group key={zone.mission} position={zone.position}>
<group key={zone.mission}>
<group position={zone.position}>
<RepairPlaygroundZoneMarker color={zone.color} />
<RepairGame mission={zone.mission} position={[0, 0, 0]} />
</group>
<RepairGame mission={zone.mission} position={zone.position} />
</group>
))}
</Physics>