Files
2026-08-12 04:47:48 -04:00

63 lines
2.9 KiB
TypeScript

import type { ShapeKeyIR, SkinWeightsIR } from "./scene-ir";
import type { SkinSimplifyPolicy } from "./simplify";
export interface NormalizedSkinWeights {
indices: number[];
weights: number[];
droppedInfluences: number;
}
export interface SkinSimplifyGate {
allowed: boolean;
reason?: "MISSING_SKIN_ATTRIBUTES" | "INVALID_SKIN_ATTRIBUTES" | "SHAPE_KEYS_REJECTED" | "SHAPE_KEY_DATA_INVALID";
message?: string;
normalized?: NormalizedSkinWeights;
}
function finiteWeight(value: number): boolean {
return Number.isFinite(value) && value >= 0;
}
export function normalizeSkinWeights(vertexCount: number, skin: SkinWeightsIR, policy: SkinSimplifyPolicy): NormalizedSkinWeights {
if (!Number.isInteger(vertexCount) || vertexCount <= 0 || skin.indices.length !== vertexCount * 4 || skin.weights.length !== vertexCount * 4) {
throw new Error("skin arrays must contain four influences per vertex");
}
const indices: number[] = [];
const weights: number[] = [];
let droppedInfluences = 0;
for (let vertex = 0; vertex < vertexCount; vertex++) {
const influences = [0, 1, 2, 3].map((slot) => ({
index: skin.indices[vertex * 4 + slot],
weight: skin.weights[vertex * 4 + slot],
})).filter((influence) => finiteWeight(influence.weight) && influence.weight >= policy.minWeight)
.sort((left, right) => right.weight - left.weight)
.slice(0, policy.maxInfluences);
droppedInfluences += 4 - influences.length;
const total = influences.reduce((sum, influence) => sum + influence.weight, 0);
if (total <= 0) throw new Error(`vertex ${vertex} has no surviving skin influence`);
for (let slot = 0; slot < 4; slot++) {
const influence = influences[slot];
indices.push(influence?.index ?? 0);
weights.push(influence ? influence.weight / total : 0);
}
}
return { indices, weights, droppedInfluences };
}
export function evaluateSkinSimplifyGate(vertexCount: number, skin: SkinWeightsIR | undefined, shapeKeys: readonly ShapeKeyIR[] | undefined, policy: SkinSimplifyPolicy): SkinSimplifyGate {
if (!skin) return { allowed: false, reason: "MISSING_SKIN_ATTRIBUTES", message: "Skin simplification requires exported bone indices, weights and bind matrix" };
try {
const normalized = normalizeSkinWeights(vertexCount, skin, policy);
if (shapeKeys && shapeKeys.length > 0 && policy.shapeKeys === "REJECT") {
return { allowed: false, reason: "SHAPE_KEYS_REJECTED", message: "Shape keys must be explicitly rejected before skin simplification" };
}
if (shapeKeys?.some((shape) => shape.positions.length !== vertexCount * 3)) {
return { allowed: false, reason: "SHAPE_KEY_DATA_INVALID", message: "Shape key positions do not match the source vertex count" };
}
return { allowed: true, normalized };
}
catch (error) {
return { allowed: false, reason: "INVALID_SKIN_ATTRIBUTES", message: error instanceof Error ? error.message : "Invalid skin attributes" };
}
}