Appearance
编辑能力
到这里为止的指南讲的都是浏览:怎么看、看哪里、看到什么。这一页讲编辑——修改模型的呈现并把结果保存下来。纯阅读器可以完全跳过本页。
需要先讲清楚一件事:引擎从不修改 .fmbv 里的几何数据。所有编辑都是叠在原始数据之上的覆盖层,导出时是一份独立的 sidecar(编辑器写成 .fmbview.json),原始模型文件始终不变。
能编辑什么
| 类型 | 管在哪 | 改什么 | 作用域 |
|---|---|---|---|
| 位置变换 | viewer.transform | 零件挪到哪、转成什么姿态 | 视图级 |
| 编辑态爆炸 | viewer.explodeEdit | 按范围 / 方向 / 层级批量摆位(写进变换通道) | 视图级 |
| 序号标签 | viewer.labels | 装配图上的 ①②③ 气泡与引线 | 视图级 |
| 外观 | viewer.model | 颜色、金属度粗糙度、材质预设(可细到面组) | 视图级 |
| 可见性 | viewer.model | 藏了哪些零件 | 视图级 |
| 结构删除合并 | viewer.model | 结构树上删掉 / 折叠哪些节点 | 文档级 |
视图级 vs 文档级是一条重要分界:视图级的改动随视图各存一份(同一个零件在「总览」视图里是红的、在「剖视」视图里是灰的,互不干扰);文档级的改动跨视图恒生效(一个零件被删掉了,就是在所有视图里都不该出现)。结构删除与合并是唯一的文档级编辑。
统一撤销栈
上表里的全部编辑,共用一条撤销栈(viewer.history)。用户不需要知道上一步动的是哪个域——⌘Z 就是回到上一步:
ts
viewer.on("history-changed", (e) => {
undoBtn.disabled = !e.canUndo;
redoBtn.disabled = !e.canRedo;
});
undoBtn.onclick = () => viewer.history.undo();早期各域各有一条栈,结果是「隐藏一个零件再挪一下,撤销两次却只退回一步」。现在只有一条。各域的 undo()(如 transform.undo())也都转发到它,新代码直接用 viewer.history。
多视图宿主还要多做一步视图对齐:撤销一步之前,先看它属于哪个视图,必要时切过去,否则用户会看到「按了撤销但画面没变」(实际变的是另一个视图):
ts
const scope = viewer.history.peekUndoScope();
if (scope?.viewId && scope.viewId !== currentViewId) switchToView(scope.viewId);
viewer.history.undo();引擎本身不切视图——这是刻意的守层:引擎不该反过来驱动宿主的 UI 导航。
连续交互的三段式
拖滑块、拖 gizmo、拖气泡这类连续交互有一个共同模式:拖动期间实时预览且不入栈,松手才压成一条撤销步。三个域用的是同一套形状:
ts
// 外观(滑块 / 取色器)
viewer.model.beginAppearanceEdit(ids);
viewer.model.inputAppearance(patch); // 逐帧
viewer.model.commitAppearanceEdit();
// 编辑态爆炸(滑块)
viewer.explodeEdit.begin();
viewer.explodeEdit.preview(amount); // 逐帧
viewer.explodeEdit.commit();
// 标签气泡(拖拽)
viewer.labels.beginMove(number);
viewer.labels.moveLabel(number, pos); // 逐帧
viewer.labels.commitMove();三段必须配齐。跳过 begin 直接调中间那一步,不会形成正确的撤销基线——改动会生效,但撤销时退不回去。
变换的真值模型
理解 transform 只需要记住一个公式:
text
节点渲染世界矩阵 = Translate(浏览态爆炸) · D · 原始世界矩阵D 是每个节点一个的世界增量矩阵,未编辑时是单位阵。数值输入、gizmo 拖拽、编辑态爆炸——全都归结为改这个 D。所以:
- 编辑可以叠加:先用编辑态爆炸批量散开,再手动微调某个零件,两者写的是同一个
D。 - 编辑可以序列化:
getOverrides()导出[{ nodeId, delta }],setOverrides()还原。 - 浏览态爆炸(
viewer.explode)走独立的乘法通道、不写D,所以它和编辑正交——可以在已编辑的摆位上再预览散开。
编辑只对带几何实例的叶节点生效。选中一个装配整体平移是成立的,引擎会先把它展开成子树叶子再逐叶施加;isTransformable(id) 给出与「实际能被移动的集合」一致的判定,适合用来 gate UI 按钮。
持久化
引擎不做文件 I/O,持久化是宿主的事。需要导出的东西:
ts
const doc = {
views: [{ id, name, snap: viewer.views.capture() }], // 视图级的全部编辑都在 snap 里
structure: viewer.model.exportStructureBlock(), // 文档级:删除 + 合并
};views.capture() 已经包含了可见性、变换、外观、标签——不需要逐域导出。结构块是唯一需要单独取的,因为它是文档级的、与视图平行。
还原时反过来:
ts
viewer.model.applyStructureBlock(doc.structure); // 先应用文档级结构
viewer.views.apply(doc.views[0].snap, { animate: false }); // 再套视图级状态顺序有讲究——结构编辑改变的是「有哪些有效叶子」,视图状态里的节点 id 要落在这个结果上。