Files
workinf_Blender_Wasm/web/protocol/modifier.ts
mes123456 f3e21c6b98
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
chore: checkpoint web Blender migration source, tools and docs
Accumulated uncommitted work: GHOST Web backend, WebGL/GPU web layer,
web workflow engine and protocol, tooling, docs and task records.
Large generated evidence, user-testing archives, build snapshots and
imported test assets remain untracked.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-11-10 11:18:01 +08:00

80 lines
3.4 KiB
TypeScript

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")}`;
}