feat(repair): expose case part anchors and fix lid node name
- Fix REPAIR_CASE_LID_NODE_NAME from "partiesup" to "partsup" (the actual node name in packderelance.gltf), restoring the lid open/close animation that was silently no-oping since introduction. - Add REPAIR_CASE_PART_ANCHOR_NAMES (cabledroit, cablegauche, pucehaut, pucebas, refroidisseur) and REPAIR_CASE_PART_ANCHOR_FALLBACKS for case-local positions used when the GLTF lacks a node (refroidisseur). - RepairCaseModel now resolves these anchor nodes on mount, hides existing meshes underneath them, and creates lightweight Object3D placeholders for missing names so the anchoring pipeline is uniform. - Each frame, anchor world positions are converted to step-local space and emitted via the new onAnchorsChange callback (debounced via signature like placeholders). Consumers added in subsequent commits.
This commit is contained in:
@@ -15,11 +15,15 @@ import {
|
|||||||
REPAIR_CASE_OPEN_ROTATION_OFFSET_DEGREES,
|
REPAIR_CASE_OPEN_ROTATION_OFFSET_DEGREES,
|
||||||
REPAIR_CASE_CLOSE_SOUND_PATH,
|
REPAIR_CASE_CLOSE_SOUND_PATH,
|
||||||
REPAIR_CASE_OPEN_SOUND_PATH,
|
REPAIR_CASE_OPEN_SOUND_PATH,
|
||||||
|
REPAIR_CASE_PART_ANCHOR_FALLBACK_QUATERNION,
|
||||||
|
REPAIR_CASE_PART_ANCHOR_FALLBACKS,
|
||||||
|
REPAIR_CASE_PART_ANCHOR_NAMES,
|
||||||
REPAIR_CASE_PLACEHOLDER_NAME_PREFIX,
|
REPAIR_CASE_PLACEHOLDER_NAME_PREFIX,
|
||||||
REPAIR_CASE_POP_DURATION,
|
REPAIR_CASE_POP_DURATION,
|
||||||
REPAIR_CASE_POP_Y_OFFSET,
|
REPAIR_CASE_POP_Y_OFFSET,
|
||||||
REPAIR_CASE_ROTATION_AMPLITUDE_DEGREES,
|
REPAIR_CASE_ROTATION_AMPLITUDE_DEGREES,
|
||||||
REPAIR_CASE_ROTATION_RESET_SPEED,
|
REPAIR_CASE_ROTATION_RESET_SPEED,
|
||||||
|
type RepairCasePartAnchorName,
|
||||||
} from "@/data/gameplay/repairCaseConfig";
|
} from "@/data/gameplay/repairCaseConfig";
|
||||||
import { useClonedObject } from "@/hooks/three/useClonedObject";
|
import { useClonedObject } from "@/hooks/three/useClonedObject";
|
||||||
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
|
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
|
||||||
@@ -32,6 +36,10 @@ export interface RepairCasePlaceholder {
|
|||||||
position: Vector3Tuple;
|
position: Vector3Tuple;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type RepairCasePartAnchors = Partial<
|
||||||
|
Record<RepairCasePartAnchorName, Vector3Tuple>
|
||||||
|
>;
|
||||||
|
|
||||||
interface RepairCaseModelProps extends ModelTransformProps {
|
interface RepairCaseModelProps extends ModelTransformProps {
|
||||||
modelPath: string;
|
modelPath: string;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -40,6 +48,7 @@ interface RepairCaseModelProps extends ModelTransformProps {
|
|||||||
onPlaceholdersChange?:
|
onPlaceholdersChange?:
|
||||||
| ((placeholders: readonly RepairCasePlaceholder[]) => void)
|
| ((placeholders: readonly RepairCasePlaceholder[]) => void)
|
||||||
| undefined;
|
| undefined;
|
||||||
|
onAnchorsChange?: ((anchors: RepairCasePartAnchors) => void) | undefined;
|
||||||
onExitComplete?: (() => void) | undefined;
|
onExitComplete?: (() => void) | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,6 +68,7 @@ export function RepairCaseModel({
|
|||||||
exiting = false,
|
exiting = false,
|
||||||
floating = true,
|
floating = true,
|
||||||
onPlaceholdersChange,
|
onPlaceholdersChange,
|
||||||
|
onAnchorsChange,
|
||||||
onExitComplete,
|
onExitComplete,
|
||||||
position = [0, 0, 0],
|
position = [0, 0, 0],
|
||||||
rotation = [0, 0, 0],
|
rotation = [0, 0, 0],
|
||||||
@@ -81,6 +91,7 @@ export function RepairCaseModel({
|
|||||||
const pop = useRef({ scale: 0.001, yOffset: REPAIR_CASE_POP_Y_OFFSET });
|
const pop = useRef({ scale: 0.001, yOffset: REPAIR_CASE_POP_Y_OFFSET });
|
||||||
const onExitCompleteRef = useRef(onExitComplete);
|
const onExitCompleteRef = useRef(onExitComplete);
|
||||||
const onPlaceholdersChangeRef = useRef(onPlaceholdersChange);
|
const onPlaceholdersChangeRef = useRef(onPlaceholdersChange);
|
||||||
|
const onAnchorsChangeRef = useRef(onAnchorsChange);
|
||||||
const initialOpen = useRef(open);
|
const initialOpen = useRef(open);
|
||||||
const previousOpen = useRef(open);
|
const previousOpen = useRef(open);
|
||||||
const openedRotationZ = useRef(0);
|
const openedRotationZ = useRef(0);
|
||||||
@@ -89,6 +100,12 @@ export function RepairCaseModel({
|
|||||||
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());
|
||||||
|
const anchorNodes = useRef<Map<RepairCasePartAnchorName, THREE.Object3D>>(
|
||||||
|
new Map(),
|
||||||
|
);
|
||||||
|
const anchorSignature = useRef("__initial__");
|
||||||
|
const anchorWorldPosition = useRef(new THREE.Vector3());
|
||||||
|
const anchorLocalPosition = useRef(new THREE.Vector3());
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onExitCompleteRef.current = onExitComplete;
|
onExitCompleteRef.current = onExitComplete;
|
||||||
@@ -98,6 +115,10 @@ export function RepairCaseModel({
|
|||||||
onPlaceholdersChangeRef.current = onPlaceholdersChange;
|
onPlaceholdersChangeRef.current = onPlaceholdersChange;
|
||||||
}, [onPlaceholdersChange]);
|
}, [onPlaceholdersChange]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onAnchorsChangeRef.current = onAnchorsChange;
|
||||||
|
}, [onAnchorsChange]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const popAnimation = pop.current;
|
const popAnimation = pop.current;
|
||||||
|
|
||||||
@@ -153,6 +174,37 @@ export function RepairCaseModel({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Resolve part anchor nodes (cabledroit, cablegauche, pucehaut, pucebas,
|
||||||
|
// refroidisseur). Existing GLTF nodes are reused and their meshes are
|
||||||
|
// hidden so the standalone model injected at the same position becomes
|
||||||
|
// the only visible representation. Missing nodes are created on the fly
|
||||||
|
// at the configured fallback case-local position.
|
||||||
|
anchorNodes.current = new Map();
|
||||||
|
REPAIR_CASE_PART_ANCHOR_NAMES.forEach((anchorName) => {
|
||||||
|
let node = model.getObjectByName(anchorName);
|
||||||
|
if (node) {
|
||||||
|
node.traverse((descendant) => {
|
||||||
|
if ((descendant as THREE.Mesh).isMesh) {
|
||||||
|
descendant.visible = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const placeholder = new THREE.Object3D();
|
||||||
|
placeholder.name = anchorName;
|
||||||
|
const fallback = REPAIR_CASE_PART_ANCHOR_FALLBACKS[anchorName];
|
||||||
|
placeholder.position.set(fallback[0], fallback[1], fallback[2]);
|
||||||
|
placeholder.quaternion.set(
|
||||||
|
REPAIR_CASE_PART_ANCHOR_FALLBACK_QUATERNION[0],
|
||||||
|
REPAIR_CASE_PART_ANCHOR_FALLBACK_QUATERNION[1],
|
||||||
|
REPAIR_CASE_PART_ANCHOR_FALLBACK_QUATERNION[2],
|
||||||
|
REPAIR_CASE_PART_ANCHOR_FALLBACK_QUATERNION[3],
|
||||||
|
);
|
||||||
|
model.add(placeholder);
|
||||||
|
node = placeholder;
|
||||||
|
}
|
||||||
|
anchorNodes.current.set(anchorName, node);
|
||||||
|
});
|
||||||
|
|
||||||
if (lid) {
|
if (lid) {
|
||||||
lid.rotation.z =
|
lid.rotation.z =
|
||||||
openedRotationZ.current +
|
openedRotationZ.current +
|
||||||
@@ -250,6 +302,31 @@ export function RepairCaseModel({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (anchorNodes.current.size > 0) {
|
||||||
|
const anchors: RepairCasePartAnchors = {};
|
||||||
|
const signatureParts: string[] = [];
|
||||||
|
anchorNodes.current.forEach((node, anchorName) => {
|
||||||
|
node.getWorldPosition(anchorWorldPosition.current);
|
||||||
|
anchorLocalPosition.current.copy(anchorWorldPosition.current);
|
||||||
|
group.parent?.worldToLocal(anchorLocalPosition.current);
|
||||||
|
const tuple: Vector3Tuple = [
|
||||||
|
anchorLocalPosition.current.x,
|
||||||
|
anchorLocalPosition.current.y,
|
||||||
|
anchorLocalPosition.current.z,
|
||||||
|
];
|
||||||
|
anchors[anchorName] = tuple;
|
||||||
|
signatureParts.push(
|
||||||
|
`${anchorName}:${tuple.map((value) => value.toFixed(3)).join(",")}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
signatureParts.sort();
|
||||||
|
const nextAnchorSignature = signatureParts.join("|");
|
||||||
|
if (nextAnchorSignature !== anchorSignature.current) {
|
||||||
|
anchorSignature.current = nextAnchorSignature;
|
||||||
|
onAnchorsChangeRef.current?.(anchors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
animationActiveRef.current = isNear;
|
animationActiveRef.current = isNear;
|
||||||
|
|
||||||
if (animationActiveRef.current) {
|
if (animationActiveRef.current) {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export const REPAIR_CASE_MODEL_PATH = "/models/packderelance/model.gltf";
|
|||||||
export const REPAIR_CASE_OPEN_SOUND_PATH = "/sounds/effect/open-malette.mp3";
|
export const REPAIR_CASE_OPEN_SOUND_PATH = "/sounds/effect/open-malette.mp3";
|
||||||
export const REPAIR_CASE_CLOSE_SOUND_PATH = "/sounds/effect/close-malette.mp3";
|
export const REPAIR_CASE_CLOSE_SOUND_PATH = "/sounds/effect/close-malette.mp3";
|
||||||
|
|
||||||
export const REPAIR_CASE_LID_NODE_NAME = "partiesup";
|
export const REPAIR_CASE_LID_NODE_NAME = "partsup";
|
||||||
export const REPAIR_CASE_CLOSED_ROTATION_OFFSET_DEGREES = 0;
|
export const REPAIR_CASE_CLOSED_ROTATION_OFFSET_DEGREES = 0;
|
||||||
export const REPAIR_CASE_OPEN_ROTATION_OFFSET_DEGREES = 115;
|
export const REPAIR_CASE_OPEN_ROTATION_OFFSET_DEGREES = 115;
|
||||||
export const REPAIR_CASE_ANIMATION_DURATION = 0.8;
|
export const REPAIR_CASE_ANIMATION_DURATION = 0.8;
|
||||||
@@ -27,3 +27,50 @@ export const REPAIR_CASE_FOCUS_SCALE = 2.25;
|
|||||||
export const REPAIR_CASE_PLACEHOLDER_NAME_PREFIX = "placeholder_";
|
export const REPAIR_CASE_PLACEHOLDER_NAME_PREFIX = "placeholder_";
|
||||||
export const REPAIR_CASE_PLACEHOLDER_SNAP_RADIUS = 0.65;
|
export const REPAIR_CASE_PLACEHOLDER_SNAP_RADIUS = 0.65;
|
||||||
export const REPAIR_CASE_PLACEHOLDER_SNAP_DURATION = 0.25;
|
export const REPAIR_CASE_PLACEHOLDER_SNAP_DURATION = 0.25;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Names of nodes inside the packderelance GLTF where standalone part models
|
||||||
|
* are anchored (visually injected). The original meshes under these nodes are
|
||||||
|
* hidden at runtime so the standalone model takes their place.
|
||||||
|
*
|
||||||
|
* Some entries (e.g. `refroidisseur`) do not exist as nodes in the GLTF; an
|
||||||
|
* empty Object3D is created at mount time at the corresponding case-local
|
||||||
|
* fallback position so the anchoring pipeline is uniform.
|
||||||
|
*/
|
||||||
|
export const REPAIR_CASE_PART_ANCHOR_NAMES = [
|
||||||
|
"cabledroit",
|
||||||
|
"cablegauche",
|
||||||
|
"pucehaut",
|
||||||
|
"pucebas",
|
||||||
|
"refroidisseur",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type RepairCasePartAnchorName =
|
||||||
|
(typeof REPAIR_CASE_PART_ANCHOR_NAMES)[number];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Case-local positions used when an anchor node is missing from the GLTF.
|
||||||
|
* Values are expressed in the case model's local coordinate system (the case
|
||||||
|
* is rendered at small intrinsic scale; magnitudes are in the 0.01-0.25 range
|
||||||
|
* to match the existing nodes such as `cabledroit`).
|
||||||
|
*/
|
||||||
|
export const REPAIR_CASE_PART_ANCHOR_FALLBACKS: Record<
|
||||||
|
RepairCasePartAnchorName,
|
||||||
|
Vector3Tuple
|
||||||
|
> = {
|
||||||
|
cabledroit: [0.0087, 0.0139, 0.1921],
|
||||||
|
cablegauche: [0.0087, 0.0139, 0.2477],
|
||||||
|
pucehaut: [-0.0207, 0.009, -0.0479],
|
||||||
|
pucebas: [0.0987, 0.009, -0.0479],
|
||||||
|
refroidisseur: [0.05, 0.014, 0.05],
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Quaternion applied to anchor nodes that are created at runtime (because
|
||||||
|
* the corresponding node is absent from the GLTF). Matches the rotation of
|
||||||
|
* the existing part nodes in packderelance to keep visual orientation
|
||||||
|
* consistent.
|
||||||
|
*/
|
||||||
|
export const REPAIR_CASE_PART_ANCHOR_FALLBACK_QUATERNION = [
|
||||||
|
0.7071068286895752, 0, 0, 0.7071068286895752,
|
||||||
|
] as const satisfies readonly [number, number, number, number];
|
||||||
|
|||||||
Reference in New Issue
Block a user