Skip to content

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-starttransform-changedtransform-end);gizmo 拖拽期间 transform-changed 逐帧发,松手才发 transform-end。要在拖拽结束后才做重活(重算 BOM、写盘),监听 transform-end
  • TransformHistory / HistoryStep 两个导出类型已废弃,改用 EditHistorySnapshot / SerializedEditStep

完整签名与延伸