import type { ModifierIR } from "./scene-ir"; function fnv1a(value: string): number { let hash = 0x811c9dc5; for (let index = 0; index < value.length; index++) { hash ^= value.charCodeAt(index); hash = Math.imul(hash, 0x01000193); } return hash >>> 0; } export type ModifierParameterMetadata = { key: string; value: unknown; kind: "boolean" | "number" | "string" | "enum" | "object"; options?: string[]; }; /** Derives editable fields from the metadata carried by the authoritative modifier IR. */ export function modifierParameterMetadata(modifier: ModifierIR): ModifierParameterMetadata[] { return Object.entries(modifier.parameters ?? {}).map(([key, rawValue]) => { // Main may carry a descriptor when RNA exposes enum choices alongside the // current value. Keep the descriptor opaque to callers while deriving the // editable value and choices from it. const descriptor = rawValue && typeof rawValue === "object" && !Array.isArray(rawValue) && "value" in rawValue && ("kind" in rawValue || "options" in rawValue) ? rawValue as { value: unknown; kind?: ModifierParameterMetadata["kind"]; options?: unknown[] } : undefined; const rawOptions = descriptor?.options ?? (Array.isArray(rawValue) && rawValue.every((entry) => typeof entry === "string") ? rawValue : undefined); const options = rawOptions?.map(String); const value = descriptor?.value ?? (options ? options[0] : rawValue); const kind = descriptor?.kind ?? ( typeof value === "boolean" ? "boolean" : typeof value === "number" ? "number" : typeof value === "string" ? (options ? "enum" : "string") : "object" ); return { key, value, kind, options: options ?? undefined }; }); } /** Hashes evaluated modifier identity/state, excluding display timestamps. */ export function modifierStackHash(stack: readonly ModifierIR[] | undefined): string { const canonical = (stack ?? []).map((modifier) => ({ uuid: modifier.uuid, type: modifier.type, enabled: modifier.enabled, showViewport: modifier.showViewport, showRender: modifier.showRender, showEditMode: modifier.showEditMode, showOnCage: modifier.showOnCage, parameters: modifier.parameters, dependsOn: modifier.dependsOn, evaluationStatus: modifier.evaluationStatus, })); return `mod-${fnv1a(JSON.stringify(canonical)).toString(16).padStart(8, "0")}`; }