feat(web): classify and route complete Blender inventory
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user