> **本文回答**：`mapcadLayer.fx` —— GPU 高性能特效图层：能干什么、和实体层怎么分工。

## 一、FX 是什么

FX = Visual Effects，是 vjmapext 内置的**GPU 端批量特效图层**：

- 入口：`mapcadLayer.fx`（`FxManager` 实例，`MapCadLayer` 自动构造）
- 原理：instanced rendering，shader 自驱动
- 特点：**1 万个发光/流光实例不掉帧**

## 二、和实体层的分工

| | 实体层（`addEntity` + AnimationManager） | FX 层（`mapcadLayer.fx`） |
|---|---|---|
| 驱动 | JS 每帧改 bindData | GPU 上传一次 buffer，shader 自驱 |
| 适合数量 | 几十~几百 | **上千~上万** |
| 形态 | CAD 几何 + 可编辑 | 发光 / 脉冲 / 流光 / Shadertoy |
| 场景 | 可编辑图形 / 拖尾 / 变色 | 告警光环 / 飞线态势 / 粒子 |
| 入口 | `new LineEnt` / `mapcadLayer.animationManager.add` | `fx.addPoint` / `fx.addLine` |

两套系统**共存**：实体驱动几何形变，FX 铺大量粒子 / 光效。

## 三、能力清单

| 能力 | API | 数量 |
|---|---|---|
| 点预设 | `fx.addPoint(preset, opts)` | 12 个（fire / pulseRing / halo / radarSweep / smoke / lightning / meteor / vortex / shield / firework / crosshair / shadertoy）——⚠️ `addPoint('shadertoy')` 忽略 `fragmentShader`、实际画的是 fire；自定义 GLSL 只能走 `fx.addShadertoy` |
| 线预设 | `fx.addLine(preset, opts)` | 5 个（flowLine / meteor / lightning / arrowFlow / dashFlow） |
| Shadertoy 入口 | `fx.addShadertoy(...)` / `fx.addShadertoyLine(...)` / `fx.addShadertoyFullscreen(...)` | 3 个 |
| CAD 实体绑定 | `fx.bindToEntity(entity, preset, opts)` | 联动实体几何 |
| Symbol Shader | `fx.symbolImage.addPreset` / `addShadertoy` / `addVariants` | 大量同形图标 |
| 批量 | `fx.addPoints / replaceAllPoints / setPointData` | 万级点用 |
| 追加 | `fx.addPoints`（重复调用即追加） | ⚠️ `FxManager` **没有 `appendPoints`**——`appendPoints` 是 `FxLayer` 上的方法，manager 层追加就再调 `addPoints` |
| 一次性触发 | `{ triggerOnce: true, ttl: 2000 }` | 闪电 / 烟花 |
| 三档分桶 | `bucket: 'below' / 'above' / 'topmost'` | z-order |
| 画质档位 | `fx.setQuality('lite' / 'standard' / 'cinematic')` | 降画质保 FPS |
| 事件 / 指标 | `fx.on('render / empty / overflow / compact', ...)` + `fx.getMetrics()` | 监控 |

## 四、Hello FX

```js
const mapcadLayer = new vjmapext.MapCadLayer();
map.addControl(mapcadLayer);

const fx = mapcadLayer.fx;

// 加一个 halo（光晕）
const h = fx.addPoint('halo', {
  cad: [100, 200],         // CAD 坐标
  size: 80,                // 半径（默认 map 单位）
  color: [1, 0.5, 0, 1]    // RGBA
});

// 改色
fx.setPointData(h, { color: [1, 0, 0, 1] });

// 删除
fx.remove(h);
```

## 五、坐标与单位

```js
fx.addPoint('halo', {
  cad: [100, 200],     // CAD 坐标（DWG 内部）
  // 或
  lngLat: [116.39, 39.9],   // 经纬度
  // 或
  mercator: [...],      // Web Mercator
  size: 80,
  sizeUnits: 'map'     // 'map' 跟随缩放 / 'pixels' 屏幕恒定
});
```

## 六、批量加点（万级）

```js
// 循环 addPoint
for (const p of list) fx.addPoint('fire', { cad: p, size: 30 });

// 一次 addPoints（写法更简洁，直接拿到 handle 数组）
const handles = fx.addPoints('fire', list.map(p => ({ cad: p, size: 30 })));
```

两种写法**性能基本相同**：1.0.1 的 `addPoints` 内部就是逐个调 `addPoint`；每个点只写 CPU 端影子缓冲并标记脏区间，GPU 上传都在下一帧 `draw` 时合并成一次 `bufferSubData`——同步循环 `addPoint` 并不会每次上传。选 `addPoints` 是为了代码简洁与按 bucket 分组。

## 七、CAD 实体绑定

```js
const circle = new vjmapext.CircleEnt([100, 200], 50);
mapcadLayer.addEntity(circle);

fx.bindToEntity(circle, 'pulseRing', { color: [1, 0, 0, 1] });

// 实体动，FX 自动跟随；实体删，FX 自动清理
circle.move(circle.center, [200, 300]);   // center 必须是 Point2D，别直接赋 [x,y] 数组；move 内部已 changed()
```

`bindToEntity(entity, preset, opts)` 返回 token：`token.handle`（当前 FX 句柄）、`token.unbind()`、`token.setEntityVisibleIn(m)` / `setFxVisibleIn(m)` / `setFxFollowsEntityVisibility(on)`。`opts` 除点特效字段外还有：`anchor`（锚点名，如 `'center'|'start'|'end'|'midpoint'`）、`removeOnDelete`（默认 true）、`hideWhileInvalid`（默认 true）、`entityVisibleIn` / `fxVisibleIn`（`'both'|'edit'|'browse'|'none'`，按模式控制实体/特效显隐，默认 `'both'`）、`fxFollowsEntityVisibility`（实体被隐藏/图层隐藏/超出显示级别时 FX 一起隐藏，默认 false）。

> ⚠️ **撤销"修改"会杀掉绑定的 FX**：undo/redo 恢复修改快照的方式是 `store.delete` 旧实体再 `store.add` 快照（id 不变）——`entity:deleted` 触发绑定的 `removeOnDelete` 清理，而重新 add 不会自动重新绑定。拖夹点、MOVE 后再 Ctrl+Z，FX 就没了；需要时监听 `undo:performed`/`redo:performed`（eventBus 字面量事件）按 id 取回新实体重新 `bindToEntity`。

## 八、Symbol Shader 图标

大量**同形状**的图标（比如 1 万个相同图标的点）→ 走 `fx.symbolImage`，性能与点数解耦：

```js
// addPreset(id, preset, opts?) — opts 是渲染目标参数：width/height(像素)/color/params/fps/animate
fx.symbolImage.addPreset('warn-halo', 'halo', { width: 64, height: 64, color: [1,0,0,1] });
map.addSymbolLayer('alerts', 'alert-src', {
  iconImage: 'warn-halo',
  iconSize: 1
});
```

`fx.symbolImage` 把 FX shader 渲染成 sprite，注入到底座 symbol 图层使用，适合百万级点。

## 九、典型场景

- 应急指挥：告警节点 `halo` / `pulseRing`，态势飞线 `meteor` / `flowLine`
- 智慧管线：管道 `arrowFlow` 方向指示，故障点 `lightning`
- 战术演示：`crosshair` 锁定 / `shield` 范围 / `vortex` 动态压制
- 大屏：`radarSweep` 扫描 / `firework` 庆祝 / 全屏 Shadertoy 滤镜

## 十、为什么选 FX 而不是 Marker

- Marker 每个是 DOM → 500 个就明显卡
- FX 每个是 GPU instance → 1 万也顺
- FX 有真正的"发光 / 光晕 / 流光"，Marker 要靠 PNG

## 十一、常见坑

1. **以为逐个 addPoint 慢**：同步循环 `addPoint` 与 `addPoints` 一样，都在下一帧合并成一次上传；真正的开销在实例数量与预设 shader 复杂度（看 `getMetrics()` / 降 `setQuality`）。
2. **所有都 `cinematic`**：中端 GPU 撑不住；按重要性给 `lite`。
3. **未 remove 的 FX 实例累积**：`fx.getMetrics()` 看 live 数。
4. **FX 绑实体但实体删了 FX 没删**：实际应自动删；如果没删，排查 `bindToEntity` 返回值是否被覆写。
5. **bucket 'topmost' 覆盖全部 UI**：确实会覆盖所有图层；慎用。
6. **自建 `new FxLayer()` 的默认 id 撞车**：`FxLayer` 缺省 `id="vjmapext-fx"`——`cad.fx` 管理器已占用该 id，再 `attach` 报 `Layer with id "vjmapext-fx" already exists`，且 `detach()` 会 `map.removeLayer("vjmapext-fx")` **把管理器的共享层一并拆掉**——之后 `fx.addPoint` 抛 `Cannot read properties of undefined (reading 'toMercator')`（coord 已随层移除而清空）。自建层必须传独立 id：`new vjmapext.FxLayer({ id: "my-fx" })`。

## 相关

- 调优 → [fx-quality](fx-quality.md)
- 预设 → [fx-presets](fx-presets.md)
- 大量点替代 → [large-markers](../../../vjmap/references/perf/large-markers.md)
