feat(web): bind scene camera and world settings
This commit is contained in:
BIN
web/app/public/vendor/blender/web_engine.wasm
vendored
BIN
web/app/public/vendor/blender/web_engine.wasm
vendored
Binary file not shown.
@@ -945,6 +945,7 @@ function Properties({ snapshot, selectedFaceIndices, selectedVertexIndices, grea
|
||||
{activeScene?.unitSettings ? <div className="property-section" data-testid="scene-unit-settings-workbench"><h3>Scene Units</h3><output>System {activeScene.unitSettings.system} · Scale {activeScene.unitSettings.scaleLength.toFixed(4)} · Rotation {activeScene.unitSettings.rotationSystem}</output><output>Length {activeScene.unitSettings.lengthUnit} · Mass {activeScene.unitSettings.massUnit} · Time {activeScene.unitSettings.timeUnit}</output><div className="property-actions"><button type="button" onClick={() => onCommand(animationRenderCommands.setSceneUnitSettings(activeScene.id, { system: activeScene.unitSettings!.system === 1 ? 2 : 1 }))}>切换单位系统</button><button type="button" onClick={() => onCommand(animationRenderCommands.setSceneUnitSettings(activeScene.id, { scaleLength: Math.min(1000000, Math.max(0.000001, activeScene.unitSettings!.scaleLength || 1) * 1.1) }))}>调整单位缩放</button><button type="button" onClick={() => onCommand(animationRenderCommands.setSceneUnitSettings(activeScene.id, { rotationSystem: activeScene.unitSettings!.rotationSystem === 2 ? 0 : 2 }))}>切换旋转单位</button></div></div> : null}
|
||||
{activeScene?.timelineSettings ? <div className="property-section" data-testid="scene-timeline-settings-workbench"><h3>Timeline Settings</h3><output>{activeScene.timelineSettings.fps} / {activeScene.timelineSettings.fpsBase.toFixed(3)} fps · Step {activeScene.timelineSettings.frameStep}</output><output>{activeScene.timelineSettings.usePreviewRange ? `Preview ${activeScene.timelineSettings.previewStart} - ${activeScene.timelineSettings.previewEnd}` : "Full frame range"}</output><div className="property-actions"><button type="button" onClick={() => onCommand(animationRenderCommands.setSceneTimelineSettings(activeScene.id, { fps: Math.min(1000, activeScene.timelineSettings!.fps + 1) }))}>增加帧率</button><button type="button" onClick={() => onCommand(animationRenderCommands.setSceneTimelineSettings(activeScene.id, { frameStep: Math.min(1000, activeScene.timelineSettings!.frameStep + 1) }))}>增加步进</button><button type="button" onClick={() => onCommand(animationRenderCommands.setSceneTimelineSettings(activeScene.id, { usePreviewRange: !activeScene.timelineSettings!.usePreviewRange }))}>{activeScene.timelineSettings.usePreviewRange ? "关闭预览范围" : "启用预览范围"}</button></div></div> : null}
|
||||
{activeScene?.pipelineSettings ? <div className="property-section" data-testid="scene-pipeline-settings-workbench"><h3>Render Pipeline</h3><output>{activeScene.pipelineSettings.useCompositor ? "Compositor" : "Direct"} · {activeScene.pipelineSettings.useSequencer ? "Sequencer" : "No Sequencer"}</output><output>{activeScene.pipelineSettings.saveOutput ? "Save output" : "Preview only"} · {activeScene.pipelineSettings.noOverwrite ? "No overwrite" : "Overwrite allowed"}</output><div className="property-actions"><button type="button" onClick={() => onCommand(animationRenderCommands.setScenePipelineSettings(activeScene.id, { useCompositor: !activeScene.pipelineSettings!.useCompositor }))}>切换 Compositor</button><button type="button" onClick={() => onCommand(animationRenderCommands.setScenePipelineSettings(activeScene.id, { useSequencer: !activeScene.pipelineSettings!.useSequencer }))}>切换 Sequencer</button><button type="button" onClick={() => onCommand(animationRenderCommands.setScenePipelineSettings(activeScene.id, { saveOutput: !activeScene.pipelineSettings!.saveOutput }))}>切换输出保存</button></div></div> : null}
|
||||
{activeScene ? <div className="property-section" data-testid="scene-bindings-workbench"><h3>Scene Bindings</h3><output>Camera {activeScene.cameraObjectId ?? "None"} · World {activeScene.worldId ?? "None"}</output><div className="property-actions"><button type="button" disabled={!snapshot?.nodes.some((node) => node.id === activeScene.cameraObjectId && node.type === "CAMERA")} onClick={() => onCommand(animationRenderCommands.setSceneBindings(activeScene.id, { cameraObjectId: activeScene.cameraObjectId }))}>确认相机绑定</button><button type="button" disabled={!activeWorld} onClick={() => onCommand(animationRenderCommands.setSceneBindings(activeScene.id, { worldId: activeWorld?.id ?? null }))}>绑定当前世界</button></div></div> : null}
|
||||
{activeFont ? <div className="property-section" data-testid="font-workbench"><h3>Font</h3><label>Text <textarea aria-label="Font body" value={fontBody} maxLength={65536} onChange={(event) => setFontBody(event.target.value)} /></label><div className="property-actions"><button type="button" disabled={fontBody === activeFont.text} onClick={() => onCommand(nonMeshGeometryCommands.setFontBody(activeFont.id, fontBody))}>Apply Text</button>{activeFont.fontProperties ? <button type="button" onClick={() => onCommand(nonMeshGeometryCommands.setFontProperties(activeFont.id, { extrude: Math.min(1000, activeFont.fontProperties!.extrude + 0.01) }))}>Increase Extrude</button> : null}</div><output>{activeFont.text?.length ?? 0} characters</output></div> : null}
|
||||
{activeVolume ? <div className="property-section" data-testid="volume-workbench"><h3>Volume</h3><output>{activeVolume.sourcePath ?? "No source path"}</output><output>{activeVolume.volumeGrids?.length ?? 0} grids</output>{activeVolume.volumeProperties ? <><output>Density {activeVolume.volumeProperties.displayDensity.toFixed(3)} · Step {activeVolume.volumeProperties.stepSize.toFixed(4)}</output><div className="property-actions"><button type="button" onClick={() => onCommand(nonMeshGeometryCommands.setVolumeProperties(activeVolume.id, activeVolume.sourcePath ?? "", Math.min(1e6, activeVolume.volumeProperties!.displayDensity + 0.1), activeVolume.volumeProperties!.interpolation, activeVolume.volumeProperties!.stepSize))}>Increase Density</button><button type="button" onClick={() => onCommand(nonMeshGeometryCommands.setVolumeProperties(activeVolume.id, activeVolume.sourcePath ?? "", activeVolume.volumeProperties!.displayDensity, activeVolume.volumeProperties!.interpolation === "LINEAR" ? "NEAREST" : "LINEAR", activeVolume.volumeProperties!.stepSize))}>Toggle Interpolation</button></div></> : <output>Volume properties unavailable</output>}</div> : null}
|
||||
{activeMetaball ? <div className="property-section" data-testid="metaball-workbench"><h3>Metaball</h3><output>{activeMetaball.elements?.length ?? 0} elements</output>{activeMetaball.elements?.length ? <div className="property-actions"><button type="button" onClick={() => onCommand(nonMeshGeometryCommands.setMetaballElements(activeMetaball.id, activeMetaball.elements!.map((element, index) => index === 0 ? { ...element, radius: Math.min(100000, element.radius + 0.05) } : element)))}>Increase Active Radius</button></div> : <output>No editable elements</output>}</div> : null}
|
||||
|
||||
BIN
web/app/src/vendor/blender/web_engine.wasm
vendored
BIN
web/app/src/vendor/blender/web_engine.wasm
vendored
Binary file not shown.
@@ -665,6 +665,16 @@ function assertFutureCapability(payload: Extract<WebEngineRequest["command"], {
|
||||
for (const field of ["useCompositor", "useSequencer", "noOverwrite", "saveOutput", "alphaPremultiplied"] as const) if (properties[field] !== undefined && typeof properties[field] !== "boolean") throw report("RENDER_PROPERTY_INVALID", `Scene ${field} must be boolean`);
|
||||
return;
|
||||
}
|
||||
case "setSceneBindings": {
|
||||
const scene = currentSnapshot?.scenes.find((candidate) => candidate.id === payload.dataId);
|
||||
const properties = payload.properties;
|
||||
if (!scene || !validPropertyKeys(properties, ["cameraObjectId", "worldId"])) throw report("RENDER_PROPERTY_INVALID", "Scene binding target or properties are invalid");
|
||||
for (const field of ["cameraObjectId", "worldId"] as const) {
|
||||
const value = properties[field];
|
||||
if (value !== undefined && value !== null && (typeof value !== "string" || value.length === 0 || new TextEncoder().encode(value).byteLength > 127)) throw report("RENDER_PROPERTY_INVALID", `Scene ${field} must be a bounded object or data id`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case "setGeometryNodeGraph": {
|
||||
const snapshot = currentSnapshot;
|
||||
const availableResourceIds = new Set([
|
||||
|
||||
@@ -47,4 +47,7 @@ export const animationRenderCommands = Object.freeze({
|
||||
setScenePipelineSettings(dataId: string, properties: AnimationCommand<"setScenePipelineSettings">["properties"]): AnimationCommand<"setScenePipelineSettings"> {
|
||||
return { type: "setScenePipelineSettings", dataId, properties: { ...properties } };
|
||||
},
|
||||
setSceneBindings(dataId: string, properties: AnimationCommand<"setSceneBindings">["properties"]): AnimationCommand<"setSceneBindings"> {
|
||||
return { type: "setSceneBindings", dataId, properties: { ...properties } };
|
||||
},
|
||||
} satisfies Record<string, (...args: never[]) => WebEngineEditCommand>);
|
||||
|
||||
@@ -85,6 +85,7 @@ export const WEB_ENGINE_COMMAND_CAPABILITIES = {
|
||||
setSceneUnitSettings: { family: "CORE_SCENE_GRAPH", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
|
||||
setSceneTimelineSettings: { family: "CORE_SCENE_GRAPH", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
|
||||
setScenePipelineSettings: { family: "CORE_SCENE_GRAPH", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
|
||||
setSceneBindings: { family: "CORE_SCENE_GRAPH", 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" },
|
||||
|
||||
@@ -149,6 +149,7 @@ 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: "setSceneBindings"; dataId: string; properties: { cameraObjectId?: string | null; worldId?: string | null } }
|
||||
| { type: "setMetaballElements"; dataId: string; elements: Array<{ type: number; position: [number, number, number]; radius: number; scale: [number, number, number] }> }
|
||||
| { type: "sculptStroke"; stroke: SculptStrokeIR }
|
||||
| { type: "setSculptMeshAttributes"; attributes: SculptMeshAttributesIR }
|
||||
|
||||
@@ -4,7 +4,7 @@ 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", "setSceneProperties", "setSceneRenderSettings", "setSceneAudioSettings", "setSceneUnitSettings", "setSceneTimelineSettings", "setScenePipelineSettings"],
|
||||
RENDER: ["setCameraProperties", "setLightProperties", "setWorldProperties", "setSceneProperties", "setSceneRenderSettings", "setSceneAudioSettings", "setSceneUnitSettings", "setSceneTimelineSettings", "setScenePipelineSettings", "setSceneBindings"],
|
||||
IO: ["importImage", "importVFont", "setMaterialImageNode"],
|
||||
} as const satisfies Record<string, readonly WebEngineCommandType[]>);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user