> **本文回答**：`SnapManager` 对象捕捉 —— 10 种模式、位标志组合、开关、`querySnap`。

## 一、10 种捕捉模式

`SnapMode` 是位标志枚举，成员名为**缩写**（`END`/`MID`/…不是 `Endpoint`/`Midpoint`）：

| 成员 | 位标志 | SnapType | 说明 |
|---|---|---|---|
| `SnapMode.END` | `1` | `'end'` | 线 / 弧 端点 |
| `SnapMode.MID` | `2` | `'mid'` | 线段中点 |
| `SnapMode.CEN` | `4` | `'cen'` | 圆 / 弧 圆心 |
| `SnapMode.NOD` | `8` | `'nod'` | 点实体节点 |
| `SnapMode.QUA` | `16` | `'qua'` | 圆 / 椭圆象限点 |
| `SnapMode.INT` | `32` | `'int'` | 实体间交点 |
| `SnapMode.INS` | `64` | `'ins'` | 块引用插入点 |
| `SnapMode.PER` | `128` | `'per'` | 垂足 |
| `SnapMode.TAN` | `256` | `'tan'` | 切点 |
| `SnapMode.NEA` | `512` | `'nea'` | 实体上最近点 |
| `SnapMode.ALL` | `1023` | — | 全部 10 种（1\|2\|…\|512） |
| `SnapMode.NONE` | `16384` | — | 特殊值，不与其他位组合 |

## 二、启用 / 模式

```js
const { SnapMode } = vjmapext;

mapcadLayer.setSnapEnabled(true);
mapcadLayer.setSnapMode(
  SnapMode.END | SnapMode.MID | SnapMode.CEN | SnapMode.INT
);

// 全启 / 全关
mapcadLayer.setSnapMode(SnapMode.ALL);   // 1023
mapcadLayer.setSnapMode(0);              // 全关

// 关开关但保留模式
mapcadLayer.setSnapEnabled(false);
```

等价直读 / 直写：

```js
mapcadLayer.snapManager.enabled;      // bool
mapcadLayer.snapManager.snapMode;     // 当前位掩码
mapcadLayer.snapManager.setSnapMode(SnapMode.END | SnapMode.INT);
mapcadLayer.snapManager.setOnChange(() => refreshSnapUI());  // 模式变化回调
```

## 三、阈值（像素）

```js
mapcadLayer.snapManager.snapPixelRadius;   // 当前像素半径
mapcadLayer.snapManager.setSnapRadius(15); // 像素，不是 CAD 单位
```

`querySnap` 内部把像素半径换算成 CAD 距离 `snapRadiusCad`，zoom 变化不影响命中手感。`snapPixelRadius` 默认 `10` 像素。

> 补充：`snapMode` 会持久化到 `localStorage['mapcad-snap-mode']`（`setSnapMode` 自动写入、构造时读回）——刷新页面后捕捉模式仍在；`enabled` 开关不持久化。

## 四、querySnap：手动查捕捉点

```js
const hit = mapcadLayer.snapManager.querySnap(
  [120, 88],              // 光标 CAD 坐标（Point2DLike = Point2D | [x,y]）
  snapRadiusCad,          // CAD 单位半径
  referencePoint          // 可选：参考点（PER/TAN 需要），参数名 referencePoint
);
// hit = SnapPoint | null
```

`SnapPoint` 结构（**字段是 `position` 不是 `point`**）：

```ts
interface SnapPoint {
  type: SnapType;           // 'end'|'mid'|'cen'|'nod'|'qua'|'int'|'ins'|'per'|'tan'|'nea'
  position: Point2D;
  entityId: string;         // 命中的实体
  primary?: boolean;        // 距离最近的候选点
}
```

## 五、getSnapModeList（用于 UI）

```js
const { getSnapModeList } = vjmapext;

getSnapModeList();
// [{ mode: 1, type: 'end', label: '端点' },
//  { mode: 2, type: 'mid', label: '中点' },
//  ... 共 10 项，字段名是 label 不是 name]
```

可直接拿来渲染"对象捕捉"对话框。

## 六、和 getPoint 的关系

`getPoint` 默认走 `SnapManager.querySnap`：鼠标靠近实体特征点时橡皮筋终点会"吸"过去，`result.value` 就是捕捉点的精确坐标。

```js
const r = await im.getPoint({ message: '指定终点', useBasePoint: true, basePoint: p1 });
```

某一步需要**禁用**捕捉：

```js
mapcadLayer.setSnapEnabled(false);
const r = await im.getPoint({ message: '指定任意位置' });
mapcadLayer.setSnapEnabled(true);
```

## 七、bindDraw：把捕捉挂到 UI 控件

```js
const btn = document.getElementById('btn-snap-endpoint');
btn.onclick = () => {
  const cur = mapcadLayer.snapManager.snapMode;
  mapcadLayer.setSnapMode(cur ^ SnapMode.END);   // 按位异或切换
  btn.classList.toggle('active');
};
```

## 八、临时点捕捉

`setTemporaryPoints` 注入临时捕捉源（如绘制中的辅助点）：

```js
mapcadLayer.snapManager.setTemporaryPoints([[0, 0], [50, 0]]);
mapcadLayer.snapManager.clearTemporaryPoints();
```

⚠️ 在 `getPoint` 等待期间，**每次鼠标移动** InputManager 都会执行 `snapManager.setTemporaryPoints(previewManager.polylineVertices)`——手动注入的点下一次 mousemove 就被覆盖；且临时点只在 `SnapMode.END` 开启时参与（按 `end` 类型）。要在 getPoint 中捕捉自定义辅助点，改喂 PreviewManager：

```js
ctx.previewManager.setPolylineVertices(pts);   // pts: Point2D[]；会被当作临时捕捉点
// 注意：useBasePoint 橡皮筋会把这些点连成折线预览；previewManager.clear() 会清掉它们
```

## 九、内部机制

```
用户鼠标移动 → SnapManager.querySnap:
  1. SpatialIndex.search(bbox) 取候选实体
  2. 每个实体算各模式候选点（NEA 单独计算，与其他点去重）
  3. 合并 → 过滤距离 > snapRadiusCad 的
  4. 按距离排序，最近的标 primary
```

## 十、关于正交 / F 键

1.0.1 **没有内置正交模式**（`PointInputOptions.ignoreOrthoMode` 是预留字段，引擎侧没有 ortho 实现），也**没有 F8/F3 快捷键**——`ShortcutMap` 的 19 个动作里不含 ortho/snap 切换，要自己监听 keydown 再调 `setSnapEnabled` / `useForceAngle`。

## 十一、常见坑

1. **枚举名是缩写**：写 `SnapMode.Endpoint` 会得到 `undefined`，位或后等于没开——用 `END`/`MID`/`CEN`/`NOD`/`QUA`/`INT`/`INS`/`PER`/`TAN`/`NEA`。
2. **`SnapPoint.point` 不存在**：字段叫 `position`，取坐标写 `hit.position`。
3. **`snapPixelRadius` 是像素**：不是 CAD 单位；`setSnapRadius` 同样按像素。
4. **PER/TAN 需要参考点**：`querySnap` 第三参 `referencePoint` 缺省时这两个模式算不出。
5. **复杂实体多候选**：多段线每个顶点都算 END，命中后 `primary` 标记最近的那个。
6. **栅格底图实体不参与捕捉**：捕捉只作用于 MapCadLayer 内部实体（`addEntity` 进去的）；DWG 底图原实体不在 SpatialIndex 里。
7. **模式变化要刷新 UI**：捕捉对话框勾选态不会自动同步，用 `snapManager.setOnChange` 监听。

## 相关

- 点输入 → [input-manager](input-manager.md)
- 夹点编辑（拖拽不走捕捉） → [grip](grip.md)
- 橡皮筋预览 → [preview](preview.md)

8. **`snapManager.snap(...)` 是内部方法**：实例上存在 `snap` 但 `snap(0.1,0.1)` 静默返 `undefined`——它不吃 `(x,y)` 坐标参；公开查询入口只有 `querySnap(cursorCadPt, snapRadiusCad, referencePoint?)`。`snapManager` 宿主是 `mapcadLayer.snapManager`（已实例化），`setTemporaryPoints/setSnapMode/setSnapRadius/setOnChange` 均可程序化直调。
