Appearance
viewer.transform — TransformManager
设计意图
viewer.transform 是零部件位置编辑域:给单个零件实例叠加一个世界增量变换(平移 + 旋转),把「这颗螺丝往外挪 20mm」这类改动落到场景上。真值模型只有一条:
text
节点渲染世界矩阵 = Translate(explode) · D · 原始世界矩阵D 就是本域维护的世界增量矩阵(列主序 number[16]),每个节点一个,未编辑时是单位阵。所有 API——数值输入、gizmo 拖拽、位姿设置——最终都归结为改这个 D。这样设计的好处是编辑可叠加、可序列化、可撤销,且与浏览态爆炸(viewer.explode)完全正交:爆炸走独立的乘法预览通道,不写 D,两者可以同时存在。
编辑只对带几何实例的叶节点生效。装配 / 内部节点没有实例,传进来会先解析:合并根展开为成员、装配展开为子树叶子,再逐叶施加——所以选中一个装配整体平移是成立的。isTransformable(id) 给出与「实际能被移动的集合」一致的判定,适合用来 gate UI 按钮。
本域不做文件 I/O:持久化由宿主经 getOverrides() / setOverrides() 完成(编辑器把它写进 .fmbview.json sidecar)。撤销栈是与可见性 / 外观 / 标签共享的单栈,见 viewer.history。
何时用它:装配位置调整、拆解演示的自定义摆位、把外部系统算出的位姿套到零件上。只想临时散开看内部而不改数据,用 viewer.explode。
典型用法
数值平移与旋转(面板输入的典型接法,一次调用 = 一条撤销步):
ts
// 沿世界轴平移 20mm(单位同模型,通常是 mm)
viewer.transform.applyTranslation(selectedIds, [20, 0, 0], "world");
// 绕零件自身轴旋转 15°(欧拉度,XYZ 顺序;多选时 local 回退为 world)
viewer.transform.applyRotation(selectedIds, [0, 0, 15], "local");读回绝对位姿驱动面板显示(两种坐标系,与上面两种 space 一一对应):
ts
const world = viewer.transform.getNodePose(nodeId); // { pos: [x,y,z], rot: [rx,ry,rz] }(度)
const local = viewer.transform.getNodeLocalPose(nodeId); // 平移投影到零件当前有效局部轴
viewer.transform.setNodePose(nodeId, { pos: [0, 0, 50], rot: [0, 0, 0] }); // 绝对设值,一步历史唤起 gizmo 交互(编辑器工具切换):
ts
viewer.transform.setEditMode(true); // 显示 gizmo
viewer.transform.setGizmoMode("rotate"); // "translate"(默认) | "rotate"
viewer.transform.setGizmoSpace("local"); // "world"(默认) | "local"
viewer.on("transform-editmode-changed", (e) => (panel.hidden = !e.editing));持久化与还原(宿主自己决定存哪):
ts
const overrides = viewer.transform.getOverrides(); // [{ nodeId, delta: number[16] }]
await fs.writeFile(sidecarPath, JSON.stringify(overrides));
// ……下次加载完模型后
viewer.transform.setOverrides(JSON.parse(await fs.readFile(sidecarPath, "utf8")));复位:
ts
viewer.transform.resetNode(nodeId); // 单个
viewer.transform.resetNodes(ids); // 一批(一条撤销步)
viewer.transform.resetAll(); // 全部
viewer.transform.hasEdits(); // 有没有任何编辑(驱动「未保存」标记)注意事项
- 只有带实例的叶节点会被改:装配 / 内部节点先解析成叶子集再逐叶施加;解析后为空(空装配、不存在的 id)时静默返回,不抛错。用
isTransformable(id)提前判定。 - 旋转绕选中集当前有效世界包围盒中心,不是各自零件中心;
getSelectionPivot(ids)返回这个 pivot,gizmo 也摆在这里。 space: "local"多选时回退 world:局部轴对多选没有一致定义。单选 local 沿零件当前有效朝向(已含既有编辑),所以按差量反复施加能自洽往返。getNodePose是绝对位姿、绕零件原始几何中心(设计原位),不是相对上一次编辑的增量;getNodeLocalPose是同一位姿在局部轴下的投影,旋转分量两者相同。- 非有限数值一律拒绝:
applyTranslation/applyRotation/setNodePose/setNodeTransform遇到NaN/Infinity直接返回,不会污染D,也不发事件。 - 撤销栈是共享的:
transform.undo()/redo()与viewer.history操作同一条栈,变换与可见性 / 外观 / 标签编辑按时间交错撤销。新代码优先用viewer.history。 - 一次数值提交发三个事件(
transform-start→transform-changed→transform-end);gizmo 拖拽期间transform-changed逐帧发,松手才发transform-end。要在拖拽结束后才做重活(重算 BOM、写盘),监听transform-end。 TransformHistory/HistoryStep两个导出类型已废弃,改用EditHistorySnapshot/SerializedEditStep。
完整签名与延伸
TransformManager— 全部方法的精确签名。NodePose/GizmoSpace/GizmoMode— 位姿与 gizmo 取值。- 指南:编辑能力 — 变换 / 结构 / 标签三类编辑与统一撤销栈的全景。
- 相邻域:viewer.history(统一撤销栈)、viewer.explodeEdit(把爆炸位移写进同一个
D通道)、viewer.explode(不写D的浏览态爆炸)。