105 lines
3.5 KiB
TypeScript
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;
|
|
}
|