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