# Gesture (手势与事件)

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

## Schema

> **Definition**: [GestureEvent](../../five/utils/event.d.ts)

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

```typescript
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)

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

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

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

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

```typescript
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("未点击到任何模型");
  }
});
```

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

```typescript
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

*   **查看事件触发**: 在控制台监听所有手势。
    ```typescript
    ["pan", "tap", "pinch", "press"].forEach(type => {
      five.on(`gesture.${type}` as any, (e) => console.log(type, e));
    });
    ```
*   **Helper Line**: 如果射线检测不准，可以画出射线辅助线。
    ```typescript
    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 (模式)](./mode.md): 了解不同模式下的默认行为。
*   [State (状态)](./state.md): 手势最终改变的是 State。
*   [Raycast (射线检测)](./raycast.md): 深入了解 `intersectRaycaster` 的工作原理。

---

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