# Gesture (手势与事件)

- Summary: Five 提供了一套跨平台的手势事件系统，抹平了 PC 与移动端的差异，并针对不同 Mode 提供了默认的交互逻辑。
- Schema: `GestureEvent` 继承自 `StateEvent`，包含了手势中心点、射线等信息。
- Concepts: Gesture Type, Default Behavior, PreventDefault。
- Examples: 阻止默认行为、点击添加标记。

## Schema

> Definition: GestureEvent

所有手势事件的回调参数均为 `GestureEvent` 类型：

    import { State } from "@realsee/five";
    import * as THREE from "three";

    interface GestureEvent {
      type: string;             // 事件类型，如 "gesture.tap"
      timeStamp: number;        // 时间戳
      target: EventTarget | null;
      pointerType: 'touch' | 'pen' | 'mouse' | 'kinect';

      // 手势中心点信息
      center: {
        x: number;              // 屏幕 X 坐标
        y: number;              // 屏幕 Y 坐标
        raycaster: THREE.Raycaster; // 从相机发出的射线（已包含坐标变换）
      };

      // 当前相机状态
      state: State;

      // 是否是用户触发（区别于代码调用）
      userAction: boolean;

      // 阻止默认行为
      preventDefault(): void;
      defaultPrevented: boolean;
    }

## Concepts

Five 的手势系统基于 Hammer.js 封装，主要包含以下几种手势：

- gesture.fire: 任意事件被触发。
- gesture.pan: 拖拽/平移。
- gesture.pinch: 双指缩放。
- gesture.tap: 点击。
- gesture.dbltap: 双击。
- gesture.press: 长按。
- gesture.mousewheel: 鼠标滚轮。

### 模式差异 (Mode Specifics)

不同的 Mode 对手势有不同的默认响应：

手势 | Panorama (全景) | Mapview (模型) | 说明
--- | --- | --- | ---
Pan | 旋转相机视角 (Look around) | 围绕模型旋转/平移相机 (Orbit/Pan) |
Pinch | 调整 FOV (Zoom in/out) | 调整相机距离 (Distance) |
Tap | 移动点位 (Move to pano) | 切换到全景 (Enter pano) | 若点击到地面/模型，Mapview 下默认无响应，除非点击到点位标记
DblTap | 动画调整 FOV | 聚焦并拉近视角 (Fly to) |
MouseWheel | 调整 FOV | 调整相机距离 |

### 事件捕获与阻止 (Event Handling)

Five 的事件触发顺序如下：

1. Emit Event: Five 触发 `gesture.xxx` 事件。
2. User Callback: 开发者注册的监听函数被执行。
3. Default Behavior: 如果开发者没有调用 `event.preventDefault()`，Five 执行默认逻辑（如旋转相机、移动点位）。

因此，如果你想自定义交互（例如：点击不走点，而是弹出一个框），必须调用 `event.preventDefault()`。

## Examples

### 快速上手 (Quick Example)

监听点击事件并打印日志：

    five.on("gesture.tap", (event) => {
      console.log("点击位置:", event.center.x, event.center.y);
      // event.center.raycaster 可以用来做射线检测
    });

### 场景交互：点击添加标签球 (Add Label on Click)

这个示例展示了如何通过点击事件在场景中添加一个红色的球体，并阻止 Five 默认的点位移动行为。

    import * as THREE from "three";
    import { Five } from "@realsee/five";

    const five = new Five();

    five.on("gesture.tap", (event) => {
      // 1. 阻止默认行为（防止点击地面时 Five 自动走点或切换模式）
      event.preventDefault();

      // 2. 获取射线
      const raycaster = event.center.raycaster;

      // 3. 检测与模型的碰撞
      // five.model.intersectRaycaster 是 Five 提供的便捷方法，会自动检测所有可见模型
      const intersects = five.model.intersectRaycaster(raycaster);

      if (intersects.length > 0) {
        const intersect = intersects[0];
        const point = intersect.point; // 碰撞点世界坐标

        console.log("点击到了模型，坐标:", point);

        // 4. 创建一个小球
        const geometry = new THREE.SphereGeometry(0.1, 32, 32);
        const material = new THREE.MeshBasicMaterial({ color: 0xff0000 });
        const sphere = new THREE.Mesh(geometry, material);

        // 5. 设置位置并添加到场景
        sphere.position.copy(point);
        five.scene.add(sphere);

        // 强制刷新渲染，确保小球立即出现
        five.needsRender = true;
      } else {
        console.log("未点击到任何模型");
      }
    });

### 进阶用法：自定义双指缩放范围

    five.on("gesture.pinch", (event) => {
      // 获取当前 FOV
      const currentFov = five.state.fov;

      // 假设我们限制 FOV 在 80 到 100 之间
      // 注意：这只是逻辑演示，Five 内部有自己的 minFov/maxFov 配置，
      // 这里演示如何完全接管控制权。
      if (currentFov < 80 || currentFov > 100) {
        // 阻止 Five 的默认缩放
        event.preventDefault();
        console.log("已达到缩放极限");
      }
    });

## Debugging

- 查看事件触发: 在控制台监听所有手势。

      ["pan", "tap", "pinch", "press"].forEach(type => {
        five.on(`gesture.${type}` as any, (e) => console.log(type, e));
      });

- Helper Line: 如果射线检测不准，可以画出射线辅助线。

      five.scene.add(new THREE.ArrowHelper(raycaster.ray.direction, raycaster.ray.origin, 10, 0xff0000));
      five.needsRender = true;

## Common Pitfalls

1. 忘记 preventDefault: 如果你发现点击后既生成了小球，相机又自动飞走了，通常是因为没有调用 `event.preventDefault()`。
2. 坐标系混淆: `event.center.x/y` 是屏幕像素坐标，`event.center.raycaster` 已经处理了屏幕到世界的转换。直接使用 `raycaster` 最安全。
3. 点击穿透: 如果场景中有 HTML 覆盖层（如按钮），HTML 元素的点击事件可能会冒泡或被 Five 捕获。建议在 HTML 元素上使用 `stopPropagation` 或判断 `event.target`。

## Related

- Mode (模式): 了解不同模式下的默认行为。
- State (状态): 手势最终改变的是 State。
- Raycast (射线检测): 深入了解 `intersectRaycaster` 的工作原理。

    tags: [手势, 点击, 拖拽, 缩放, 长按, 鼠标, 触摸, 交互控制, 自定义交互, gesture, event, interaction, tap, pan, pinch, preventDefault]