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" }; } }