gis-view-webgl2 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +6 -3
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.d.ts +172 -8
  4. package/dist/index.js +1 -1
  5. package/docs/AI/345/257/271/350/257/235/346/261/207/346/200/273.md +121 -0
  6. package/docs/GisView.md +1 -1
  7. package/docs/ai/06-/344/274/232/350/257/235/347/264/242/345/274/225/344/270/216/345/244/215/346/237/245/346/226/271/346/263/225.md +117 -0
  8. package/docs/demos/LabelDemo.vue +2 -2
  9. package/docs/demos/MinimapDemo.vue +154 -1
  10. package/docs/demos/SearchFilterDemo.vue +113 -1
  11. package/docs/demos/StyleDemo.vue +1 -1
  12. package/docs/demos/api-index.js +22 -14
  13. package/docs/demos/demos.js +1 -1
  14. package/docs/demos/gis-view-demo/useGisViewDemo.js +2 -2
  15. package/docs/internals/13-/351/271/260/347/234/274/345/233/276.md +2 -0
  16. package/docs/internals/16-01-/344/274/230/345/214/226/344/270/216/345/217/226/350/210/215-/344/270/212.md +13 -2
  17. package/docs/internals/16-03-/344/274/230/345/214/226/344/270/216/345/217/226/350/210/215-/344/270/213.md +11 -1
  18. package/docs/internals/18-/346/200/247/350/203/275/345/256/236/346/265/213/344/270/216WebGPU/350/257/204/344/274/260.md +61 -1
  19. package/docs/manual/03-/351/205/215/347/275/256/351/241/271.md +5 -4
  20. package/docs/manual/04-/344/272/213/344/273/266/344/270/216/345/233/236/350/260/203.md +38 -10
  21. package/docs/manual/05-/347/224/273/350/264/250/344/270/216/346/211/271/351/207/217/346/233/264/346/226/260.md +1 -1
  22. package/docs/manual/07-/346/214/207/351/222/210/344/270/216/350/246/201/347/264/240/347/274/226/350/276/221.md +7 -6
  23. package/docs/manual/08-/351/200/211/344/270/255/344/270/216/346/213/220/347/202/271.md +59 -3
  24. package/docs/manual/09-/351/200/232/351/201/223/344/270/216/345/244/232/345/233/236/347/272/277.md +14 -1
  25. package/docs/manual/14-/350/247/206/345/233/276/344/270/216/347/255/233/351/200/211.md +23 -5
  26. package/docs/manual/16-/345/256/236/344/276/213/346/226/271/346/263/225/346/200/273/350/241/250.md +12 -7
  27. package/docs/manual/17-/345/217/202/346/225/260/351/200/237/346/237/245.md +16 -8
  28. package/docs/manual/18-/351/200/220/346/226/271/346/263/225/347/244/272/344/276/213.md +27 -2
  29. package/docs/manual/19-/350/257/273/346/225/260/344/270/216/346/213/226/346/213/275/351/242/204/350/247/210.md +4 -0
  30. package/docs/manual/20-/346/200/247/350/203/275/344/270/216/345/244/247/346/225/260/346/215/256.md +9 -3
  31. package/docs/manual/21-/350/257/264/346/230/216/344/270/216/345/270/270/350/247/201/351/227/256/351/242/230.md +1 -1
  32. package/docs/steps/README.md +5 -5
  33. package/docs/steps/selftest-demos.html +105 -6
  34. package/docs/steps/selftest-docs.html +3 -3
  35. package/docs/steps/selftest-features.html +1 -1
  36. package/docs/steps/selftest-full.html +42 -1
  37. package/docs/steps/selftest-perf.html +600 -421
  38. package/docs/steps/selftest-select.html +168 -1
  39. package/package.json +1 -1
@@ -2,7 +2,7 @@
2
2
 
3
3
  > 十五轮实测数据、瓶颈定位与 WebGPU 值不值得上。
4
4
 
5
- 结论先说:**当前瓶颈不在 GPU,也不在 WebGL 的绘制提交上,所以换成 WebGPU(或把渲染挪进 Worker)都不会带来明显收益**。
5
+ 结论先说:**绘制提交与 JS 不是当前瓶颈;十万级长线的主要剩余成本是 GPU 填充率,换 WebGPU(或把渲染挪进 Worker)不会减少相同像素量的填充,因此仍不会带来明显收益**。
6
6
  十万级数据下真正的开销在 JS / DOM 侧,几轮优化都落在这里:先是标签覆盖层的候选上限(`label.scanLimit`,0.8 → 36.5 FPS),
7
7
  再是把标签图层做成「交互期整体变换 + 停手后重排」,平移 / 缩放 / 拖点从此与「关掉标签」同档(17.9 / 13.5 / 20.1 → 55 ~ 58 FPS),
8
8
  整屏框选也从 97 ms 降到 52 ms;随后一轮把「整组拖动」的增量补丁补齐到每个实例批次(点线按槽位合并上传、圆头批次欠账到松手后分摊补齐),
@@ -263,6 +263,66 @@
263
263
 
264
264
  优化后的 5 次读数依次是 5.6 / 6.3 / 6.3 / 6.4 / 10.7 ms;同页新增三条断言分别核对条数与线路实例数不变、通道声明未变时派生折线原样保留、换通道骨架时线路几何必须重推。该快路只对原样回灌生效,内容真的换了或删除导致下标平移时仍走原来的整份 / 增量路径。
265
265
 
266
+ #### 第十七轮:线路样式窄路 —— 只换线宽 / 颜色不再重跑整份解析
267
+
268
+ 上一轮把「同一批点线原样回灌」收掉,这一轮继续看样式面板上最常发生的补丁:只拖线宽、只换线色。点侧已有尺寸 / 配色两条窄路,线侧仍会在 `resolveLineStyleInto` 里重跑规则、热力、虚线、阴影与效果归一化。现在把「基准宽 / 实际半宽 / 颜色 / 透明度」抽成 `resolveLinePaintInto`(整份解析与窄路共用),命中窄路时逐条只重算这四项并补实例槽位;规则、热力图、虚线、方向箭头等结构输入存在时仍回退整份解析。`getLineStyle(index)` 读的是同一条链的最新结果。
269
+
270
+ 1000 点 + 50000 线,连续 5 次交替(`node scripts/bench-scale.mjs --points=1000 --lines=50000 --gpu=true`;profile 时另用临时表达式文件挂 `--profileFile`,跑完即删):
271
+
272
+ | 操作 | 改前 | 改后 | 稳态收益 |
273
+ | --- | --- | --- | --- |
274
+ | 只换 `lineStyle.color` | 27 / 11.7 / 10.1 / 10.1 / 10.5 ms | 18.9 / 10.8 / 7.4 / 6.1 / 6.6 ms | 约 40% |
275
+ | 只换 `lineStyle.width` | 20.4 / 10.4 / 9.2 / 10.2 / 10.4 ms | 17.3 / 7.8 / 6.2 / 5.8 / 6.4 ms | 约 40% |
276
+
277
+ 首轮包含槽位表与交互期细线账本的首次重建;稳态从 9 ~ 11.7 ms 降到 5.8 ~ 7.4 ms。改后的 CPU profile(42 ms 采样窗口,208 samples):`patchLineSlotsInner` 30.8%、`patchLineSlots` 17.3%、`applyLineNarrowFast` 13.0%、`estimateLinePatchRuns` 8.7%、`bufferSubData` 5.8%、`resolveLinePaintInto` 3.8%;原先的 `applyResolvedStyles` / `refreshLine` 已不在热点列表里。这说明剩余成本主要是「实例槽位补丁 + 上传」,继续压缩解析侧能拿的收益已经很小。
278
+
279
+ 正确性回归:`selftest-perf.html` 新增 4 条断言,覆盖窄路后 `getLineStyle` 的最新读数、`dataVersion` 不递增、逐线自身线宽 / 颜色优先、虚线图案不被覆盖,以及存在线路规则时回退整份解析且规则仍压过新的全局样式;另加 1 条 DIAG 记录 2000 线的窄路耗时。`selftest-demos.html` 覆盖 `flyToCamera`、`getLineStyle`、`dataVersion` 三个 demo 读数与 `onDataChange` 同步。
280
+
281
+ #### 第十八轮:程序化相机运动复用交互快路径,关闭聚合不再空转整排
282
+
283
+ 这一轮先查「没有指针事件时,相机 API 为什么没吃到交互期的优化」。`zoomBy` / `setCamera` / `centerOn` / `flyToCamera` 最终都走 `notifyCameraChange()`,但轻量画法的判定只看 `interacting` 与动画,所以外部控件、定时视角同步与飞行动画仍按全质量路径绘制。另一半问题是聚合脏标记:只要缩放比例变了就无条件 `clusterDirty = true`,即使 `pointCluster` 根本没开,下一帧也会跑一次 `layoutScene()` 并重写 / 上传整份点实例缓冲。
284
+
285
+ 现在的口径:
286
+
287
+ - **聚合只在该算的时候算**:关闭聚合、当前缩放档不聚合且上一帧也没有聚合圆 / 隐藏点时,`flushPointClusterLayout()` 只收掉脏标记,不调用 `layoutScene()`。真正跨出聚合档、关配置、改数据 / 选中 / 拖动时仍立即重排,不能延迟状态生效。
288
+ - **相机运动期只推迟成员关系**:相机导致的聚合成员变化先沿用上一档,停稳后一次重算;平移不标聚合脏,因为聚合半径是屏幕像素、比例没变。
289
+ - **程序化运动复用两种轻量画法**:真实相机位置变化后 160 ms 内,非 MSAA 后备缓冲与 `gl.LINES` 细线路径同时生效;每次真实变化重新计时,同一位置重复 `setCamera` 不续期。停稳后补一张全质量帧,刷新通道宽度并恢复符号预烤。
290
+ - **停稳条件不只盯轻量帧**:如果当前场景没有线、又显式关了 `interactionNoMsaa`,运动帧可能一种轻量几何都没画;只要还有被推迟的聚合重排,计时器也补一帧,避免聚合结果停在旧缩放档。
291
+
292
+ 实测环境与命令:无头 Chrome + ANGLE(AMD Radeon Graphics,D3D11),1280×800,10 万点 / 5 万线,`quality: 'high'`:
293
+
294
+ ```powershell
295
+ node scripts/bench-scale.mjs --points=100000 --lines=50000 --gpu=true --panFrames=40 --zoomFrames=30
296
+ ```
297
+
298
+ | 指标 | 改前 | 改后 |
299
+ | --- | --- | --- |
300
+ | 连续缩放 p50 | 67.9 ms | **22.5 ms** |
301
+ | 连续缩放 p95 | 76.2 ms | **25.1 ms** |
302
+ | 连续缩放 avg | 60.77 ms | **20.57 ms** |
303
+ | 连续平移 p50 | 17.1 ms | 17.2 ms(不变,属于帧节拍波动) |
304
+ | `refresh()` | 56.2 ms | **53.7 ms** |
305
+
306
+ 改后终态读数为 `thinLines: false`、`noMsaa: false`、`frameMs: 0.10`;CPU profile 里主要是 idle / program,`render` 自身约占 0.1%。所以这里的收益不是把引擎单帧压到 1 ms,而是消除了「关闭聚合时每次缩放都整份重排 + 上传」以及运动期的 MSAA / 细线光栅化成本;剩余时间基本是 GPU 光栅化与 60 Hz 帧节拍。`selftest-perf.html` 增加 7 条断言(其中 1 条覆盖没有轻量帧时的延迟聚合补算),四页回归 `full 94 / demos 365 / docs 134 / perf 181` 共 **774** 条通过 / 0 失败;公开实例成员、options 与数据字段计数不变。
307
+
308
+ #### 第十九轮:线片段着色管线专用化
309
+
310
+ 这一轮先回答「为什么上一轮之后还是感觉没提升多少」。10 万线交互的 CPU profile 仍约 **98.6% idle**、`render` 自身约 **0.1%**,说明主线程不是瓶颈;上一轮省掉的是场景重排、实例上传与相机排帧。剩余成本落在 GPU:`gl.LINES` 细线虽然已经避开细长三角形,但片元仍复用了通用线路着色器,逐像素执行虚线取模、`smoothstep`、边缘覆盖率与光晕分支,即使这一遍画的是 1px 实线。
311
+
312
+ - `THIN_LINE_FRAGMENT_SHADER`:交互期 `gl.LINES` 只把实例颜色写到输出,顶点侧也不再携带虚线 / 边缘 / 光晕 varying。
313
+ - `SOLID_LINE_FRAGMENT_SHADER`:整批无虚线且这一遍不是热力光晕的四边形线路只算颜色与(非 MSAA 帧的)覆盖率;有虚线或光晕仍走 `LINE_FRAGMENT_SHADER`。
314
+ - `engine.js` 的 `drawLineBatch` 按批选择管线,`refreshLineThinLedger` 同时提供「是否全实线」;端点圆头保持绘制,所有交互帧仍是全分辨率,停稳 160 ms 后照旧补 MSAA 静态帧。
315
+
316
+ 实测环境:无头 Chrome + ANGLE(AMD Radeon Graphics,D3D11),1280×800,`quality: 'high'`,三次运行取中位。
317
+
318
+ | 场景 | 改前 p50 / p95 | 改后 p50 / p95 |
319
+ | --- | --- | --- |
320
+ | 10 万点 / 5 万线缩放 | 23.2 / 28.6 ms | **16.7 / 16.8 ~ 16.9 ms** |
321
+ | 10 万点 / 10 万线缩放 | 42.6 / 46.3 ms | **18.0 / 21.4 ms** |
322
+ | 10 万点 / 10 万线、2px 实线(只走四边形轻量片元) | 82.4 / — | **63.9 / — ms** |
323
+
324
+ 16 ~ 18 ms 已接近 60Hz 帧节拍,不能再用它宣称无上限提升;要继续明显提速,只能减少可见线段数或屏幕覆盖面积。细线快路径的实际门槛是最大半宽 ≤ 0.6 设备像素且无虚线(dpr = 1 时约 1.2px 线宽),2px 实线不会走 `gl.LINES`,这也是「某些场景感觉提升有限」的直接原因。`selftest-perf.html` 新增 1 条像素级对拍,验证细线与四边形在同一交互状态下位置 / 颜色一致、不会整批画丢;该页 182 / 1(共 183 条,标签读数失败在 HEAD 已复现)。
325
+
266
326
  ### WebGPU 值不值得上
267
327
 
268
328
  | 维度 | 现状 | WebGPU 能带来的变化 |
@@ -43,7 +43,7 @@
43
43
  | `labelStates` | `GisViewLabelState[]` | 无 | 标签的运行时状态(`getLabelStates()` 的那一份:拖出来的偏移、改过的内容与样式、运行时加的标签、隐藏覆盖):创建时喂回来就回显,等价于创建之后调 `setLabelStates(states)`;`updateOptions({ labelStates })` 同样认。`exportData({ format: 'input' })` 的结果里就叫 `labelStates`,存档 → 回灌直接接上 |
44
44
  | `graphics` | `GisViewGraphic[]` | 无 | 与节点无关的扩展图形:按世界坐标放 SVG 路径 / 图片 / Canvas,支持锚点、偏移、旋转、透明度、`zIndex` 与 `scaleWithView`;位于画布之上、标签之下,运行时用 `addGraphic` / `addGraphics` / `updateGraphic` / `removeGraphic` / `clearGraphics` 增删改,见「扩展图形(graphic)」 |
45
45
  | `tooltip` | `GisViewTooltipOptions` | 无 | 悬停提示:点 / 线 / 通道分别配置内容,见「悬浮提示」 |
46
- | `minimap` | `GisViewMinimapOptions \| null` | 无 | 内置鹰眼图:位置、尺寸、节点 / 线路 / 视口框三份样式与交互都可配,`extent` 决定小图按数据范围还是「数据 ∪ 底图」铺(默认 `auto`,GeoJSON 底图不会缩成一个点),`extentFollowCamera` 默认 `false`、打开后才允许 `auto` 范围随相机收窄。`zoomable` 三态互斥:`false` 关闭、`true` 手动缩放小图自身、`'follow'` 让整体范围跟随主图(`followZoomScale` 是主图倍数的指数,跟随模式下 `setMinimapZoom` / `zoomMinimapBy` 不抢控制权);空白处拖动可平移小图或直接框选让主图显示过去(`dragAction`,`box` 不依赖缩放,手动模式下 Ctrl / ⌘ 才切换动作),每格倍率 `zoomStep`。通道还能只画一根;小图同样支持规则(`pointRules` / `lineRules`,缺省沿用主图规则的颜色、层级、最终节点尺寸与图标、样式链线宽,节点 / 线路尺寸按两边视口比例缩放且不受主图相机倍数影响),可以只画某几层(`levels`)、关掉分层叠画(`layered`)、按三组系数给节点与线路定尺寸(整体倍数 `pointSizeScale` / `lineWidthScale` 对自有与继承要素都生效;跟随小图倍数 `pointZoomScale` / `lineZoomScale` 在小图自身放大时也作用于继承要素;跟随主视图地图倍数 `pointMapZoomScale` / `lineMapZoomScale` 只作用于自有规则 / 样式),视口框与框选矩形的颜色 / 背景 / 边框 / 圆角也能配(`boxStyle` / `selectStyle`),`extent` 还能直接写死一份世界范围、容器可加 `className` / `style`、`layers` 按 `placement` 分数据前 / 数据后、段内按 `order` 拆出多层自绘内容(旧 `draw` 钩子仍兼容并排在最后)、`onInteraction` 在手势结束时给出类型 / 世界坐标 / 相机、`maxPixelRatio` 限制备份倍率上限,不配则不产生任何 DOM,见「鹰眼图」 |
46
+ | `minimap` | `GisViewMinimapOptions \| null` | 无 | 内置鹰眼图:位置、尺寸、节点 / 线路 / 视口框三份样式与交互都可配,`extent` 决定小图按数据范围还是「数据 ∪ 底图」铺(默认 `auto`,GeoJSON 底图不会缩成一个点),`extentFollowContent` 默认 `false`,首次铺好后内容变化不再改小图范围;`extentFollowCamera` 默认 `false`、打开后才允许 `auto` 范围随相机收窄。`zoomable` 三态互斥:`false` 关闭、`true` 手动缩放小图自身、`'follow'` 让整体范围跟随主图(`followZoomScale` 是主图倍数的指数,跟随模式下 `setMinimapZoom` / `zoomMinimapBy` 不抢控制权);空白处拖动可平移小图或直接框选让主图显示过去(`dragAction`,`box` 不依赖缩放,手动模式下 Ctrl / ⌘ 才切换动作),每格倍率 `zoomStep`。通道还能只画一根;小图同样支持规则(`pointRules` / `lineRules`,缺省沿用主图规则的颜色、层级、最终节点尺寸与图标、样式链线宽,节点 / 线路尺寸按两边视口比例缩放且不受主图相机倍数影响),可以只画某几层(`levels`)、关掉分层叠画(`layered`)、按三组系数给节点与线路定尺寸(整体倍数 `pointSizeScale` / `lineWidthScale` 对自有与继承要素都生效;跟随小图倍数 `pointZoomScale` / `lineZoomScale` 在小图自身放大时也作用于继承要素;跟随主视图地图倍数 `pointMapZoomScale` / `lineMapZoomScale` 只作用于自有规则 / 样式),视口框与框选矩形的颜色 / 背景 / 边框 / 圆角也能配(`boxStyle` / `selectStyle`),`extent` 还能直接写死一份世界范围、容器可加 `className` / `style`、`layers` 按 `placement` 分数据前 / 数据后、段内按 `order` 拆出多层自绘内容(旧 `draw` 钩子仍兼容并排在最后)、`onInteraction` 在手势结束时给出类型 / 世界坐标 / 相机、`maxPixelRatio` 限制备份倍率上限,不配则不产生任何 DOM,见「鹰眼图」 |
47
47
  | `filter` | `GisViewFilterInput \| null` | 无 | 初始筛选条件,等价于创建之后调用 `setFilter`;`mode` 缺省 `'hide'`,写 `'remove'` 则删除不命中的要素,见「筛选」 |
48
48
  | `filterWorker` | `boolean \| GisViewFilterWorkerOptions` | `{ threshold: 20000 }` | 筛选是否用 Web Worker 计算:默认点 + 线总数超过阈值才用,`false` 表示始终同步算 |
49
49
  | `searchWorker` | `boolean \| GisViewSearchWorkerOptions` | `{ threshold: 20000 }` | [异步搜索](14-视图与筛选.md#搜索与定位)是否用 Web Worker 计算:默认点 + 线总数超过阈值才用,`false` 表示 `searchAsync` 也同步算;同步的 `search` 一直在主线程算,不受这一项影响 |
@@ -93,7 +93,7 @@
93
93
  | `pixelRatio` | `number` | `devicePixelRatio` | 手动指定设备像素比(夹在 0.5 ~ 3);宿主缩放(CSS `zoom` / `transform: scale`)的比例会乘在它上面,合计夹在 0.5 ~ 4 |
94
94
  | `maxCanvasPixels` | `number` | `4000000` | 画布总像素预算:`cssWidth × cssHeight × 倍率²` 超过它时按 `sqrt(预算 / CSS 面积)` 压渲染倍率(全屏与高分屏不因像素数翻十倍而掉帧);传 `0` 不设上限 |
95
95
  | `antialias` | `boolean` | 开(软件光栅自动关) | 画布是否开 MSAA。同一份十万级线段实测:MSAA 4× 下单帧光栅化是关掉时的近 5 倍(见「画质」里的实测表),关掉能明显提速、代价是线条与圆形边缘有锯齿;当前实际状态见 `getStats().antialias`(软件光栅下引擎默认关,写 `true` 可强制打开) |
96
- | `interactionThinLines` | `boolean` | `true` | 交互期细线快路径:可见线段全部细到 3px 以内且没有虚线时,拖动 / 缩放期间改用 `gl.LINES` 画(1 设备像素、无光晕与虚线),停手立刻回到四边形展开。位置不跳、悬停 / 选中不受影响;这一帧走没走快路径见 `getStats().thinLines`,实测见「画质」 |
96
+ | `interactionThinLines` | `boolean` | `true` | 交互期细线快路径:可见线段最大半宽 ≤ 0.6 设备像素(dpr = 1 时约 1.2px 线宽)且整批没有虚线时,拖动 / 缩放期间改用 `gl.LINES` 画(1 设备像素、无光晕与虚线),停手立刻回到四边形展开。2px 实线不会走这一档,但全实线四边形另有轻量片元。位置不跳、悬停 / 选中不受影响;这一帧走没走快路径见 `getStats().thinLines`,实测见「画质」 |
97
97
  | `interactionNoMsaa` | `boolean` | `true` | 非 MSAA 后备缓冲(交互 / 动画期):拖动 / 缩放 / 框选期间与动画在跑的那些帧,整帧先画进一张不带 MSAA 的后备贴图,帧末整幅贴回画布;停手(或动画停帧)之后那一帧照旧直接画进带 MSAA 的画布,所以静止画面逐像素不变(代价是这些活动帧的边缘暂时没有抗锯齿)。十万级实测交互期单帧 133 → 36.6 ms、拖动 p50 130 → 16 ~ 22 ms,动画帧 212 → 53 ms/帧(只开涟漪 187 → 48、只开线路动点 199 → 47);透明底 / 自定义底色的导出场景与没开 MSAA 的画布不走这条,这一帧走没走见 `getStats().noMsaa`,实测见「画质」 |
98
98
  | `renderer` | `'auto' \| 'webgl2' \| 'webgpu'`(也认布尔开关) | `'auto'` | 渲染后端开关:当前实装的绘制后端只有 WebGL2 —— 十万级下 `frameMs` 只有 0.05 ~ 0.2 ms,开销在填充率与 DOM 覆盖层上,换 WebGPU 省不到(第 18 章),所以 `'auto'` 只会在后台探一次 WebGPU 能力、绘制仍走 WebGL2,`'webgl2'` 连探测都不跑(无 GPU 的远程桌面里省一次 `requestAdapter`),`'webgpu'` 探完明确报一句「已回退 WebGL2、为什么」;`true` / `false` 等价于 `'webgpu'` / `'webgl2'`。读数用 `getRendererInfo()`,见「渲染后端(WebGPU 开关)」 |
99
99
  | `ariaLabel` | `string` | `GIS 视图` | 画布的无障碍名称 |
@@ -887,7 +887,7 @@ view.updateOptions({ quality: 'high' })
887
887
 
888
888
  - **类型**:`boolean`
889
889
  - **默认值**:`true`
890
- - **说明**:交互期细线快路径:可见线段全部细到 3px 以内且没有虚线时,拖动 / 缩放期间改用 `gl.LINES` 画(1 设备像素、无光晕与虚线),停手立刻回到四边形展开。位置不跳、悬停 / 选中不受影响;这一帧走没走快路径见 `getStats().thinLines`,实测见「画质」
890
+ - **说明**:交互期细线快路径:可见线段最大半宽 ≤ 0.6 设备像素(dpr = 1 时约 1.2px 线宽)且整批没有虚线时,拖动 / 缩放期间改用 `gl.LINES` 画(1 设备像素、无光晕与虚线),停手立刻回到四边形展开。2px 实线不会走这一档,但全实线四边形另有轻量片元。位置不跳、悬停 / 选中不受影响;这一帧走没走快路径见 `getStats().thinLines`,实测见「画质」
891
891
  - **参考**:见「MSAA 与交互期细线」一节,子字段在那里的表里逐个展开
892
892
 
893
893
  #### `interactionNoMsaa`
@@ -1004,7 +1004,7 @@ view.setFilter(null) // 清掉筛选
1004
1004
 
1005
1005
  - **类型**:`GisViewMinimapOptions | null`
1006
1006
  - **默认值**:无
1007
- - **说明**:内置鹰眼图:位置、尺寸、节点 / 线路 / 视口框三份样式与交互都可配;`position` / `margin` 定角落,`offset` 相对角落动态移动整张图,`container` 可挂到外部 `div`(`getMinimapElement()` 取根节点做整图拖动)。`extent` 决定小图按数据范围还是「数据 ∪ 底图」铺(默认 `auto`,GeoJSON 底图不会缩成一个点),`extentFollowCamera` 默认 `false`、打开后才允许 `auto` 范围随相机收窄,避免外围要素消失时包围盒与视口框跳动。`zoomable` 三态互斥:`false` 关闭、`true` 手动缩放小图自身、`'follow'` 让整体范围跟随主图(`followZoomScale` 是主图倍数的指数;跟随模式下 `setMinimapZoom` / `zoomMinimapBy` 不抢控制权,`zoom` 是基准倍数);空白处拖动可平移小图或直接框选让主图显示过去(`dragAction`,`box` 不依赖缩放,手动模式下 Ctrl / ⌘ 才切换动作),每格倍率 `zoomStep`。通道关闭 `channelCircuits` 时按当前可见成员逐顶点平均成一根居中代表线;小图同样支持规则(`pointRules` / `lineRules`,缺省沿用主图规则的颜色 / 层级、最终节点尺寸与图标及样式链线宽,节点 / 线路尺寸按两边视口比例缩放且不受主图相机倍数影响),可以只画某几层(`levels`)、关掉分层叠画(`layered`)、按三组系数给节点与线路定尺寸(整体倍数 `pointSizeScale` / `lineWidthScale` 对自有与继承要素都生效;跟随小图倍数 `pointZoomScale` / `lineZoomScale` 在小图自身放大时也作用于继承要素;跟随主视图地图倍数 `pointMapZoomScale` / `lineMapZoomScale` 只作用于自有规则 / 样式),视口框与框选矩形的颜色 / 背景 / 边框 / 圆角也能配(`boxStyle` / `selectStyle`),`backgroundStyle` 显式设置时接管画布底色,`extent` 还能直接写死一份世界范围、容器可加 `className` / `style`、`layers` 按 `placement` 分数据前 / 数据后、段内按 `order` 拆出多层自绘内容(旧 `draw` 钩子仍兼容并排在最后)、`onInteraction` 在手势结束时给出类型 / 世界坐标 / 相机、`maxPixelRatio` 限制备份倍率上限,不配则不产生任何 DOM,见「鹰眼图」
1007
+ - **说明**:内置鹰眼图:位置、尺寸、节点 / 线路 / 视口框三份样式与交互都可配;`position` / `margin` 定角落,`offset` 相对角落动态移动整张图,`container` 可挂到外部 `div`(`getMinimapElement()` 取根节点做整图拖动)。`extent` 决定小图按数据范围还是「数据 ∪ 底图」铺(默认 `auto`,GeoJSON 底图不会缩成一个点);`extentFollowContent` 默认 `false`,首次铺好后筛选 / 显隐 / 增删内容不再改范围与缩放,打开后才跟进当前可见内容;`extentFollowCamera` 默认 `false`、打开后才允许 `auto` 范围随相机收窄。`zoomable` 三态互斥:`false` 关闭、`true` 手动缩放小图自身、`'follow'` 让整体范围跟随主图(`followZoomScale` 是主图倍数的指数;跟随模式下 `setMinimapZoom` / `zoomMinimapBy` 不抢控制权,`zoom` 是基准倍数);空白处拖动可平移小图或直接框选让主图显示过去(`dragAction`,`box` 不依赖缩放,手动模式下 Ctrl / ⌘ 才切换动作),每格倍率 `zoomStep`。通道关闭 `channelCircuits` 时按当前可见成员逐顶点平均成一根居中代表线;小图同样支持规则(`pointRules` / `lineRules`,缺省沿用主图规则的颜色 / 层级、最终节点尺寸与图标及样式链线宽,节点 / 线路尺寸按两边视口比例缩放且不受主图相机倍数影响),可以只画某几层(`levels`)、关掉分层叠画(`layered`)、按三组系数给节点与线路定尺寸(整体倍数 `pointSizeScale` / `lineWidthScale` 对自有与继承要素都生效;跟随小图倍数 `pointZoomScale` / `lineZoomScale` 在小图自身放大时也作用于继承要素;跟随主视图地图倍数 `pointMapZoomScale` / `lineMapZoomScale` 只作用于自有规则 / 样式),视口框与框选矩形的颜色 / 背景 / 边框 / 圆角也能配(`boxStyle` / `selectStyle`),`backgroundStyle` 显式设置时接管画布底色,`extent` 还能直接写死一份世界范围、容器可加 `className` / `style`、`layers` 按 `placement` 分数据前 / 数据后、段内按 `order` 拆出多层自绘内容(旧 `draw` 钩子仍兼容并排在最后)、`onInteraction` 在手势结束时给出类型 / 世界坐标 / 相机、`maxPixelRatio` 限制备份倍率上限,不配则不产生任何 DOM,见「鹰眼图」
1008
1008
  - **参考**:见「鹰眼图」一节,子字段在那里的表里逐个展开
1009
1009
 
1010
1010
  ```js
@@ -1018,6 +1018,7 @@ createGisView(container, {
1018
1018
  container: '#minimap-host',
1019
1019
  backgroundStyle: 'rgba(255, 255, 255, 0.92)',
1020
1020
  extent: 'auto',
1021
+ extentFollowContent: false,
1021
1022
  extentFollowCamera: false,
1022
1023
  dragAction: 'box',
1023
1024
  zoomable: 'follow',
@@ -7,7 +7,7 @@
7
7
  | 字段 | 载荷 | 说明 |
8
8
  | --- | --- | --- |
9
9
  | `onReady` | `{ canvas, gl }` | WebGL2 上下文与缓冲区就绪 |
10
- | `onSelect` | `GisViewSelectPayload \| null` | 选中状态变化:点击为 `{ items, box: null, originalEvent }`,框选(默认按住 Shift)为 `{ items, box, originalEvent }`,点击空白处清空时为 `null`;Ctrl 点击节点的累加选中也走这里 |
10
+ | `onSelect` | `GisViewSelectPayload \| null` | 选中操作回调:回答「这一下选了什么」。真实单击 / Ctrl / `multiSelect` 取反 / 框选 / `selectAt()` 每次成功操作都发(清空与「取反取没了」的载荷是 `null`);`selectFeatures()` / `deselect()` / `focusOn({ select })` 等按引用接口只在选中集实际变化时发。载荷带 `items` / `box` / `originalEvent` / `mode`;单通道时顶层还有 `channelId` / `channelIndex` / `channel` / `groupId` / `groupIndex` / `group` / `groups`,多通道看 `channelContexts`,`items[]` 里的回线只带 `channelId` / `channelIndex` / `groupId` / `groupIndex` 标量归属,不重复挂 `group` / `groups` |
11
11
  | `onClick` | `GisViewClickPayload \| null` | 单击要素或空白处(空白处为 `null`):`type` / `item` / `index` / `screen` / `coordinate` / `button` / `selection` / `originalEvent` 之外还有 `action`(`type` 是 `select` / `toggleSelect` / `clearSelection` / `click`,`double` 说明这一下是不是双击里的第二下,`press` / `duration` / `longPress` 说明这一次按下想做什么、按了多久、算不算长按),见「指针事件」。`selectTrigger: 'dblClick'` 时第一下只发 `action.type: 'click'`,第二下才应用选中并带 `double: true` |
12
12
  | `onClusterClick` | `GisViewClusterClickPayload` | 点中多成员聚合圆:带 `id` / `count` / `pointIndexes` / `pointIds` / `bounds` / `radius` / `coordinate` / `screen` / `selection` / `action`(`action.type` 恒为 `clusterClick`、`action.target` 为 `cluster`)。这一条**不会同时发 `onClick`**,也不会改动选中集;聚合圆内部的原始节点不参与普通点击 |
13
13
  | `onDblClick` | `GisViewClickPayload \| null` | 双击要素或空白处(320ms / 6px 内连续两次单击):`action.type` 是 `dblClick`、`action.double` 为 `true`。普通双击在两种 `selectTrigger` 下都会于第二下派发;`dblClick` 模式下选中 / 清空 / 多选取反也在这一下应用,`click` 模式下第一下已经应用过。`click` 模式的 Ctrl / `multiSelect` 取反快捷路保持原口径,不额外发 `onClick` / `onDblClick` |
@@ -36,8 +36,8 @@
36
36
  | `onCameraChange` | `GisViewCamera` | 平移或缩放后触发 |
37
37
  | `onViewport` | `GisViewViewport` | 视口尺寸 / 显示比例变化(画布按新尺寸重铺之后才发):`{ cssWidth, cssHeight, pixelRatio, displayScale }`,自建面板与小图按它跟着调整 |
38
38
  | `onStats` | `GisViewEngineStats` | 渲染统计(点数、线数、线路批次真正铺的段数 `lineInstances`、符号数、绘制次数、`fps` / `idle` / 帧耗时、拾取耗时 `pickMs`、密度场读数 `heatDensity`,以及这一帧的光栅与线段路径 `antialias` / `software` / `thinLines`),按 `statsInterval` 节流(默认 250ms 一次)。静止画面按需渲染,这时 `idle` 为 `true`、`fps` 为 `0`,`frameMs` 是最近一帧的耗时;首帧一定会报一次(面板打开就有读数,不会一直空着) |
39
- | `onDataChange` | `GisViewDataChangePayload` | 数据变更的统一出口:调用接口改数据、拖拽 / 复制 / 合并落地、拐点增删、通道新增 / 移除 / 分组都会发,载荷带动作 `type`、最新数据与条数,见「数据变更与其它事件」 |
40
- | `onSelectChange` | `GisViewSelectChangePayload` | 选中集变化:只在选中集真的变了时发一条(框选过程中的重复同步与数据补发都不发),接口选中也走它;载荷带 `ids` / `count` / 各类型条数与来源 `cause` |
39
+ | `onDataChange` | `GisViewDataChangePayload` | 数据变更的统一出口:调用接口改数据、拖拽 / 复制 / 合并落地、拐点增删、通道新增 / 移除 / 分组都会发,载荷带动作 `type`、数据修订号 `dataVersion`、最新数据与条数,见「数据变更与其它事件」 |
40
+ | `onSelectChange` | `GisViewSelectChangePayload` | 选中集变化:只在选中集真的变了时发一条(框选过程中的重复同步与数据补发都不发),点击与 `selectFeatures` / `deselect` / `selectAt` 等入口都走它;载荷带 `items` / `ids` / `count` / 各类型条数、来源 `cause`、`action` 与同一份通道上下文;顺序固定排在 `onSelect` 之后 |
41
41
  | `onShortcut` | `GisViewShortcutPayload` | 快捷键命中:`action`(`cancel` / `copy` / `paste` / `remove` / `fitView` / `undo` / `redo`)、`key` 与 `handled`(组件有没有接住,`false` 时可以接着做自己的处理 —— 没得撤销 / 重做时也是 `false`) |
42
42
  | `onRender` | `GisViewRenderPayload` | 节流过的帧回调:`onStats` 的读数 + `frame` / `interval` / `timestamp` 与当前 `camera`,间隔见 `renderEmitInterval` |
43
43
  | `onDataRendered` | `GisViewDataRenderedPayload` | 数据变更之后的第一帧画完(连着几次变更合并成一帧一条):载荷带那一帧的读数、这次的动作 `type`、合并的笔数 `changes` 与各列条数 `counts` —— 切场景后收 loading 用它最稳,见「动态换数据与加载完成」 |
@@ -53,7 +53,7 @@
53
53
 
54
54
  组件的交互回调很多(选中、拖拽、复制、合并、拐点、通道…),要把「数据变了」写回自己的状态不必每个都接一遍:
55
55
  `onDataChange` 是数据变更的统一出口,`type` 说明这次是什么动作,`points` / `lines` / `channels`
56
- 就是组件内部那一份数据(只读引用),`counts` 是三个条数。
56
+ 就是组件内部那一份数据(只读引用),`counts` 是三个条数,`dataVersion` 是数据修订号:每次数据变更 +1,只改全局配置 / 样式时不递增。
57
57
 
58
58
  ```js
59
59
  const view = createGisView('#map', {
@@ -85,16 +85,44 @@ const view = createGisView('#map', {
85
85
  | `ok` / `reason` | 这一次调用成没成(`addPoints` / `addLines` / `removePoints` 这类批量接口会带上),`reason` 说明没成的原因 |
86
86
  | 动作专属字段 | 拖拽带 `movedPoints` / `movedLines`,复制带 `item` / `source` / `revert()`(`copyPoints` / 整组粘贴另有 `sources` / `items` / `indexes` / `lines` / `channels` / `count`),合并带 `source` / `target` / `removedLines` / `mergedChannels`,拐点带 `lineIndex` / `pathIndex` / `phase`,新增带 `lineIds` / `lineIndex` / `channelId` / `groupId`(`addPoints` / `addLines` 另有 `indexes` / `items` / `position`:新要素在数组里的位置与加进去的那几条,`addChannelGroups` 另有 `results` / `groupIds` / `items`),删除带 `indexes` / `item` / `removedLineIds`,分组带 `index` / `groups` / `action`(细分动作,与 `onChannelGroupChange` 同一份);批量补丁(`patchPoints` / `patchLines`)的 `updatePoints` / `updateLines` 带 `indexes` / `items` / `count` / `skipped`,`removeSelected` 带 `pointIds` / `lineIds` / `pointIndexes` / `lineIndexes` / `removedChannels`,`filterRemove` 带 `mode: 'remove'` / `pointIndexes` / `removedPoints` / `removedLineIndexes` / `removedLineItems` / `lineIds` / `count` / `removedLines` / `removedChannels`(`points` / `lines` / `channels` 仍是删除后的最新数据) |
87
87
 
88
- `onSelectChange` 与 `onSelect` 的区别只有一个:**只在选中集真的变了时发** —— 框选过程中的重复同步、
89
- 数据变化引起的补发都不发;并且**接口选中**(`selectFeatures()` / `selectFeatures(null)` / `selectInBox()`)也会发
90
- (引擎那条 `setSelection` 自己不派发 onSelect,组件层在这里补上)。载荷里的 `cause` 说明这次是怎么选上的:
91
- `click`(指针点击)/ `boxSelect`(框选)/ `keyboard`(Esc 清空)/ `api`(接口调用)。
88
+ `onSelect` 与 `onSelectChange` 监听的是**同一份选中集**,不存在「按引用选中不算选中」这回事:点击、
89
+ `selectFeatures()` / `deselect()` / `selectAt()` / `focusOn({ select })`、框选与键盘清空改的都是它。
90
+ 两者不是二选一:`onSelect` 回答「这一下选了什么」,适合跟一次具体操作;`onSelectChange` 回答「选中集
91
+ 变了什么」,只在集合真的变化时发,适合统一同步面板 / URL / 外部状态。
92
+
93
+ - `onSelect`:真实单击、Ctrl / `multiSelect` 取反、框选(含 `selectInBox()`)与 `selectAt()` 每次成功操作都发
94
+ —— 即使结果和上一份选中集相同也发;点空白 / 框选空框 / 取反取没了时载荷是 `null`。按引用改选中集的
95
+ `selectFeatures()` / `deselect()` / `focusOn({ select })` 只在选中集实际变化时发。
96
+ - `onSelectChange`:覆盖所有来源,**只有选中集真的变了才发一条**;载荷另带 `ids` / `count`、
97
+ `points` / `lines` / `channels`(后三项是条数)与来源 `cause`、`action`。要同步面板又不想处理重复通知,订阅它。
98
+
99
+ 派发顺序固定(`onSelect` 总在 `onSelectChange` 前面,事件总线里的 `select` / `selectChange` 同序;
100
+ 下表里的 `onSelectChange` 只在实际改变选中集时出现):
101
+
102
+ | 这一下怎么选 | 实际顺序 |
103
+ | --- | --- |
104
+ | 普通单击命中 / 点空白 | `onClick → onSelect → onSelectChange`(末项有变化时;点空白时选中已清空,`onSelect` 载荷为 `null`) |
105
+ | Ctrl / `multiSelect` 取反 | `onSelect → onSelectChange`(末项有变化时;不经过 `onClick`) |
106
+ | 框选 / `selectInBox()` | `onSelect → onSelectChange`(末项有变化时;不经过 `onClick`) |
107
+ | `selectAt()` / `selectFeatures()` / `deselect()` / `focusOn({ select })` | `onSelect → onSelectChange`(不派发 `onClick` / `onDblClick`;`selectAt` 每次命中都发 `onSelect`,后三个按引用接口只在集合变化时发) |
108
+ | `selectTrigger: 'dblClick'` 的第二下 | `onClick → onSelect → onSelectChange`(有变化时)`→ onDblClick` |
109
+
110
+ `onSelect` 与 `onSelectChange` 带同一份通道上下文:选中集只涉及**一条通道**时,载荷顶层平铺
111
+ `channelId` / `channelIndex` / `channel` / `groupId` / `groupIndex` / `group` / `groups`;`group` 是当前组
112
+ (真实单击 / `selectAt` 是命中的那一组,框选与按引用接口是选中集里第一条通道回线所在的组),`groups`
113
+ 是整条通道的全部分组。选中集涉及**多条通道**时顶层不再摊平,改看 `channelContexts`(每项带这组字段与
114
+ 本次选中的 `lineIndexes` / `lineIds`)。单通道不会再额外造一份 `channelContexts`。两个事件共享同一批
115
+ `items` 与分组数组引用,不会深拷贝;`items[]` 里每条回线只保留标量归属字段,需要分组读数从顶层
116
+ 或 `channelContexts` 查,不再给每条回线重复挂 `group` / `groups`。纯节点或空选中没有这些通道字段。
117
+
118
+ `cause` 取 `click`(指针点击)/ `boxSelect`(框选)/ `keyboard`(Esc 清空)/ `api`(接口调用)。
92
119
 
93
120
  `action` 是更细的一份判读(与指针事件同一套写法):`action.type` 是 `select`(点中一个或接口选中)/
94
121
  `toggleSelect`(Ctrl 点击取反,取反取没了也是它)/ `boxSelect`(框选,没框到东西也是它)/
95
122
  `clearSelection`(点空白、Esc 或接口清空),`action.target` 是 `selection` / `empty`,`action.data` 就是
96
- 这一批 `items`,`action.modifiers` 是这一次的修饰键。单击触发模式下,Ctrl 取反与点空白清空这两条路**不经过 `onClick`**;
97
- `selectTrigger: 'dblClick'` 时第一下仍会发 `onClick`,第二下应用选中 / 取反 / 清空后会同时发 `onClick` 与 `onDblClick`。
123
+ 这一批 `items`,`action.modifiers` 是这一次的修饰键。单击触发模式下,Ctrl 取反与框选这两条路**不经过 `onClick`**;
124
+ 点空白仍会发 `onClick`(载荷为 `null`),顺序同样是 `onClick → onSelect → onSelectChange(有变化时)`。`selectTrigger: 'dblClick'`
125
+ 时第一下仍会发 `onClick`,第二下应用选中 / 取反 / 清空后按 `onClick → onSelect → onSelectChange(有变化时)→ onDblClick` 派发。
98
126
  要按操作分支的面板认这一个字段:
99
127
 
100
128
  ```js
@@ -40,7 +40,7 @@ getStats().quality // onStats 里也是同一份读数:读数面
40
40
  | `gl.LINES`(每段两个顶点) | 112 ms/帧 | 23 ms/帧 |
41
41
 
42
42
  - **`antialias`**(缺省开):MSAA 会把这类细长三角形放大约 5 倍,是最省事的一根杠杆 —— 关掉线条边缘会有锯齿(点与圆符号本来就有距离场抗锯齿,几乎看不出来)。软件光栅(远程桌面 / 驱动被拉黑回落 SwiftShader)下引擎自动关掉它;读数看 `getStats().antialias`。
43
- - **`interactionThinLines`**(缺省开):交互期细线快路径 —— 可见线段全部细到 3px 以内、且整批没有虚线时,拖动 / 缩放期间改用 `gl.LINES` 画(1 设备像素、不画光晕),停手立刻回到带像素宽度与虚线的四边形展开。两种画法共用同一张实例缓冲,所以位置不会跳、选中与悬停都不受影响,只是交互那几帧的线更细更硬。同一页交替开关实测:交互期帧耗时 p50 **194 ~ 208 ms → 132 ~ 139 ms**,同一 2.2 s 窗口的帧数 **13 → 19 ~ 24**;这一帧走没走快路径看 `getStats().thinLines`。
43
+ - **`interactionThinLines`**(缺省开):交互期细线快路径 —— 可见线段最大半宽 ≤ 0.6 设备像素(dpr = 1 时约 1.2px 线宽)、且整批没有虚线时,拖动 / 缩放期间改用 `gl.LINES` 画(1 设备像素、不画光晕),停手立刻回到带像素宽度与虚线的四边形展开。2px 实线不会走这一档,但全实线四边形另有轻量片元;两种画法共用同一张实例缓冲,所以位置不会跳、选中与悬停都不受影响,只是交互那几帧的线更细更硬。同一页交替开关实测:交互期帧耗时 p50 **194 ~ 208 ms → 132 ~ 139 ms**,同一 2.2 s 窗口的帧数 **13 → 19 ~ 24**;这一帧走没走快路径看 `getStats().thinLines`。
44
44
  - **`interactionNoMsaa`**(缺省开):上一轮把账拆到「MSAA 4× 把细长三角形放大约 5 倍」,那就干脆不带 MSAA 画 —— 拖动 / 缩放 / 框选期间与动画在跑的那些帧,整帧先画进一张不带 MSAA 的后备贴图(分辨率跟画布一致,画布尺寸变了当帧重建;建不出来就放弃这一档、这一帧照旧直接画),帧末用一笔满屏四边形整幅贴回画布;停手 160ms(或动画停帧)之后的下一帧照旧直接画进带 MSAA 的画布,所以**静止画面一个像素都没变**,缺的只是这些活动帧的边缘抗锯齿。真机实测(898×558 画布、10 万点 / 12 万线、`quality: 'high'`):交互期单帧(`readPixels` 强制同步读回)**133 → 36.6 ms**,真实输入长跑(120 步)拖动 p50 **130 → 16 ~ 22 ms**、缩放 p50 **239.5 → 16.7 ms**、平移 p95 **129 → 17 ms**;同一台机器上的动画帧(10 万点 + 12 万线、涟漪与线路动点都开)**212 → 53 ms/帧**(只开涟漪 187 → 48 ms、只开线路动点 199 → 47 ms,2.6 s 窗口里的帧数 12 ~ 15 → 49 ~ 57);同一交互状态下开 / 关后备缓冲各抓一帧整幅对拍,差异像素 6.4%、平均差 0.53 / 255(都在抗锯齿边缘上)。这一帧走没走看 `getStats().noMsaa`,`updateOptions({ interactionNoMsaa })` 可以运行时改;透明底 / 自定义底色的导出场景(要逐像素可控)与本来就没开 MSAA 的画布不走这条。
45
45
  - **点符号的方框也留了一圈**(这一轮):上一条「静止画面一个像素都没变」当初对线路成立、对**节点符号并不成立** —— `gl.POINTS` 的方框正好是形状的外接框,MSAA 那一遍按方框的采样覆盖率合成、光栅化又按「像素中心落在方框内」判覆盖,方框边压住描边的那圈像素于是只在 MSAA 那一遍被削掉;交互期走非 MSAA 后备缓冲时没人削它,拖动节点就是「描边一厚一薄、一动一动」。现在方框每边多留 1 个设备像素(`shaders.js` 的 `POINT_EDGE_PAD`):顶点着色器按 `size + 2` 定 `gl_PointSize` 并传出 `v_quadRatio = quadSize / size`,片元里把 `gl_PointCoord` 缩回形状自身的尺度,距离场与线宽一个字节都没动。真机(`antialias: true`)对拍:同一节点拖动帧与松手后的静止帧**逐像素一致(差异 0 字节)**,改之前同一窗口差异 86 字节 / 最大差 35;代价是方框面积大了一圈,10 万点满屏实测 10 → 11.8 ms/帧(0.5 个设备像素实测还躲不干净,所以取 1.0)。
46
46
  - **贴图符号的方框吸附到设备像素边界的「栅格」上**(这一轮,接着上一条):上一条管的是 `gl.POINTS` 那条形状管线,SVG / 图片走的是四边形贴图 —— 它的吸附一直在**引擎像素**坐标里做(上一轮那版 `floor(anchor - halfSize) + halfSize` 只挪了「吸中心还是吸边」),而引擎像素坐标的原点在**画布中心**(`gl_Position = pixel * 2 / resolution`)、画布中心又正好落在某个设备像素的中心上:设备像素的边界在引擎坐标里是一张固定的栅格 —— 分辨率是偶数时落在整数上、奇数时落在半整数上(就是「分辨率一半」的小数部分)。只在整数上吸附的话,奇数分辨率(DPR 1.5、奇数宽度的画布)下四条边全压在设备像素中心上:MSAA 那一遍按采样点给半格覆盖率、非 MSAA 那一遍(`interactionNoMsaa`)按「像素中心在方框里」判覆盖、整个像素丢,于是拖动 / 缩放 / 平移期间图标最外圈那圈描边整行消失、停手又回来(「一闪一闪」),与图标的尺寸、圆方都无关(上一轮「只在奇数尺寸时露出来」是 318×238 那块偶数画布带出来的读数)。现在 `SPRITE_VERTEX_SHADER` 按这张栅格吸附:`vec2 grid = fract(u_resolution * 0.5); anchor = floor(clip * 0.5 * u_resolution + u_offsetPx - halfSize - grid) + halfSize + grid;` —— 四条边永远落在设备像素边界上,两条路径要么整格画、要么整格不画;偶数分辨率退化成上一轮那一行、逐像素不变(318×238 画布上 20 / 21 / 22 px × 四个位置共 12 个图标与旧写法 0 差异),贴图也还是 1:1 落在屏幕上。真机对拍(`antialias: true`):319×238 画布上 20 / 21 px 的贴图符号旧写法「平移帧 vs 交互帧」差 120 / 126 字节、本轮 0;DPR 1.5 的 449×357 画布上中心吸附 / 上一轮那行 / 本轮是 298 / 255、373 / 255、0 / 0。
@@ -65,7 +65,7 @@ view.updateOptions({ cascadeHighlight: false }) // 关掉并收掉当前高亮
65
65
 
66
66
  ## 指针事件
67
67
 
68
- 鼠标的每一种动作都有回调,载荷里都能拿到「当前是哪个元素」:`onHover`(悬停)、`onClick`(单击左键)、`onDblClick`(双击)、`onPointerDown` / `onPointerUp`(按下与抬起,含左 / 中 / 右键)、`onContextMenu`(右键)。悬停是 `GisViewHoverPayload`、点击与双击是 `GisViewClickPayload` —— 都是在 `GisViewPickResult`(点 / 线 / 通道回线的 `type`、`index`、`item`、`screen`、`coordinate` 与通道字段)上再补光标位置与一份 `action`;按下 / 抬起 / 右键统一发 `GisViewPointerPayload`(同样是拾取结果 + `action`)。
68
+ 鼠标的每一种动作都有回调,载荷里都能拿到「当前是哪个元素」:`onHover`(悬停)、`onClick`(单击左键)、`onDblClick`(双击)、`onPointerDown` / `onPointerUp`(按下与抬起,含左 / 中 / 右键)、`onContextMenu`(右键)。悬停是 `GisViewHoverPayload`、点击与双击是 `GisViewClickPayload` —— 都是在 `GisViewPickResult`(点 / 线 / 通道回线的 `type`、`index`、`item`、`screen`、`coordinate`,以及通道回线的 `group` / `groups`)上再补光标位置与一份 `action`;按下 / 抬起 / 右键统一发 `GisViewPointerPayload`(同样是拾取结果 + `action`)。
69
69
 
70
70
  ### action:一次操作的判读
71
71
 
@@ -118,7 +118,7 @@ createGisView(container, {
118
118
  console.log(payload.world, payload.x, payload.y) // 光标处的世界坐标与画布 CSS 像素
119
119
  },
120
120
  onPointerUp: payload => console.log('抬起', payload.type, payload.item),
121
- // 右键:点 / 线 / 空白都发,命中通道回线时载荷里还有 channelId / groupId / channelIndex
121
+ // 右键:点 / 线 / 空白都发,命中通道回线时载荷里还有 channelId / groupId / channelIndex / group / groups
122
122
  onContextMenu: (payload, event) => {
123
123
  if (!payload.hit) return // 空白处也可以在这里收掉自己的菜单
124
124
  event.preventDefault() // 想自己弹菜单就拦住浏览器默认菜单
@@ -128,12 +128,13 @@ createGisView(container, {
128
128
  })
129
129
  ```
130
130
 
131
- - 载荷形状一致,取信息只需认三处:`type`(`'point'` / `'line'` / `null`)、`item`(要素原始数据,可读业务字段)、`world` / `coordinate`(前者是光标处的世界坐标,后者是要素自身的坐标)。命中通道回线时,`channelIndex` / `channelId` / `groupId` / `groupIndex` / `channel` 一并给出;右键载荷还会在 `channelContext` 上带整份分组上下文。
131
+ - 载荷形状一致,取信息只需认三处:`type`(`'point'` / `'line'` / `null`)、`item`(要素原始数据,可读业务字段)、`world` / `coordinate`(前者是光标处的世界坐标,后者是要素自身的坐标)。命中通道回线时,`channelIndex` / `channelId` / `groupId` / `groupIndex` / `channel` 一并给出;`group` 是**当前命中的这一组**,`groups` 是**同一通道的全部分组**(按声明顺序,每项带自己的 `groupIndex`),所以不用再查一次 `getChannelGroups()` 就能画分组切换。
132
+ - 右键载荷还会在 `channelContext` 上带整份分组上下文(当前组、组数、可用动作);`groups` 是各鼠标载荷共有的纯读数,`channelContext.actions` 只在右键里给。
132
133
  - 配了 GeoJSON 背景时,以上载荷都多一个 `lngLat`:按光标的世界坐标(要素命中用 `coordinate`,悬停用最近一次光标位置)反算出来的真实经纬度,点 / 线 / 通道都一样;想自己换算就用 `worldToLngLat` / `screenToLngLat`,光标级别的屏幕坐标 + 世界坐标 + 经纬度看 `onPointer` 与 `getPointer()`。
133
- - `onPointerDown` / `onPointerUp` 不改变组件行为:选中、拖拽、框选、拐点编辑照旧。左键的一次「点」(没有拖动)仍然走 `onClick` / `onDblClick`,两者是并列的,不会互相吞掉 —— 载荷里的 `action.type` 各说各话:按下是**意图**、松开是**刚做完的那件事**(`action.moved` 说明是拖还是点)、点击是**真正发生的那一件**。
134
+ - `onPointerDown` / `onPointerUp` 不改变组件行为:选中、拖拽、框选、拐点编辑照旧。左键的一次「点」(没有拖动)仍然走 `onClick` / `onDblClick`,两者是并列的,不会互相吞掉 —— 载荷里的 `action.type` 各说各话:按下是**意图**、松开是**刚做完的那件事**(`action.moved` 说明是拖还是点)、点击是**真正发生的那一件**。这一下如果同时改了选中,顺序固定是 `onClick → onSelect → onSelectChange`;`selectTrigger: 'dblClick'` 的第二下再在最后补 `onDblClick`。
134
135
  - 点空白处照发:`onPointerDown` / `onPointerUp` 的 `action.target` 是 `empty`(开了平移时按下是 `pan` 的意图),`onClick` / `onDblClick` 的载荷是 `null`(选中在那一刻已经清空)——「点了空白」这件事也可以看 `onSelectChange` 的 `action.type: 'clearSelection'`。
135
136
  - **中键按住就是临时平移**(`pan` 开关开着时):`onPointerDown` 的 `action.type` 是 `pan`、`action.button` 是 `'middle'`,按住拖动平移画布、松开结束(`onPointerUp` 的 `action.type` 也是 `pan`);这一路**不经过** `onClick`,中键也不会改选中 —— 想自己接中键(中键菜单 / 快捷操作)就按 `action.button === 'middle'` 认它。长按(触摸 / 触控笔)与中键一样有明确的读数:看松开 / 点击载荷的 `action.longPress` 与 `action.press` / `action.duration`。
136
- - `onContextMenu` 每次都只发这一条;命中通道回线时,分组上下文挂在同一份载荷的 `channelContext` 上(见「通道右键交互框」),不再额外发第二个回调。**右键缺省整个不接**(`contextMenu` 开关没开时系统菜单照弹、回调也不发);接住之后一律 `preventDefault()` 收掉系统菜单(点 / 线 / 空白 / 通道都一样),回调照常发。想自己弹菜单就写 `interactive: true` / `contextMenu: true` / `features: 'view'`,想让系统菜单自己弹出来就写 `preventContextMenu: false`。
137
+ - `onContextMenu` 每次都只发这一条;命中通道回线时,分组读数在同一份载荷的 `group` / `groups` 上,可执行动作挂在 `channelContext` 上(见「通道右键交互框」),不再额外发第二个回调。**右键缺省整个不接**(`contextMenu` 开关没开时系统菜单照弹、回调也不发);接住之后一律 `preventDefault()` 收掉系统菜单(点 / 线 / 空白 / 通道都一样),回调照常发。想自己弹菜单就写 `interactive: true` / `contextMenu: true` / `features: 'view'`,想让系统菜单自己弹出来就写 `preventContextMenu: false`。
137
138
  - 命中的层级是**点 → 线 → 通道**:右键落在节点上时载荷是 `type: 'point'`,没有 `channelContext` —— 节点压在通道回线上(端点最常见)不会被当成通道右键;点空白处同样没有 `channelContext`,可以直接用它收框。
138
139
  - 悬停是高频率回调:大数据量下建议只做节流的读数或轻量样式切换(demo 里的读数就是这么做的)。
139
140
 
@@ -159,7 +160,7 @@ createGisView(container, {
159
160
  })
160
161
  ```
161
162
 
162
- - Ctrl + 点击(多选取反)与 Shift + 拖动(默认框选键,可用 `shortcuts.boxSelect` 改)**不经过** `onClick`:这两下都走 `onSelectChange` —— 取反的 `action.type` 是 `toggleSelect`(取反取没了也是它,此时 `action.target` 是 `empty`)、框选是 `boxSelect`(没框到东西也是它)、点空白清空是 `clearSelection`;载荷里的 `items` 是操作之后的选中集、`count` 是新的条数。想在这条路里知道「点下去的那一刻它是不是已选中的」,看 `onPointerDown` / `onPointerUp` 载荷的 `selected`,或者在回调里读一次 `getSelectionData()`。
163
+ - Ctrl + 点击(多选取反)与 Shift + 拖动(默认框选键,可用 `shortcuts.boxSelect` 改)**不经过** `onClick`:顺序是 `onSelect → onSelectChange`。取反的 `action.type` 是 `toggleSelect`(取反取没了也是它,此时 `action.target` 是 `empty`)、框选是 `boxSelect`(没框到东西也是它)、点空白清空是 `clearSelection`;载荷里的 `items` 是操作之后的选中集、`count` 是新的条数。想在这条路里知道「点下去的那一刻它是不是已选中的」,看 `onPointerDown` / `onPointerUp` 载荷的 `selected`,或者在回调里读一次 `getSelectionData()`。选中事件里通道回线的 `group` / `groups` 在单通道载荷顶层;多通道一起选中时看 `channelContexts`,`items[]` 里的回线不再逐条重复挂这两个字段。
163
164
  - 不订阅事件、只想知道「这儿是什么」时用 `pickAt(clientX, clientY)`:按鼠标事件里的客户端坐标拾取,返回与指针载荷同一份结果(另加 `target` 与 `lngLat`),空白处返回 `null`;只读 —— 不改选中、不动光标,自建浮层 / 自定义菜单 / 拖拽放下时用它。框内有什么(自建框选界面、框内统计)用 `getFeaturesInRect(rect)`、当前画面里有哪几条通道用 `getChannelsInView()`,三个方法都在「实例方法」里。
164
165
  - 悬停也可以**程序化驱动**:`hoverAt(clientX, clientY)` 让指针「停」到某个位置(悬停高亮、光标与 `onHover` 一起更新,返回与 `onHover` 同一份读数),`clearHover()` 撤掉并补发一条 `onHover(null)`,`getHover()` 随时读当前这一份 —— 外部列表 / 表格 hover 到某一行时联动高亮图上的要素用它,不必自己派发指针事件;不需要了就不用订阅 `onHover`。
165
166
  - 选中集整个拿走用 `getSelectionData()`,一份就够:读数(`ids` / `count` / `pointCount` / `lineCount` / `channelCount`)加**要素本身**(`points` / `lines` / `channels`(按通道去重))与 `items`(与 `getSelections()` 同一份)、`pointIndexes` / `lineIndexes`、`bounds`(世界包围盒,空选中时 `null`):
@@ -2,6 +2,33 @@
2
2
 
3
3
  > 选中与拖动的层级、框选、接口选中、线路拐点的增删改。
4
4
 
5
+ ## 选中与取消选中速查
6
+
7
+ 选中与取消选中共用同一份 state:鼠标点击、Ctrl / ⌘ 多选、框选和接口调用改的都是这一份集合,读回统一用 `isSelected()` / `getSelections()` / `getSelectionData()`。
8
+
9
+ | 想做什么 | 怎么做 |
10
+ | --- | --- |
11
+ | 选中一个 | 单击节点 / 线路;或 `view.selectFeatures(ref)` |
12
+ | 按坐标像单击一样选中 | `view.selectAt(clientX, clientY)`(通道回线按 `channelSelectMode` 摊开) |
13
+ | 换一份选中集 | `view.selectFeatures(refs)`(缺省 `mode: 'replace'`) |
14
+ | 追加 / 取反 / 移除 | `view.selectFeatures(refs, { mode: 'add' \| 'toggle' \| 'remove' })` |
15
+ | 多选 | 打开 `multiSelect` 后单击,或按住 Ctrl / ⌘ 单击;框选先开 `boxSelect`,再按住 Shift 拖动 |
16
+ | 取消指定项 | `view.deselect(refs)`,等价于 `view.selectFeatures(refs, { mode: 'remove' })` |
17
+ | 取消全部 | 单击空白处、无其它编辑态时按 Esc、`view.deselect()`、`view.selectFeatures(null)` 或 `view.selectFeatures([])` |
18
+ | 读回结果 | `view.isSelected(ref)` / `view.getSelections()` / `view.getSelectionData()` |
19
+
20
+ ```js
21
+ view.selectFeatures(['station-1', { type: 'point', index: 3 }]) // 换一份选中集
22
+ view.selectFeatures('station-2', { mode: 'add' }) // 加选
23
+ view.selectFeatures('station-1', { mode: 'toggle' }) // 已选中就取消
24
+ view.deselect('station-2') // 只取消这一项
25
+ view.deselect() // 取消全部
26
+ ```
27
+
28
+ `refs` 可以是节点 / 线路 id、`{ type, index }`、通道 / 分组写法、拾取载荷或搜索命中。公开实例没有 `select()`,旧代码统一改用 `selectFeatures()`;`deselect([])` 是「没有目标」,不会改选中集。上面按缺省 `selectTrigger: 'click'` 写;改成 `'dblClick'` 后,选中与点空白清空都由第二下完成,接口调用不受影响。
29
+
30
+ **选中状态只有一份**:`selectFeatures()` / `deselect()` 与单击、Ctrl 多选、框选改的是同一份选中 state,样式、`isSelected()` 与 `getSelectionData()` 都立即更新。选中集实际变化时,它们与点击一样会按 `onSelect → onSelectChange` 发回调(接口来源 `cause: 'api'`);区别只在入口和通知口径,不在“算不算选中”。`onSelect` 是**本次操作**回调(真实点击 / 框选 / `selectAt` 每次成功操作都发),`onSelectChange` 是**集合状态**回调(只在集合真的变了时发,适合同步面板)。选中通道回线时两条回调都带 `channelId` / `group` / `groups` 等字段;多通道看 `channelContexts`,见 [04 事件与回调](04-事件与回调.md#回调)。
31
+
5
32
  ## 选中 / 拖动的层级(selectionOnTop)
6
33
 
7
34
  `selectionOnTop`(默认 `true`)把「选中的节点、拖动中的整组、待确认合并的目标节点」以及「选中线路所在的整条通道」画在所有要素之上,与 `zIndex` 无关 —— 选中了就一定看得见。写 `selectionOnTop: false` 就完全按 `zIndex` 与数据顺序绘制。抬层只在选中 / 拖动 / 合并状态变化时整份重排一次,悬停这类高频路径不参与,十万级数据下不会拖慢交互。
@@ -54,7 +81,7 @@ createGisView(container, {
54
81
  view.updateOptions({ selectTrigger: 'click' }) // 运行时切回单击选中
55
82
  ```
56
83
 
57
- 运行时切换 `selectTrigger` 会清掉上一次点击的双击判定状态,切换后的第一下不会被算成旧点击序列的第二下。`selectFeatures()` / `select()` 这类接口选中不受触发方式影响。
84
+ 运行时切换 `selectTrigger` 会清掉上一次点击的双击判定状态,切换后的第一下不会被算成旧点击序列的第二下。`selectFeatures()` 这类接口选中不受触发方式影响。
58
85
 
59
86
  ### 选择类型过滤(selectionKinds)
60
87
 
@@ -81,7 +108,7 @@ view.selectInBox({ x: 40, y: 40, width: 320, height: 200 }, { selectionKinds: ['
81
108
 
82
109
  ## 接口选中(selectFeatures)与读回选中
83
110
 
84
- 点选、Ctrl 多选、框选与接口选中落在**同一份选中 state** 上:任何一种选上,`getSelections()` / `getSelectionData()` 与 `onSelectChange` 读到的都是它(接口选中会补一条 `cause: 'api'` 的选中集变更,所以一个回调就能覆盖所有来源)。
111
+ 点选、Ctrl 多选、框选与接口选中落在**同一份选中 state** 上:任何一种选上,`getSelections()` / `getSelectionData()` 读到的都是它。真实操作(点击 / Ctrl 取反 / 框选 / `selectAt`)每次成功操作都发 `onSelect`;按引用接口(`selectFeatures` / `deselect` / `focusOn({ select })`)只在实际改变选中集时发。`onSelectChange` 覆盖所有来源,并且只在集合真的变化时发;两条都在时顺序固定为 `onSelect → onSelectChange`(真实单击最前面还有 `onClick`)。
85
112
 
86
113
  `selectFeatures(refs, options?)` 认这些写法(传 `null` / 空数组等于清空):
87
114
 
@@ -112,6 +139,35 @@ view.selectFeatures({ type: 'channel', channelId: 'channel-a' }, { mode: 'remove
112
139
 
113
140
  拾取载荷(`pickAt` 或指针事件里的那一份)与搜索命中里可能带着 `channelId` / `channelIndex` / `groupId` —— 那是「这条回线属于哪条通道」的上下文,不是「要选整条通道」:它们的 `type` / `kind` 写着 `line` / `point`,落地选中的就是那一条要素自己(与在画布上点它一个结果)。要整条通道,显式写 `{ type: 'channel', channelId: 'channel-a' }`。
114
141
 
142
+ ### 按坐标单击选中(selectAt)
143
+
144
+ 如果手上只有鼠标坐标、又希望组件像用户单击一样选中,用 `selectAt(clientX, clientY)`:命中顺序、通道回线摊开(`channelSelectMode`)与真实点击一致,但不需要先自己拆拾取载荷。命中空白或聚合圆时返回 `null`,不改原选中集。
145
+
146
+ 它立即生效,不受 `selectTrigger: 'dblClick'` 等待影响,也不派发 `onClick` / `onDblClick`;每次成功命中都发 `onSelect`(模拟一次单击),`onSelectChange` 只在选中集实际变化时发,`cause` 都是 `'api'`。选中通道回线时两条回调都带当前命中的 `group`、整条通道的 `groups` 与顶层 `channelId`;多通道看 `channelContexts`。要取消指定项或全部,继续用 `deselect()`。
147
+
148
+ `selectAt()` 与 `selectFeatures()` 最终落在同一份选中 state,选中样式和读回结果没有区别;区别在入口、是否经过坐标命中与返回值:
149
+
150
+ | | `selectAt(clientX, clientY)` | `selectFeatures(refs, options?)` |
151
+ | --- | --- | --- |
152
+ | 输入 | 浏览器客户端坐标 | id / 下标 / 通道 / 分组等引用 |
153
+ | 命中方式 | 与真实单击同一套 `pick()`,通道回线按 `channelSelectMode` 摊开 | 按引用直接解析;整条通道要显式写 `{ type: 'channel', channelId }` |
154
+ | 怎么改选中集 | 始终像普通单击一样换一份 | `mode` 可 `'replace'` / `'add'` / `'toggle'` / `'remove'` |
155
+ | 空白 | 返回 `null`,原选中集不动 | `null` / 空数组清空;认不出的条目逐条跳过 |
156
+ | 回调 | 每次成功命中都发 `onSelect`;选中集实际变化时再发 `onSelectChange`。顺序 `onSelect → onSelectChange`(`cause: 'api'`),不发 `onClick` / `onDblClick` | 选中集实际变化时发 `onSelect → onSelectChange`(`cause: 'api'`),不发 `onClick` / `onDblClick` |
157
+ | 返回 | 本次选中载荷或 `null` | 实例本身,可链式调用 |
158
+
159
+ ```js
160
+ const at = view.worldToClient(120, 60)
161
+ const selected = view.selectAt(at[0], at[1]) // 节点 / 线路 / 通道回线,命中什么选什么
162
+
163
+ view.updateOptions({ channelSelectMode: 'channel' })
164
+ view.selectAt(at[0], at[1]) // 命中通道里任一回线 = 选中整条通道
165
+ view.selectAt(-1000, -1000) // 空白:返回 null,原选中集不动
166
+ ```
167
+
168
+ 已经知道节点 id 时直接用 `view.selectFeatures('station-1024')`,不必先求坐标;`selectAt` 留给「只有鼠标 / 触摸坐标」
169
+ 的场景(画布外点击转发、命中测试、浮层把坐标交给组件)。它不是选中节点的必经入口,也不是 `selectFeatures` 的替代品。
170
+
115
171
  聚焦同一套目标写法交给 `focusOn`,显式 `type` 还能解决节点与线路同 id 的歧义;整条通道可以写 `{ channelId }`,分组写 `{ type: 'group', channelId?, groupId }`(`groupId` 全局唯一时可省略 `channelId`)。节点、线路、通道与分组的取范围与缩放都走 `zoomToBounds`,不会因为类型不同换一套放大倍率:
116
172
 
117
173
  ```js
@@ -144,7 +200,7 @@ view.focusOn(selection.items, { select: true, zoom: true }) // 反过来也行
144
200
 
145
201
  ### 取消选中(deselect / selectFeatures(null))
146
202
 
147
- 取消某一个 / 某一类 / 全部都是**一句话**(`deselect()` 与 `selectFeatures(null)` 都表示全部取消):与选中落在同一份 state 上,清完 `getSelections()` / `getSelectionData()` 立刻读得到,`onSelectChange` 补一条 `cause: 'api'` 的选中集变更。
203
+ 取消某一个 / 某一类 / 全部都是**一句话**(`deselect()` 与 `selectFeatures(null)` 都表示全部取消):与选中落在同一份 state 上,清完 `getSelections()` / `getSelectionData()` 立刻读得到;选中集实际变化时按 `onSelect → onSelectChange` 派发,`onSelect` 的载荷为 `null`、`cause` 为 `'api'`。
148
204
 
149
205
  ```js
150
206
  view.deselect('station-1024') // 取消一个节点(id,或 { type: 'point', index: 3 })
@@ -40,7 +40,20 @@ view.showFeatures('channel', 'c1')
40
40
  view.updateOptions({ channelSelectMode: 'channel' }) // 点一下回线 = 选中整条通道
41
41
  ```
42
42
 
43
- Ctrl / ⌘ + 点击在 `'group'` / `'channel'` 下是**整批取反**:这一组 / 这一条通道都在选中集里时一起取消,否则一起选上(与单击走同一套摊开规则)。按住 `shortcuts.addWaypoint`(默认也是 Ctrl / ⌘)点击通道回线加点时,加完由这一档决定选中并高亮一条、一组还是整条通道。框选、接口选中(`select` / `selectFeatures`)与「按通道写法的接口选中」不受影响。改这一项只重画一帧、不重排场景(`updateOptions({ channelSelectMode })`)。
43
+ Ctrl / ⌘ + 点击在 `'group'` / `'channel'` 下是**整批取反**:这一组 / 这一条通道都在选中集里时一起取消,否则一起选上(与单击走同一套摊开规则)。按住 `shortcuts.addWaypoint`(默认也是 Ctrl / ⌘)点击通道回线加点时,加完由这一档决定选中并高亮一条、一组还是整条通道。框选与 `selectAt()` 也按这一档摊开;`selectFeatures` 的显式通道写法按你给的目标选中(要整条通道就写 `{ type: 'channel', channelId }`),不靠这一档推断。改这一项只重画一帧、不重排场景(`updateOptions({ channelSelectMode })`)。
44
+
45
+ 鼠标载荷(`onPointerDown` / `onPointerUp` / `onClick` / `onDblClick` / `onHover` / `pickAt()`)命中通道回线时,
46
+ `group` 是当前命中的那一组,`groups` 是同一通道的全部分组(按声明顺序,每项带 `groupIndex` / `lineIds` /
47
+ 骨架与 `path`)。点击一条回线时不必先扫一遍 `getChannelGroups()` 再自己判断是哪组;`groups.length` 就是组数。
48
+
49
+ 选中回调(`onSelect` / `onSelectChange`)带同一份通道上下文:选中集只涉及一条通道时,载荷顶层平铺
50
+ `channelId` / `channelIndex` / `channel` / `groupId` / `groupIndex` / `group` / `groups`。真实单击与
51
+ `selectAt()` 的 `group` 是命中的那一组;框选、`selectFeatures()` 这类没有单次命中组的入口,取选中集里
52
+ 第一条通道回线所在的组。`groups` 始终是整条通道的全部分组,不是只含选中的组。一次选中多条通道时顶层
53
+ 不再摊平,改看 `channelContexts`(每项带同样的字段与本次选中的 `lineIndexes` / `lineIds`);单通道不会
54
+ 额外创建 `channelContexts`。`items[]` 里每条回线只带 `channelId` / `channelIndex` / `groupId` /
55
+ `groupIndex` 标量归属,不再逐条重复挂 `group` / `groups`;分组读数统一从顶层或 `channelContexts` 取。
56
+ `onSelect` 与 `onSelectChange` 共用同一批对象引用,两条都订阅时不会各拷一份。
44
57
 
45
58
  ### 分组拐点拖动模式(channelWaypointDragMode)
46
59
 
@@ -165,10 +165,22 @@ view.focusOn('station-1024', { center: true }) // 要素:id / { ty
165
165
  view.focusOn({ type: 'channel', channelId: 'channel-a' }, { center: true }) // 通道:取它范围的中心
166
166
  view.focusOn({ lngLat: [116.4, 39.9] }, { center: true }) // 配了 GeoJSON 底图时按经纬度
167
167
  view.focusOn({ minX: 116, minY: 39, maxX: 117, maxY: 40 }, { center: true }) // 一段范围:取中心
168
+
169
+ // flyTo:目标与选项和 focusOn 完全一致,只多 duration / easing,把相机平滑飞过去
170
+ view.flyTo(hit, { zoom: true, duration: 600, easing: 'ease-in-out' })
171
+ view.flyTo(grouped, { zoom: true, select: true })
172
+ view.flyTo('station-1024', { duration: 0 }) // 立即落位,不播动画
173
+ view.stopCameraAnimation() // 停在当前帧,不补跳到终点
174
+ const flight = view.getCameraAnimation() // { active, progress, duration, from, to } 或 null
175
+ const flying = view.cameraAnimating // 只读:按钮在「飞向 / 停止」之间切换
168
176
  ```
169
177
 
170
178
  字符串 / 数字仍然兼容旧写法:先按节点 id 找,再按线路 id 找;同一个 id 同时出现在节点与线路里时用 `{ type: 'point' | 'line', id }` 消歧。裸字符串不表示通道;整条通道可以写 `{ channelId }`,要和通用 `id` / `index` 写法区分时再用 `{ type: 'channel', ... }`。分组必须写 `{ type: 'group', channelId?, groupId }`,`groupId` 在全部通道里唯一时可以省略通道。节点、线路、通道与分组都先解析成世界坐标范围;**缺省只按这个范围的中心调 `centerOn`,比例一点不动**,要适配就显式写 `zoom: true`,这时才交给 `zoomToBounds` 按解析出的真实 `bounds` 计算,没有为某类目标额外乘放大倍率。`scale` 的优先级高于 `zoom`;`maxZoom` 只给单点 / 单线这类零面积目标补最小范围。
171
179
 
180
+ 需要动画过渡时把同一个目标与选项交给 `flyTo`:默认 450 ms,中心线性、比例按对数插值;`easing` 可写 `'linear'` / `'ease-in'` / `'ease-out'` / `'ease-in-out'` 或自定义函数。`duration <= 0` 与系统 `prefers-reduced-motion: reduce` 都直接落位。飞行中用户拖拽 / 滚轮、再次 `flyTo`,或调用 `setCamera` / `centerOn` / `zoomTo` / `fitView` / `resetView` / `applyInitialView` 都会停在当前帧;`stopCameraAnimation()` 也是同一口径,`cameraAnimating` / `getCameraAnimation()` 用来驱动停止按钮与进度读数。
181
+
182
+ 如果目标本来就是一份相机读数(例如保存 / 恢复视图、`getCamera()` 的回灌、或外部时间轴算出来的中心与比例),用 `flyToCamera({ center?, scale? }, { duration, easing })` 直接飞向它:缺省的字段沿用当前相机,不经过要素解析,也不改选中与数据;`duration <= 0` 同样立即落位。它和 `setCamera` 的分工是「要不要过渡」:`setCamera` 当场落位,`flyToCamera` 走同一套飞行动画与取消口径。
183
+
172
184
  | `focusOn` 的选项 | 说明 |
173
185
  | --- | --- |
174
186
  | `zoom: true` | 显式要求按目标范围适配视图;不写就只居中、比例不动 |
@@ -193,7 +205,7 @@ view.focusOn({ minX: 116, minY: 39, maxX: 117, maxY: 40 }, { center: true }) //
193
205
  - 默认全字段模式会跳过 `symbol` / `style` / `tooltip` 这类外观结构;但 `fields` 显式点名时按路径直接读取,不受这条跳过规则限制。例如 `fields: ['symbol']`、`fields: ['style.color']` 能命中对应值。
194
206
  - 搜索基于当前数据:`hide` 模式下被隐藏的要素仍在数据里,因此仍然可以被搜到;`remove` 模式下已经删掉的要素搜不到。`scope` 写错或传空数组时会回落到四类全搜;重复线路 id 按数据里的第一条解析(见 [ID 解析口径](02-坐标与数据.md#id-解析口径))。
195
207
 
196
- 搜索**不动视角**:命中里带着 `bounds`,要定位就自己调 `focusOn(result.hits, …)` —— 缺省只把一批命中的整体中心当屏幕中心;要按范围适配就写 `{ zoom: true }`,`{ select: true }` 还能顺带把它们选上。`zoom` / `scale` / `padding` / `maxZoom` 属于 `focusOn` 的选项,搜索本身没有这些参数。
208
+ 搜索**不动视角**:命中里带着 `bounds`,要定位就自己调 `focusOn(result.hits, …)` —— 缺省只把一批命中的整体中心当屏幕中心;要按范围适配就写 `{ zoom: true }`,`{ select: true }` 还能顺带把它们选上。需要平滑过渡就把同一份调用换成 `flyTo(result.hits, { zoom: true, duration: 600 })`。`zoom` / `scale` / `padding` / `maxZoom` / `duration` / `easing` 属于定位方法的选项,搜索本身没有这些参数。
197
209
 
198
210
  匹配的是要素自己的字段:`id`、`name`、`from` / `to`,以及任意业务字段——`props` 这类普通对象会再往里摊一层,所以 `props.vn_kv` 里的值也能搜到;样式与符号字段不参与匹配。**通道连成员回线一起搜**:命中的是通道里的某条回线时,命中里会带上 `channelIndex`、`memberIndex`(第几回,0 开始)与 `memberLineIndex` / `memberLineId`,`bounds` 也换成那条回线的范围,`focusOn(hit)` 会把中心移到通道里的这条线路上,`focusOn(hit, { zoom: true })` 才按它的范围适配;反过来搜线路时,属于通道的线路也会带上 `channelIndex` / `memberIndex`。
199
211
 
@@ -230,6 +242,8 @@ const search = async keyword => {
230
242
 
231
243
  同步的 `search` 与 `searchAsync` 在**同一个数据版本 + 同一个请求**上结果逐字段一致(四类命中、命中的字段与值、`item` 引用、`bounds`);分组范围的当前轻量快照会随请求交给 Worker,分组字段或分组 id 改过之后也不会拿旧快照匹配。环境不支持 Worker、数据量不到阈值或 Worker 报错时,`searchAsync` 会退回同步计算再 resolve,调用方不用写两条路。
232
244
 
245
+ 这个「数据版本」现在也对外开放为只读属性 `view.dataVersion`:每次数据变更(`updatePoint` / `updateLine` / 批量补丁 / 拖拽落地 / 增删 / 撤销重做 / 换数据)递增,样式类 `updateOptions` / `updateLineStyle` 不递增,`onDataChange` 载荷里的 `dataVersion` 与它同一时刻同一个数。外部搜索缓存、增量统计或「这份结果是否过期」的判定可以直接比它,不必自己维护修订号;要读某条线路当前解析后的线宽 / 颜色快照用 `getLineStyle(index)`。
246
+
233
247
  ## 初始视图
234
248
 
235
249
  不配 `initialView` 时,首次拿到数据会 `fitView()`:整个数据范围铺满画布。数据一多,一屏全是点线看不出重点,`initialView` 用一份配置决定首屏落在哪里:
@@ -396,6 +410,8 @@ createGisView(container, {
396
410
  // 范围:'auto'(默认)数据与底图合起来、'data' 只看数据(老行为)、'background' 只看底图
397
411
  // 也能直接写死一份世界范围:extent: { minX: 0, minY: 0, maxX: 1000, maxY: 800 }(固定按它铺,不跟数据与底图走)
398
412
  extent: 'auto',
413
+ // 动态范围是否跟着当前可见内容重算:默认 false,首次铺好后筛选 / 隐藏 / 删除都不会改小图范围
414
+ extentFollowContent: false,
399
415
  // 仅 extent: 'auto' 生效;默认 false 始终按数据 ∪ 底图,打开后才允许按相机位置收窄
400
416
  extentFollowCamera: false,
401
417
  // 小图背景:inherit(默认,复用主视图解析好的背景)/ false 关掉 / 对象自己配一份
@@ -475,7 +491,8 @@ createGisView(container, {
475
491
  | `container` | `HTMLElement \| string \| null` | 无(主视图根节点) | 把鹰眼图挂到外部 `div`:可传元素或 CSS 选择器。给了就把同一根节点移进它,`position` / `margin` / `offset` 都相对这个宿主;宿主需要 `position: relative`(或 `absolute` / `fixed`)。选择器找不到时回落到主视图根节点;运行时换容器只移动节点,不重建画布。用 `view.getMinimapElement()` 可以拿到这个根节点做整图拖动 |
476
492
  | `padding` | `number` | `0.04` | 数据四周留白,取短边比例 |
477
493
  | `extent` | `'auto' \| 'data' \| 'background' \| GisViewBounds \| [[number, number], [number, number]]` | `'auto'` | 小图按哪段世界范围铺:`auto` 取「数据范围 ∪ 底图范围」,`data` 只看数据(旧行为),`background` 只看底图;数据与 GeoJSON 底图不在一个坐标系时,`auto` 能把两者一起框进小图,底图不会缩成一个点/糊成一片。也可以直接写一份世界范围(`{ minX, minY, maxX, maxY }`,也认 `[[minX, minY], [maxX, maxY]]`):固定按它铺,不跟数据与底图走 |
478
- | `extentFollowCamera` | `boolean` | `false` | `extent: 'auto'` 时是否允许包围盒跟着相机收窄。默认关闭,始终按「数据 ∪ 底图」铺,数据变化或相机进出某一份范围时包围盒与视口框不会自己跳;打开后,相机正落在其中一份范围里、而那一份合并后连 6 个像素都占不到时,只画正在看的那一份(数据还没映射到底图、两者离得很远时最明显),相机挪进 / 挪出会各重画一次 |
494
+ | `extentFollowContent` | `boolean` | `false` | 动态范围是否跟着当前可见内容重算。默认关闭:首次拿到数据 / 底图范围后锁住小图的世界范围,筛选、删除、隐藏或新增要素都不会改缩放与中心,外围内容消失时鹰眼图和视口框不会自己跳;打开后,每次数据或筛选结果变化都会按当前内容重算。显式改 `extent`(包括直接写固定范围)始终立即生效 |
495
+ | `extentFollowCamera` | `boolean` | `false` | `extent: 'auto'` 时是否允许包围盒跟着相机收窄。默认关闭,始终按「数据 ∪ 底图」铺;打开后,相机正落在其中一份范围里、而那一份合并后连 6 个像素都占不到时,只画正在看的那一份(数据还没映射到底图、两者离得很远时最明显),相机挪进 / 挪出会各重画一次。`extentFollowContent: false` 时参与比较的是锁定下来的那一份数据 / 底图范围 |
479
496
  | `className` / `style` | `string` / `Record<string, string \| number>` | 无 | 小图容器自己的类名与内联样式(中划线 / CSS 变量键与驼峰键都认):想按自己的设计系统改外观时用;换配置时上一轮写过、这一轮没写的键会被清掉 |
480
497
  | `draw` | `(info: GisViewMinimapDrawInfo) => void` | 无 | 兼容旧写法的最终一层:在所有 `layers` 之后、视口框之前调用(`info.stage === 'overlay'`;视口框由 DOM 画在它上面)。`info` 里是 `context`(已按备份像素比缩放好,直接按 CSS 像素画)、`canvas`、`width` / `height` / `ratio`、`extent` / `bounds` / `scale` / `zoom`、`project` / `unproject`(与内置图层同一套投影)与这一帧的 `data` / `background`;钩子里改过的上下文状态由组件 save / restore 收好,不会漏进下一帧 |
481
498
  | `layers` | `GisViewMinimapLayer[]` | 无 | 可拆分的自定义绘制层:每项写 `{ id?, order?, placement?, enabled?, draw }`。`placement: 'below-data'` 画在内置线路 / 节点之前,`'above-data'`(默认)画在之后;同一段内按 `order` 从小到大,相同或未写时按数组顺序。每层独立 `save` / `restore`,一层改过的画布状态不会串到下一层;`enabled: false` 可临时关掉一层。回调里的 `stage` 就是当前段,旧的单层 `draw` 固定最后执行。排序在 `updateOptions` 时缓存,绘制帧不再逐层排序 |
@@ -511,7 +528,7 @@ createGisView(container, {
511
528
 
512
529
  规则的用法与主图一致(同一套条件语言、按顺序命中、后命中的覆盖前面的),但只管小图的画法:节点按 `zIndex` 分层,`levels` 只留下指定层的节点,`layered: false` 时不排序、按数据顺序画;线路按「层级 + 颜色 + 线宽 + 透明度」分桶,一桶一条路径、一次 `stroke`,规则再多也不会把小图的绘制次数拉爆。没配自己的规则时(`inheritRules !== false`)完整沿用主图样式链:颜色 / 描边色 / 层级来自主图规则,节点尺寸与图标取主图最终解析出的那一份(点自身 > `pointRules` > `pointStyle`),线路线宽取主图样式链解析出的原始值,再按小图与主图 CSS 视口宽高的较小比例缩一次(不跟主图相机倍数走)—— 圆、方、图片、SVG 与自定义图元都保留主图的形状,但不会在小图上仍和主图一样大。显式配了小图自己的 `pointRules` / `lineRules`(或 `inheritRules: false`)时,对应的尺寸 / 线宽不受这一步影响,仍由小图自己的规则与样式决定;只配 `pointStyle` / `lineStyle` 不会单独关掉继承。节点的颜色取规则里的 `strokeColor`、没有才回落 `color`:主图的节点是白底 + 一圈电压色的描边,小图上一个点是一块实心像素,不这样取就会画成一堆白点(自己配的小图规则同样可以写 `strokeColor`)。
513
530
 
514
- **动态位置与外部挂载**:`offset` 是整张鹰眼图相对当前角落的位移,最适合做「拖动整个小图」的底移量 —— 指针每移动一段,就把累计位移写回 `updateOptions`;`position` / `margin` 仍决定基准角落。
531
+ **动态位置与外部挂载**:`offset` 是整张鹰眼图相对当前角落的位移,最适合做「拖动整个小图」的底移量 —— 指针每移动一段,就把累计位移写回 `updateOptions`;`position` / `margin` 仍决定基准角落。`docs/demos/MinimapDemo.vue`(`?demo=minimap`)在小图顶部内置了一个「拖动整图」手柄:把 `getMinimapElement()` 返回的根节点当作拖动目标,指针位移累计到 `offset`,同时保留了小图内部的框选 / 平移手势;右侧还有「整图位置复位」按钮可以直接回到 `[0, 0]`。
515
532
 
516
533
  ```js
517
534
  const minimapHost = document.querySelector('#minimap-slot')
@@ -548,7 +565,7 @@ console.log(minimapElement?.parentElement === minimapHost) // true
548
565
 
549
566
  `container` 写元素或选择器都可以;外部宿主要设成 `position: relative`(`absolute` / `fixed` 也行),否则绝对定位会继续向上找基准。换容器只是移动同一个 `.gis-view__minimap` 根节点,画布、缓存与事件监听都保留;`destroy()` 会把它从当前外部宿主里一并摘掉。没配 `minimap` 时 `getMinimapElement()` 返回 `null`。
550
567
 
551
- `levels` 挡掉的节点会按 `cascade`(默认 `'binding'`)把连在它们身上的线路一起隐藏 —— 与主视图筛选的联动是同一套写法、同一个默认值:只看某几层的电压时,层外的节点与它们的线路一起消失,不会在小图上留下一堆凭空断掉的线。`cascade: 'coordinate'` 认的是端点坐标(线路数据只有 `path`、没有 `from` / `to` 时用它),`'both'` 两种写法都认,`false` 表示挡掉节点时线路照旧画。联动只影响小图的画法:数据、主视图、主视图的筛选结果都不动;小图的范围也跟着剩下的可见要素走(`measure` 只量小图真正画出来的节点与线路)。
568
+ `levels` 挡掉的节点会按 `cascade`(默认 `'binding'`)把连在它们身上的线路一起隐藏 —— 与主视图筛选的联动是同一套写法、同一个默认值:只看某几层的电压时,层外的节点与它们的线路一起消失,不会在小图上留下一堆凭空断掉的线。`cascade: 'coordinate'` 认的是端点坐标(线路数据只有 `path`、没有 `from` / `to` 时用它),`'both'` 两种写法都认,`false` 表示挡掉节点时线路照旧画。联动只影响小图的画法:数据、主视图、主视图的筛选结果都不动;`measure` 只量小图真正画出来的节点与线路,但 `extentFollowContent: true` 时范围才跟着剩下的可见要素重算,默认锁定首次范围,避免筛选后整张小图缩放漂移。
552
569
 
553
570
  小图的尺寸系数分三组,前两组对所有节点 / 线路生效,第三组只作用于小图自有规则 / 样式:`pointSizeScale` / `lineWidthScale` 是整体倍数(常数,屏幕上恒定),`pointZoomScale` / `lineZoomScale` 跟的是**小图自身的倍数**(手动缩放 `zoomable: true` 或跟随主图 `zoomable: 'follow'` 都会改这个倍数;小图倍数到 4 倍时,`pointZoomScale: 0.5` 让节点半径放大 2 倍、`1` 是同倍率放大、`0` 保持不变;沿用主图的节点 / 线路也按这一份联动),`pointMapZoomScale` / `lineMapZoomScale` 跟的是**主视图的地图倍数**(相机比例 ÷ 基准,基准就是主视图 `pointZoomScale` 用的那一份,可以用 `mapZoomBaseScale` 覆盖):主图放大 4 倍时 `0.5` 让小图节点放大 2 倍、`1` 同倍率;这一组只作用于小图自有规则 / 样式;沿用主图时节点尺寸与线宽按「小图视口 ÷ 主图视口」换算,主图放大缩小不会改变它们的大小(两边视口尺寸变化除外)。注意前两个指数量的是**小图自身的倍数**:小图没开 `zoomable`、或者倍数还在 `1`(刚好铺满)时它们是恒等系数,怎么调都看不出变化;只想让小图的整体显示范围跟着主图缩放,用 `zoomable: 'follow'`。
554
571
 
@@ -567,7 +584,7 @@ console.log(minimapElement?.parentElement === minimapHost) // true
567
584
 
568
585
  只有手动缩放(`zoomable: true`)时,按住 Ctrl / ⌘ 才会把 `dragAction` 换成另一个动作;关闭缩放 / 跟随主图时修饰键不改变动作,不能把唯一可用的框选换没。`pan` 需要小图倍数大于 1 才有可移动余量;关闭手动缩放时,`pan` 档的空白处长按仍跟着主图视角走,想直接框选就把 `dragAction` 设成 `'box'`。平移过内容之后,小图不再自动把内容拉回视口框(否则一边看别处一边被拽回去);手动模式下 `setMinimapZoom` / 关掉再打开 `zoomable` 会回到居中,跟随主图时倍数与居中由主图决定。滚轮(或手动模式下的 `zoomMinimapBy`)以光标处的世界坐标为锚点改倍数,每一格的倍率就是 `zoomStep`(放大用 `zoomInStep`、缩小用 `zoomOutStep` 可以单独覆盖,缺省都用 `zoomStep`),`getMinimapZoom` / `setMinimapZoom` 读写当前倍数(夹在 `minZoom` ~ `maxZoom`)。`channelCircuits: false` 时每个通道分组只画一根代表线:`collectMinimapData()` 按 `getChannelMembers()` 找到这一组当前仍可见的回线,逐顶点取平均,双回线正好落在两根中间、三回线落在中间那条;底图的分辨率本来就在一个像素上下,多回线挤在一起只会糊成一片。
569
586
 
570
- 小图的范围(`extent`)默认是 `auto`:取「筛选后可见数据的范围」与「底图范围」的并集(GeoJSON 底图用画出来的 `bounds`,图片底图用四角算出的范围),所以数据与 GeoJSON 不在一个坐标系时两者会一起进小图 —— 底图铺满小图、数据落在自己该在的位置,不会出现「按数据范围缩放、底图缩成一个点或糊成一片」的旧问题。`extent: 'data'` 退回旧行为(只看数据),`extent: 'background'` 只看底图范围;数据为空时 `auto` 直接取底图范围。**`extentFollowCamera` 默认 `false`**:数据因筛选 / 显隐 / 增删变化时包围盒不会自行收窄,外围要素消失也不会让可视图突然换尺寸或让视口框跳位置。打开后才在相机落在数据 / 底图其中一份、且另一份在合并范围里小到不足 6 像素时,按正在看的那一份收窄;相机挪进 / 挪出会各补一次重画。
587
+ 小图的范围(`extent`)默认是 `auto`:取「筛选后可见数据的范围」与「底图范围」的并集(GeoJSON 底图用画出来的 `bounds`,图片底图用四角算出的范围),所以数据与 GeoJSON 不在一个坐标系时两者会一起进小图 —— 底图铺满小图、数据落在自己该在的位置,不会出现「按数据范围缩放、底图缩成一个点或糊成一片」的旧问题。`extent: 'data'` 退回旧行为(只看数据),`extent: 'background'` 只看底图范围;数据为空时 `auto` 直接取底图范围。**`extentFollowContent` 默认 `false`**:首次铺好后锁定数据与底图的世界范围,筛选 / 显隐 / 增删只会重画内容,不会改小图缩放、中心或视口框位置;打开后才按当前可见内容重算。`extentFollowCamera` 也默认 `false`:仅在打开后在相机落在数据 / 底图其中一份、且另一份在合并范围里小到不足 6 像素时,按正在看的那一份收窄;相机挪进 / 挪出会各补一次重画。
571
588
 
572
589
  **配置、扩展与开销**:`extent` 除了三档预设,还能直接写一份世界范围(`{ minX, minY, maxX, maxY }` 或 `[[minX, minY], [maxX, maxY]]`)—— 固定按它铺、不跟数据与底图走,「只看某个片区」或者让两张小图的视野对齐(前后对比、打印出图)时用;容器还能加 `className` 与 `style`(中划线 / CSS 变量键与驼峰键都认,换配置时上一轮写过、这一轮没写的键会清掉),按自己的设计系统做阴影、字号、层级这类外观不必再绕 `backgroundStyle`。**可扩展**:`layers` 把每帧的自定义内容拆成一组互不干扰的绘制层 —— `placement` 决定画在数据前还是数据后,同段内按 `order` 从小到大,独立 `save` / `restore`,适合把网格、分区底色、指北针、业务标记分开维护;排序与分段在 `updateOptions` 时缓存,绘制帧只遍历当前段,层数多也不会每帧重复排序;旧的单层 `draw` 继续兼容,`stage` 固定为 `overlay` 并排在最后。回调拿到 `context`(已经按备份像素比缩放好,直接按 CSS 像素画)、`width` / `height` / `ratio`、当前 `extent` / `bounds` / `scale` / `zoom` 与 `project` / `unproject`(与内置图层同一套投影),以及这一帧的 `data` 与 `background`。`maxPixelRatio` 是性能旋钮:默认 4,实际倍率取 `devicePixelRatio × 宿主显示比例` 与该上限的较小值;小图在高分屏 / 宿主放大时不必无限加像素,调到 1 ~ 2 能省填充率。
573
590
 
@@ -590,6 +607,7 @@ console.log(minimapElement?.parentElement === minimapHost) // true
590
607
  | `project(x, y)` / `unproject(x, y)` ↔ `clientToScreen(clientX, clientY)` / `screenToClient(x, y)` ↔ `clientToWorld(clientX, clientY)` / `worldToClient(x, y)` | 同一套换算链的三档入口:`project` / `unproject` 收**画布 CSS 像素**,`clientTo*` 这一对吃鼠标事件里的 `clientX` / `clientY`(自动减 canvas rect、除宿主缩放)只换到画布像素,再接一层就是世界坐标(`clientToWorld` / `worldToClient`);经纬度那一对是 `lngLatToScreen` / `screenToLngLat` |
591
608
  | `fitView(padding?)` / `applyInitialView()` / `resetView()` / `zoomToBounds(bounds, padding?)` | 视角的四个语义:适应**数据范围**(并记下首次落位)/ 按 `options.initialView` 落位(可反复调用)/ 回到**首次落位那一刻**的相机 / 按给定的世界坐标范围适应 |
592
609
  | `focusOn(ref, options?)` ↔ `zoomToBounds(bounds, padding?)` | `focusOn` 认要素(id / 下标 / 搜索命中 / 坐标 / 矩形 / 一批),缺省只按范围中心居中;写 `{ zoom: true }` 才走适应那一条,另带 `center` / `scale` / `maxZoom` / `select` 这些选项 |
610
+ | `flyTo(target, options?)` ↔ `focusOn(target, options?)` | 同一套目标与选项解析,区别只在落位方式:`focusOn` 立即改相机,`flyTo` 默认用 450 ms 平滑过去,并多认 `duration` / `easing`;需要随时停在当前帧用 `stopCameraAnimation()` |
593
611
  | `pick(x, y)` ↔ `pickAt(clientX, clientY)` | 坐标口径不同:`pick` 收画布 CSS 像素,`pickAt` 直接吃鼠标事件里的客户端坐标(结果里另加 `target` 与 `lngLat`) |
594
612
  | `hoverAt(clientX, clientY)` / `clearHover()` / `getHover()` | 三个动作:程序化悬停 / 撤掉并补发 `onHover(null)` / 读当前那一份(`hoverAt` 的返回与 `onHover` 载荷同一份) |
595
613
  | `getData()` / `getVisibleData()` / `getOptions()` | 读的层不同:全量数据副本(含拖拽后的最新坐标)/ 筛掉隐藏之后的点与线 / 整份配置与数据 |