feat(web): classify and route complete Blender inventory
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 21:10:31 +08:00
parent 66d3f4e0f4
commit e2e81c7c00
41 changed files with 116672 additions and 37 deletions

View File

@@ -37,7 +37,9 @@ 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";
import type { BlenderFeatureRouteEntry, BlenderFeatureRouteInvocation, BlenderOperatorDescriptor, BlenderOperatorInvocation, BlenderOperatorPropertySpec, BlenderOperatorPropertyType } from "../../../protocol/blender-operator";
import { parseServerOperatorJobRequest, type ServerOperatorJobRequestIR, type ServerOperatorJobResultIR } from "../../../protocol/server-operator-job";
import featureRouteCatalog from "../capabilities/blender-feature-route-catalog.json";
interface PendingRequest {
resolve: (result: WebEngineResult) => void;
@@ -114,6 +116,58 @@ function normalizeOperatorCatalog(value: unknown): BlenderOperatorDescriptor[] {
});
}
function normalizeFeatureRouteCatalog(value: unknown): BlenderFeatureRouteEntry[] {
const root = value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
const entries = Array.isArray(value) ? value : root && (Array.isArray(root.entries) ? root.entries : Array.isArray(root.routes) ? root.routes : Array.isArray(root.units) ? root.units : []);
return entries.filter((entry): entry is Record<string, unknown> => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))).map((entry) => {
const route = entry.route === "SERVER_NATIVE" || entry.route === "BROWSER_EQUIVALENT" || entry.route === "UNAVAILABLE" ? entry.route : "WEB_MAIN";
const locations = Array.isArray(entry.locations) ? entry.locations.filter((location): location is Record<string, unknown> => Boolean(location && typeof location === "object" && !Array.isArray(location))).map((location) => ({
path: String(location.path ?? ""),
...(typeof location.line === "number" ? { line: location.line } : {}),
...(typeof location.evidence === "string" ? { evidence: location.evidence } : {}),
})) : undefined;
return {
id: String(entry.id ?? entry.identifier ?? "").trim(),
kind: String(entry.kind ?? "feature"),
identifier: String(entry.identifier ?? entry.id ?? "").trim(),
family: String(entry.family ?? "Blender"),
route,
reason: String(entry.reason ?? "classified source-unit route"),
...(typeof entry.label === "string" ? { label: entry.label } : {}),
...(typeof entry.description === "string" ? { description: entry.description } : {}),
...(Array.isArray(entry.properties) ? { properties: entry.properties as BlenderFeatureRouteEntry["properties"] } : {}),
...(locations && locations.length > 0 ? { locations } : {}),
};
}).filter((entry) => entry.id.length > 0 && entry.identifier.length > 0);
}
const FEATURE_ROUTE_CATALOG = normalizeFeatureRouteCatalog(featureRouteCatalog);
function mergeFeatureRoutes(operators: BlenderOperatorDescriptor[]): BlenderFeatureRouteEntry[] {
const merged = FEATURE_ROUTE_CATALOG.map((route) => ({ ...route }));
for (const operator of operators) {
const existing = merged.find((route) => route.route === "WEB_MAIN" && route.identifier === operator.id && route.kind.toLowerCase().includes("operator"));
if (existing) {
existing.label = operator.label;
existing.description = operator.description;
existing.properties = operator.properties;
continue;
}
merged.push({
id: `operator:${operator.id}`,
kind: "operator",
identifier: operator.id,
family: operator.family,
route: "WEB_MAIN",
reason: "typed native WebEngine operator",
label: operator.label,
description: operator.description,
properties: operator.properties,
});
}
return merged;
}
export class WebEngineClient {
private readonly timeoutMs: number;
private readonly workerFactory: () => Worker;
@@ -186,6 +240,11 @@ export class WebEngineClient {
return { status, operators: normalizeOperatorCatalog(result) };
}
async listFeatureRoutes(): Promise<{ status: WebEngineStatus; routes: BlenderFeatureRouteEntry[] }> {
const { status, operators } = await this.listBlenderOperators();
return { status, routes: mergeFeatureRoutes(operators) };
}
async listRNADataBlocks(params: Record<string, unknown> = {}): Promise<{ status: WebEngineStatus; dataBlocks: RNADataBlockDescriptor[] }> {
const { status, result } = await this.callJson<unknown>("listRNADataBlocks", params);
const dataBlocks = Array.isArray(result) ? result : (result && typeof result === "object" && Array.isArray((result as { dataBlocks?: unknown }).dataBlocks) ? (result as { dataBlocks: unknown[] }).dataBlocks : []);
@@ -208,6 +267,30 @@ export class WebEngineClient {
return { status, collections: collections.filter((item): item is RNACollectionDescriptor => Boolean(item && typeof item === "object")) };
}
async readRNAPropertySchema(params: Record<string, unknown>): Promise<{ status: WebEngineStatus; schema: RNAPropertyDescriptor }> {
const { status, result } = await this.callJson<unknown>("readRNAPropertySchema", params);
const schema = result && typeof result === "object" && result.schema && typeof result.schema === "object"
? result.schema as RNAPropertyDescriptor
: result as RNAPropertyDescriptor;
return { status, schema };
}
async setRNAArrayProperty(params: Record<string, unknown>): Promise<{ status: WebEngineStatus; result: unknown }> {
return this.callJson("setRNAArrayProperty", params);
}
async setRNAEnumProperty(params: Record<string, unknown>): Promise<{ status: WebEngineStatus; result: unknown }> {
return this.callJson("setRNAEnumProperty", params);
}
async readRNACollection(params: Record<string, unknown> = {}): Promise<{ status: WebEngineStatus; collection: RNACollectionDescriptor }> {
const { status, result } = await this.callJson<unknown>("readRNACollection", params);
const collection = result && typeof result === "object" && result.collection && typeof result.collection === "object"
? result.collection as RNACollectionDescriptor
: result as RNACollectionDescriptor;
return { status, collection };
}
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> });
@@ -227,6 +310,40 @@ export class WebEngineClient {
};
}
async invokeFeatureRoute(invocation: BlenderFeatureRouteInvocation & { serverJob?: ServerOperatorJobRequestIR }): Promise<BlendOpenResult & { delta: SceneDelta; operatorResult?: unknown }> {
const route = invocation.route;
if (route.route === "WEB_MAIN" && (route.kind.toLowerCase().includes("operator") || route.identifier.startsWith("operator."))) {
return this.invokeBlenderOperator({
operator: route.identifier.replace(/^operator:/, ""),
properties: invocation.properties,
...(invocation.baseRevision === undefined ? {} : { baseRevision: invocation.baseRevision }),
});
}
if (route.route === "SERVER_NATIVE") {
if (!invocation.serverJob) throw this.report("INVALID_ARGUMENT", "SERVER_NATIVE feature routes require a typed server job payload", true);
parseServerOperatorJobRequest(invocation.serverJob);
const { status, result } = await this.callJson<ServerOperatorJobResultIR>("submitServerOperatorJob", invocation.serverJob as unknown as Record<string, unknown>);
return { status, snapshot: result as unknown as BlendOpenResult["snapshot"], delta: result as unknown as SceneDelta, geometryBuffers: [...this.geometryBuffers], nonMeshGeometryBuffers: [...this.nonMeshGeometryBuffers], operatorResult: result };
}
throw this.report("INVALID_ARGUMENT", `Feature route ${route.identifier} is not executable in the current browser context`, true);
}
async submitServerOperatorJob(request: ServerOperatorJobRequestIR): Promise<{ status: WebEngineStatus; job: ServerOperatorJobResultIR }> {
const validated = parseServerOperatorJobRequest(request);
const result = await this.callJson<ServerOperatorJobResultIR>("submitServerOperatorJob", validated as unknown as Record<string, unknown>);
return { status: result.status, job: result.result };
}
async cancelServerOperatorJob(jobId: string, requestSha256: string): Promise<{ status: WebEngineStatus; job: ServerOperatorJobResultIR }> {
const result = await this.callJson<ServerOperatorJobResultIR>("cancelServerOperatorJob", { jobId, requestSha256 });
return { status: result.status, job: result.result };
}
async readServerOperatorResult(jobId: string, requestSha256: string): Promise<{ status: WebEngineStatus; job: ServerOperatorJobResultIR }> {
const result = await this.callJson<ServerOperatorJobResultIR>("readServerOperatorResult", { jobId, requestSha256 });
return { status: result.status, job: result.result };
}
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);