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_CLOSE_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_POP_DURATION,
|
||||
REPAIR_CASE_POP_Y_OFFSET,
|
||||
REPAIR_CASE_ROTATION_AMPLITUDE_DEGREES,
|
||||
REPAIR_CASE_ROTATION_RESET_SPEED,
|
||||
type RepairCasePartAnchorName,
|
||||
} from "@/data/gameplay/repairCaseConfig";
|
||||
import { useClonedObject } from "@/hooks/three/useClonedObject";
|
||||
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
|
||||
@@ -32,6 +36,10 @@ export interface RepairCasePlaceholder {
|
||||
position: Vector3Tuple;
|
||||
}
|
||||
|
||||
export type RepairCasePartAnchors = Partial<
|
||||
Record<RepairCasePartAnchorName, Vector3Tuple>
|
||||
>;
|
||||
|
||||
interface RepairCaseModelProps extends ModelTransformProps {
|
||||
modelPath: string;
|
||||
open: boolean;
|
||||
@@ -40,6 +48,7 @@ interface RepairCaseModelProps extends ModelTransformProps {
|
||||
onPlaceholdersChange?:
|
||||
| ((placeholders: readonly RepairCasePlaceholder[]) => void)
|
||||
| undefined;
|
||||
onAnchorsChange?: ((anchors: RepairCasePartAnchors) => void) | undefined;
|
||||
onExitComplete?: (() => void) | undefined;
|
||||
}
|
||||
|
||||
@@ -59,6 +68,7 @@ export function RepairCaseModel({
|
||||
exiting = false,
|
||||
floating = true,
|
||||
onPlaceholdersChange,
|
||||
onAnchorsChange,
|
||||
onExitComplete,
|
||||
position = [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 onExitCompleteRef = useRef(onExitComplete);
|
||||
const onPlaceholdersChangeRef = useRef(onPlaceholdersChange);
|
||||
const onAnchorsChangeRef = useRef(onAnchorsChange);
|
||||
const initialOpen = useRef(open);
|
||||
const previousOpen = useRef(open);
|
||||
const openedRotationZ = useRef(0);
|
||||
@@ -89,6 +100,12 @@ export function RepairCaseModel({
|
||||
const placeholderSignature = useRef("__initial__");
|
||||
const placeholderPosition = 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(() => {
|
||||
onExitCompleteRef.current = onExitComplete;
|
||||
@@ -98,6 +115,10 @@ export function RepairCaseModel({
|
||||
onPlaceholdersChangeRef.current = onPlaceholdersChange;
|
||||
}, [onPlaceholdersChange]);
|
||||
|
||||
useEffect(() => {
|
||||
onAnchorsChangeRef.current = onAnchorsChange;
|
||||
}, [onAnchorsChange]);
|
||||
|
||||
useEffect(() => {
|
||||
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) {
|
||||
lid.rotation.z =
|
||||
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;
|
||||
|
||||
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_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_OPEN_ROTATION_OFFSET_DEGREES = 115;
|
||||
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_SNAP_RADIUS = 0.65;
|
||||
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