第8章:手势与交互处理
本章概要
核心问题:如何优雅地处理用户的各种交互操作?
无限画布的交互体验是用户感知最直接的部分。本章将深入分析三大交互系统:
- 手势系统:滚轮缩放、滚轮平移、触摸板操作
- 快捷键系统:全局快捷键注册与响应
- 抓手模式:画布拖拽平移的交互实现
目录
8.1 鼠标事件处理
8.1.1 滚轮缩放原理
滚轮缩放是无限画布最核心的交互之一。其关键在于以鼠标位置为中心进行缩放,而不是以画布中心。
typescript
// 来源:infinite-plugins/src/plugins/viewport-plugin/hooks/use-gesture.ts
function handleZoom(event: WheelEvent) {
const { left, top } = editor.containerRect;
// 1. 计算鼠标在画布容器内的相对位置
const x = event.clientX - left;
const y = event.clientY - top;
const { viewport } = editor;
// 2. 限制 deltaY 范围,避免高速滚动产生突变
const deltaY = clamp(event.deltaY, -80, 80);
// 3. 计算缩放因子:deltaY 越大,缩小越多
// 0.6 是采样系数,控制缩放速率
const delta = (100 - deltaY * 0.6) / 100;
// 4. 应用缩放限制
const { minZoom, maxZoom } = editor.viewport.limit;
const zoom = clamp(delta * viewport.zoom, minZoom, maxZoom);
// 5. 构造变换矩阵:以鼠标位置为中心缩放
const matrix = Matrix.IDENTITY
.translate(-x, -y) // 平移到原点
.scale(1 / viewport.zoom, 1 / viewport.zoom) // 还原当前缩放
.scale(zoom, zoom) // 应用新缩放
.translate(x, y); // 平移回原位
// 6. 计算新的视口位置
const point = matrix.apply({
x: editor.viewport.x,
y: editor.viewport.y,
});
// 7. 更新视口状态
editor.viewport.setZoom(zoom);
editor.viewport.setPosition(point.x, point.y);
}缩放数学原理:
以鼠标位置 (mx, my) 为中心,从 zoom1 缩放到 zoom2:
步骤分解:
┌──────────────────────────────────────────────────────────────┐
│ 原始状态 目标状态 │
│ │
│ Viewport: (x1, y1, zoom1) Viewport: (x2, y2, zoom2) │
│ │
│ 鼠标位置 (mx, my) 指向的世界坐标点在缩放前后应保持不变 │
│ │
│ 世界坐标 = (mx - x) / zoom │
│ │
│ 缩放前:worldX = (mx - x1) / zoom1 │
│ 缩放后:worldX = (mx - x2) / zoom2 │
│ │
│ 由 worldX 相等可得: │
│ (mx - x1) / zoom1 = (mx - x2) / zoom2 │
│ │
│ 解得: │
│ x2 = mx - (mx - x1) * zoom2 / zoom1 │
│ = mx - (mx - x1) * (zoom2 / zoom1) │
└──────────────────────────────────────────────────────────────┘
矩阵表示法:
┌ ┐ ┌ ┐ ┌ ┐
T(-mx, -my) = │ 1 0 -mx│ │ x1 │ │x1 - mx│
│ 0 1 -my│ × │ y1 │ = │y1 - my│
│ 0 0 1 │ │ 1 │ │ 1 │
└ ┘ └ ┘ └ ┘
S(k) = zoom2/zoom1
┌ ┐ ┌ ┐ ┌ ┐
S(1/z1) × S(z2) = │ k 0 0 │ │x1 - mx│ │k(x1 - mx) │
│ 0 k 0 │ × │y1 - my│ = │k(y1 - my) │
│ 0 0 1 │ │ 1 │ │ 1 │
└ ┘ └ ┘ └ ┘
T(mx, my) 平移回原位:
┌ ┐ ┌ ┐
│k(x1-mx)+mx │ │mx + k(x1 - mx) │
结果 = │k(y1-my)+my │ = │my + k(y1 - my) │
│ 1 │ │ 1 │
└ ┘ └ ┘8.1.2 滚轮平移实现
普通滚轮(无 Ctrl 键)用于平移画布:
typescript
// 来源:infinite-plugins/src/plugins/viewport-plugin/hooks/use-gesture.ts
function handleScroll(event: WheelEvent) {
// deltaX: 水平滚动量(触摸板左右滑动)
// deltaY: 垂直滚动量(触摸板上下滑动/鼠标滚轮)
editor.viewport.translate(-event.deltaX, -event.deltaY);
}Viewport.translate 实现:
typescript
// 来源:infinite-renderer/src/viewport/viewport.ts
translate(dx: number, dy: number): void {
this.setPosition(
this.position.x + dx,
this.position.y + dy
);
}
setPosition(x = this.position.x, y = this.position.y): void {
this.page.setState({ x, y });
this.position.set(x, y);
}8.1.3 Ctrl 键区分策略
系统通过 Ctrl/Cmd 键来区分缩放和平移操作:
typescript
// 来源:infinite-plugins/src/plugins/viewport-plugin/hooks/use-gesture.ts
function handleWheel(event: WheelEvent) {
event.preventDefault();
event.stopPropagation();
// Ctrl/Cmd + 滚轮 = 缩放
// 普通滚轮 = 平移
if (event.ctrlKey || event.metaKey) {
handleZoom(event);
} else {
handleScroll(event);
}
}交互模式对照表:
| 操作 | 鼠标 | 触摸板 | 效果 |
|---|---|---|---|
| 滚轮/双指上下滑动 | 滚轮上下 | 双指上下 | 垂直平移 |
| 双指左右滑动 | - | 双指左右 | 水平平移 |
| Ctrl + 滚轮 | Ctrl + 滚轮 | Ctrl + 双指 | 以光标为中心缩放 |
| 双指捏合 | - | 双指捏合 | 以捏合中心缩放 |
事件绑定与清理:
typescript
// 来源:infinite-plugins/src/plugins/viewport-plugin/hooks/use-gesture.ts
export function useGesture(editor: VPEditor, surface: BoardSurface, options: GestureOptions = {}) {
const { gesturerSelectors = [], disableGesture = false } = options;
const container = editor.container[0] as HTMLDivElement;
// 全局滚轮事件处理(禁用浏览器默认缩放行为)
function handleGlobalWheel(event: WheelEvent) {
if (event.ctrlKey || event.metaKey) {
event.preventDefault(); // 阻止浏览器缩放
}
// 检查事件是否来自允许的选择器
const closestValid = gesturerSelectors.some((selector) => {
return !!(event.target as Element).closest(selector);
});
if (closestValid) {
handleWheel(event);
}
}
onMounted(() => {
if (disableGesture) return;
// 容器内事件
container.addEventListener('wheel', handleWheel);
// 全局事件(阻止默认缩放)
document.body.addEventListener('wheel', handleGlobalWheel, {
passive: false, // 必须为 false 才能调用 preventDefault
});
});
onBeforeUnmount(() => {
if (disableGesture) return;
container.removeEventListener('wheel', handleWheel);
document.body.removeEventListener('wheel', handleGlobalWheel);
});
}8.2 抓手模式
8.2.1 模式切换机制
抓手模式允许用户通过拖拽来平移画布,类似于 Figma 的 "Hand Tool"。
抓手模式状态机:
┌────────────────┐ ┌────────────────┐
│ │ 按下 H 键 / 空格 │ │
│ 普通模式 │ ─────────────────→ │ 抓手模式 │
│ cursor:default│ │ cursor:hand │
│ │ ←───────────────── │ │
└────────────────┘ 松开空格/按 ESC └────────────────┘
点击元素
新增/删除元素抓手模式实现:
typescript
// 来源:infinite-plugins/src/plugins/hand-plugin/hand-plugin.tsx
export function createInfiniteHandPlugin(): Plugin {
const isMoving = ref(false);
let modeOpened = false; // 抓手模式开启状态
const quitMode = (editor: VPEditor) => {
editor.cursor = 'default';
modeOpened = false;
editor?.plugins.invokeCommand('toolbarManage:open');
};
return {
name: 'board-hand',
version: '1.0.0',
commands(editor) {
return {
// 退出抓手模式
quit(type: 'h' | 'space' | 'esc' | 'click') {
// 抓手模式下空格键失效(不能用空格退出 H 开启的模式)
if (type === 'space' && modeOpened === true) return;
quitMode(editor);
},
// 进入抓手模式
open(type: 'h' | 'space' | 'click') {
// 退出当前编辑状态
const element = editor.currentSubElement || editor.currentElement;
if (element?.$editing) {
editor.hideElementEditor(element);
}
editor?.plugins.invokeCommand('toolbarManage:close');
editor.cursor = 'hand';
nextTick(() => {
if (type === 'h' || type === 'click') {
modeOpened = true;
editor.cursor = 'hand';
}
});
},
};
},
// 监听事件,自动退出抓手模式
events(editor: VPEditor) {
return {
'element.dragMove'() { isMoving.value = true; },
'element.dragEnd'() { isMoving.value = false; },
'element.transformStart'() { isMoving.value = true; },
'element.transformEnd'() { isMoving.value = false; },
// 元素操作时退出抓手模式
'base.commit'(action: VPEAction, transaction: YTransaction) {
const tags = ['add_element', 'remove_element', 'change_element'];
if (modeOpened && transaction.local && tags.includes(action.tag)) {
if (action.tag === 'change_element' && !action?.modifiedPropsKeys?.length) {
return;
}
quitMode(editor);
}
},
};
},
};
}8.2.2 拖拽平移实现
抓手模式下的拖拽平移通过 Vue 组件实现:
typescript
// 来源:infinite-plugins/src/plugins/hand-plugin/hand-plugin.tsx
mount(editor) {
const box = document.createElement('div');
const container = editor.container[0] as HTMLDivElement;
container?.append(box);
vm = new Vue({
setup() {
function handleGrab(x: number, y: number) {
// 直接使用拖拽位移更新视口
const deltaX = x;
const deltaY = y;
editor.viewport.translate(deltaX, deltaY);
}
const { handComponentRef } = useMiddleButtonDrag(editor);
return () => {
const props = {
editor,
toggle: !isMoving.value && editor.cursor === 'hand',
onGrab: handleGrab,
};
return <Hand ref={handComponentRef} {...{ props }} />;
};
},
}).$mount(box);
}拖拽计算:
拖拽平移示意:
mousedown (x0, y0) mousemove (x1, y1)
│ │
▼ ▼
┌──────●─────────────────────────────────────┐
│ │ │
│ │ deltaX = x1 - x0 │
│ └────────────────→ │
│ deltaY = y1 - y0 │
│ ↓ │
│ │
│ viewport.translate(deltaX, deltaY) │
│ │
└─────────────────────────────────────────────┘8.2.3 中键拖拽
支持鼠标中键(滚轮按下)拖拽平移:
typescript
// 来源:infinite-plugins/src/plugins/hand-plugin/use-middle-button-drag.ts
export function useMiddleButtonDrag(editor: VPEditor) {
const handComponentRef = ref<ComponentPublicInstance | null>(null);
let isDragging = false;
let lastX = 0;
let lastY = 0;
function handleMouseDown(event: MouseEvent) {
// 检测中键(button === 1)
if (event.button === 1) {
isDragging = true;
lastX = event.clientX;
lastY = event.clientY;
event.preventDefault();
}
}
function handleMouseMove(event: MouseEvent) {
if (!isDragging) return;
const deltaX = event.clientX - lastX;
const deltaY = event.clientY - lastY;
editor.viewport.translate(deltaX, deltaY);
lastX = event.clientX;
lastY = event.clientY;
}
function handleMouseUp(event: MouseEvent) {
if (event.button === 1) {
isDragging = false;
}
}
onMounted(() => {
const container = editor.container[0];
container.addEventListener('mousedown', handleMouseDown);
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
});
onBeforeUnmount(() => {
const container = editor.container[0];
container.removeEventListener('mousedown', handleMouseDown);
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
});
return { handComponentRef };
}8.3 Hover 交互
8.3.1 悬停检测流程
Hover 插件负责检测鼠标悬停位置的元素,并触发高亮效果:
typescript
// 来源:infinite-plugins/src/plugins/hover-plugin/hover-plugin.ts
function createHoverPlugin(): Plugin {
return {
name: 'hover',
version: '1.0.0',
events(editor) {
return {
'base.mouseMove': (event) => {
// 1. 检查前置条件
if (checkIsMousedown() || editor.cursor !== 'default') return;
// 2. 检查是否有 DOM 遮挡
const target = document.elementFromPoint(event.pageX, event.pageY);
const wrap = editor.shell.querySelector('.editor-board');
const ignore = !wrap || !wrap.contains(target);
// 3. hover 输入框时清除选中态
if (ignore || dom.isEditable(event.target as HTMLElement)) {
editor.plugins.invokeCommand('highlight:inactivate');
return;
}
// 4. 通过坐标查找元素
const point = editor.pointFromEvent(event);
const element = editor.focusElementByPoint(
point.x,
point.y,
editor.currentRoot,
false,
);
// 5. 触发高亮
if (element && element !== editor.currentElement) {
editor.plugins.invokeCommand('highlight:activate', [element]);
editor.currentHoverElement = element;
} else {
editor.plugins.invokeCommand('highlight:inactivate');
editor.currentHoverElement = null;
}
},
'base.mouseLeave': () => {
// 绘制期间不清除高亮
if (checkIsMousedown() || editor.cursor !== 'default') return;
editor.plugins.invokeCommand('highlight:inactivate');
},
'base.mouseUp': () => {
editor.plugins.invokeCommand('highlight:inactivate');
},
};
},
};
}Hover 检测流程图:
┌──────────────────────────────────────────────────────────────┐
│ mouseMove 事件 │
└──────────────────────────────────────────────────────────────┘
│
▼
┌───────────────────────────────┐
│ 正在拖拽 || cursor !== default │
└───────────────────────────────┘
Yes │ │ No
▼ ▼
返回 ┌──────────────────┐
│ 有 DOM 遮挡? │
└──────────────────┘
Yes │ │ No
▼ ▼
清除高亮 ┌──────────────────┐
返回 │ 是输入框区域? │
└──────────────────┘
Yes │ │ No
▼ ▼
清除高亮 ┌──────────────────┐
返回 │ 查找目标元素 │
│ focusElementByPoint│
└──────────────────┘
│
▼
┌───────────────────────────────┐
│ element && element !== selected │
└───────────────────────────────┘
Yes │ │ No
▼ ▼
┌──────────────────┐ 清除高亮
│ 激活高亮 │
│ highlight:activate│
└──────────────────┘8.3.2 高亮联动
Hover 状态与图层管理面板联动:
typescript
// 来源:infinite-plugins/src/plugins/hover-plugin/hover-plugin.ts
commands(editor) {
return {
initialize() {
unwatch!.push(
// 选中元素或非默认 cursor 时清除高亮
watch(
() => !!editor.currentElement || editor.cursor !== 'default',
(bool) => {
if (bool) {
editor.plugins.invokeCommand('highlight:inactivate');
}
},
),
// 同步图层管理面板的 hover 状态
watch(
() => editor.currentHoverElement,
(element) => {
if (element) {
editor.plugins.invokeCommand('highlight:activate', [element]);
} else {
editor.plugins.invokeCommand('highlight:inactivate');
}
},
),
);
},
};
}8.4 快捷键系统
8.4.1 Hotkeys 架构
快捷键系统基于 hotkeys-js 库封装,支持组合键和作用域:
typescript
// 来源:infinite-plugins/src/plugins/hotkeys-plugin/hotkeys-plugin.tsx
function createHotkeysPlugin(
handleMap: HotkeyMap = createHandlers(),
getRootElement: () => Element = () => document.documentElement,
scope: string = hotkeys.DefaultScope,
): Plugin {
const PLUGIN_NAME = 'hotkeys';
const state = {
disabled: false,
hotkeys,
handleMap,
};
let hotkeyScope: HotkeyScope | null = null;
return {
name: PLUGIN_NAME,
version: '1.0.0',
commands(editor) {
return {
getState() {
return state;
},
enable() {
state.disabled = false;
if (!hotkeyScope) {
// 包装处理函数,添加拦截逻辑
Object.keys(handleMap).forEach((key) => {
const originFunc = handleMap[key];
handleMap[key] = async function (e: KeyboardEvent) {
await Promise.resolve();
if (e.defaultPrevented) return; // 已被阻止
if (state.disabled) return; // 插件禁用
if (!editor.editable) return; // 编辑器不可编辑
return originFunc.call(editor, e);
};
});
hotkeys.init(handleMap, scope, editor);
hotkeyScope = hotkeys.createScope(getRootElement(), scope);
}
},
disable() {
state.disabled = true;
},
bind(hotkey: string, scope: string, callback: HotkeyHandler) {
state.hotkeys.hotkeys(hotkey, scope, callback);
},
unbind(hotkey: string, scope: string) {
state.hotkeys.unbind(hotkey, scope);
},
};
},
mount(editor) {
editor.plugins.invokeCommand(`${PLUGIN_NAME}:enable`);
},
};
}8.4.2 快捷键映射表
系统预定义了一套完整的快捷键:
typescript
// 来源:infinite-plugins/src/plugins/hotkeys-plugin/hotkeys-plugin.tsx
const createHandlers = () => {
return {
// 空格键:临时进入抓手模式
'space'(this: VPEditor) {
const { enableSurfaceRender } = this;
if (enableSurfaceRender && (this.cursor === 'default' || this.cursor === 'hand')) {
const elem = this.currentSubElement || this.currentElement;
if (elem && isVideoElementModel(elem)) {
// 视频元素:播放/暂停
} else if (this.cursor !== 'hand') {
this.plugins.invokeCommand('board-hand:open', 'space');
}
}
},
// H 键:切换抓手模式
'h'(this: VPEditor) {
const { enableSurfaceRender } = this;
if (enableSurfaceRender) {
if (this.cursor === 'hand') {
this.plugins.invokeCommand('board-hand:quit', 'h');
} else {
this.plugins.invokeCommand('board-hand:open', 'h');
}
}
},
// ESC 键:退出当前模式
'esc'(this: VPEditor, e: KeyboardEvent) {
if (this.cursor !== 'default') {
this.plugins.invokeCommand('board-hand:quit', 'esc');
}
// 调用原有 esc 处理
if ('esc' in _handleMap) {
try {
_handleMap.esc.call(this, e);
} catch (error) {
console.error(error);
}
}
},
// Ctrl/Cmd + 1:适应画布
'ctrl+1, command+1'(this: VPEditor, e: KeyboardEvent) {
this.plugins.invokeCommand('board-viewport:zoomToFit');
e.preventDefault();
},
// Ctrl/Cmd + 0:重置缩放为 100%
'ctrl+0, command+0'(this: VPEditor, e: KeyboardEvent) {
this.plugins.invokeCommand('board-viewport:zoomTo', 1);
e.preventDefault();
},
// Ctrl/Cmd + -:缩小
'ctrl+-, command+-, ctrl+minus, command+minus'(this: VPEditor, e: KeyboardEvent) {
this.plugins.invokeCommand('board-viewport:zoomIn');
e.preventDefault();
},
// Ctrl/Cmd + +:放大
'ctrl+=, command+=, ctrl+plus, command+plus'(this: VPEditor, e: KeyboardEvent) {
this.plugins.invokeCommand('board-viewport:zoomOut');
e.preventDefault();
},
// Ctrl/Cmd + Enter:快速插入便签
'ctrl+enter, command+enter'(this: VPEditor, e: KeyboardEvent) {
if (this.mode === 'board') {
this.services.get('stickyNote')?.quickInsertStickyNote({});
e.preventDefault();
}
},
};
};快捷键总览:
| 快捷键 | 功能 | 说明 |
|---|---|---|
Space | 临时抓手 | 按住时进入抓手模式,松开退出 |
H | 切换抓手 | 按下进入,再按退出 |
ESC | 退出模式 | 退出抓手模式或当前编辑状态 |
Ctrl/Cmd + 0 | 重置缩放 | 缩放到 100% |
Ctrl/Cmd + 1 | 适应画布 | 缩放以显示所有内容 |
Ctrl/Cmd + + | 放大 | 增加缩放比例 |
Ctrl/Cmd + - | 缩小 | 减小缩放比例 |
Ctrl/Cmd + Enter | 添加便签 | 快速插入便签元素 |
8.4.3 作用域机制
快捷键支持作用域隔离,避免与其他组件冲突:
typescript
// 快捷键作用域
// 全局作用域
hotkeys.init(handleMap, hotkeys.DefaultScope, editor);
// 创建作用域
hotkeyScope = hotkeys.createScope(getRootElement(), scope);
// 绑定到特定作用域
hotkeys.hotkeys('ctrl+s', 'editor', (e) => {
// 只在 editor 作用域生效
e.preventDefault();
save();
});
// 解绑
hotkeys.unbind('ctrl+s', 'editor');空格键松开检测:
typescript
// 来源:infinite-plugins/src/plugins/hotkeys-plugin/hotkeys-plugin.tsx
events(editor) {
return {
'base.keyUp': async (e) => {
await Promise.resolve();
// 空格键松开时退出抓手模式
if (e.keyCode === 32 && editor.cursor === 'hand') {
editor.plugins.invokeCommand('board-hand:quit', 'space');
}
},
};
}8.5 交互性能优化
8.5.1 节流与防抖
对高频事件进行节流处理:
typescript
// deltaY 限制,避免高速滚动
const deltaY = clamp(event.deltaY, -80, 80);
// 缩放速率控制
const delta = (100 - deltaY * 0.6) / 100;为什么限制 deltaY?
不同设备的 deltaY 差异:
普通鼠标滚轮:
├── 一格滚动:deltaY ≈ ±100
└── 高速滚动:deltaY 可达 ±500+
触摸板:
├── 轻触滑动:deltaY ≈ ±10~30
└── 快速滑动:deltaY ≈ ±100~200
高精度鼠标:
├── 一格滚动:deltaY ≈ ±1~10
└── 连续滚动:deltaY 逐渐累积
限制 deltaY 在 [-80, 80] 范围:
1. 避免高速滚动导致的视口跳变
2. 统一不同设备的缩放体验
3. 防止意外的极端缩放8.5.2 事件委托
Hover 检测使用事件委托避免频繁的 DOM 查询:
typescript
// 使用 document.elementFromPoint 而不是遍历所有元素
const target = document.elementFromPoint(event.pageX, event.pageY);
const wrap = editor.shell.querySelector('.editor-board');
const ignore = !wrap || !wrap.contains(target);8.5.3 渲染调度
交互事件与渲染帧分离,避免阻塞:
typescript
// 快捷键处理使用异步
handleMap[key] = async function (e: KeyboardEvent) {
await Promise.resolve(); // 让出执行权
if (e.defaultPrevented) return;
if (state.disabled) return;
if (!editor.editable) return;
return originFunc.call(editor, e);
};nextTick 延迟更新:
typescript
// 来源:infinite-plugins/src/plugins/hand-plugin/hand-plugin.tsx
open(type: 'h' | 'space' | 'click') {
editor.cursor = 'hand';
// 等待微任务结束再确认模式
nextTick(() => {
if (type === 'h' || type === 'click') {
modeOpened = true;
editor.cursor = 'hand';
}
});
}8.6 本章小结
交互系统架构
┌─────────────────────────────────────────────────────────────┐
│ 交互系统架构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 事件监听层 │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ wheel │ │ keyboard │ │ mouse │ │ │
│ │ │ 事件 │ │ 事件 │ │ 事件 │ │ │
│ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │
│ └───────┼─────────────┼─────────────┼─────────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 插件处理层 │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Gesture │ │ Hotkeys │ │ Hover │ │ │
│ │ │ Plugin │ │ Plugin │ │ Plugin │ │ │
│ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │
│ │ │ │ │ │ │
│ │ │ │ │ │ │
│ │ ┌────┴─────┐ ┌────┴─────┐ ┌────┴─────┐ │ │
│ │ │ Hand │ │ Command │ │Highlight │ │ │
│ │ │ Plugin │ │ System │ │ Plugin │ │ │
│ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │
│ └───────┼─────────────┼─────────────┼─────────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 视口响应层 │ │
│ │ │ │
│ │ viewport.setZoom() viewport.translate() │ │
│ │ viewport.setPosition() │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘关键代码路径
| 功能模块 | 文件路径 |
|---|---|
| 手势处理 | infinite-plugins/src/plugins/viewport-plugin/hooks/use-gesture.ts |
| 抓手模式 | infinite-plugins/src/plugins/hand-plugin/hand-plugin.tsx |
| 快捷键 | infinite-plugins/src/plugins/hotkeys-plugin/hotkeys-plugin.tsx |
| Hover 检测 | infinite-plugins/src/plugins/hover-plugin/hover-plugin.ts |
核心要点
- 以光标为中心缩放:通过矩阵变换实现,保证用户关注点稳定
- Ctrl 键区分:滚轮操作通过 Ctrl 键区分缩放和平移
- 抓手模式状态机:空格临时、H 键切换、ESC 退出
- 快捷键作用域:避免与编辑态冲突
- 性能优化:deltaY 限制、异步处理、事件委托
下一章预告
第9章将讨论插件系统架构,包括:
- PluginSystem 设计模式
- 插件类型分类
- 核心插件解析
- 自定义插件开发指南
