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

105 lines
3.5 KiB
TypeScript

import { parseLODManifest, SimplifyValidationError, type LODManifest, type SimplifyProfile } from "./simplify";
export type LODPoseState = "REST" | "POSE";
export interface LODGenerationLevelRequest {
profile: SimplifyProfile;
triangleBudget: number;
maxGeometricError?: number;
screenSpaceError?: number;
}
export interface LODGenerationRequest {
meshId: string;
sourceMeshRevision: number;
levels: LODGenerationLevelRequest[];
}
export interface LODCacheKeyInput {
objectId: string;
meshRevision: number;
modifierStackHash: string;
profileHash: string;
poseOrRestState: LODPoseState;
}
export interface LODCacheRecord extends LODManifest {
cacheKey: string;
objectId: string;
modifierStackHash: string;
profileHash: string;
poseOrRestState: LODPoseState;
generatedAt: string;
byteLength?: number;
lastAccessAt?: string;
}
function fnv1a(value: string, seed: number): number {
let hash = seed >>> 0;
for (let index = 0; index < value.length; index++) {
hash ^= value.charCodeAt(index);
hash = Math.imul(hash, 0x01000193);
}
return hash >>> 0;
}
function canonicalKey(input: LODCacheKeyInput): string {
return JSON.stringify([
input.objectId,
input.meshRevision,
input.modifierStackHash,
input.profileHash,
input.poseOrRestState,
]);
}
/** Stable, path-safe key. It intentionally excludes mutable timestamps. */
export function buildLODCacheKey(input: LODCacheKeyInput): string {
if (!input.objectId || !Number.isInteger(input.meshRevision) || input.meshRevision < 0) {
throw new Error("Invalid LOD cache key input");
}
if (!input.modifierStackHash || !input.profileHash) throw new Error("LOD cache hashes are required");
const value = canonicalKey(input);
const first = fnv1a(value, 0x811c9dc5).toString(16).padStart(8, "0");
const second = fnv1a(value, 0x9e3779b9).toString(16).padStart(8, "0");
return `lod-${first}${second}`;
}
export function lodManifestCacheKey(manifest: LODCacheRecord): string {
return buildLODCacheKey({
objectId: manifest.objectId,
meshRevision: manifest.sourceMeshRevision,
modifierStackHash: manifest.modifierStackHash,
profileHash: manifest.profileHash,
poseOrRestState: manifest.poseOrRestState,
});
}
export function parseLODCacheRecord(value: unknown): LODCacheRecord {
const manifest = parseLODManifest(value);
if (typeof value !== "object" || value === null || Array.isArray(value)) {
throw new SimplifyValidationError("INVALID_LOD_MANIFEST", "LOD cache record must be an object");
}
const input = value as Record<string, unknown>;
const fields = ["cacheKey", "objectId", "modifierStackHash", "profileHash", "generatedAt"];
if (fields.some((field) => typeof input[field] !== "string" || !input[field])) {
throw new SimplifyValidationError("INVALID_LOD_MANIFEST", "LOD cache record metadata is incomplete");
}
if (input.poseOrRestState !== "REST" && input.poseOrRestState !== "POSE") {
throw new SimplifyValidationError("INVALID_LOD_MANIFEST", "LOD poseOrRestState must be REST or POSE");
}
const record: LODCacheRecord = {
...manifest,
cacheKey: input.cacheKey as string,
objectId: input.objectId as string,
modifierStackHash: input.modifierStackHash as string,
profileHash: input.profileHash as string,
poseOrRestState: input.poseOrRestState,
generatedAt: input.generatedAt as string,
};
if (lodManifestCacheKey(record) !== record.cacheKey) {
throw new SimplifyValidationError("INVALID_LOD_MANIFEST", "LOD cacheKey does not match its source inputs");
}
return record;
}