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" | "enum_set" | "object" | "collection" | "texture" | "cachefile" | "curve_mapping" | "profile"; options?: string[]; items?: Array<{ id: string; label: string }>; label?: string; description?: string; /** Native MOD_subsurf panel text for derived, read-only values. */ display?: string; visible?: boolean; enabled?: boolean; editable?: boolean; nullable?: boolean; suggestionsOnly?: boolean; maxLength?: number; unit?: 'ANGLE'; integer?: boolean; length?: number; minimum?: number; maximum?: number; }; export type ModifierCurveProfile = { view: [number, number, number, number]; clip: [number, number, number, number]; points: Array<{ location: [number, number]; handle1: [number, number]; handle2: [number, number]; handleType: 'AUTO' | 'VECTOR' | 'FREE' | 'ALIGN' }>; samples: Array<[number, number]>; use_clip: boolean; use_sample_straight_edges: boolean; use_sample_even_lengths: boolean; }; export function isModifierCurveProfile(value: unknown): value is ModifierCurveProfile { if (!value || typeof value !== 'object') return false; const profile = value as ModifierCurveProfile; const pair = (point: unknown) => Array.isArray(point) && point.length === 2 && point.every(n => typeof n === 'number' && Number.isFinite(n)); const rect = (value: unknown) => Array.isArray(value) && value.length === 4 && value.every(n => typeof n === 'number' && Number.isFinite(n)) && value[2] > value[0] && value[3] > value[1]; return rect(profile.view) && rect(profile.clip) && typeof profile.use_clip === 'boolean' && typeof profile.use_sample_straight_edges === 'boolean' && typeof profile.use_sample_even_lengths === 'boolean' && Array.isArray(profile.points) && profile.points.length >= 2 && profile.points.length <= 32767 && profile.points.every(point => point && pair(point.location) && pair(point.handle1) && pair(point.handle2) && ['AUTO', 'VECTOR', 'FREE', 'ALIGN'].includes(point.handleType)) && Array.isArray(profile.samples) && profile.samples.length <= 1048576 && profile.samples.every(pair); } /** Derives editable fields from the metadata carried by the authoritative modifier IR. */ export function modifierParameterMetadata(modifier: ModifierIR): ModifierParameterMetadata[] { return (modifier.editableParameters ?? []).map((field) => ({ ...field, options: field.items?.map((item) => item.id), })); } /** 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")}`; }