渲染器无关的框选 / 多选拖拽引擎。来自 X6 issue 4823 的大画布拖拽性能经验,核心不绑定 X6、DOM 或任何具体渲染器。
三条性能保证(也是全部设计动机):
- rAF 合帧:高频
pointermove只累计位移,每帧最多 flush 一次。 - 拖拽快照:pointerdown 一次性冻结"要动谁",拖动中零重复计算。
- preview / commit 分离:拖动中只动预览层,真实模型只在
pointerup提交一次。
npm test # 4 项核心行为测试
npm run dev # 启动 demoDemo:http://127.0.0.1:5173/demos/svg-selection-demo.html(1000 个 SVG 对象,面板实时显示 pointermove / rAF flush / commit 次数与帧耗时)。
引擎本身零依赖;宿主只需注入一个 adapter 对象(可参考 src/svg-selection-adapter.js,136 行):
import { SelectionDragEngine } from 'selection-drag-engine'
const engine = new SelectionDragEngine({ adapter: myAdapter })| 方法 | 调用时机 | 职责 |
|---|---|---|
hitTest(rect, { mode }) |
仅 pointerup | 返回框选命中的 id 列表;mode: 'intersects' | 'contains' |
setSelection(ids) |
选择集变化时 | 应用新选择集 |
getSelection() |
可选 | 返回当前选择集;不实现则用引擎内部状态 |
createDragSnapshot(ids) |
pointerdown | 冻结选中对象的可变副本(引擎不解释其结构,只透传) |
beginDragPreview(snapshot) |
pointerdown 后 | 建立预览层(clone / 遮罩 / 半透明,宿主自定) |
previewDrag(snapshot, delta) |
每帧最多 1 次 | 只更新预览层 transform;必须廉价,禁止写真实模型 |
commitDrag(snapshot, delta) |
仅 pointerup | 把 delta 应用到真实数据与渲染;全拖拽唯一一次 |
endDragPreview(snapshot) |
pointerup / cancel | 销毁预览层、恢复源对象状态 |
renderRubberband(rect | null) |
框选期间每帧最多 1 次 | 画/隐藏橡皮筋矩形;null 表示隐藏 |
实现守则:
previewDrag里只改 transform(或等价的廉价属性),不遍历业务数据、不发事件。- 所有真实写入收敛到
commitDrag;hitTest只在抬起时跑——O(n) 只付一次。 - 忽略引擎在非
pointerup时刻的任意中间态。
- 已实现:框选、多选、拖拽预览、拖拽提交、指标回调。
- 未实现(按需自加):resize / rotate、edge routing、undo/redo、spatial index(当前 hitTest 为 O(n),百~千级对象够用)、DOM/Canvas adapter。
- 框选命中允许 O(n),但只发生在框选结束时;snapshot / commit 允许 O(k),k 为选中数量。
src/selection-drag-engine.js— 状态机 + rAF 合帧核心(引擎唯一必读文件)src/svg-selection-adapter.js— SVG adapter 参考实现src/geometry.js/src/raf-scheduler.js— 矩形工具、rAF 适配tests/selection-drag-engine.test.js— 合帧、命中时机、几何行为docs/design.md— 设计动机与性能约束demos/svg-selection-demo.html— 1000 对象实例
X6 issue 4823:大数据量框选拖拽时,旧实现把"查图 + 移动 + 更新高亮框"全挂在高频 mousemove 上,成本 = 事件数 × 选中数。本库把同一套修复(合帧 / 缓存 / 冻结)固化为独立内核。背景分析见 issue 讨论。