> **本文回答**：夹点编辑 —— `GripManager` / `GripPointDef` / `GripType` / 实体的 `getGripPoints` + `gripEdit`（全部已对照 `vjmapext.d.ts` 与运行时核实）。

## 一、夹点是什么

选中实体后，实体上显示的**可拖拽小图形**。拖它 = 修改实体几何（AutoCAD 的 Grip）。

```
                ◆
      ────────◆────▲────◆────▲────
      (起点)   (中点)  (终点) (中点)
                ◆
```

`▲`（三角形）= `mid` 边中点，`◆`/`■` = 端点/顶点，`●` = 圆心。按住拖拽即修改。

## 二、工作方式（与文档旧版不同的真实模型）

- 挂在 `mapcadLayer.gripManager`（`GripManager`）。输入状态机命中夹点（`hitTestGrip`）后进入 `grip_drag`：拖动过程**实时改实体**（走 `GripManager.gripEdit(entityId, cadPos, gripKey)`），松开时包进一次 undo 事务。
- 夹点只在**默认模式**下出现：`getActiveGrips()` 在 `browse` 模式或输入非 `idle` 态时返回 `[]`；**`editable === false` 的实体也被跳过**。**没有 `enabled` 开关属性**——要抑制夹点，把图层切到 `browse` 模式（见 [mode](mode.md)）、不选中实体，或设 `editable = false`。
- 拖拽期间**没有对象捕捉、也没有预览实体**：每次鼠标移动直接用原始光标的 CAD 坐标调 `gripEdit` 改真实实体（捕捉只在 `getPoint` 的点输入状态生效），松开时再 `gripEdit` 一次并 `previewManager.clear()`。要精确落点请用命令（MOVE/STRETCH 等走 `getPoint`，带捕捉）。

## 三、GripPointDef / GripProvider（自定义实体按这个实现）

```ts
interface GripPointDef {
  key: string;        // 夹点身份串（自定义实体自取，如 "start"/"mid_0"/"base"）
  position: Point2D;  // 位置（CAD 坐标）
  type: GripType;     // 视觉形状提示
}

interface GripProvider {
  getGripPoints(entity: EntityBase): GripPointDef[];
  gripEdit(entity: EntityBase, newPos: Point2DLike, gripKey: string): void;
}
```

**模型是「key 分发」不是「每夹点一个回调」**：`getGripPoints()` 只声明有哪些夹点（`key`+位置+形状）；拖到新位置时框架回调**实体自身的 `gripEdit(newPos, gripKey)`**（`EntityBase.gripEdit`），由它按 `gripKey` 决定改哪段几何。改完内部 `changed()`。

## 四、GripType → GripShape（渲染形状由 `gripTypeToShape` 映射）

| GripType | 形状 | 典型用途 |
|---|---|---|
| `vertex` | ■ square | 顶点（移动单点） |
| `endpoint` | ■ square | 端点 |
| `mid` | ▲ triangle | 边中点（拖动=整边平移/插点） |
| `center` | ● circle | 圆心（拖动=整体移动） |
| `quad` | ◆ diamond | 象限点（缩放/半径类控制） |
| `insertion` | ◆ diamond | 插入点（文字/块参照） |
| `rotation` | ● circle | 旋转控制 |
| `scale` | ◆ diamond | 缩放控制 |

注意是**小写字符串联合**，不存在 `VERTEX`/`MIDPOINT`/`RADIUS`/`CONTROL` 这类枚举成员。

## 五、内置实体的夹点（key 约定）

| 实体 | 夹点 key | 行为 |
|---|---|---|
| `PointEnt` | `base` | 移动点位置 |
| `LineEnt` | `start` / `end` / `mid` | 动端点 / 中点拖=整体平移 |
| `PolylineEnt` | `'0'…'N'`（顶点，**纯索引字符串**）+ `mid_0…`（边中点） | 顶点动点、中点整边平移 |
| `SplineEnt` | `'0'…'N'` + `mid_0…`（作用于 activePoints） | 同上 |
| `CircleEnt` | `center` / `quad0…3` | 圆心平移；象限点拖=改半径 |
| `ArcEnt` | `start` / `end` / `center` / `mid` | 起终点、圆心平移、弧中点 |
| `EllipseEnt` | `center` / `major` / `minor` | 圆心、长轴端、短轴端 |
| `TextEnt`/`MTextEnt` | `ins` | 插入点 |
| `BlockRefEnt` | `insertion` / `rotation` / `scale` | 插入点、旋转角、缩放 |
| `MLeaderEnt` | `arrow` / `vtx{i}` / `landing` | 箭头点、折线顶点、落点 |
| `LinearDimEnt`/`AlignedDimEnt` | `start`/`end`/`dimLine` | 两界线原点 + 尺寸线位置 |
| `AngleDimEnt` | `vertex`/`start`/`end`/`arc` | 顶点、两边端点、弧位置 |
| `RadialDimEnt` | `center`/`chord` | 圆心、弦点 |
| `HatchEnt` | `${loop}_${i}` + `center` | 各环顶点 + 整体中心 |

## 六、自定义实体加夹点（正确写法）

```js
class StarEnt extends vjmapext.EntityBase {
  constructor(center, outerR, innerR) {
    super();
    this.center = center.clone();
    this.outerR = outerR;
    this.innerR = innerR;
  }

  // ① 声明夹点：key + position + type
  getGripPoints() {
    return [
      { key: 'center', position: this.center, type: 'center' },
      { key: 'outer', position: new vjmapext.Point2D(this.center.x + this.outerR, this.center.y), type: 'quad' },
      { key: 'inner', position: new vjmapext.Point2D(this.center.x + this.innerR, this.center.y), type: 'quad' },
    ];
  }

  // ② 按 key 应用新几何（框架自动包 undo 事务 + changed()）
  gripEdit(newPos, gripKey) {
    const p = new vjmapext.Point2D(newPos.x ?? newPos[0], newPos.y ?? newPos[1]);
    if (gripKey === 'center') this.center = p;
    else if (gripKey === 'outer') this.outerR = p.distanceTo(this.center);
    else if (gripKey === 'inner') this.innerR = Math.min(p.distanceTo(this.center), this.outerR - 0.1);
    this.changed();
  }
}
```

> ⚠️ `GripPointDef` 的字段是 **`position`（不是 `point`）**、**`key`（不是回调 `onMove`）**；`gripEdit` 的 newPos 是 `Point2DLike`（`Point2D` 实例或 `[x,y]` 数组——**不能**传 `{x,y}` 字面量）。`toPoint2D` 只认 `instanceof Point2D` 和元组，传 `{x,y}` 会取 `p[0]/p[1]`=undefined 再被 `Point2D` 构造的 `x=0` 默认值兜底——**静默得到 (0,0)，夹点直接跳到原点**（不是 NaN）。

详见 [entity/custom-entity](../entity/custom-entity.md)。

## 七、GripManager 直接可用 API

```ts
gripManager.getActiveGrips(): GripPoint[]            // 当前选集的全部夹点 {key,position,type,entityId}
gripManager.getGripFeatures(): GeoJSON.Feature[]     // 渲染用 feature（框架内部用）
gripManager.gripEdit(entityId, newCadPos, gripKey)   // 程序化拖一个夹点（→ entity.gripEdit）
gripManager.hitTestGrip(point:{x,y}, map): GripPoint | undefined // 屏幕点命中测试
```

> ⚠️ `hitTestGrip` 不是几何距离判定——它是对**已渲染的 grip 图层**做 `queryRenderedFeatures`（±10px 包围盒，再校验 `_meta===GRIP`）。所以夹点图层还没渲染完成 / 坐标偏离超过 10px 时都返回 `undefined`；自动化脚本里要先确认选集夹点已上屏再测。

程序化触发夹点编辑（不走鼠标）可直接 `gripManager.gripEdit(ent.id, [x, y], 'center')`——注意必须传 `[x,y]` 元组或 `Point2D`，**不能传 `{x,y}` 字面量**（见下坑位）。

> ⚠️ 程序化 `gripManager.gripEdit` **不落 undo**——undo 记录发生在 `InputManager` 拖拽路径（拖动开始时 `recordModify`），直接调 `gripEdit` 只改几何+`changed()`，`undoStack` 不变，之后 `undo()` 也不会回退这次修改。要可撤销就自己包 `undoManager.recordModify(entity)` + `endTransaction()`，或改用 transform 命令。

## 八、连续夹点模式

AutoCAD「选中夹点→选动作」两步法在 vjmapext 不做——夹点只做几何修改（移动/改半径/动顶点）。要移动/旋转/缩放整体用 [entity/transform](../entity/transform.md) 的 `move/rotate/scale/mirror`。

## 九、常见坑

1. **gripEdit 里不改几何/不调 changed()**：画面不刷新，感觉夹点失效。
2. **position 必须在合理位置**（如 Arc 中点要在弧上）：位置不合理看起来会乱跑。
3. **`getGripPoints` 返回空数组**：合法，不显示夹点。
4. **browse 模式下不出现夹点**：`getActiveGrips()` 直接返回 `[]`——夹点不显示先查当前模式。
5. **onMove 越界**：`gripEdit` 里自己做 clamp（如 `innerR < outerR - eps`）。

## 相关

- 命令与输入 → [custom-command](custom-command.md)
- 变换（move/rotate/scale/mirror） → [entity/transform](../entity/transform.md)
- 自定义实体 → [entity/custom-entity](../entity/custom-entity.md)
