feat(web): complete product workflow implementation
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

This commit is contained in:
mes123456
2026-09-06 08:33:27 +08:00
parent 7e3f6da78b
commit 1184a51ddf
39 changed files with 3736 additions and 160 deletions

View File

@@ -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);