# Screen & Space Projection (屏幕与空间坐标转换)

- **Summary**: 详解屏幕二维坐标与三维空间坐标/射线之间的相互转换关系，以及如何在 DOM 层与 3D 场景进行联动（如显示标签）。
- **Schema**: `five.project2d` 接口定义。
- **Concepts**: World Space, Screen Space, NDC, Raycasting.
- **Examples**: 空间点位转屏幕坐标 (标签功能)、屏幕坐标转空间射线。

## Schema

> **Definition**: [Five](../../five/application/five.d.ts)

```typescript
// defined in Five class
interface Five {
  /**
   * 计算三维坐标对应到屏幕的二维坐标
   * @param vector - 三维空间坐标 (World Space)
   * @param testModel - 是否进行模型遮挡检测 (默认 false)。如果为 true，且该点被模型遮挡，则返回 null。
   * @returns 二维屏幕坐标 (相对于 Canvas 左上角) 或 null (如果点在视野外或被遮挡)
   */
  project2d(vector: THREE.Vector3, testModel?: boolean): THREE.Vector2 | null;
}
```

## Concepts

### 1. 坐标系转换流程

在 Five (Three.js) 中，坐标转换通常遵循以下流程：

*   **World Space (世界坐标)**: 三维场景中的绝对坐标 (x, y, z)，单位通常为米。
*   **Camera Space (相机坐标)**: 相对于相机的坐标。
*   **NDC (Normalized Device Coordinates)**: 归一化设备坐标，x, y, z 范围均为 [-1, 1]。
*   **Screen Space (屏幕坐标)**: 对应 Canvas 画布的像素坐标。原点 (0, 0) 在左上角，x 向右增加，y 向下增加。

### 2. 转换关系

| 方向 | 方法 | 核心逻辑 | 用途 |
| :--- | :--- | :--- | :--- |
| **3D ➔ 2D** | `five.project2d(vector)` | World ➔ NDC ➔ Screen | 标签 (Tag)、HUD、跟随 3D 物体的 UI |
| **2D ➔ 3D** | `Raycaster` | Screen ➔ NDC ➔ Ray | 点击拾取 (Picking)、地面检测、放置物体 |

### 3. five.project2d

`five.project2d` 是 Five 提供的便捷方法，它封装了以下逻辑：
1.  将世界坐标投影 (Project) 到相机的 NDC 空间。
2.  判断点是否在视锥体 (Frustum) 内（即是否在视野范围内）。
3.  (可选) 发射射线检测该点与相机之间是否有模型遮挡。
4.  将 NDC 坐标转换为相对于 Canvas 左上角的像素坐标 (px)。

## Examples

### 1. 空间标签 (3D Point to DOM Label)

最典型的应用场景是在 3D 场景中的某个物体上方显示一个 HTML 标签。

```typescript
import { Five, Mode } from "@realsee/five";
import * as THREE from "three";

const five = new Five();
// ... 初始化 five ...

// 1. 创建标签 DOM 元素
const label = document.createElement("div");
label.textContent = "Living Room";
label.style.position = "absolute";
label.style.padding = "4px 8px";
label.style.background = "rgba(0, 0, 0, 0.6)";
label.style.color = "white";
label.style.borderRadius = "4px";
label.style.pointerEvents = "none"; // 避免阻挡鼠标事件
label.style.transform = "translate(-50%, -100%)"; // 居中并显示在点上方
label.style.display = "none"; // 默认隐藏
document.body.appendChild(label); // 添加到 container (注意 container 的定位 context)

// 2. 定义目标空间坐标 (例如客厅中心)
const targetPosition = new THREE.Vector3(1.5, 2.0, -3.0);

// 3. 在每一帧渲染时更新标签位置
five.on("render.prepare", () => {
  // 计算屏幕坐标
  // 参数2: false 表示不检测遮挡 (性能更好)；true 表示如果被墙挡住则隐藏
  const screenPos = five.project2d(targetPosition, true);

  if (screenPos === null) {
    // 如果返回 null，说明点在相机背面，或者被遮挡（如果开启了 testModel）
    label.style.display = "none";
  } else {
    label.style.display = "block";
    // 更新 DOM 位置
    // 注意：screenPos 是相对于 Canvas 的坐标。
    // 如果 Canvas 占满全屏，直接使用即可。
    // 如果 Canvas 有偏移，需加上 Canvas 的 offset。
    label.style.left = `${screenPos.x}px`;
    label.style.top = `${screenPos.y}px`;
  }
});
```

### 2. 屏幕点击转射线 (Screen to Ray)

将鼠标点击的屏幕坐标转换为 3D 射线，用于检测物体。

> **注意**: 更多射线检测细节请参考 [Raycast](./raycast.md)。

```typescript
import * as THREE from "three";

// 假设 container 是 five 的容器元素
const container = five.getElement()!.parentElement!;

container.addEventListener("click", (event) => {
  const raycaster = new THREE.Raycaster();
  const mouse = new THREE.Vector2();

  // 1. 计算 NDC 坐标 (x, y 范围 [-1, 1])
  // event.clientX/Y 是相对于视口的坐标
  // getBoundingClientRect 用于获取容器的位置
  const rect = container.getBoundingClientRect();

  mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
  mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1; // 注意 Y 轴翻转

  // 2. 从相机发射射线
  raycaster.setFromCamera(mouse, five.camera);

  // 3. 进行相交检测
  const intersects = five.model.intersectRaycaster(raycaster);

  if (intersects.length > 0) {
    console.log("Hit point:", intersects[0].point);
  }
});
```

## Common Pitfalls

1.  **坐标系原点**:
    *   DOM/CSS 坐标系原点在左上角，Y 轴向下。
    *   NDC/WebGL 坐标系原点在中心，Y 轴向上。
    *   `project2d` 返回的是 **DOM 风格** 的坐标 (左上角原点)，可以直接赋给 CSS `left`/`top`。
    *   但在进行 Raycaster 转换时，需要手动将 DOM 坐标转换为 NDC (Y 轴翻转)。

2.  **性能开销**:
    *   `project2d(pos, true)` 开启 `testModel` 会进行射线检测，性能开销较大。如果在 `render.prepare` 中对大量点进行检测，可能会导致掉帧。
    *   建议：对于大量标签，默认使用 `false`；或者仅对可视范围内的关键标签进行遮挡检测；或者限制检测频率（如每 10 帧检测一次）。

3.  **Canvas 尺寸**:
    *   `project2d` 内部使用 `five.renderer.getSize()` 和 `five.viewport` 计算。确保 Canvas 尺寸与 DOM 显示尺寸一致 (通常 Five 会自动处理，但如果使用了 CSS 缩放需注意)。

4.  **Null 返回值**:
    *   务必检查 `project2d` 的返回值是否为 `null`。点在相机背面、超出视锥体裁剪范围、或被遮挡（开启 testModel 时）都会返回 `null`。

## Related

*   [Raycast](./raycast.md): 详细的射线检测文档。
*   [Five](./five.md): Five 核心类说明。
*   [State](./state.md): 了解相机位姿 (Pose)。
*   [Coordinate System](./coordinate-system.md): 坐标系定义，理解世界坐标与屏幕坐标的关系。

---

```yaml
tags: [标签, 坐标转换, 信息面板, coordinate, screen, project, label, ui, tag, HUD, 3D转2D, overlay, tooltip]
```
