feat(web): continue source-backed implementation waves
This commit is contained in:
@@ -15,6 +15,10 @@ import { ioMediaCommands } from "../../../protocol/io-media-commands";
|
||||
import { isWorkbenchCommandAvailable } from "../../../protocol/workbench-command-groups";
|
||||
import { shaderGraphCommands } from "../../../protocol/shader-graph-commands";
|
||||
import { renderCompositorCommands } from "../../../protocol/render-compositor-commands";
|
||||
import type { RenderCapabilityRequest } from "../../../protocol/render-capabilities";
|
||||
import { compositorCommands } from "../../../protocol/compositor-commands";
|
||||
import { geometryNodeCommands } from "../../../protocol/geometry-node-commands";
|
||||
import { physicsCommands } from "../../../protocol/physics-commands";
|
||||
import type { NonMeshGeometryChunk } from "../../../protocol/nonmesh-binary";
|
||||
import type { SimplifyAttributePolicy, SimplifyDelimit, SimplifyMode, SimplifyProfile } from "../../../protocol/simplify";
|
||||
import { StorageClient } from "../storage/StorageClient";
|
||||
@@ -637,7 +641,7 @@ function GreasePencilCanvas2D({ drawing, scope, selection, onSelect }: {
|
||||
onKeyDown={keyboardSelect} />;
|
||||
}
|
||||
|
||||
function Properties({ snapshot, selectedFaceIndices, selectedVertexIndices, greasePencilSelection, onGreasePencilCanvasPointSelect, onCommand, onImportImage, onApplyDecimate, onPreviewDecimate, onGenerateLOD, onSetModifierEnabled, previewActive, onCancelPreview }: {
|
||||
function Properties({ snapshot, selectedFaceIndices, selectedVertexIndices, greasePencilSelection, onGreasePencilCanvasPointSelect, onCommand, onImportImage, onRequestAsset, onQueryRenderCapability, onApplyDecimate, onPreviewDecimate, onGenerateLOD, onSetModifierEnabled, previewActive, onCancelPreview }: {
|
||||
snapshot: SceneSnapshotIR | null;
|
||||
selectedFaceIndices: number[];
|
||||
selectedVertexIndices: number[];
|
||||
@@ -645,6 +649,8 @@ function Properties({ snapshot, selectedFaceIndices, selectedVertexIndices, grea
|
||||
onGreasePencilCanvasPointSelect: (point: GreasePencilPointRef, additive: boolean, baseSelectionRevision: number) => void;
|
||||
onCommand: (command: WebEngineEditCommand) => void;
|
||||
onImportImage: (file: File) => void;
|
||||
onRequestAsset: (assetId: string) => void;
|
||||
onQueryRenderCapability: (request: RenderCapabilityRequest) => void;
|
||||
onApplyDecimate: (profile: SimplifyProfile, meshId: string) => void;
|
||||
onPreviewDecimate: (profile: SimplifyProfile, meshId: string) => void;
|
||||
onGenerateLOD: (meshId: string, triangleCount: number) => void;
|
||||
@@ -690,12 +696,23 @@ function Properties({ snapshot, selectedFaceIndices, selectedVertexIndices, grea
|
||||
const [greasePencilReorderError, setGreasePencilReorderError] = useState("");
|
||||
const [curveSplineIndex, setCurveSplineIndex] = useState(0);
|
||||
const [curveTopologyError, setCurveTopologyError] = useState("");
|
||||
const [fontBody, setFontBody] = useState("");
|
||||
const [textSource, setTextSource] = useState("");
|
||||
const activeNode = snapshot?.nodes.find((node) => node.id === snapshot.activeObjectId);
|
||||
const activeMesh = activeNode?.dataId ? snapshot?.meshes.find((mesh) => mesh.id === activeNode.dataId) : undefined;
|
||||
const activeCurve = activeNode?.dataId ? snapshot?.nonMeshData?.find((data) => data.id === activeNode.dataId && data.type === "CURVE") : undefined;
|
||||
const activeFont = activeNode?.dataId ? snapshot?.nonMeshData?.find((data) => data.id === activeNode.dataId && data.type === "FONT") : undefined;
|
||||
const activeText = activeNode?.dataId ? snapshot?.texts?.find((data) => data.id === activeNode.dataId) : undefined;
|
||||
const activeVolume = activeNode?.dataId ? snapshot?.nonMeshData?.find((data) => data.id === activeNode.dataId && data.type === "VOLUME") : undefined;
|
||||
const activeMetaball = activeNode?.dataId ? snapshot?.nonMeshData?.find((data) => data.id === activeNode.dataId && data.type === "METABALL") : undefined;
|
||||
const activeGreasePencil = activeNode?.dataId ? snapshot?.greasePencils?.find((data) => data.id === activeNode.dataId) : undefined;
|
||||
const activeGeometryGraph = snapshot?.geometryNodeGraphs?.[0];
|
||||
const selectedGreasePencilPoint = greasePencilPointSelection.find((point) => point.dataId === activeGreasePencil?.id);
|
||||
const activeMaterial = snapshot?.materials.find((material) => material.id === activeMesh?.materialSlotIds?.[0]);
|
||||
const activeCamera = snapshot?.cameras.find((camera) => camera.id === activeNode?.dataId);
|
||||
const activeLight = snapshot?.lights.find((light) => light.id === activeNode?.dataId);
|
||||
const activeWorld = snapshot?.worlds.find((world) => world.id === activeNode?.dataId);
|
||||
const packedImages = snapshot?.images.filter((image) => image.packed && image.assetId) ?? [];
|
||||
useEffect(() => {
|
||||
if (!activeMaterial) return;
|
||||
setMaterialColor(`#${activeMaterial.baseColor.slice(0, 3).map((component) => Math.round(Math.max(0, Math.min(1, component)) * 255).toString(16).padStart(2, "0")).join("")}`);
|
||||
@@ -709,6 +726,8 @@ function Properties({ snapshot, selectedFaceIndices, selectedVertexIndices, grea
|
||||
setMaterialEmissionStrength(activeMaterial.emissionStrength ?? 1);
|
||||
}, [activeMaterial]);
|
||||
useEffect(() => setRenameValue(activeNode?.name ?? ""), [activeNode?.id]);
|
||||
useEffect(() => setFontBody(activeFont?.text ?? ""), [activeFont?.id, activeFont?.text]);
|
||||
useEffect(() => setTextSource(activeText?.source ?? ""), [activeText?.id, activeText?.source]);
|
||||
useEffect(() => {
|
||||
if (!activeCurve || curveSplineIndex >= activeCurve.splineCount) setCurveSplineIndex(0);
|
||||
setCurveTopologyError("");
|
||||
@@ -881,6 +900,20 @@ function Properties({ snapshot, selectedFaceIndices, selectedVertexIndices, grea
|
||||
</div>
|
||||
<div className="property-section"><h3>Transform</h3><label>位置 <output>0.000, 0.000, 0.000</output></label><label>旋转 <output>0°, 0°, 0°</output></label><label>缩放 <output>1.000, 1.000, 1.000</output></label></div>
|
||||
{activeNode ? <div className="property-section"><h3>Object & Hierarchy</h3><label>名称 <input aria-label="对象名称" value={renameValue} onChange={(event) => setRenameValue(event.target.value)} /></label><div className="property-actions"><button type="button" onClick={() => renameValue && onCommand({ type: "renameId", id: activeNode.id, name: renameValue })}>重命名</button><button type="button" onClick={() => onCommand({ type: "applyObjectTransform", objectId: activeNode.id })}>应用变换</button><button type="button" onClick={() => onCommand({ type: "setObjectOrigin", objectId: activeNode.id, mode: "GEOMETRY" })}>原点到几何体</button></div><label>Collection <select aria-label="移动到 Collection" value="" onChange={(event) => event.target.value && onCommand({ type: "moveObjectToCollection", objectId: activeNode.id, collectionId: event.target.value })}><option value="">选择...</option>{snapshot?.collections.map((collection) => <option key={collection.id} value={collection.id}>{collection.name}</option>)}</select></label></div> : null}
|
||||
{snapshot ? <div className="property-section" data-testid="scene-context-workbench"><h3>Scene Context</h3><output data-testid="scene-context-counts">Scenes {snapshot.scenes.length} · Collections {snapshot.collections.length} · Armatures {snapshot.armatures?.length ?? 0} · Libraries {snapshot.libraries?.length ?? 0}</output>{snapshot.scenes.map((scene) => <output key={scene.id} data-resource-id={scene.id}>Scene: {scene.name}{scene.renderEngine ? ` · ${scene.renderEngine}` : ""}</output>)}{snapshot.collections.slice(0, 32).map((collection) => <output key={collection.id} data-resource-id={collection.id}>Collection: {collection.name} · {collection.objectIds.length} objects · {collection.childCollectionIds.length} children</output>)}{snapshot.armatures?.slice(0, 16).map((armature) => <output key={armature.id} data-resource-id={armature.id}>Armature: {armature.name} · {armature.bones.length} bones</output>)}{snapshot.libraries?.slice(0, 16).map((library) => <output key={library.id} data-resource-id={library.id}>Library: {library.name} · {library.status}</output>)}</div> : null}
|
||||
{snapshot && (snapshot.releaseCandidateStatus || snapshot.scriptingPolicyStatus || snapshot.renderRoutingStatus || snapshot.serverJobBindingStatus || snapshot.performanceFaultMatrixStatus || snapshot.editorWorkflowStatus) ? <div className="property-section" data-testid="release-status-workbench"><h3>Runtime Status</h3><output data-testid="runtime-status-summary">Scripting {snapshot.scriptingPolicyStatus ?? "UNKNOWN"} · Render {snapshot.renderRoutingStatus ?? "UNKNOWN"} · Jobs {snapshot.serverJobBindingStatus ?? "UNKNOWN"}</output><output>Editor workflow {snapshot.editorWorkflowStatus ?? "UNKNOWN"} · Performance faults {snapshot.performanceFaultMatrixStatus ?? "UNKNOWN"}</output><output>Release candidate {snapshot.releaseCandidateStatus ?? "UNKNOWN"}</output></div> : null}
|
||||
{activeNode?.dataId?.startsWith("mesh:") ? <div className="property-section" data-testid="geometry-node-workbench"><h3>Geometry Nodes</h3><output data-testid="geometry-node-graph-count">{snapshot?.geometryNodeGraphs?.length ?? 0} graphs</output>{activeGeometryGraph ? <><output data-testid="geometry-node-active-graph">{activeGeometryGraph.name}</output><output>{activeGeometryGraph.nodes.length} nodes / {activeGeometryGraph.links.length} links</output><button type="button" onClick={() => onCommand(geometryNodeCommands.setGraph(activeNode.dataId!, activeGeometryGraph))}>提交当前节点图</button></> : <output>当前文件没有 Geometry Node 图</output>}</div> : null}
|
||||
{snapshot?.physicsSimulation ? <div className="property-section" data-testid="physics-workbench"><h3>Physics</h3><output data-testid="physics-system-count">{snapshot.physicsSimulation.systems.length} systems</output><output>{[...new Set(snapshot.physicsSimulation.systems.map((system) => system.family))].join(", ") || "No systems"}</output><button type="button" disabled={!activeNode} onClick={() => onCommand(physicsCommands.setManifest(snapshot.physicsSimulation!))}>提交物理清单</button></div> : null}
|
||||
{packedImages.length > 0 ? <div className="property-section" data-testid="resource-workbench"><h3>Resources</h3><output>{packedImages.length} packed image assets</output>{packedImages.map((image) => <button key={image.assetId} type="button" onClick={() => onRequestAsset(image.assetId)}>加载 {image.name}</button>)}</div> : null}
|
||||
{(snapshot?.vfonts?.length ?? 0) > 0 || (snapshot?.sounds?.length ?? 0) > 0 || (snapshot?.speakers?.length ?? 0) > 0 || (snapshot?.texts?.length ?? 0) > 0 || (snapshot?.textures?.length ?? 0) > 0 || (snapshot?.particleSettings?.length ?? 0) > 0 ? <div className="property-section" data-testid="resource-scene-workbench"><h3>Scene Resources</h3><output data-testid="resource-scene-counts">VFonts {snapshot?.vfonts?.length ?? 0} · Sounds {snapshot?.sounds?.length ?? 0} · Speakers {snapshot?.speakers?.length ?? 0} · Texts {snapshot?.texts?.length ?? 0} · Textures {snapshot?.textures?.length ?? 0} · Particles {snapshot?.particleSettings?.length ?? 0}</output>{snapshot?.vfonts?.map((font) => <output key={font.id} data-resource-id={font.id}>VFont: {font.name} {font.packed ? "(packed)" : "(external)"}</output>)}{snapshot?.sounds?.map((sound) => <output key={sound.id} data-resource-id={sound.id}>Sound: {sound.name} · {sound.audioChannels}ch @ {sound.sampleRate}Hz</output>)}{snapshot?.speakers?.map((speaker) => <output key={speaker.id} data-resource-id={speaker.id}>Speaker: {speaker.name} · volume {speaker.volume.toFixed(2)}</output>)}{snapshot?.texts?.map((text) => <output key={text.id} data-resource-id={text.id}>Text: {text.name} · {text.lineCount} lines{ text.internal ? " (internal)" : ""}</output>)}{snapshot?.textures?.map((texture) => <output key={texture.id} data-resource-id={texture.id}>Texture: {texture.name}</output>)}{snapshot?.particleSettings?.map((settings) => <output key={settings.id} data-resource-id={settings.id}>Particles: {settings.name} · {settings.totalParticles} total</output>)}</div> : null}
|
||||
{activeText ? <div className="property-section" data-testid="text-datablock-workbench"><h3>Text Data-Block</h3><output>{activeText.name} · {activeText.lineCount} lines · {activeText.byteLength} bytes</output><textarea aria-label="Text source" value={textSource} maxLength={1_048_576} onChange={(event) => setTextSource(event.target.value)} /><div className="property-actions"><button type="button" disabled={textSource === activeText.source || new TextEncoder().encode(textSource).byteLength > 1_048_576} onClick={() => onCommand({ type: "setTextSource", dataId: activeText.id, source: textSource })}>Apply Text Source</button></div></div> : null}
|
||||
{activeCamera ? <div className="property-section" data-testid="camera-workbench"><h3>Camera</h3><output>{activeCamera.projection} · {activeCamera.lensMm.toFixed(1)}mm</output><button type="button" onClick={() => onCommand(animationRenderCommands.setCameraProperties(activeCamera.id, { projection: activeCamera.projection === "ORTHOGRAPHIC" ? "PERSPECTIVE" : "ORTHOGRAPHIC" }))}>切换投影</button><button type="button" onClick={() => onCommand(animationRenderCommands.setCameraProperties(activeCamera.id, { lensMm: Math.min(10000, activeCamera.lensMm + 5) }))}>增加焦距</button></div> : null}
|
||||
{activeLight ? <div className="property-section" data-testid="light-workbench"><h3>Light</h3><output>{activeLight.energy.toFixed(1)} energy</output><button type="button" onClick={() => onCommand(animationRenderCommands.setLightProperties(activeLight.id, { energy: Math.min(1e9, activeLight.energy + 10) }))}>增加能量</button><button type="button" onClick={() => onCommand(animationRenderCommands.setLightProperties(activeLight.id, { castsShadow: !(activeLight.castsShadow ?? true) }))}>切换阴影</button></div> : null}
|
||||
{activeWorld ? <div className="property-section" data-testid="world-workbench"><h3>World</h3><output>Exposure {activeWorld.exposure.toFixed(2)}</output><button type="button" onClick={() => onCommand(animationRenderCommands.setWorldProperties(activeWorld.id, { exposure: Math.max(-20, Math.min(20, activeWorld.exposure + 0.5)) }))}>增加曝光</button></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}
|
||||
<div className="property-section" data-testid="render-capability-workbench"><h3>Render Capabilities</h3><button type="button" onClick={() => onQueryRenderCapability(renderCompositorCommands.boundedShaderGraph())}>检查 Shader</button><button type="button" onClick={() => onQueryRenderCapability(renderCompositorCommands.webGPUBackend())}>检查 WebGPU</button><button type="button" onClick={() => onQueryRenderCapability(renderCompositorCommands.postProcess(["FXAA", "TAA"]))}>检查 Post Process</button>{snapshot?.scenes[0]?.compositorGraph ? <button type="button" onClick={() => { compositorCommands.evaluate(snapshot.scenes[0].compositorGraph!); }}>准备 Compositor 图</button> : null}</div>
|
||||
<div className="property-section"><h3>Viewport Display</h3><label>显示颜色 <span className="swatch" /></label><label>可见性 <input aria-label="可见性" type="checkbox" defaultChecked /></label></div>
|
||||
{activeCurve && curveTopologyOperatorGate("TOGGLE_CYCLIC").status === "READY" ? <div className="property-section" data-testid="curve-topology-editor" data-ready-operators="TOGGLE_CYCLIC" data-curve-id={activeCurve.id} data-cyclic-u={(activeCurve.cyclicU ?? []).join(",")} data-spline-types={(activeCurve.splineTypes ?? []).join(",")} data-point-count={activeCurve.pointCount}><h3>Curve Topology</h3><label>Spline <select aria-label="Curve spline" value={Math.min(curveSplineIndex, Math.max(0, activeCurve.splineCount - 1))} onChange={(event) => setCurveSplineIndex(Number(event.target.value))}>{Array.from({ length: activeCurve.splineCount }, (_, index) => <option key={index} value={index}>{index + 1} {activeCurve.splineTypes?.[index] ?? "UNKNOWN"}</option>)}</select></label><label>Cyclic U <input aria-label="Curve cyclic U" type="checkbox" checked={activeCurve.cyclicU?.[curveSplineIndex] ?? false} onChange={toggleCurveCyclic} /></label><output data-testid="curve-cyclic-state">{activeCurve.cyclicU?.[curveSplineIndex] ? "Cyclic" : "Open"}</output>{curveTopologyError ? <output role="alert">{curveTopologyError}</output> : null}</div> : null}
|
||||
{activeMesh ? <div className="property-section"><h3>UV Maps</h3><label>活动 UV <select aria-label="活动 UV Map" value={activeMesh.activeUVMap ?? ""} onChange={(event) => event.target.value && onCommand({ type: "setActiveUVMap", meshId: activeMesh.id, name: event.target.value })}><option value="">None</option>{activeMesh.uvLayers?.map((layer) => <option key={layer.name} value={layer.name}>{layer.name}</option>)}</select></label><div className="property-actions"><button type="button" onClick={() => onCommand({ type: "createUVMap", meshId: activeMesh.id, name: `UVMap.${(activeMesh.uvLayers?.length ?? 0) + 1}` })}>新增 UV</button><button type="button" disabled={selectedFaceIndices.length === 0} onClick={() => onCommand({ type: "unwrapUV", meshId: activeMesh.id, faceIndices: selectedFaceIndices, method: "PLANAR" })}>Planar</button><button type="button" disabled={selectedFaceIndices.length === 0} onClick={() => onCommand({ type: "unwrapUV", meshId: activeMesh.id, faceIndices: selectedFaceIndices, method: "CUBE" })}>Cube</button></div></div> : null}
|
||||
@@ -1042,6 +1075,8 @@ function Timeline({ snapshot, frame, start, end, onFrameChange, onCommand }: { s
|
||||
const third = Math.round(start + (end - start) * 0.6);
|
||||
const fourth = Math.round(start + (end - start) * 0.8);
|
||||
const animation = snapshot?.animations.find((candidate) => candidate.targetId === snapshot.activeObjectId);
|
||||
const actions = snapshot?.animations.filter((candidate) => candidate.targetId === snapshot.activeObjectId) ?? [];
|
||||
const nlaTracks = snapshot?.nlaTracks?.filter((track) => track.ownerId === snapshot.activeObjectId) ?? [];
|
||||
const activeNode = snapshot?.nodes.find((candidate) => candidate.id === snapshot.activeObjectId);
|
||||
const greasePencil = snapshot?.greasePencils?.find((candidate) => candidate.id === activeNode?.dataId);
|
||||
const keyframes = [...new Set(greasePencil
|
||||
@@ -1054,7 +1089,7 @@ function Timeline({ snapshot, frame, start, end, onFrameChange, onCommand }: { s
|
||||
<div className="timeline-controls"><button type="button" aria-label="跳到第一帧" onClick={() => { setPlaying(false); onFrameChange(start); }}>|◀</button><button type="button" aria-label="上一帧" onClick={() => onFrameChange(Math.max(start, frame - 1))}>◀</button><button type="button" aria-label="播放" onClick={() => setPlaying((current) => !current)}>{playing ? "Ⅱ" : "▶"}</button><button type="button" aria-label="下一帧" onClick={() => onFrameChange(Math.min(end, frame + 1))}>▶|</button><button type="button" aria-label="跳到最后一帧" onClick={() => { setPlaying(false); onFrameChange(end); }}>▶|</button><output className="frame-number">{frame}</output>{snapshot?.activeObjectId ? <><button type="button" aria-label="插入位置关键帧" onClick={() => onCommand(animationRenderCommands.insertKeyframe(snapshot.activeObjectId!, frame, "LOCATION"))}>Loc</button><button type="button" aria-label="插入旋转关键帧" onClick={() => onCommand(animationRenderCommands.insertKeyframe(snapshot.activeObjectId!, frame, "ROTATION_EULER"))}>Rot</button><button type="button" aria-label="插入缩放关键帧" onClick={() => onCommand(animationRenderCommands.insertKeyframe(snapshot.activeObjectId!, frame, "SCALE"))}>Scale</button><button type="button" aria-label="删除当前关键帧" onClick={() => onCommand(animationRenderCommands.deleteKeyframe(snapshot.activeObjectId!, frame))}>Del Key</button></> : null}</div>
|
||||
<input className="frame-slider" type="range" min={start} max={end} value={Math.min(end, Math.max(start, frame))} onChange={(event) => onFrameChange(Number(event.target.value))} aria-label="当前帧" />
|
||||
<div className="timeline-scale"><span>{start}</span><span>{mid}</span><span>{second}</span><span>{third}</span><span>{fourth}</span><span>{end}</span></div>
|
||||
<div className="dope-sheet" aria-label="Dope Sheet"><span className="channel-name">{greasePencil?.name ?? animation?.name ?? "No Action"}</span><div className="key-track">{keyframes.map((keyframe) => <button key={keyframe} type="button" className={keyframe === frame ? "key-dot active" : "key-dot"} style={{ left: `${((keyframe - start) / Math.max(1, end - start)) * 100}%` }} aria-label={`${greasePencil ? "Grease Pencil 帧" : "关键帧"} ${keyframe}`} onClick={() => onFrameChange(keyframe)} />)}</div>{!greasePencil && animation?.channels[0] ? <select aria-label="FCurve 插值" value={animation.channels[0].interpolation ?? "BEZIER"} onChange={(event) => onCommand({ type: "setFCurveInterpolation", animationId: animation.id, path: animation.channels[0].path, interpolation: event.target.value as "CONSTANT" | "LINEAR" | "BEZIER" })}><option value="CONSTANT">Constant</option><option value="LINEAR">Linear</option><option value="BEZIER">Bezier</option></select> : <span />}</div>
|
||||
<div className="dope-sheet" aria-label="Dope Sheet"><span className="channel-name">{greasePencil?.name ?? animation?.name ?? "No Action"}</span><div className="key-track">{keyframes.map((keyframe) => <button key={keyframe} type="button" className={keyframe === frame ? "key-dot active" : "key-dot"} style={{ left: `${((keyframe - start) / Math.max(1, end - start)) * 100}%` }} aria-label={`${greasePencil ? "Grease Pencil 帧" : "关键帧"} ${keyframe}`} onClick={() => onFrameChange(keyframe)} />)}</div>{!greasePencil && animation?.channels[0] ? <select aria-label="FCurve 插值" value={animation.channels[0].interpolation ?? "BEZIER"} onChange={(event) => onCommand({ type: "setFCurveInterpolation", animationId: animation.id, path: animation.channels[0].path, interpolation: event.target.value as "CONSTANT" | "LINEAR" | "BEZIER" })}><option value="CONSTANT">Constant</option><option value="LINEAR">Linear</option><option value="BEZIER">Bezier</option></select> : <span />}{snapshot?.activeObjectId && actions.length > 0 ? <select aria-label="活动 Action" defaultValue="" onChange={(event) => onCommand(animationRenderCommands.setActiveAction(snapshot.activeObjectId!, event.target.value || null))}><option value="">选择 Action...</option>{actions.map((action) => <option key={action.id} value={action.id}>{action.name}</option>)}</select> : null}{snapshot?.activeObjectId && nlaTracks.length > 0 ? <button type="button" onClick={() => onCommand(animationRenderCommands.setNLAStack(snapshot.activeObjectId!, nlaTracks, snapshot.revision))}>提交 NLA</button> : null}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2505,7 +2540,7 @@ export function App() {
|
||||
<div className="workspace-grid">
|
||||
<Area className="viewport-area" editor="3D Viewport"><ViewportPlaceholder snapshot={preview?.snapshot ?? snapshot} geometryBuffers={preview?.geometryBuffers ?? geometryBuffers} nonMeshGeometryBuffers={preview?.nonMeshGeometryBuffers ?? nonMeshGeometryBuffers} textureAssets={gpuTextureAssets} volumeProject={volumeProject} lodLevels={preview ? null : lodLevels} selectedObjectIds={selectedObjectIds} editMode={uiState.context.mode === "Edit"} meshSelection={meshSelection} greasePencilSelectionRevision={greasePencilSelection?.revision ?? 0} onSelect={selectObject} onElementSelect={selectMeshElement} onGreasePencilPointSelect={selectGreasePencilPoint} onGreasePencilMarqueeSelect={selectGreasePencilMarquee} onTransform={transformActive} onDiagnostic={recordDiagnostic} /></Area>
|
||||
<Area className="outliner-area" editor="Outliner"><Outliner snapshot={snapshot} onSelect={selectObject} onToggleVisibility={(id, visible) => void applyEditCommand({ type: "setObjectVisibility", objectId: id, visible })} /></Area>
|
||||
<Area className="properties-area" editor="Properties"><Properties snapshot={snapshot} selectedFaceIndices={meshSelection.mode === "FACE" ? [...meshSelection.indices] : []} selectedVertexIndices={meshSelection.mode === "VERT" ? [...meshSelection.indices] : []} greasePencilSelection={greasePencilSelection} onGreasePencilCanvasPointSelect={(point, additive, baseSelectionRevision) => { commitGreasePencilSelection("CANVAS_2D", additive ? "TOGGLE" : "REPLACE", [point], point, baseSelectionRevision); }} onCommand={(command) => void applyEditCommand(command)} onImportImage={(file) => void importImage(file)} onApplyDecimate={applyDecimate} onPreviewDecimate={previewDecimate} onGenerateLOD={(meshId, triangleCount) => void generateLOD(meshId, triangleCount)} onSetModifierEnabled={setModifierEnabled} previewActive={Boolean(preview)} onCancelPreview={() => { setPreview(null); setLodLevels(null); setEngineStatus(`Engine: SceneIR r${snapshot?.revision ?? 0}`); }} /></Area>
|
||||
<Area className="properties-area" editor="Properties"><Properties snapshot={snapshot} selectedFaceIndices={meshSelection.mode === "FACE" ? [...meshSelection.indices] : []} selectedVertexIndices={meshSelection.mode === "VERT" ? [...meshSelection.indices] : []} greasePencilSelection={greasePencilSelection} onGreasePencilCanvasPointSelect={(point, additive, baseSelectionRevision) => { commitGreasePencilSelection("CANVAS_2D", additive ? "TOGGLE" : "REPLACE", [point], point, baseSelectionRevision); }} onCommand={(command) => void applyEditCommand(command)} onImportImage={(file) => void importImage(file)} onRequestAsset={(assetId) => { const client = webClientRef.current; if (!client) return; void client.requestAsset(assetId).then((asset) => setEngineStatus(`Asset: ${asset.assetId} (${asset.status})`)).catch((error: unknown) => setEngineStatus(recordDiagnostic("ENGINE", "PBR_ASSET_INVALID", error, { assetId }))); }} onQueryRenderCapability={(request) => { const client = webClientRef.current; if (!client) return; void client.queryRenderCapability(request).then((gate) => setEngineStatus(`Render: ${gate.status} (${gate.capability})`)).catch((error: unknown) => setEngineStatus(recordDiagnostic("ENGINE", "GLB_EXPORT_BLOCKED", error))); }} onApplyDecimate={applyDecimate} onPreviewDecimate={previewDecimate} onGenerateLOD={(meshId, triangleCount) => void generateLOD(meshId, triangleCount)} onSetModifierEnabled={setModifierEnabled} previewActive={Boolean(preview)} onCancelPreview={() => { setPreview(null); setLodLevels(null); setEngineStatus(`Engine: SceneIR r${snapshot?.revision ?? 0}`); }} /></Area>
|
||||
<Area className="timeline-area" editor="Timeline"><Timeline snapshot={snapshot} frame={frame} start={frameRange.start} end={frameRange.end} onFrameChange={(value) => void applyEditCommand({ type: "setFrame", frame: value })} onCommand={(command) => void applyEditCommand(command)} /></Area>
|
||||
</div>
|
||||
{uiState.operatorSearchOpen ? <OperatorSearch commands={operatorCommands} onClose={closeOperatorSearch} /> : null}
|
||||
|
||||
@@ -23,6 +23,7 @@ import type { CapabilityGateResult } from "../../../protocol/capability-gates";
|
||||
import { applyMeshGeometryDelta } from "../../../protocol/mesh-geometry-delta";
|
||||
import { createWorkerFault, type WorkerFault } from "../../../protocol/worker-fault";
|
||||
import { getWebEngineCommandCapability } from "../../../protocol/web-engine-capabilities";
|
||||
import { resourceCommands } from "../../../protocol/resource-commands";
|
||||
import type {
|
||||
PaintStrokeSessionBeginIR,
|
||||
PaintStrokeSessionCancelIR,
|
||||
@@ -206,7 +207,7 @@ export class WebEngineClient {
|
||||
|
||||
async requestAsset(assetId: string): Promise<AssetRequestResult> {
|
||||
if (!assetId) throw this.report("INVALID_ARGUMENT", "Asset ID 不能为空", true);
|
||||
const result = await this.request({ type: "requestAsset", assetId });
|
||||
const result = await this.request(resourceCommands.requestAsset(assetId));
|
||||
if (!result.asset) throw this.report("INVALID_ARGUMENT", "WebEngine 未返回资产结果", true);
|
||||
return result.asset;
|
||||
}
|
||||
|
||||
BIN
web/app/src/vendor/blender/web_engine.wasm
vendored
BIN
web/app/src/vendor/blender/web_engine.wasm
vendored
Binary file not shown.
@@ -414,6 +414,12 @@ function assertFutureCapability(payload: Extract<WebEngineRequest["command"], {
|
||||
if (!payload.links || [payload.links.regular, payload.links.bold, payload.links.italic, payload.links.boldItalic].some((id) => typeof id !== "string" || !available.has(id))) throw report("NON_MESH_RESOURCE_MISSING", "Font style links must reference VFonts already present in the current Main");
|
||||
return;
|
||||
}
|
||||
case "setTextSource": {
|
||||
const text = currentSnapshot?.texts?.find((candidate) => candidate.id === payload.dataId);
|
||||
if (!text) throw report("NON_MESH_DATA_UNSUPPORTED", `Text data block is unavailable: ${payload.dataId}`);
|
||||
if (typeof payload.source !== "string" || new TextEncoder().encode(payload.source).byteLength > 1_048_576) throw report("NON_MESH_PROPERTY_INVALID", "Text source must be valid UTF-8 within the 1 MiB budget");
|
||||
return;
|
||||
}
|
||||
case "setVolumeProperties": {
|
||||
const data = currentSnapshot?.nonMeshData?.find((candidate) => candidate.id === payload.dataId);
|
||||
if (!data || data.type !== "VOLUME") throw report("NON_MESH_DATA_UNSUPPORTED", `N-015 Volume data block is unavailable: ${payload.dataId}`);
|
||||
|
||||
Reference in New Issue
Block a user