> **本文回答**：自定义实体 —— 继承 `EntityBase`、实现抽象方法（`toGeoJSON` / `boundingBox` / `getGripPoints` / `gripEdit` / `clone` / 变换四件套）、注册到 `entityRegistry`。

## 一、什么时候需要

内置实体覆盖不了的场景：

- 业务对象：电线杆、井盖、信号灯，带业务数据 + 专属几何
- 非标几何：星形、齿轮、特殊符号
- 复合展示：一个实体产生多个 GeoJSON feature（线 + 标签 + 填充）

## 二、EntityBase 契约（按 d.ts 核对）

`EntityBase` 是抽象类，`constructor()` 无参。必须实现的抽象成员：

| 成员 | 签名 | 作用 |
|---|---|---|
| `type` | `abstract readonly type: string` | 实体类型名（如 `'GEAR'`） |
| `toGeoJSON()` | `(): GeoJSON.Feature[]` | **渲染入口**——实体转成带样式属性的 feature 交给 GPU 层绘制 |
| `boundingBox()` | `(): BBox` | CAD 空间外接盒 `[minX,minY,maxX,maxY]`，框选 / 空间索引 / zoomExtents 依赖 |
| `getGripPoints()` | `(): GripPointDef[]` | 夹点定义 |
| `gripEdit(newPos, gripKey)` | `void` | 夹点拖拽回调 |
| `clone()` | `(): EntityBase` | 复制（undo / 复制粘贴依赖） |
| `move(from, to)` | `void` | 平移：从 `from` 点移到 `to` 点（注意是两点参数，不是 dx/dy） |
| `rotate(center, angle)` | `void` | 绕 `center` 旋转 `angle`（弧度） |
| `scale(center, factor)` | `void` | 以 `center` 为基准缩放 |
| `mirror(p1, p2)` | `void` | 沿 p1-p2 轴镜像 |

常用可写属性：`color` `layerId` `lineWidth` `visible` `editable` `dynamic` `minDisplayLevel`/`maxDisplayLevel` `effectColor`/`effectOpacity` 等；自定义业务字段走 `setCustomProperty(key, value)`。

变更通知：几何变了调 `this.changed()`，只变样式调 `this.styleChanged()`（快路径，见 [batch/bulk-style-update](../batch/bulk-style-update.md)）。

## 三、骨架示例

```js
class GearEnt extends vjmapext.EntityBase {
  get type() { return 'GEAR'; }

  constructor(center = [0, 0], radius = 50, teeth = 12) {
    super();
    this.center = new vjmapext.Point2D(center[0], center[1]);
    this.radius = radius;
    this.teeth = teeth;
  }

  // 渲染：转成 GeoJSON features（用 makeFeature 带上公共样式属性）
  toGeoJSON() {
    return [this.makeFeature(
      { type: 'LineString', coordinates: [...this._teethPoints(), this._teethPoints()[0]] },
      'fill'   // role，可按需拆多条 feature 表达描边+填充
    )];
  }

  boundingBox() {
    const { x, y } = this.center, r = this.radius;
    return [x - r, y - r, x + r, y + r];
  }

  getGripPoints() {   // GripPointDef 字段是 key + position + type（没有 point/onMove）
    return [
      { key: 'center', position: this.center, type: 'center' },
      { key: 'radius', position: new vjmapext.Point2D(this.center.x + this.radius, this.center.y), type: 'quad' }
    ];
  }

  gripEdit(newPos, gripKey) {
    if (gripKey === 'center') this.move(this.center, newPos);
    else this.radius = this.center.distanceTo(newPos);
    this.changed();
  }

  clone() {
    const g = new GearEnt(this.center.toArray(), this.radius, this.teeth);
    this.copyBaseProps(g);
    return g;
  }

  // 变换四件套必须自己调 this.changed()：MOVE/MIRROR/SCALE 命令、方向键微移等
  // 只调 ent.move()/rotate()… 不会再替你标脏（内置实体也是在方法内部 changed()）
  move(from, to) {
    // from/to 是 Point2DLike（运行时会传 Point2D 对象，不能按下标取）
    this.center = this.center.add(to).subtract(from);
    this.changed();
  }
  rotate(center, angle) { this.center = this.center.rotateAround(center, angle); this.changed(); }
  scale(center, factor) {
    this.center = this.center.scaleFrom(center, factor);
    this.radius *= factor;
    this.changed();
  }
  mirror(p1, p2) { this.center = this.center.mirrorAcross(p1, p2); this.changed(); }

  _teethPoints() {
    const pts = [];
    for (let i = 0; i < this.teeth * 2; i++) {
      const a = (i / (this.teeth * 2)) * Math.PI * 2;
      const r = i % 2 === 0 ? this.radius : this.radius * 0.8;
      pts.push([this.center.x + r * Math.cos(a), this.center.y + r * Math.sin(a)]);
    }
    return pts;
  }
}
```

## 四、注册

实体注册表在每个 `MapCadLayer` 实例上：

```js
mapcadLayer.entityRegistry.register({ type: 'GEAR', entityClass: GearEnt });

const gear = new GearEnt([100, 100], 60, 16);
gear.color = 0xff8800;
mapcadLayer.addEntity(gear);
```

`EntityTypeDefinition = { type, entityClass, gripProvider? }`。相关方法：`entityRegistry.unregister(type)` / `create(type, ...args)` / `has(type)` / `getDefinition(type)`。

注册的实际作用：①`entityRegistry.create(type, ...)` 程序化构造；②给该 type 挂自定义 `gripProvider` 接管夹点行为。**注册不参与序列化**（见常见坑 1）。

## 五、与 Custom 属性 vs 自定义实体

| 场景 | 推荐 |
|---|---|
| 只在标准实体上加业务字段 | `setCustomProperty('bizId', ...)` |
| 需要自定义外观 / 编辑逻辑 | 自定义实体类 |

## 六、常见坑

1. **`toJSON`/`fromJSON` 不支持自定义实体**：Serializer 是硬编码 `instanceof` 链——序列化时自定义实体落到 `data: {}`（几何全丢），反序列化时返回 `null` 被静默跳过，**注册 entityRegistry 也救不了**（注册表只服务 `create()` 和 gripProvider 查找；`clone()` 是实例方法本来就不需要它）。要持久化只能自建：比如在 `customProperties` 里存构造参数（customProperties 会正常往返），读回 doc 后按 `type` 自己重建。
2. **boundingBox 不准**：框选 / 空间索引 / `zoomExtents` 全错；务必精确。
3. **变换四件套签名**：`move(from,to)` 是两点参数（不是 dx,dy）；`rotate/scale` 以 center 为基准。
4. **改了字段忘 `changed()`**：界面不更新；只改样式用 `styleChanged()`。尤其 `move/rotate/scale/mirror` 内部必须 `this.changed()`——内置命令与微移快捷键调完变换方法后不会再标脏。
5. **`copyBaseProps`**：`clone()` 里记得 `this.copyBaseProps(target)` 把 color/layerId/visible 等公共属性带过去。
