第 7 章:vtkRenderWindowInteractor 与交互样式

学习目标

本章解释鼠标拖动为何能改变相机。读完后,你应该能取得 Interactor、指定交互样式、暂时停止交互,并正确订阅一次交互的开始与结束事件。你还应知道“DOM 输入事件”和“相机操作规则”由不同对象负责。

7.1 输入经过哪些对象

第 2 阶段示例的交互路径可以概括为:

鼠标、触摸或键盘
        ↓
vtkRenderWindowInteractor
        ↓
vtkInteractorStyleTrackballCamera
        ↓
vtkCamera 的位置、方向或尺度发生变化
        ↓
重新渲染

<code>vtkRenderWindowInteractor</code> 接收并整理浏览器输入,把事件交给当前的交互样式。交互样式决定“这个输入意味着什么”:左键拖动是旋转相机,还是移动某个对象;滚轮是缩放,还是切换切片。把这两层分开,才能在不重写底层输入处理的情况下换一套操作方式。

<code>vtkGenericRenderWindow</code> 创建时已经准备了 Interactor,并给它设置默认的相机轨迹球样式。示例仍显式设置一次,目的是让你看清连接关系:

const interactor = genericRenderWindow.getInteractor();
const interactorStyle = vtkInteractorStyleTrackballCamera.newInstance();
interactor.setInteractorStyle(interactorStyle);

<code>interactor.getInteractorStyle()</code> 可以用于检查当前样式。第 1 阶段中即使你没有写这些代码,也能拖动球体,是因为全屏辅助对象替你完成了交互初始化。

7.2 TrackballCamera 样式做了什么

本教程版本的 <code>vtkInteractorStyleTrackballCamera</code> 中,普通左键拖动会旋转相机;按 Shift 再左键拖动会平移;滚轮可以调整观察距离。触摸手势也由交互体系转成相应操作。不同鼠标组合可能触发旋转、平移或缩放,因此自定义交互前应先核对所用样式的实际行为。

关键是变动对象是相机。拖动后,球体与立方体的 Source 数据和 Actor 位置不会因此改变。第 2 阶段示例底部的相机位置读数会变化,可用来验证这一点。

未来显示二维切片时,可能更适合使用 <code>vtkInteractorStyleImage</code>;它与三维相机轨迹球的交互目标不同。选择样式应根据用户要操作的内容,而不是只看哪个样式的类名熟悉。

7.3 暂停和恢复交互

示例中的“暂停交互”按钮使用:

interactor.disable();

恢复时使用:

interactor.enable();

可以用 <code>interactor.getEnabled()</code> 检查当前状态。暂停之后,用户在画布上的拖动不再改变视角;程序仍然可以通过按钮调用 Camera API 和 <code>renderWindow.render()</code>。因此,<code>disable()</code> 控制的是这条用户输入路径,并不是把整个渲染窗口关掉。

这类操作可用于临时进入标注、选择或界面弹窗模式。不过,实际项目中还要考虑怎样退出该模式、是否有其他监听器仍在处理输入。

7.4 订阅一次交互的开始与结束

有时你需要知道用户是否开始拖动,例如交互期间降低渲染质量、结束后恢复质量,或记录一个操作次数。这个示例在当前交互样式上订阅高层事件:

const startSubscription = interactorStyle.onStartInteractionEvent(() => {
  interactionCount += 1;
  updateStatus();
});

const endSubscription =
  interactorStyle.onEndInteractionEvent(updateStatus);

示例只显示计数,后续体渲染性能章节会用同样的“开始/结束”边界调整交互质量。

这里有一个经过本教程固定版本运行验证的细节:<code>onStartInteractionEvent</code> 和 <code>onEndInteractionEvent</code> 应从交互样式对象订阅。本示例若把它们写成 <code>interactor.onStartInteractionEvent(…)</code>,运行时会报方法不存在。Interactor 自身负责更底层的输入事件;学习 API 时要分清事件由哪个对象发出。

订阅会返回可取消的对象。在会卸载的页面组件里,要保留它并清理:

startSubscription.unsubscribe();
endSubscription.unsubscribe();

若更换交互样式,旧订阅仍属于旧样式。应先取消旧样式上的订阅,再为新样式建立订阅,避免计数失效或重复回调。

7.5 Interactor、DOM 和视图的生命周期

在本示例里,<code>genericRenderWindow.setContainer(viewport)</code> 不只关系到 canvas 放在哪里;Generic 对象还会让 Interactor 从对应容器接收事件。若把渲染视图移到另一个容器,应重新连接容器并更新尺寸,而不是仅仅通过 DOM 手动搬动 canvas。

单页应用组件卸载时,通常需要按资源归属清理:

startSubscription.unsubscribe();
endSubscription.unsubscribe();
resizeObserver.disconnect();
genericRenderWindow.delete();

本教程的纯页面示例在关闭页面时结束生命周期;在 React、Vue 等框架里,清理应放到组件卸载逻辑中。否则重新进入页面时,可能出现多个事件监听器、重复 canvas 或残留的图形资源。

7.6 如何判断一个交互问题来自哪里

现象 优先检查
能看到图像但拖动无效 Interactor 是否启用;当前样式是否正确;容器是否接收事件
只有某种鼠标操作无效 当前样式对该按钮或修饰键的定义
画面在动,状态计数不变 事件是否订阅在当前样式上;样式是否后来被替换
鼠标位置与画面不一致 容器与绘制视图尺寸是否同步,见第 5 章
点击页面按钮后相机仍会动 是否有其他输入监听器或控件把事件继续传入视图

本章小结

Interactor 处理输入,交互样式解释输入,Camera 是本阶段三维浏览操作的主要被修改对象。第 2 阶段至此完成了渲染窗口、场景相机和交互路径的拆解。下一阶段会回到 vtk.js 最关键的数据对象,详细学习数组、点、单元和规则网格。