Files
healer-man/src/avatar/attachmentPoints.ts
T
2026-08-30 14:16:56 -04:00

64 lines
2.1 KiB
TypeScript

import { SkinnedMesh, Vector3, type Object3D } from "three";
import type { AvatarAttachmentPoint } from "./characterManifest";
export interface AvatarAttachmentTransform {
readonly target: Object3D;
readonly position: Vector3;
readonly scale: Vector3;
}
export interface AvatarAttachmentOptions {
/** The manifest point is the authored origin of its referenced attachment joint. */
readonly useTargetOrigin?: boolean;
}
const safeScaleRatio = (rootScale: number, targetScale: number): number => (
Number.isFinite(rootScale)
&& Number.isFinite(targetScale)
&& Math.abs(targetScale) > 1e-6
? rootScale / targetScale
: 1
);
function attachmentScale(root: Object3D, target: Object3D): Vector3 {
const rootScale = root.getWorldScale(new Vector3());
const targetScale = target.getWorldScale(new Vector3());
return new Vector3(
safeScaleRatio(rootScale.x, targetScale.x),
safeScaleRatio(rootScale.y, targetScale.y),
safeScaleRatio(rootScale.z, targetScale.z),
);
}
export function findAvatarSkinBone(root: Object3D, boneIndex: number): Object3D | null {
let bone: Object3D | null = null;
root.traverse((node) => {
if (bone) return;
const skinnedMesh = node as SkinnedMesh;
if (!skinnedMesh.isSkinnedMesh) return;
bone = skinnedMesh.skeleton.bones[boneIndex] ?? null;
});
return bone;
}
/** Converts a manifest point from avatar-local space into its target bone. */
export function resolveAvatarAttachmentPoint(
root: Object3D,
point: AvatarAttachmentPoint | undefined,
fallbackTarget: Object3D | null,
options: AvatarAttachmentOptions = {},
): AvatarAttachmentTransform | null {
const skinBone = point ? findAvatarSkinBone(root, point.bone) : null;
const target = skinBone ?? fallbackTarget;
if (!target) return null;
root.updateWorldMatrix(true, true);
const scale = attachmentScale(root, target);
if (!point || (skinBone && options.useTargetOrigin)) {
return { target, position: new Vector3(), scale };
}
const position = root.localToWorld(new Vector3(...point.position));
target.worldToLocal(position);
return { target, position, scale };
}