feat(web): switch to implementation-first capability wave
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-08-29 01:22:03 +08:00
parent 5fb12cd6a5
commit 7549b9a15c
159 changed files with 14110 additions and 74 deletions

View File

@@ -0,0 +1,26 @@
import type { FCurveInterpolation } from "./web-engine";
import type { WebEngineEditCommand } from "./web-engine";
type AnimationCommand<T extends WebEngineEditCommand["type"]> = Extract<WebEngineEditCommand, { type: T }>;
/** Typed constructors for animation, constraint and render property edits. */
export const animationRenderCommands = Object.freeze({
insertKeyframe(objectId: string, frame: number, property: AnimationCommand<"insertObjectKeyframe">["property"], interpolation: FCurveInterpolation = "BEZIER"): AnimationCommand<"insertObjectKeyframe"> {
return { type: "insertObjectKeyframe", objectId, frame, property, interpolation };
},
deleteKeyframe(objectId: string, frame: number, property?: AnimationCommand<"deleteObjectKeyframe">["property"]): AnimationCommand<"deleteObjectKeyframe"> {
return { type: "deleteObjectKeyframe", objectId, frame, ...(property ? { property } : {}) };
},
setConstraint(objectId: string, constraintName: string, enabled: boolean, influence: number): AnimationCommand<"setConstraint"> {
return { type: "setConstraint", objectId, constraintName, enabled, influence };
},
setLightProperties(dataId: string, properties: AnimationCommand<"setLightProperties">["properties"]): AnimationCommand<"setLightProperties"> {
return { type: "setLightProperties", dataId, properties };
},
setCameraProperties(dataId: string, properties: AnimationCommand<"setCameraProperties">["properties"]): AnimationCommand<"setCameraProperties"> {
return { type: "setCameraProperties", dataId, properties };
},
setWorldProperties(dataId: string, properties: AnimationCommand<"setWorldProperties">["properties"]): AnimationCommand<"setWorldProperties"> {
return { type: "setWorldProperties", dataId, properties };
},
} satisfies Record<string, (...args: never[]) => WebEngineEditCommand>);

View File

@@ -0,0 +1,43 @@
import type { WebEngineEditCommand } from "./web-engine";
type CoreCommand<T extends WebEngineEditCommand["type"]> = Extract<WebEngineEditCommand, { type: T }>;
type Vector3 = [number, number, number];
const vector3 = (value: readonly number[]): Vector3 => [value[0] ?? 0, value[1] ?? 0, value[2] ?? 0];
/** Typed constructors used by the workbench for the source-backed scene API. */
export const coreSceneCommands = Object.freeze({
createCube(location: readonly number[] = [0, 0, 0], name?: string): CoreCommand<"createPrimitive"> {
return { type: "createPrimitive", primitive: "CUBE", location: vector3(location), ...(name ? { name } : {}) };
},
duplicate(objectId: string, offset: readonly number[] = [0.25, 0.25, 0], linked = false): CoreCommand<"duplicateObject"> {
return { type: "duplicateObject", objectId, offset: vector3(offset), ...(linked ? { linked: true } : {}) };
},
delete(objectId: string): CoreCommand<"deleteObject"> {
return { type: "deleteObject", objectId };
},
visibility(objectId: string, visible: boolean): CoreCommand<"setObjectVisibility"> {
return { type: "setObjectVisibility", objectId, visible };
},
transform(objectId: string, translation: readonly number[], rotationEuler: readonly number[], scale: readonly number[]): CoreCommand<"setObjectTransform"> {
return { type: "setObjectTransform", objectId, translation: vector3(translation), rotationEuler: vector3(rotationEuler), scale: vector3(scale) };
},
parent(objectId: string, parentId: string | null, keepTransform = true): CoreCommand<"setParent"> {
return { type: "setParent", objectId, parentId, keepTransform };
},
createCollection(name: string, parentCollectionId?: string | null): CoreCommand<"createCollection"> {
return { type: "createCollection", name, ...(parentCollectionId === undefined ? {} : { parentCollectionId }) };
},
moveToCollection(objectId: string, collectionId: string): CoreCommand<"moveObjectToCollection"> {
return { type: "moveObjectToCollection", objectId, collectionId };
},
rename(id: string, name: string): CoreCommand<"renameId"> {
return { type: "renameId", id, name };
},
applyTransform(objectId: string): CoreCommand<"applyObjectTransform"> {
return { type: "applyObjectTransform", objectId };
},
setOrigin(objectId: string, mode: CoreCommand<"setObjectOrigin">["mode"] = "GEOMETRY"): CoreCommand<"setObjectOrigin"> {
return { type: "setObjectOrigin", objectId, mode };
},
} satisfies Record<string, (...args: never[]) => WebEngineEditCommand>);

View File

@@ -0,0 +1,13 @@
import type { WebEngineEditCommand } from "./web-engine";
type IOCommand<T extends WebEngineEditCommand["type"]> = Extract<WebEngineEditCommand, { type: T }>;
/** Typed constructors for media import and persistence-facing edit commands. */
export const ioMediaCommands = Object.freeze({
importImage(name: string, mimeType: IOCommand<"importImage">["mimeType"], width: number, height: number, base64: string): IOCommand<"importImage"> {
return { type: "importImage", name, mimeType, width, height, base64 };
},
setMaterialImageNode(materialId: string, imageId: string, usage: IOCommand<"setMaterialImageNode">["usage"], uvMap?: string): IOCommand<"setMaterialImageNode"> {
return { type: "setMaterialImageNode", materialId, imageId, usage, ...(uvMap ? { uvMap } : {}) };
},
} satisfies Record<string, (...args: never[]) => WebEngineEditCommand>);

View File

@@ -0,0 +1,35 @@
import type { MeshEditOperation, MeshElementMode } from "./web-engine";
import type { SimplifyProfile } from "./simplify";
import type { WebEngineEditCommand } from "./web-engine";
type MeshCommand<T extends WebEngineEditCommand["type"]> = Extract<WebEngineEditCommand, { type: T }>;
type Vector3 = [number, number, number];
const vector3 = (value: readonly number[]): Vector3 => [value[0] ?? 0, value[1] ?? 0, value[2] ?? 0];
/** Typed constructors for mesh authoring commands sent to WebEngine Main. */
export const meshAuthoringCommands = Object.freeze({
edit(meshId: string, operation: MeshEditOperation, selectionMode: MeshElementMode, elementIndices: readonly number[], options: { offset?: readonly number[]; amount?: number; segments?: number } = {}): MeshCommand<"meshEdit"> {
return {
type: "meshEdit", meshId, operation, selectionMode, elementIndices: [...elementIndices],
...(options.offset ? { offset: vector3(options.offset) } : {}),
...(options.amount === undefined ? {} : { amount: options.amount }),
...(options.segments === undefined ? {} : { segments: options.segments }),
};
},
decimate(meshId: string, profile: SimplifyProfile): MeshCommand<"decimateMesh"> {
return { type: "decimateMesh", meshId, profile };
},
previewDecimate(meshId: string, profile: SimplifyProfile): MeshCommand<"previewDecimateMesh"> {
return { type: "previewDecimateMesh", meshId, profile };
},
translateVertices(meshId: string, vertexIndices: readonly number[], offset: readonly number[]): MeshCommand<"translateMeshVertices"> {
return { type: "translateMeshVertices", meshId, vertexIndices: [...vertexIndices], offset: vector3(offset) };
},
createUVMap(meshId: string, name: string): MeshCommand<"createUVMap"> {
return { type: "createUVMap", meshId, name };
},
activeUVMap(meshId: string, name: string): MeshCommand<"setActiveUVMap"> {
return { type: "setActiveUVMap", meshId, name };
},
} satisfies Record<string, (...args: never[]) => WebEngineEditCommand>);

View File

@@ -0,0 +1,28 @@
import type { WebEngineEditCommand } from "./web-engine";
type NonMeshCommand<T extends WebEngineEditCommand["type"]> = Extract<WebEngineEditCommand, { type: T }>;
/** Typed constructors for curve, font, volume and Grease Pencil editing. */
export const nonMeshGeometryCommands = Object.freeze({
createCurve(name: string, controlPoints: readonly number[], cyclic = false, resolution = 12): NonMeshCommand<"createCurve"> {
return { type: "createCurve", curveType: "CURVE", name, controlPoints: [...controlPoints], cyclic, resolution };
},
setCurveControlPoints(dataId: string, controlPoints: readonly number[], splineOffsets?: readonly number[], resolution?: number): NonMeshCommand<"setCurveControlPoints"> {
return { type: "setCurveControlPoints", dataId, controlPoints: [...controlPoints], ...(splineOffsets ? { splineOffsets: [...splineOffsets] } : {}), ...(resolution === undefined ? {} : { resolution }) };
},
setFontBody(dataId: string, body: string): NonMeshCommand<"setFontBody"> {
return { type: "setFontBody", dataId, body };
},
setVolumeProperties(dataId: string, sourcePath: string, displayDensity: number, interpolation: "NEAREST" | "LINEAR", stepSize: number): NonMeshCommand<"setVolumeProperties"> {
return { type: "setVolumeProperties", dataId, sourcePath, displayDensity, interpolation, stepSize };
},
createGreasePencilLayer(dataId: string, name: string): NonMeshCommand<"createGreasePencilLayer"> {
return { type: "createGreasePencilLayer", dataId, name };
},
removeGreasePencilLayer(dataId: string, layerId: string): NonMeshCommand<"removeGreasePencilLayer"> {
return { type: "removeGreasePencilLayer", dataId, layerId };
},
clearGreasePencilFrame(dataId: string, layerId: string, frame: number, baseRevision?: number): NonMeshCommand<"setGreasePencilStrokes"> {
return { type: "setGreasePencilStrokes", dataId, layerId, frame, ...(baseRevision === undefined ? {} : { baseRevision }), strokes: [] };
},
} satisfies Record<string, (...args: never[]) => WebEngineEditCommand>);

View File

@@ -0,0 +1,14 @@
import type { RenderCapabilityRequest } from "./render-capabilities";
/** Canonical capability requests shared by export and future compositor UI. */
export const renderCompositorCommands = Object.freeze({
boundedShaderGraph(): RenderCapabilityRequest {
return { kind: "ARBITRARY_SHADER", nodeTypes: ["PRINCIPLED", "MATERIAL_OUTPUT"] };
},
webGPUBackend(): RenderCapabilityRequest {
return { kind: "WEBGPU_BACKEND" };
},
postProcess(passes: Extract<RenderCapabilityRequest, { kind: "POSTPROCESS" }>["passes"]): RenderCapabilityRequest {
return { kind: "POSTPROCESS", passes: [...passes] };
},
} as const);

View File

@@ -0,0 +1,28 @@
import type { ShaderGraphIR, ShaderSocketIR } from "./shader-graph";
const input = (id: string, name: string, dataType: ShaderSocketIR["dataType"], defaultValue?: ShaderSocketIR["defaultValue"]): ShaderSocketIR => ({ id, name, direction: "INPUT", dataType, ...(defaultValue === undefined ? {} : { defaultValue }) });
const output = (id: string, name: string, dataType: ShaderSocketIR["dataType"]): ShaderSocketIR => ({ id, name, direction: "OUTPUT", dataType });
/** Build a valid bounded graph for the source-backed Principled writer. */
export const shaderGraphCommands = Object.freeze({
setPrincipledGraph(materialId: string, baseColor: readonly number[], roughness: number, metallic: number): ShaderGraphIR {
const principled = "shader:principled";
const materialOutput = "shader:material-output";
return {
schemaVersion: 1,
id: `shader-graph:${materialId}`,
materialId,
nodes: [
{ id: principled, type: "PRINCIPLED", name: "Principled BSDF", sockets: [
input("Base Color", "Base Color", "COLOR", [...baseColor].slice(0, 4)),
input("Metallic", "Metallic", "VALUE", metallic),
input("Roughness", "Roughness", "VALUE", roughness),
output("BSDF", "BSDF", "SHADER"),
] },
{ id: materialOutput, type: "MATERIAL_OUTPUT", name: "Material Output", sockets: [input("Surface", "Surface", "SHADER")] },
],
links: [{ fromNodeId: principled, fromSocketId: "BSDF", toNodeId: materialOutput, toSocketId: "Surface" }],
outputNodeId: materialOutput,
};
},
} as const);

View File

@@ -0,0 +1,98 @@
import type { WebEngineEditCommand } from "./web-engine";
export const WEB_ENGINE_CAPABILITY_REGISTRY_SCHEMA = 1 as const;
export type WebEngineCommandType = WebEngineEditCommand["type"];
export type WebEngineCommandRoute = "MAIN_AUTHORITATIVE" | "MAIN_GUARDED" | "LOCAL_HISTORY" | "PREVIEW";
export interface WebEngineCommandCapability {
readonly family: string;
readonly route: WebEngineCommandRoute;
readonly implementation: "SOURCE_BACKED" | "SOURCE_GUARDED";
}
/**
* The protocol is intentionally the type authority; this record makes the
* runtime dispatch contract explicit without duplicating command payloads.
*/
export const WEB_ENGINE_COMMAND_CAPABILITIES = {
setFrame: { family: "TIMELINE", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
setObjectVisibility: { family: "OBJECT", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
setModifierEnabled: { family: "MODIFIER", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
setModifierVisibility: { family: "MODIFIER", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
decimateMesh: { family: "MESH", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
previewDecimateMesh: { family: "MESH", route: "PREVIEW", implementation: "SOURCE_BACKED" },
createPrimitive: { family: "OBJECT", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
duplicateObject: { family: "OBJECT", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
deleteObject: { family: "OBJECT", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
setObjectTransform: { family: "OBJECT", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
translateMeshVertices: { family: "MESH", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
meshEdit: { family: "MESH", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
createUVMap: { family: "MESH", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
setActiveUVMap: { family: "MESH", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
unwrapUV: { family: "MESH", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
addMaterialSlot: { family: "MATERIAL", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
removeMaterialSlot: { family: "MATERIAL", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
assignMaterialFaces: { family: "MATERIAL", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
setMaterialPrincipled: { family: "MATERIAL", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
importImage: { family: "IO", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
setMaterialImageNode: { family: "MATERIAL", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
insertObjectKeyframe: { family: "ANIMATION", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
deleteObjectKeyframe: { family: "ANIMATION", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
setFCurveInterpolation: { family: "ANIMATION", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
setActiveAction: { family: "ANIMATION", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
setConstraint: { family: "ANIMATION", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
setParent: { family: "OBJECT", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
createCollection: { family: "OBJECT", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
moveObjectToCollection: { family: "OBJECT", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
renameId: { family: "OBJECT", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
joinObjects: { family: "OBJECT", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
separateMeshFaces: { family: "MESH", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
applyObjectTransform: { family: "OBJECT", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
setObjectOrigin: { family: "OBJECT", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
createCurve: { family: "GEOMETRY", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
deleteNonMeshData: { family: "GEOMETRY", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
setCurveControlPoints: { family: "GEOMETRY", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
setCurveHandle: { family: "GEOMETRY", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
setCurveTopology: { family: "GEOMETRY", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
setCurveSplines: { family: "GEOMETRY", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
setSurfaceTopology: { family: "GEOMETRY", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
setFontBody: { family: "FONT", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
setFontProperties: { family: "FONT", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
setFontAdvanced: { family: "FONT", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
importVFont: { family: "FONT", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
setFontLinks: { family: "FONT", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
setVolumeProperties: { family: "VOLUME", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
createGreasePencilLayer: { family: "GREASE_PENCIL", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
removeGreasePencilLayer: { family: "GREASE_PENCIL", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
moveGreasePencilLayer: { family: "GREASE_PENCIL", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
moveGreasePencilFrame: { family: "GREASE_PENCIL", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
insertGreasePencilFrame: { family: "GREASE_PENCIL", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
removeGreasePencilFrame: { family: "GREASE_PENCIL", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
setGreasePencilStrokes: { family: "GREASE_PENCIL", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
setVertexColors: { family: "MESH", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
setVertexWeights: { family: "MESH", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
setLightProperties: { family: "RENDER", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
setCameraProperties: { family: "RENDER", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
setWorldProperties: { family: "RENDER", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
setMetaballElements: { family: "GEOMETRY", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
sculptStroke: { family: "SCULPT", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
setSculptMeshAttributes: { family: "SCULPT", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
setGeometryNodeGraph: { family: "GEOMETRY_NODES", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
setPhysicsSimulation: { family: "SIMULATION", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
setShaderGraph: { family: "SHADER", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
setNLAStack: { family: "ANIMATION", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
moveNLAStrip: { family: "ANIMATION", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
undo: { family: "HISTORY", route: "LOCAL_HISTORY", implementation: "SOURCE_BACKED" },
redo: { family: "HISTORY", route: "LOCAL_HISTORY", implementation: "SOURCE_BACKED" },
} satisfies Record<WebEngineCommandType, WebEngineCommandCapability>;
export function getWebEngineCommandCapability(type: unknown): WebEngineCommandCapability | null {
if (typeof type !== "string") return null;
return Object.prototype.hasOwnProperty.call(WEB_ENGINE_COMMAND_CAPABILITIES, type)
? WEB_ENGINE_COMMAND_CAPABILITIES[type as WebEngineCommandType]
: null;
}
export function isKnownWebEngineCommand(type: unknown): type is WebEngineCommandType {
return getWebEngineCommandCapability(type) !== null;
}

View File

@@ -9,6 +9,7 @@ import type { LODManifest } from "./simplify";
import type { DepsgraphEvaluationIR } from "./depsgraph";
import type { SculptMeshAttributesIR, SculptStrokeIR } from "./sculpt";
import type { GeometryNodeGraphIR } from "./geometry-nodes";
import type { PhysicsSimulationManifestIR } from "./physics-simulation";
import type { ShaderGraphIR } from "./shader-graph";
import type { ShaderCompileReport } from "./shader-compiler";
import type { NlaMoveStripCommand, NlaTrackIR } from "./nla";
@@ -141,6 +142,7 @@ export type WebEngineEditCommand =
| { type: "sculptStroke"; stroke: SculptStrokeIR }
| { type: "setSculptMeshAttributes"; attributes: SculptMeshAttributesIR }
| { type: "setGeometryNodeGraph"; meshId: string; graph: GeometryNodeGraphIR }
| { type: "setPhysicsSimulation"; manifest: PhysicsSimulationManifestIR }
| { type: "setShaderGraph"; materialId: string; graph: ShaderGraphIR }
| { type: "setNLAStack"; objectId: string; tracks: NlaTrackIR[]; baseRevision?: number }
| NlaMoveStripCommand

View File

@@ -0,0 +1,16 @@
import { getWebEngineCommandCapability, type WebEngineCommandType } from "./web-engine-capabilities";
export const WORKBENCH_COMMAND_GROUPS = Object.freeze({
OBJECT: ["createPrimitive", "duplicateObject", "deleteObject", "setObjectTransform", "setParent", "createCollection", "moveObjectToCollection", "renameId", "applyObjectTransform", "setObjectOrigin"],
MESH: ["meshEdit", "translateMeshVertices", "createUVMap", "setActiveUVMap", "unwrapUV", "setVertexColors", "setVertexWeights"],
ANIMATION: ["setFrame", "insertObjectKeyframe", "deleteObjectKeyframe", "setFCurveInterpolation", "setConstraint", "setNLAStack"],
RENDER: ["setCameraProperties", "setLightProperties", "setWorldProperties"],
IO: ["importImage", "importVFont", "setMaterialImageNode"],
} as const satisfies Record<string, readonly WebEngineCommandType[]>);
export function isWorkbenchCommandAvailable(type: unknown): type is WebEngineCommandType {
return getWebEngineCommandCapability(type)?.route !== "MAIN_GUARDED";
}
export function workbenchCommandCount(): number {
return Object.values(WORKBENCH_COMMAND_GROUPS).reduce((count, group) => count + group.filter(isWorkbenchCommandAvailable).length, 0);
}