Skip to content

第8章:手势与交互处理

本章概要

核心问题:如何优雅地处理用户的各种交互操作?

无限画布的交互体验是用户感知最直接的部分。本章将深入分析三大交互系统:

  1. 手势系统:滚轮缩放、滚轮平移、触摸板操作
  2. 快捷键系统:全局快捷键注册与响应
  3. 抓手模式:画布拖拽平移的交互实现

目录


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

核心要点

  1. 以光标为中心缩放:通过矩阵变换实现,保证用户关注点稳定
  2. Ctrl 键区分:滚轮操作通过 Ctrl 键区分缩放和平移
  3. 抓手模式状态机:空格临时、H 键切换、ESC 退出
  4. 快捷键作用域:避免与编辑态冲突
  5. 性能优化:deltaY 限制、异步处理、事件委托

下一章预告

第9章将讨论插件系统架构,包括:

  • PluginSystem 设计模式
  • 插件类型分类
  • 核心插件解析
  • 自定义插件开发指南

如有转载或 CV 的请标注本站原文地址