> **本文回答**：mapext 内置的 `CoordMapper` 类（`cad.coordMapper`）的所有方法与适用场景。

## 一、谁在用它

在 vjmapext `MapCadLayer` 里，所有命令、预览层、渲染管线都通过 `this.coordMapper` 做 CAD ↔ 渲染经纬度 / 像素的转换。外部可以通过：

```js
const cad = new MapCadLayer();
map.addControl(cad);
const cm = cad.coordMapper;   // 绑定到 map 后才能用
```

它实际上是对 `prj.toLngLat / fromLngLat / toMercator / fromMercator`（经 `map.getMapProjection()` 委托）与 `map.project / unproject` 的薄封装，并补了几个 mapext 业务常用的便捷方法。

## 一之二、核心规则：实体永远存"投影源空间"坐标

`MapCadLayer` 实体（`LineEnt`/`CircleEnt`/`TextEnt`…）内部坐标 = **宿主 map 投影的源坐标**：

| 宿主 map 投影 | 实体坐标 = | 典型场景 |
|---|---|---|
| `GeoProjection(cad图bounds)` | **CAD 图纸坐标**（米/毫米） | CAD 底图（默认玩法） |
| `GeoProjection(EARTH_BOUNDS)`（`GeoBounds.fromArray([-20037508,-20037508,20037508,20037508])`） | **Web 墨卡托米**（EPSG:3857） | 互联网底图直连模式——没有 CAD 图、拿瓦片当底图让 mapext 直接画 |
| `LnglatProjection` | 经纬度（不推荐画图） | 简单撒点 |

渲染链路：`entity.toGeoJSON()`（实体坐标）→ RenderCache 逐点 `coordMapper.cadToLngLat` → 渲染经纬度 → geojson source。**所以实体构造参数一律给"源空间"坐标，不要预先转经纬度。**

⚠️ 实体类都在全局 `vjmapext` 命名空间下（**没有 `vjmap.ext`**），构造多为**位置参数**而非配置对象（以 `dist/index.d.ts` 为准）：`new vjmapext.LineEnt(startPt, endPt)`、`new vjmapext.CircleEnt(centerPt, radius)`。`MapCadLayer` 的 `createUI` 是**实例方法**不是构造选项（构造选项：`locale/mode/defaultColor/shortcuts/...`）。实体颜色是 0xRRGGBB 整数（number），不是 CSS 字符串，也不是 CAD/ACI 色号（`color = 1` 会渲染成近黑 `#000001`）。

互联网底图直连模式下喂数据：WGS84 点要 `vjmap.Projection.lngLat2Mercator` 转成墨卡托米再当实体坐标；误传经纬度会得到 `[0.001, 0.0004]` 之类的值（漂到几内亚湾，漂移上万公里）。该模式实体"单位"是 EPSG:3857 投影米，高纬度有变形，不是真实地面距离。

## 二、API 一览

```ts
class CoordMapper {
  bindMap(map: Map, onExtentChanged?: () => void): void;
  unbindMap(): void;   // ⚠️ unbind 后 map=null，cadToMercator→projection undefined→
                      // 抛 "reading 'toMercator'"；且 fx 渲染器共享同一实例，
                      // unbind 后所有 fx.addPoint 全挂——用完须 bindMap 回绑

  // CAD <-> 渲染经纬度
  cadToLngLat(x: number, y: number): [number, number];
  lngLatToCad(lng: number, lat: number): Point2D;
  cadPointToLngLat(pt: Point2D): [number, number];

  // CAD <-> 墨卡托
  cadToMercator(x: number, y: number): [number, number];
  mercatorToCad(mx: number, my: number): [number, number];

  // 墨卡托 <-> 经纬度（自带实现，独立于 map）
  mercatorToLngLat(mx: number, my: number): [number, number];
  lngLatToMercator(lng: number, lat: number): [number, number];

  // 便捷
  getCadCenter(): Point2D;
  getMapCenter(): [number, number];

  // 屏幕像素 <-> CAD 距离
  pixelsToCadDistance(pixels: number, map: Map): number;
  pixelOffsetToCadOffset(dxPx: number, dyPx: number, map: Map): Point2D;
}
```

## 三、典型用法

### 屏幕鼠标 -> CAD

```js
map.on("mousemove", (e) => {
  const cadPt = cad.coordMapper.lngLatToCad(e.lngLat.lng, e.lngLat.lat);
  document.getElementById("coord").textContent = `${cadPt.x.toFixed(2)}, ${cadPt.y.toFixed(2)}`;
});
```

### 让 Marker 对准 CAD 点

```js
const [lng, lat] = cad.coordMapper.cadToLngLat(300.5, 120.0);
new vjmap.Marker().setLngLat([lng, lat]).addTo(map);
```

### 像素距离 → CAD 距离（像素自适应缩放）

```js
// 保证 fx 光圈在屏幕上永远 40 px，无论如何缩放
function pxToCad(px) {
  return cad.coordMapper.pixelsToCadDistance(px, map);
}
```

### 绘图预览（内部）

```js
// 每帧把鼠标落点 + 实体的 CAD 几何 合成 rubber band
cad.coordMapper.cadToLngLat(fromX, fromY);
cad.coordMapper.cadToLngLat(toX, toY);
```

## 四、与 `map.toLngLat/fromLngLat` 的关系

在"关联了 `prj` 的 Map"上，`coordMapper.cadToLngLat` 与 `map.toLngLat` 底层走同一 `prj.toLngLat`，**两者对点输入都返回 `[lng,lat]` 数组**（源码核实：`map.toLngLat(pt)` → `prj.toLngLat` → `mercator2LngLat` → `[number,number]`）。反过来是 `map.fromLngLat(pt)` 返回 `GeoPoint`（`.x`/`.y`）。写入口选择：

| 需要 | 用 |
|---|---|
| `[lng,lat]` 给 Marker/GeoJSON | `map.toLngLat(cadPt)` 或 `cad.coordMapper.cadToLngLat(x, y)`（等价） |
| GeoPoint/GeoBounds 结果（`fromLngLat` 返回 GeoPoint、LngLatBounds/GeoBounds 输入返回对应 Bounds 类型） | `map.fromLngLat(...)` / `map.toLngLat(bounds)` |
| 快速取中心点 | `cad.coordMapper.getCadCenter()` / `getMapCenter()` |

## 五、`bindMap` / `unbindMap`

`map.addControl(mapcadLayer)` 触发 `onAdd` 时自动调用 `bindMap`；`map.removeControl` 时调 `unbindMap`。一般不需手动。

- `bindMap(map, onExtentChanged)` 会监听 `updateMapExtent` 事件（由业务或 `map.updateMapExtent` 触发）。
- 当图范围变了（例如绘图过程中超出了原本 `res.bounds`），`onExtentChanged` 回调里需要重建缓存（如渲染 buckets 的投影坐标）。

## 六、查询结果 → 实体管线（`queryMapEntities` / `featuresToEntities`）

```js
const fc = await cad.queryMapEntities({           // 或 vjmapext.queryMapEntities(map, opts)
  entTypes: ['AcDbLine','AcDbPolyline'],
  condition: "layerindex=3",
  toMapCoordinate: true,                           // 默认 true：FC 的 geometry 是 CAD 坐标
});
const ents = cad.featuresToEntities(fc);   // 实例方法已自动注入 this.blocks
// 等价：vjmapext.featuresToEntities(fc, { blocks: cad.blocks })
ents.forEach(e => cad.addEntity(e));
```

- `toMapCoordinate:true`（默认）→ `isMapCoordinate:true`、每条 `.geom` **不生成**、`geojson` 字段是 CAD 坐标字符串——这套 FC 正是 `featuresToEntities` 要的。**改 `toMapCoordinate:false` 拿到的是墨卡托/渲染经纬度，喂进去实体全错位**。
- `featureToEntity` 按 `properties.name`（AcDb* 可读名）分发；块引用要有 `blocks`。
- 坐标字段回退链：`geom.geometries` → `points`/`location`/`center`/`insertionPoint`/`position` 字符串（CAD 坐标；各实体按类型取用，如 Line 用 `points`/`location`、Circle/Arc 用 `center`、Text 用 `location`/`insertionPoint`/`position`）。
- 类型名→SQL ID：`name='AcDbText'` 查不到数据，要用 `entTypes` 或 `buildEntTypeCondition`。
- 这套 FC 要画到 vjmap 原生图层：`map.toLngLat(fc)` 整体转渲染经纬度再 `addSource`。字段空间的完整对照表见 [query-coords](../../../vjmap/references/coord/query-coords.md)。

## 六之二、fx 图层的坐标字段（FxCoord）

fx（GPU 特效）输入点支持三种写法，**传哪个就写哪个键，别混**：

```ts
{ cad: [x, y] }        // CAD/实体坐标——需 MapCadLayer 已绑定（CoordMapper）
{ lngLat: [lng,lat] }  // 渲染经纬度
{ mercator: [mx,my] }  // EPSG:3857 米
```

内部统一转归一化墨卡托；`cad` 模式在没绑 CoordMapper 时会抛错。

## 七、叠加 vjmap 原生覆盖物

mapext 不管 vjmap 原生 Marker/Symbol——它们仍按 vjmap 规则吃**渲染经纬度**（详见 [overlay-coords](../../../vjmap/references/coord/overlay-coords.md)）：

```js
marker.setLngLat(cad.coordMapper.cadToLngLat(x, y));        // ✅
marker.setLngLat([x, y]);                                  // ❌（除非互联网底图且 x,y 已是经纬度）
```

## 八、常见坑

1. **`MapCadLayer` 还没 `addControl` 就用 `cad.coordMapper`** → 拿到的是 null 引用，抛 TypeError。
2. **外部手动 `map.updateMapExtent` 却没触发 `mapcadLayer` 的重建** → 见 [trouble/bounds-parse](../../../vjmap/references/trouble/bounds-parse.md)。
3. **误把 `cadToLngLat` 结果写到 `Point2D`** → 它返回数组；如要 `Point2D` 用 `new Point2D(lng, lat)`。
4. **并行 `pixelsToCadDistance` 在 `zoom` 动画中调用** → 读到过渡中的 zoom，值会抖动；请在 `moveend` 后或使用 requestAnimationFrame 节流。
5. **实体构造传渲染经纬度** → 实体存"投影源空间"坐标，CAD 底图=图纸坐标、互联网底图=3857 米；预转经纬度会画到图外。
6. **互联网底图模式实体传经纬度** → 应 `lngLat2Mercator`（高德底图先转 GCJ02）；误传经纬度会得到 `[0.001,0.0004]`（几内亚湾）。
7. **`featuresToEntities` 喂 `toMapCoordinate:false` 的 FC** → 墨卡托当 CAD，实体全错位。
8. **`mercatorToLngLat` 当地图投影用** → 纯数学 3857→WGS84，不含 CAD 映射。
9. **`new vjmap.ext.XxxEnt` / `XxxEnt({points:...})`** → `vjmap.ext` 不存在，实体类在 `vjmapext` 下且构造是位置参数。
