feat(web): complete portable Blender feature gaps
This commit is contained in:
@@ -20,6 +20,21 @@ export const animationRenderCommands = Object.freeze({
|
||||
setKeyframe(animationId: string, path: string, frame: number, options: Omit<AnimationCommand<"setKeyframe">, "type" | "animationId" | "path" | "frame"> = {}): AnimationCommand<"setKeyframe"> {
|
||||
return { type: "setKeyframe", animationId, path, frame, ...options };
|
||||
},
|
||||
setFCurveKeyframes(animationId: string, path: string, points: AnimationCommand<"setFCurveKeyframes">["points"]): AnimationCommand<"setFCurveKeyframes"> {
|
||||
return { type: "setFCurveKeyframes", animationId, path, points: points.map((point) => ({ ...point, handleLeft: point.handleLeft ? [...point.handleLeft] as [number, number] : undefined, handleRight: point.handleRight ? [...point.handleRight] as [number, number] : undefined })) };
|
||||
},
|
||||
setDriverExpression(animationId: string, path: string, expression: string): AnimationCommand<"setDriverExpression"> {
|
||||
return { type: "setDriverExpression", animationId, path, expression };
|
||||
},
|
||||
setActionGroups(animationId: string, groups: AnimationCommand<"setActionGroups">["groups"]): AnimationCommand<"setActionGroups"> {
|
||||
return { type: "setActionGroups", animationId, groups: groups.map((group) => ({ name: group.name, paths: [...group.paths] })) };
|
||||
},
|
||||
setKeyingSet(sceneId: string, name: string, paths: AnimationCommand<"setKeyingSet">["paths"], active = true): AnimationCommand<"setKeyingSet"> {
|
||||
return { type: "setKeyingSet", sceneId, name, paths: paths.map((path) => ({ ...path })), active };
|
||||
},
|
||||
bakeAnimation(objectId: string, frameStart: number, frameEnd: number, step = 1): AnimationCommand<"bakeAnimation"> {
|
||||
return { type: "bakeAnimation", objectId, frameStart, frameEnd, step };
|
||||
},
|
||||
setTimelineMarkers(dataId: string, markers: AnimationCommand<"setTimelineMarkers">["markers"]): AnimationCommand<"setTimelineMarkers"> {
|
||||
return { type: "setTimelineMarkers", dataId, markers: markers.map((marker) => ({ ...marker })) };
|
||||
},
|
||||
|
||||
@@ -171,10 +171,61 @@ export function gateIORequest(value: unknown): CapabilityGateResult {
|
||||
}
|
||||
|
||||
export function gateLibraryMutation(operation: "CATALOG_EDIT" | "APPEND" | "LINK" | "OVERRIDE" | "RELOAD" | "RELOCATE"): CapabilityGateResult {
|
||||
if (operation === "CATALOG_EDIT") return readyGate("N-023", operation);
|
||||
if (["CATALOG_EDIT", "APPEND", "LINK", "OVERRIDE", "RELOAD"].includes(operation)) return readyGate("N-023", operation);
|
||||
return blockedGate("N-023", operation, [capabilityIssue("LIBRARY_MUTATION_UNAVAILABLE", `${operation} requires a real Blender Main transaction`)]);
|
||||
}
|
||||
|
||||
export type AssetLibraryCommandName = "assetMark" | "assetCatalog" | "libraryLink" | "libraryOverride" | "libraryReload";
|
||||
|
||||
export interface AssetLibraryCommandIR {
|
||||
command: AssetLibraryCommandName;
|
||||
params: Record<string, unknown>;
|
||||
}
|
||||
|
||||
const ASSET_LIBRARY_COMMANDS = new Set<AssetLibraryCommandName>(["assetMark", "assetCatalog", "libraryLink", "libraryOverride", "libraryReload"]);
|
||||
|
||||
function boundedId(value: unknown, name: string): string {
|
||||
return text(value, name, 256);
|
||||
}
|
||||
|
||||
function boundedTags(value: unknown): string[] {
|
||||
if (value === undefined) return [];
|
||||
if (!Array.isArray(value) || value.length > ASSET_LIBRARY_BUDGET.maxTagsPerAsset) throw new AssetLibraryValidationError("ASSET_MANIFEST_INVALID", "tags are invalid");
|
||||
return value.map((tag, index) => text(tag, `tags[${index}]`, 64));
|
||||
}
|
||||
|
||||
/** Validate browser-originated asset/library commands before they cross into WASM. */
|
||||
export function parseAssetLibraryCommand(command: string, value: unknown): AssetLibraryCommandIR {
|
||||
if (!ASSET_LIBRARY_COMMANDS.has(command as AssetLibraryCommandName) || !record(value)) throw new AssetLibraryValidationError("ASSET_MANIFEST_INVALID", "Unsupported asset/library command");
|
||||
const name = command as AssetLibraryCommandName;
|
||||
const params: Record<string, unknown> = { ...value };
|
||||
if (name === "assetMark") {
|
||||
params.dataId = boundedId(params.dataId, "dataId");
|
||||
params.catalogId = params.catalogId === undefined || params.catalogId === null ? null : boundedId(params.catalogId, "catalogId");
|
||||
params.tags = boundedTags(params.tags);
|
||||
params.marked = params.marked === undefined ? true : params.marked;
|
||||
if (typeof params.marked !== "boolean") throw new AssetLibraryValidationError("ASSET_MANIFEST_INVALID", "marked is invalid");
|
||||
}
|
||||
else if (name === "assetCatalog") {
|
||||
const operation = params.operation === undefined ? "LIST" : params.operation;
|
||||
if (!["LIST", "UPSERT", "REMOVE"].includes(operation as string)) throw new AssetLibraryValidationError("ASSET_MANIFEST_INVALID", "catalog operation is invalid");
|
||||
params.operation = operation;
|
||||
if (operation !== "LIST") {
|
||||
params.catalogId = boundedId(params.catalogId, "catalogId");
|
||||
if (operation === "UPSERT") {
|
||||
params.name = text(params.name, "name", 256);
|
||||
params.parentId = params.parentId === undefined || params.parentId === null ? null : boundedId(params.parentId, "parentId");
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
params.libraryId = boundedId(params.libraryId, "libraryId");
|
||||
if (name === "libraryLink" || params.sourcePath !== undefined) params.sourcePath = projectPath(params.sourcePath, "sourcePath", "IO_EXTERNAL_URI_BLOCKED");
|
||||
if (name === "libraryLink" || name === "libraryOverride") params.dataId = boundedId(params.dataId, "dataId");
|
||||
}
|
||||
return { command: name, params };
|
||||
}
|
||||
|
||||
export function assetStorageCapabilities(scope: typeof globalThis = globalThis): { contentAddressedIndex: "LOCAL_BOUNDED"; opfs: "PROBE_REQUIRED" | "UNAVAILABLE" } {
|
||||
const storage = (scope.navigator as Navigator & { storage?: { getDirectory?: unknown } } | undefined)?.storage;
|
||||
return { contentAddressedIndex: "LOCAL_BOUNDED", opfs: storage && typeof storage.getDirectory === "function" ? "PROBE_REQUIRED" : "UNAVAILABLE" };
|
||||
|
||||
@@ -5,4 +5,7 @@ export const compositorCommands = Object.freeze({
|
||||
evaluate(graph: CompositorGraphIR): CompositorGraphIR {
|
||||
return structuredClone(graph);
|
||||
},
|
||||
setNodeGraph(sceneId: string, graph: CompositorGraphIR) {
|
||||
return { type: "setCompositorNodeGraph", sceneId, graph } as const;
|
||||
},
|
||||
} as const);
|
||||
|
||||
@@ -17,6 +17,24 @@ export const meshAuthoringCommands = Object.freeze({
|
||||
...(options.segments === undefined ? {} : { segments: options.segments }),
|
||||
};
|
||||
},
|
||||
meshSubdivide(meshId: string, selectionMode: MeshElementMode, elementIndices: readonly number[], segments = 1): MeshCommand<"meshSubdivide"> {
|
||||
return { type: "meshSubdivide", meshId, selectionMode, elementIndices: [...elementIndices], segments: Math.max(1, Math.min(64, Math.trunc(segments))) };
|
||||
},
|
||||
meshTriangulate(meshId: string, faceIndices: readonly number[]): MeshCommand<"meshTriangulate"> {
|
||||
return { type: "meshTriangulate", meshId, faceIndices: [...faceIndices] };
|
||||
},
|
||||
meshRecalculateNormals(meshId: string, faceIndices: readonly number[]): MeshCommand<"meshRecalculateNormals"> {
|
||||
return { type: "meshRecalculateNormals", meshId, faceIndices: [...faceIndices] };
|
||||
},
|
||||
meshBridgeLoops(meshId: string, edgeIndices: readonly number[]): MeshCommand<"meshBridgeLoops"> {
|
||||
return { type: "meshBridgeLoops", meshId, edgeIndices: [...edgeIndices] };
|
||||
},
|
||||
meshDeleteElements(meshId: string, selectionMode: MeshElementMode, elementIndices: readonly number[]): MeshCommand<"meshDeleteElements"> {
|
||||
return { type: "meshDeleteElements", meshId, selectionMode, elementIndices: [...elementIndices] };
|
||||
},
|
||||
meshUVProject(meshId: string, faceIndices: readonly number[], method: "PLANAR" | "CUBE" | "CYLINDER" | "SPHERE" = "PLANAR", seamEdgeIndices: readonly number[] = []): MeshCommand<"meshUVProject"> {
|
||||
return { type: "meshUVProject", meshId, faceIndices: [...faceIndices], method, ...(seamEdgeIndices.length ? { seamEdgeIndices: [...seamEdgeIndices] } : {}) };
|
||||
},
|
||||
decimate(meshId: string, profile: SimplifyProfile): MeshCommand<"decimateMesh"> {
|
||||
return { type: "decimateMesh", meshId, profile };
|
||||
},
|
||||
|
||||
@@ -13,6 +13,15 @@ export const nonMeshGeometryCommands = Object.freeze({
|
||||
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 }) };
|
||||
},
|
||||
setCurveBevel(dataId: string, bevelDepth?: number, bevelResolution?: number, bevelFactorStart?: number, bevelFactorEnd?: number): NonMeshCommand<"setCurveBevel"> {
|
||||
return { type: "setCurveBevel", dataId, ...(bevelDepth === undefined ? {} : { bevelDepth }), ...(bevelResolution === undefined ? {} : { bevelResolution }), ...(bevelFactorStart === undefined ? {} : { bevelFactorStart }), ...(bevelFactorEnd === undefined ? {} : { bevelFactorEnd }) };
|
||||
},
|
||||
setCurveTaper(dataId: string, taperObjectId: string | null, taperRadiusMode?: number): NonMeshCommand<"setCurveTaper"> {
|
||||
return { type: "setCurveTaper", dataId, taperObjectId, ...(taperRadiusMode === undefined ? {} : { taperRadiusMode }) };
|
||||
},
|
||||
convertCurveType(dataId: string, targetType: "CURVE" | "SURFACE"): NonMeshCommand<"convertCurveType"> {
|
||||
return { type: "convertCurveType", dataId, targetType };
|
||||
},
|
||||
setCurveHandle(dataId: string, pointIndex: number, side: "LEFT" | "RIGHT", position: readonly [number, number, number]): NonMeshCommand<"setCurveHandle"> {
|
||||
return { type: "setCurveHandle", dataId, pointIndex, side, position: [...position] as [number, number, number] };
|
||||
},
|
||||
@@ -58,6 +67,9 @@ export const nonMeshGeometryCommands = Object.freeze({
|
||||
cyclicV: [...cyclicV],
|
||||
};
|
||||
},
|
||||
setSurfaceResolution(dataId: string, resolutionU: number, resolutionV?: number): NonMeshCommand<"setSurfaceResolution"> {
|
||||
return { type: "setSurfaceResolution", dataId, resolutionU, ...(resolutionV === undefined ? {} : { resolutionV }) };
|
||||
},
|
||||
setFontBody(dataId: string, body: string): NonMeshCommand<"setFontBody"> {
|
||||
return { type: "setFontBody", dataId, body };
|
||||
},
|
||||
|
||||
@@ -28,6 +28,72 @@ export type PhysicsFamily = typeof PHYSICS_FAMILIES[number];
|
||||
export type PhysicsExecutionRequest = "METADATA" | "CACHE_MANIFEST" | "CACHE_PLAYBACK" | "LOCAL_SOLVER" | "SERVER_JOB";
|
||||
export type PhysicsSettingValue = boolean | number | string | null;
|
||||
|
||||
/** Typed Main-authoring payloads for the physics families exposed by WebEngine. */
|
||||
export interface PhysicsRigidBodySettings {
|
||||
type?: number;
|
||||
mass?: number;
|
||||
friction?: number;
|
||||
restitution?: number;
|
||||
collisionShape?: number;
|
||||
kinematic?: boolean;
|
||||
enabled?: boolean;
|
||||
linearDamping?: number;
|
||||
angularDamping?: number;
|
||||
stepsPerSecond?: number;
|
||||
solverIterations?: number;
|
||||
}
|
||||
|
||||
export interface PhysicsClothSettings {
|
||||
mass?: number;
|
||||
airDamping?: number;
|
||||
tensionStiffness?: number;
|
||||
compressionStiffness?: number;
|
||||
shearStiffness?: number;
|
||||
bendingStiffness?: number;
|
||||
springDamping?: number;
|
||||
pinStiffness?: number;
|
||||
quality?: number;
|
||||
usePin?: boolean;
|
||||
}
|
||||
|
||||
export interface PhysicsFluidSettings {
|
||||
fluidType?: number;
|
||||
domainType?: number;
|
||||
resolution?: number;
|
||||
timeScale?: number;
|
||||
viscosity?: number;
|
||||
gravity?: number;
|
||||
cacheFrameStart?: number;
|
||||
cacheFrameEnd?: number;
|
||||
cacheType?: number;
|
||||
}
|
||||
|
||||
export interface PhysicsSoftBodySettings {
|
||||
nodeMass?: number;
|
||||
gravity?: number;
|
||||
mediaFriction?: number;
|
||||
physicsSpeed?: number;
|
||||
goalSpring?: number;
|
||||
goalFriction?: number;
|
||||
solverFlags?: number;
|
||||
}
|
||||
|
||||
export interface PhysicsParticleCacheSettings {
|
||||
frameStart?: number;
|
||||
frameEnd?: number;
|
||||
cacheType?: number;
|
||||
compression?: number;
|
||||
useDiskCache?: boolean;
|
||||
pointCacheId?: string;
|
||||
}
|
||||
|
||||
export interface PhysicsSimulationPlaybackSettings {
|
||||
frame?: number;
|
||||
playing?: boolean;
|
||||
direction?: -1 | 1;
|
||||
reset?: boolean;
|
||||
}
|
||||
|
||||
export interface PhysicsCacheFrameIR {
|
||||
frame: number;
|
||||
byteOffset: number;
|
||||
|
||||
@@ -3,19 +3,22 @@ import type { ShaderNodeType } from "./shader-graph";
|
||||
|
||||
export type RenderBackend = "WEBGL2" | "WEBGPU";
|
||||
export type PostProcessPass = "FXAA" | "BLOOM" | "SSAO" | "SSR" | "TAA" | "DOF" | "MOTION_BLUR";
|
||||
export type RenderPassName = "COMBINED" | "Z" | "MIST" | "NORMAL" | "VECTOR" | "UV" | "OBJECT_INDEX" | "MATERIAL_INDEX" | "SHADOW" | "AO" | "EMISSION" | "ENVIRONMENT" | "DIFFUSE_DIRECT" | "DIFFUSE_INDIRECT" | "GLOSSY_DIRECT" | "GLOSSY_INDIRECT" | "TRANSMISSION_DIRECT" | "TRANSMISSION_INDIRECT" | "VOLUME_DIRECT" | "VOLUME_INDIRECT" | "CRYPTOMATTE_OBJECT" | "CRYPTOMATTE_MATERIAL" | "CRYPTOMATTE_ASSET";
|
||||
|
||||
export type RenderCapabilityRequest =
|
||||
| { kind: "ARBITRARY_SHADER"; nodeTypes: string[] }
|
||||
| { kind: "VOLUME" }
|
||||
| { kind: "SUBSURFACE" }
|
||||
| { kind: "WEBGPU_BACKEND" }
|
||||
| { kind: "POSTPROCESS"; passes: PostProcessPass[] };
|
||||
| { kind: "POSTPROCESS"; passes: PostProcessPass[] }
|
||||
| { kind: "RENDER_PASSES"; passes: RenderPassName[] };
|
||||
|
||||
export interface RenderCapabilityContext {
|
||||
webgpuAvailable?: boolean;
|
||||
webgpuRendererBundled?: boolean;
|
||||
supportedShaderNodes?: ReadonlySet<ShaderNodeType>;
|
||||
supportedPostProcessPasses?: ReadonlySet<PostProcessPass>;
|
||||
supportedRenderPasses?: ReadonlySet<RenderPassName>;
|
||||
volumeRendererAvailable?: boolean;
|
||||
subsurfaceRendererAvailable?: boolean;
|
||||
}
|
||||
@@ -57,6 +60,13 @@ export function gateRenderCapability(
|
||||
const reason = context.webgpuAvailable ? "The WebGPU renderer bundle is not installed" : "WebGPU is unavailable in this browser or device";
|
||||
return blockedGate("PBR-012", "WEBGPU_BACKEND", [capabilityIssue("WEBGPU_RENDERER_UNAVAILABLE", reason)]);
|
||||
}
|
||||
if (request.kind === "RENDER_PASSES") {
|
||||
const supported = context.supportedRenderPasses ?? new Set<RenderPassName>(["COMBINED", "Z", "MIST", "NORMAL", "VECTOR", "UV", "OBJECT_INDEX", "MATERIAL_INDEX", "SHADOW", "AO", "EMISSION", "ENVIRONMENT", "DIFFUSE_DIRECT", "DIFFUSE_INDIRECT", "GLOSSY_DIRECT", "GLOSSY_INDIRECT", "TRANSMISSION_DIRECT", "TRANSMISSION_INDIRECT", "VOLUME_DIRECT", "VOLUME_INDIRECT", "CRYPTOMATTE_OBJECT", "CRYPTOMATTE_MATERIAL", "CRYPTOMATTE_ASSET"]);
|
||||
const unsupported = [...new Set(request.passes.filter((pass) => !supported.has(pass)))];
|
||||
return unsupported.length === 0
|
||||
? readyGate("PBR-012", "RENDER_PASSES")
|
||||
: blockedGate("PBR-012", "RENDER_PASSES", [capabilityIssue("RENDER_PASS_UNAVAILABLE", `Unsupported render passes: ${unsupported.join(", ")}`, "passes")]);
|
||||
}
|
||||
const supported = context.supportedPostProcessPasses ?? new Set<PostProcessPass>();
|
||||
const unsupported = [...new Set(request.passes.filter((pass) => !supported.has(pass)))];
|
||||
return unsupported.length === 0
|
||||
|
||||
@@ -41,6 +41,17 @@ export interface RenderRoutingResultIR {
|
||||
issues: Array<{ code: ErrorCode; message: string; recoverable: boolean }>;
|
||||
}
|
||||
|
||||
export type RenderCommandKind = "renderFrame" | "renderAnimation";
|
||||
|
||||
export interface RenderCommandRequestIR {
|
||||
kind: RenderCommandKind;
|
||||
routing: RenderRoutingRequestIR;
|
||||
frame?: number;
|
||||
frameStart?: number;
|
||||
frameEnd?: number;
|
||||
step?: number;
|
||||
}
|
||||
|
||||
function blocked(
|
||||
target: RenderRoutingTarget,
|
||||
capability: RenderRoutingResultIR["capability"],
|
||||
@@ -103,3 +114,19 @@ export function routeRenderExecution(request: RenderRoutingRequestIR, context: R
|
||||
}
|
||||
return blocked("SERVER_JOB", "UNSUPPORTED_RENDER_ENGINE", "UNSUPPORTED_ENGINE", "PLATFORM_CAPABILITY_UNAVAILABLE", "The requested render engine has no declared Web or server route");
|
||||
}
|
||||
|
||||
/** Routes a user-visible frame or animation render through the declared native/server capability. */
|
||||
export function routeRenderCommand(request: RenderCommandRequestIR, context: RenderRoutingContextIR = {}): RenderRoutingResultIR {
|
||||
if (request.kind === "renderFrame" && request.frame !== undefined && (!Number.isSafeInteger(request.frame) || request.frame < -1000000 || request.frame > 1000000)) {
|
||||
throw new Error("INVALID_ARGUMENT: render frame is outside the bounded range");
|
||||
}
|
||||
if (request.kind === "renderAnimation") {
|
||||
if (!Number.isSafeInteger(request.frameStart) || !Number.isSafeInteger(request.frameEnd) || request.frameStart! > request.frameEnd!) {
|
||||
throw new Error("INVALID_ARGUMENT: render animation range is invalid");
|
||||
}
|
||||
if (request.step !== undefined && (!Number.isSafeInteger(request.step) || request.step < 1 || request.step > 1000)) {
|
||||
throw new Error("INVALID_ARGUMENT: render animation step is invalid");
|
||||
}
|
||||
}
|
||||
return routeRenderExecution(request.routing, context);
|
||||
}
|
||||
|
||||
@@ -139,6 +139,14 @@ export interface ArmatureBoneIR {
|
||||
connected?: boolean;
|
||||
restMatrix: number[];
|
||||
poseMatrix?: number[];
|
||||
poseConstraints?: Array<{
|
||||
name: string;
|
||||
type: "COPY_LOCATION" | "COPY_ROTATION" | "COPY_SCALE" | "IK" | "TRACK_TO" | "UNKNOWN";
|
||||
targetObjectId?: string | null;
|
||||
targetBoneName?: string;
|
||||
influence: number;
|
||||
enabled: boolean;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface ArmatureIR {
|
||||
@@ -661,6 +669,21 @@ export interface SceneIR {
|
||||
sequencerStatus?: "AVAILABLE" | "BLOCKED";
|
||||
}
|
||||
|
||||
export interface SceneRNADataBlockIR {
|
||||
type: string;
|
||||
name: string;
|
||||
libraryLinked: boolean;
|
||||
editable: boolean;
|
||||
}
|
||||
|
||||
export interface SceneRNAPropertyIR {
|
||||
identifier: string;
|
||||
type: "BOOLEAN" | "INT" | "FLOAT" | "STRING" | "ENUM";
|
||||
arrayLength: number;
|
||||
readonly: boolean;
|
||||
value?: boolean | number | string | null | Array<boolean | number | string | null>;
|
||||
}
|
||||
|
||||
export interface SceneSnapshotIR {
|
||||
schemaVersion: 1;
|
||||
revision: number;
|
||||
@@ -1375,6 +1398,18 @@ export function parseSceneSnapshotIR(value: unknown): SceneSnapshotIR {
|
||||
if (bone.connected !== undefined) requireBoolean(bone.connected, `armatures[${index}].bones[${boneIndex}].connected`);
|
||||
requireTuple(bone.restMatrix, 16, `armatures[${index}].bones[${boneIndex}].restMatrix`);
|
||||
if (bone.poseMatrix !== undefined) requireTuple(bone.poseMatrix, 16, `armatures[${index}].bones[${boneIndex}].poseMatrix`);
|
||||
if (bone.poseConstraints !== undefined) {
|
||||
const constraints = requireArray(bone.poseConstraints, `armatures[${index}].bones[${boneIndex}].poseConstraints`);
|
||||
for (const [constraintIndex, constraint] of constraints.entries()) {
|
||||
if (!isRecord(constraint)) throw new Error(`armatures[${index}].bones[${boneIndex}].poseConstraints[${constraintIndex}] must be an object`);
|
||||
requireString(constraint.name, `armatures[${index}].bones[${boneIndex}].poseConstraints[${constraintIndex}].name`);
|
||||
requireString(constraint.type, `armatures[${index}].bones[${boneIndex}].poseConstraints[${constraintIndex}].type`);
|
||||
if (constraint.targetObjectId !== undefined && constraint.targetObjectId !== null) requireString(constraint.targetObjectId, `armatures[${index}].bones[${boneIndex}].poseConstraints[${constraintIndex}].targetObjectId`);
|
||||
if (constraint.targetBoneName !== undefined) requireString(constraint.targetBoneName, `armatures[${index}].bones[${boneIndex}].poseConstraints[${constraintIndex}].targetBoneName`);
|
||||
requireNumber(constraint.influence, `armatures[${index}].bones[${boneIndex}].poseConstraints[${constraintIndex}].influence`);
|
||||
requireBoolean(constraint.enabled, `armatures[${index}].bones[${boneIndex}].poseConstraints[${constraintIndex}].enabled`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const [index, image] of (value.images as unknown[]).entries()) {
|
||||
|
||||
@@ -424,3 +424,16 @@ export function gateSequencerCodec(requestValue: unknown, resultValue: unknown):
|
||||
return blockedGate("N-021", capability, [capabilityIssue("SEQUENCER_CODEC_UNSUPPORTED", error instanceof Error ? error.message : "Codec probe is invalid")]);
|
||||
}
|
||||
}
|
||||
|
||||
/** Main-backed authoring commands for strip effects, transitions and color balance. */
|
||||
export const sequencerCommands = Object.freeze({
|
||||
setStripModifiers(sceneId: string, stripId: string, modifiers: Array<{ id: string; type: string; enabled?: boolean; properties?: Record<string, unknown> }>) {
|
||||
return { type: "setSequencerStripModifiers", sceneId, stripId, modifiers } as const;
|
||||
},
|
||||
setTransitions(sceneId: string, timeline: SequencerTimelineIR) {
|
||||
return { type: "setSequencerTransitions", sceneId, timeline } as const;
|
||||
},
|
||||
setColorBalance(sceneId: string, stripId: string, colorBalance: { lift: [number, number, number]; gamma: [number, number, number]; gain: [number, number, number]; exposure?: number; contrast?: number; saturation?: number }) {
|
||||
return { type: "setSequencerColorBalance", sceneId, stripId, colorBalance } as const;
|
||||
},
|
||||
} as const);
|
||||
|
||||
@@ -9,11 +9,20 @@ 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 {
|
||||
PhysicsClothSettings,
|
||||
PhysicsFluidSettings,
|
||||
PhysicsParticleCacheSettings,
|
||||
PhysicsRigidBodySettings,
|
||||
PhysicsSimulationManifestIR,
|
||||
PhysicsSimulationPlaybackSettings,
|
||||
PhysicsSoftBodySettings,
|
||||
} from "./physics-simulation";
|
||||
import type { ShaderGraphIR } from "./shader-graph";
|
||||
import type { ShaderCompileReport } from "./shader-compiler";
|
||||
import type { NlaMoveStripCommand, NlaTrackIR } from "./nla";
|
||||
import type { RenderCapabilityRequest } from "./render-capabilities";
|
||||
import type { RenderRoutingRequestIR } from "./render-routing";
|
||||
import type { CapabilityGateResult } from "./capability-gates";
|
||||
import type { NonMeshGeometryChunk } from "./nonmesh-binary";
|
||||
import type {
|
||||
@@ -27,6 +36,7 @@ import type { PaintPBVHCapabilityRequest } from "./paint-pbvh-capability";
|
||||
import type { LibraryMainAppendReceiptIR, LibraryMainAppendRequestIR } from "./library-main-append";
|
||||
import type { BlenderOperatorDescriptor, BlenderOperatorPropertySpec, BlenderOperatorInvocation, BlenderOperatorPropertyValue } from "./blender-operator";
|
||||
import type { SequencerTimelineIR } from "./sequencer";
|
||||
import type { CompositorGraphIR } from "./compositor";
|
||||
|
||||
export interface MeshGeometryBuffer {
|
||||
schemaVersion: 1;
|
||||
@@ -81,6 +91,28 @@ export type MeshPrimitive = "CUBE" | "PLANE" | "UV_SPHERE" | "ICO_SPHERE" | "CYL
|
||||
export type MeshEditOperation = "MERGE" | "DISSOLVE" | "EXTRUDE" | "INSET" | "BEVEL" | "LOOP_CUT" | "SUBDIVIDE" | "FILL" | "TRIANGULATE" | "RECALCULATE_NORMALS" | "BRIDGE_LOOPS" | "DELETE";
|
||||
export type FCurveInterpolation = "CONSTANT" | "LINEAR" | "BEZIER";
|
||||
export type KeyframeHandle = [number, number];
|
||||
export interface FCurvePoint {
|
||||
frame: number;
|
||||
value: number;
|
||||
handleLeft?: KeyframeHandle;
|
||||
handleRight?: KeyframeHandle;
|
||||
interpolation?: FCurveInterpolation;
|
||||
}
|
||||
export interface ActionGroupDefinition {
|
||||
name: string;
|
||||
paths: string[];
|
||||
}
|
||||
export interface KeyingSetPathDefinition {
|
||||
path: string;
|
||||
arrayIndex?: number;
|
||||
}
|
||||
export type ArmatureEditOperation =
|
||||
| { operation: "ADD"; name: string; parentName?: string | null; head: [number, number, number]; tail: [number, number, number]; roll?: number; connected?: boolean }
|
||||
| { operation: "DELETE"; boneName: string }
|
||||
| { operation: "RENAME"; boneName: string; name: string }
|
||||
| { operation: "EXTRUDE"; boneName: string; name: string; tail: [number, number, number] }
|
||||
| { operation: "DUPLICATE"; boneName: string; name: string; offset?: [number, number, number] };
|
||||
export type PoseConstraintType = "COPY_LOCATION" | "COPY_ROTATION" | "COPY_SCALE" | "IK" | "TRACK_TO";
|
||||
|
||||
export type WebEngineEditCommand =
|
||||
| { type: "setFrame"; frame: number }
|
||||
@@ -99,8 +131,19 @@ export type WebEngineEditCommand =
|
||||
| { type: "setObjectTransform"; objectId: string; translation: [number, number, number]; rotationEuler: [number, number, number]; scale: [number, number, number] }
|
||||
| { type: "setArmatureBones"; objectId: string; bones: Array<{ name: string; parentName: string | null; head: [number, number, number]; tail: [number, number, number]; roll: number; connected?: boolean }> }
|
||||
| { type: "setPoseBoneTransform"; objectId: string; boneName: string; translation: [number, number, number]; rotationEuler: [number, number, number]; scale: [number, number, number] }
|
||||
| { type: "armatureEditOperations"; objectId: string; operations: ArmatureEditOperation[] }
|
||||
| { type: "setBoneRoll"; objectId: string; boneName: string; roll: number }
|
||||
| { type: "setBoneSymmetry"; objectId: string; enabled: boolean }
|
||||
| { type: "setBoneParenting"; objectId: string; boneName: string; parentName: string | null; connected: boolean }
|
||||
| { type: "setPoseConstraint"; objectId: string; boneName: string; constraintName: string; constraintType: PoseConstraintType | null; targetObjectId?: string | null; targetBoneName?: string; influence?: number; enabled?: boolean }
|
||||
| { type: "translateMeshVertices"; meshId: string; vertexIndices: number[]; offset: [number, number, number] }
|
||||
| { type: "meshEdit"; meshId: string; operation: MeshEditOperation; selectionMode: MeshElementMode; elementIndices: number[]; offset?: [number, number, number]; amount?: number; segments?: number }
|
||||
| { type: "meshSubdivide"; meshId: string; selectionMode: MeshElementMode; elementIndices: number[]; segments?: number }
|
||||
| { type: "meshTriangulate"; meshId: string; faceIndices: number[] }
|
||||
| { type: "meshRecalculateNormals"; meshId: string; faceIndices: number[] }
|
||||
| { type: "meshBridgeLoops"; meshId: string; edgeIndices: number[] }
|
||||
| { type: "meshDeleteElements"; meshId: string; selectionMode: MeshElementMode; elementIndices: number[] }
|
||||
| { type: "meshUVProject"; meshId: string; faceIndices: number[]; method: "PLANAR" | "CUBE" | "CYLINDER" | "SPHERE"; seamEdgeIndices?: number[] }
|
||||
| { type: "createUVMap"; meshId: string; name: string }
|
||||
| { type: "setActiveUVMap"; meshId: string; name: string }
|
||||
| { type: "unwrapUV"; meshId: string; faceIndices: number[]; method: "PLANAR" | "CUBE" }
|
||||
@@ -115,6 +158,11 @@ export type WebEngineEditCommand =
|
||||
| { type: "createAction"; name: string; objectId?: string }
|
||||
| { type: "removeAction"; actionId: string }
|
||||
| { type: "setKeyframe"; animationId: string; path: string; frame: number; value?: number[]; handleLeft?: KeyframeHandle; handleRight?: KeyframeHandle; interpolation?: FCurveInterpolation }
|
||||
| { type: "setFCurveKeyframes"; animationId: string; path: string; points: FCurvePoint[] }
|
||||
| { type: "setDriverExpression"; animationId: string; path: string; expression: string }
|
||||
| { type: "setActionGroups"; animationId: string; groups: ActionGroupDefinition[] }
|
||||
| { type: "setKeyingSet"; sceneId: string; name: string; paths: KeyingSetPathDefinition[]; active?: boolean }
|
||||
| { type: "bakeAnimation"; objectId: string; frameStart: number; frameEnd: number; step?: number }
|
||||
| { type: "setTimelineMarkers"; dataId: string; markers: Array<{ frame: number; name: string }> }
|
||||
| { type: "setFCurveInterpolation"; animationId: string; path: string; interpolation: FCurveInterpolation }
|
||||
| { type: "setActiveAction"; objectId: string; actionId: string | null }
|
||||
@@ -130,10 +178,14 @@ export type WebEngineEditCommand =
|
||||
| { type: "createCurve"; curveType: "CURVE"; name?: string; controlPoints: number[]; cyclic?: boolean; resolution?: number }
|
||||
| { type: "deleteNonMeshData"; dataId: string }
|
||||
| { type: "setCurveControlPoints"; dataId: string; controlPoints: number[]; splineOffsets?: number[]; resolution?: number }
|
||||
| { type: "setCurveBevel"; dataId: string; bevelDepth?: number; bevelResolution?: number; bevelFactorStart?: number; bevelFactorEnd?: number }
|
||||
| { type: "setCurveTaper"; dataId: string; taperObjectId: string | null; taperRadiusMode?: number }
|
||||
| { type: "convertCurveType"; dataId: string; targetType: "CURVE" | "SURFACE" }
|
||||
| { type: "setCurveHandle"; dataId: string; pointIndex: number; side: "LEFT" | "RIGHT"; position: [number, number, number] }
|
||||
| { type: "setCurveTopology"; dataId: string; baseRevision?: number; splineTypes?: NonMeshCurveSplineType[]; cyclicU?: boolean[]; cyclicV?: boolean[]; handleTypes?: number[]; handlePoints?: number[] }
|
||||
| { type: "setCurveSplines"; dataId: string; splineTypes: NonMeshCurveSplineType[]; splineOffsets: number[]; ordersU: number[]; controlPoints: number[]; pointWeights: number[]; cyclicU: boolean[]; handleTypes: number[]; handlePoints: number[] }
|
||||
| { type: "setSurfaceTopology"; dataId: string; splineDimensions: Array<{ u: number; v: number; orderU: number; orderV: number }>; controlPoints: number[]; pointWeights: number[]; cyclicU: boolean[]; cyclicV: boolean[] }
|
||||
| { type: "setSurfaceResolution"; dataId: string; resolutionU: number; resolutionV?: number }
|
||||
| { type: "setFontBody"; dataId: string; body: string }
|
||||
| { type: "setFontProperties"; dataId: string; properties: Partial<NonMeshFontPropertiesIR> }
|
||||
| { type: "setFontAdvanced"; dataId: string; characters: NonMeshFontCharacterIR[]; textBoxes: NonMeshFontTextBoxIR[]; activeTextBox: number }
|
||||
@@ -152,6 +204,10 @@ export type WebEngineEditCommand =
|
||||
| { type: "insertGreasePencilFrame"; dataId: string; layerId: string; frame: number; duration?: number }
|
||||
| { type: "removeGreasePencilFrame"; dataId: string; layerId: string; frame: number }
|
||||
| { type: "setGreasePencilStrokes"; dataId: string; layerId: string; frame: number; baseRevision?: number; strokes: Array<{ cyclic?: boolean; materialIndex?: number; points: Array<{ position: [number, number, number]; radius?: number; opacity?: number; vertexColor?: [number, number, number, number] }> }> }
|
||||
| { type: "setGreasePencilDrawing"; dataId: string; layerId: string; frame: number; drawingId?: string; baseRevision?: number; strokes: Array<{ cyclic?: boolean; materialIndex?: number; points: Array<{ position: [number, number, number]; radius?: number; opacity?: number; vertexColor?: [number, number, number, number] }> }> }
|
||||
| { type: "setGreasePencilMaterial"; dataId: string; layerId: string; frame: number; materialIndex: number; strokeIndices?: number[]; baseRevision?: number }
|
||||
| { type: "setGreasePencilSelection"; dataId: string; layerId: string; frame: number; pointIndices?: number[]; strokeIndices?: number[]; mode?: "REPLACE" | "ADD" | "SUBTRACT" | "TOGGLE"; baseRevision?: number }
|
||||
| { type: "setGreasePencilInterpolate"; dataId: string; layerId: string; sourceFrame: number; targetFrame: number; factor: number; baseRevision?: number }
|
||||
| { type: "setVertexColors"; meshId: string; attributeName: string; domain: "POINT" | "CORNER"; indices: number[]; colors: number[] }
|
||||
| { type: "setVertexWeights"; objectId: string; vertexGroup: string; indices: number[]; values: number[]; normalize?: boolean; limit?: number; mirror?: boolean; mirrorAxis?: 0 | 1 | 2; mirrorTolerance?: number }
|
||||
| { type: "setLightProperties"; dataId: string; properties: { type?: "POINT" | "SUN" | "SPOT" | "AREA"; areaShape?: 0 | 1 | 4 | 5; diffuseFactor?: number; specularFactor?: number; transmissionFactor?: number; volumeFactor?: number; attenuationDistance?: number; shadowFilterRadius?: number; shadowMaximumResolution?: number; shadowJitterOverblur?: number; cascadeMaxDistance?: number; cascadeExponent?: number; cascadeFade?: number; cascadeCount?: number; areaSizeZ?: number; clipStart?: number; color?: [number, number, number]; energy?: number; exposure?: number; temperature?: number; useTemperature?: boolean; castsShadow?: boolean; radius?: number; spotAngle?: number; spotBlend?: number; areaSize?: number; areaSizeY?: number; areaSpread?: number; sunAngle?: number } }
|
||||
@@ -163,7 +219,16 @@ export type WebEngineEditCommand =
|
||||
| { type: "setSceneUnitSettings"; dataId: string; properties: { system?: number; scaleLength?: number; rotationSystem?: number; lengthUnit?: number; massUnit?: number; timeUnit?: number; temperatureUnit?: number } }
|
||||
| { type: "setSceneTimelineSettings"; dataId: string; properties: { fps?: number; fpsBase?: number; frameStep?: number; usePreviewRange?: boolean; previewStart?: number; previewEnd?: number } }
|
||||
| { type: "setScenePipelineSettings"; dataId: string; properties: { useCompositor?: boolean; useSequencer?: boolean; noOverwrite?: boolean; saveOutput?: boolean; alphaPremultiplied?: boolean } }
|
||||
| { type: "renderFrame"; dataId: string; frame?: number; route?: RenderRoutingRequestIR }
|
||||
| { type: "renderAnimation"; dataId: string; frameStart?: number; frameEnd?: number; step?: number; route?: RenderRoutingRequestIR }
|
||||
| { type: "setRenderPasses"; dataId: string; layerName: string; passes: { passMask?: number; cryptomatteMask?: number; cryptomatteLevels?: number; alphaThreshold?: number } }
|
||||
| { type: "setCameraView"; dataId: string; cameraObjectId: string | null }
|
||||
| { type: "setLightLinking"; dataId: string; target?: "OBJECT" | "WORLD"; lightgroup: string | null }
|
||||
| { type: "setSequencerStrips"; sceneId: string; timeline: SequencerTimelineIR }
|
||||
| { type: "setSequencerStripModifiers"; sceneId: string; stripId: string; modifiers: Array<{ id: string; type: string; enabled?: boolean; properties?: Record<string, unknown> }> }
|
||||
| { type: "setSequencerTransitions"; sceneId: string; timeline: SequencerTimelineIR }
|
||||
| { type: "setSequencerColorBalance"; sceneId: string; stripId: string; colorBalance: { lift: [number, number, number]; gamma: [number, number, number]; gain: [number, number, number]; exposure?: number; contrast?: number; saturation?: number } }
|
||||
| { type: "setCompositorNodeGraph"; sceneId: string; graph: CompositorGraphIR }
|
||||
| { type: "setSceneBindings"; dataId: string; properties: { cameraObjectId?: string | null; worldId?: string | null } }
|
||||
| { type: "setSceneViewLayerSettings"; dataId: string; layerName: string; properties: { useRender?: boolean; useFreestyle?: boolean; samples?: number } }
|
||||
| { type: "setSceneCollectionSettings"; dataId: string; layerName: string; collectionId: string; properties: { exclude?: boolean; holdout?: boolean; indirectOnly?: boolean; hide?: boolean } }
|
||||
@@ -178,10 +243,18 @@ export type WebEngineEditCommand =
|
||||
| { type: "setSceneSamplingSettings"; dataId: string; properties: { pixelAspectX?: number; pixelAspectY?: number; filterSize?: number; dither?: number } }
|
||||
| { type: "setSceneMultiviewSettings"; dataId: string; properties: { enabled?: boolean; viewsFormat?: 0 | 1; imageViewsFormat?: 0 | 1 | 2; stereoDisplayMode?: 0 | 1 | 2 | 3 | 4; interlaceSwap?: boolean; sideBySideCrossed?: boolean; squeezedFrame?: boolean } }
|
||||
| { type: "setSceneStampSettings"; dataId: string; properties: { stampMask?: number; fontId?: number; userText?: string; foreground?: [number, number, number, number]; background?: [number, number, number, number] } }
|
||||
| { type: "setTrackingMarkers"; clipId: string; trackId: string; markers: TrackingMarkerIR[]; sourceSha256?: string }
|
||||
| { type: "setTrackingSolve"; clipId: string; solve: CameraSolveIR; sourceSha256?: string }
|
||||
| { type: "setMaskSpline"; maskId: string; layerId: string; spline: MaskSplineIR }
|
||||
| { type: "setMaskPoint"; maskId: string; layerId: string; splineId: string; point: MaskPointIR }
|
||||
| { type: "setSceneLineSettings"; dataId: string; properties: { mode?: 1 | 2; thickness?: number } }
|
||||
| { type: "setSceneViewLayerPassSettings"; dataId: string; layerName: string; properties: { passMask?: number; cryptomatteMask?: number; cryptomatteLevels?: number; alphaThreshold?: number } }
|
||||
| { type: "setSceneViewLayerAOVs"; dataId: string; layerName: string; aovs: Array<{ name: string; type: 0 | 1 }> }
|
||||
| { type: "setSceneViewLayerLightgroups"; dataId: string; layerName: string; lightgroups: string[] }
|
||||
| { type: "clearObjectTransform"; objectId: string; channels: Array<"LOCATION" | "ROTATION" | "SCALE"> }
|
||||
| { type: "createObjectData"; objectType: "CAMERA" | "LIGHT" | "EMPTY" | "ARMATURE"; name?: string }
|
||||
| { type: "setObjectSelection"; objectId: string; selected: boolean; active?: boolean }
|
||||
| { type: "setCollectionVisibility"; collectionId: string; viewportVisible?: boolean; renderVisible?: boolean }
|
||||
| { type: "setObjectLightgroup"; objectId: string; lightgroup: string | null }
|
||||
| { type: "setWorldLightgroup"; dataId: string; lightgroup: string | null }
|
||||
| { type: "setMetaballElements"; dataId: string; elements: Array<{ type: number; position: [number, number, number]; radius: number; scale: [number, number, number] }> }
|
||||
@@ -189,6 +262,11 @@ export type WebEngineEditCommand =
|
||||
| { type: "setSculptMeshAttributes"; attributes: SculptMeshAttributesIR }
|
||||
| { type: "setGeometryNodeGraph"; meshId: string; graph: GeometryNodeGraphIR }
|
||||
| { type: "setPhysicsSimulation"; manifest: PhysicsSimulationManifestIR }
|
||||
| { type: "setRigidBodySettings"; objectId: string; family?: "RIGID_BODY" | "SOFT_BODY"; settings: PhysicsRigidBodySettings | PhysicsSoftBodySettings }
|
||||
| { type: "setClothSettings"; objectId: string; settings: PhysicsClothSettings }
|
||||
| { type: "setFluidSettings"; objectId: string; settings: PhysicsFluidSettings }
|
||||
| { type: "setParticleCache"; objectId: string; settings: PhysicsParticleCacheSettings }
|
||||
| { type: "setSimulationPlayback"; settings: PhysicsSimulationPlaybackSettings }
|
||||
| { type: "setShaderGraph"; materialId: string; graph: ShaderGraphIR }
|
||||
| { type: "setNLAStack"; objectId: string; tracks: NlaTrackIR[]; baseRevision?: number }
|
||||
| NlaMoveStripCommand
|
||||
@@ -201,6 +279,31 @@ export type WebEngineOperatorPropertySpec = BlenderOperatorPropertySpec;
|
||||
export type WebEngineOperatorDescriptor = BlenderOperatorDescriptor;
|
||||
export type WebEngineOperatorInvocation = BlenderOperatorInvocation;
|
||||
|
||||
export type RNAPropertyValue = boolean | number | string | null;
|
||||
export interface RNADataBlockDescriptor {
|
||||
type: string;
|
||||
name: string;
|
||||
libraryLinked: boolean;
|
||||
editable: boolean;
|
||||
}
|
||||
export interface RNAPropertyDescriptor {
|
||||
identifier: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
type: "BOOLEAN" | "INT" | "FLOAT" | "STRING" | "ENUM";
|
||||
subtype?: number;
|
||||
arrayLength: number;
|
||||
readonly: boolean;
|
||||
value?: RNAPropertyValue | RNAPropertyValue[];
|
||||
enumItems?: Array<{ identifier: string; name: string; value: number }>;
|
||||
}
|
||||
export interface RNACollectionDescriptor {
|
||||
identifier: string;
|
||||
name: string;
|
||||
itemType?: string;
|
||||
items: Array<{ name: string; type?: string }>;
|
||||
}
|
||||
|
||||
/** Read-only commands supported by the stable Worker-to-WASM JSON facade. */
|
||||
export type WebEngineJsonCommand =
|
||||
| "snapshot"
|
||||
@@ -211,7 +314,16 @@ export type WebEngineJsonCommand =
|
||||
| "undo"
|
||||
| "redo"
|
||||
| "listBlenderOperators"
|
||||
| "invokeBlenderOperator";
|
||||
| "invokeBlenderOperator"
|
||||
| "assetMark"
|
||||
| "assetCatalog"
|
||||
| "libraryLink"
|
||||
| "libraryOverride"
|
||||
| "libraryReload"
|
||||
| "listRNADataBlocks"
|
||||
| "readRNAProperties"
|
||||
| "setRNAProperty"
|
||||
| "listRNACollections";
|
||||
|
||||
export type WebEngineRequest =
|
||||
| { requestId: string; command: { type: "init" } }
|
||||
@@ -311,3 +423,4 @@ export type WebEngineResponse =
|
||||
| { kind: "progress"; requestId: string; progress: ProgressEvent }
|
||||
| { kind: "result"; requestId: string; ok: true; result: WebEngineResult }
|
||||
| { kind: "result"; requestId: string; ok: false; error: ErrorReport };
|
||||
import type { CameraSolveIR, MaskPointIR, MaskSplineIR, TrackingMarkerIR } from "./tracking-mask";
|
||||
|
||||
Reference in New Issue
Block a user