feat(web): complete product workflow implementation
This commit is contained in:
@@ -34,6 +34,7 @@ import type {
|
||||
} from "../../../protocol/paint-stroke-session";
|
||||
import type { PaintPBVHCapabilityRequest } from "../../../protocol/paint-pbvh-capability";
|
||||
import type { LibraryMainAppendReceiptIR, LibraryMainAppendRequestIR } from "../../../protocol/library-main-append";
|
||||
import type { BlenderOperatorDescriptor, BlenderOperatorInvocation, BlenderOperatorPropertySpec, BlenderOperatorPropertyType } from "../../../protocol/blender-operator";
|
||||
|
||||
interface PendingRequest {
|
||||
resolve: (result: WebEngineResult) => void;
|
||||
@@ -61,6 +62,55 @@ export interface BlendOpenResult {
|
||||
simplify?: SimplifyResult;
|
||||
}
|
||||
|
||||
function normalizeOperatorPropertyType(value: unknown, raw: Record<string, unknown>): BlenderOperatorPropertyType {
|
||||
const type = String(value ?? "string").toLowerCase();
|
||||
if (type === "boolean" || type === "bool") return "boolean";
|
||||
if (type === "enum" || Array.isArray(raw.enumItems) || Array.isArray(raw.items) || Array.isArray(raw.enum)) return "enum";
|
||||
if (type === "number" || type === "integer" || type === "float" || type === "double" || type.startsWith("float[")) return "number";
|
||||
return "string";
|
||||
}
|
||||
|
||||
function normalizeOperatorCatalog(value: unknown): BlenderOperatorDescriptor[] {
|
||||
const root = value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : null;
|
||||
const entries = Array.isArray(value) ? value : root && Array.isArray(root.operators) ? root.operators : [];
|
||||
return entries.flatMap((entry): BlenderOperatorDescriptor[] => {
|
||||
if (!entry || typeof entry !== "object") return [];
|
||||
const raw = entry as Record<string, unknown>;
|
||||
const id = String(raw.id ?? raw.operator ?? raw.name ?? "").trim();
|
||||
if (!id) return [];
|
||||
const properties = Array.isArray(raw.properties) ? raw.properties.flatMap((property): BlenderOperatorPropertySpec[] => {
|
||||
if (!property || typeof property !== "object") return [];
|
||||
const spec = property as Record<string, unknown>;
|
||||
const name = String(spec.name ?? "").trim();
|
||||
if (!name) return [];
|
||||
const enumValues = spec.enumItems ?? spec.items ?? spec.enum;
|
||||
const enumItems = Array.isArray(enumValues) ? enumValues.flatMap((item): { value: string; label: string }[] => {
|
||||
if (typeof item === "string" || typeof item === "number") return [{ value: String(item), label: String(item) }];
|
||||
if (!item || typeof item !== "object") return [];
|
||||
const candidate = item as Record<string, unknown>;
|
||||
const option = String(candidate.value ?? candidate.identifier ?? candidate.id ?? "");
|
||||
return option ? [{ value: option, label: String(candidate.label ?? candidate.name ?? option) }] : [];
|
||||
}) : undefined;
|
||||
const type = normalizeOperatorPropertyType(spec.type, spec);
|
||||
return [{
|
||||
name,
|
||||
type,
|
||||
required: Boolean(spec.required),
|
||||
label: typeof spec.label === "string" ? spec.label : undefined,
|
||||
description: typeof spec.description === "string" ? spec.description : undefined,
|
||||
defaultValue: typeof spec.defaultValue === "string" || typeof spec.defaultValue === "number" || typeof spec.defaultValue === "boolean"
|
||||
? spec.defaultValue
|
||||
: typeof spec.default === "string" || typeof spec.default === "number" || typeof spec.default === "boolean" ? spec.default : undefined,
|
||||
min: typeof spec.min === "number" ? spec.min : typeof spec.minValue === "number" ? spec.minValue : undefined,
|
||||
max: typeof spec.max === "number" ? spec.max : typeof spec.maxValue === "number" ? spec.maxValue : undefined,
|
||||
enumItems,
|
||||
}];
|
||||
}) : [];
|
||||
const context = raw.context === "object" || raw.context === "main" ? raw.context : "scene";
|
||||
return [{ id, label: String(raw.label ?? raw.name ?? id), description: typeof raw.description === "string" ? raw.description : undefined, family: String(raw.family ?? "Blender"), context, backgroundSafe: true, properties }];
|
||||
});
|
||||
}
|
||||
|
||||
export class WebEngineClient {
|
||||
private readonly timeoutMs: number;
|
||||
private readonly workerFactory: () => Worker;
|
||||
@@ -128,6 +178,30 @@ export class WebEngineClient {
|
||||
return { status: result.status, result: result.json as T };
|
||||
}
|
||||
|
||||
async listBlenderOperators(): Promise<{ status: WebEngineStatus; operators: BlenderOperatorDescriptor[] }> {
|
||||
const { status, result } = await this.callJson<unknown>("listBlenderOperators");
|
||||
return { status, operators: normalizeOperatorCatalog(result) };
|
||||
}
|
||||
|
||||
async invokeBlenderOperator(invocation: BlenderOperatorInvocation): Promise<BlendOpenResult & { delta: SceneDelta; operatorResult?: unknown }> {
|
||||
if (!invocation.operator.trim()) throw this.report("INVALID_ARGUMENT", "Blender operator 不能为空", true);
|
||||
const result = await this.request({ type: "jsonCommand", cmd: "invokeBlenderOperator", params: invocation as unknown as Record<string, unknown> });
|
||||
if (!result.snapshot || !result.delta) throw this.report("INVALID_ARGUMENT", "Blender operator 未返回场景结果", true);
|
||||
this.geometryBuffers = result.geometryDelta
|
||||
? applyMeshGeometryDelta(this.geometryBuffers, result.geometryDelta)
|
||||
: result.geometryBuffers ?? this.geometryBuffers;
|
||||
this.nonMeshGeometryBuffers = result.nonMeshGeometryBuffers ?? this.nonMeshGeometryBuffers;
|
||||
return {
|
||||
status: result.status,
|
||||
snapshot: result.snapshot,
|
||||
geometryBuffers: [...this.geometryBuffers],
|
||||
nonMeshGeometryBuffers: [...this.nonMeshGeometryBuffers],
|
||||
delta: result.delta,
|
||||
simplify: result.simplify,
|
||||
operatorResult: result.json,
|
||||
};
|
||||
}
|
||||
|
||||
async delta(): Promise<{ status: WebEngineStatus; delta: SceneDelta }> {
|
||||
const result = await this.request({ type: "delta" });
|
||||
if (!result.delta) throw this.report("INVALID_ARGUMENT", "WebEngine 未返回 SceneDelta", true);
|
||||
|
||||
Reference in New Issue
Block a user