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