gis-view-webgl2 0.1.2 → 0.1.3

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 (61) hide show
  1. package/README.md +12 -8
  2. package/dist/gis-view.css +1 -1
  3. package/dist/index.cjs +1 -1
  4. package/dist/index.d.ts +309 -59
  5. package/dist/index.js +1 -1
  6. package/docs/AI/345/257/271/350/257/235/346/261/207/346/200/273.md +117 -0
  7. package/docs/GisView.md +2 -2
  8. 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 +107 -0
  9. package/docs/api/index.html +1 -1
  10. package/docs/demos/ApiCoverageDemo.vue +644 -0
  11. package/docs/demos/ChannelDemo.vue +14 -12
  12. package/docs/demos/ClusterDemo.vue +251 -0
  13. package/docs/demos/CopyMergeDemo.vue +3 -4
  14. package/docs/demos/EventDemo.vue +3 -3
  15. package/docs/demos/GisViewDemo.vue +64 -8809
  16. package/docs/demos/api-index.js +48 -41
  17. package/docs/demos/demos.js +33 -16
  18. package/docs/demos/gis-view-demo/GisViewDemoCardHeader.vue +28 -0
  19. package/docs/demos/gis-view-demo/GisViewDemoDialogs.vue +82 -0
  20. package/docs/demos/gis-view-demo/GisViewDemoFooter.vue +16 -0
  21. package/docs/demos/gis-view-demo/GisViewDemoHeading.vue +16 -0
  22. package/docs/demos/gis-view-demo/GisViewDemoPanelStack.vue +71 -0
  23. package/docs/demos/gis-view-demo/GisViewDemoStage.vue +219 -0
  24. package/docs/demos/gis-view-demo/demo.css +1230 -0
  25. package/docs/demos/gis-view-demo/demoContext.js +12 -0
  26. package/docs/demos/gis-view-demo/panels/DataPanels.vue +303 -0
  27. package/docs/demos/gis-view-demo/panels/OpsPanels.vue +174 -0
  28. package/docs/demos/gis-view-demo/panels/StylePanels.vue +561 -0
  29. package/docs/demos/gis-view-demo/panels/ViewPanels.vue +396 -0
  30. package/docs/demos/gis-view-demo/useGisViewDemo.js +6188 -0
  31. package/docs/format/07-/344/272/213/344/273/266/350/275/275/350/215/267/344/270/216/347/244/272/344/276/213/346/225/260/346/215/256.md +3 -2
  32. package/docs/internals/16-03-/344/274/230/345/214/226/344/270/216/345/217/226/350/210/215-/344/270/213.md +1 -1
  33. package/docs/manual/02-/345/235/220/346/240/207/344/270/216/346/225/260/346/215/256.md +3 -2
  34. package/docs/manual/03-/351/205/215/347/275/256/351/241/271.md +87 -14
  35. package/docs/manual/04-/344/272/213/344/273/266/344/270/216/345/233/236/350/260/203.md +8 -8
  36. 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 +40 -3
  37. package/docs/manual/08-/351/200/211/344/270/255/344/270/216/346/213/220/347/202/271.md +26 -0
  38. package/docs/manual/09-/351/200/232/351/201/223/344/270/216/345/244/232/345/233/236/347/272/277.md +6 -2
  39. package/docs/manual/10-/351/200/232/351/201/223/347/274/226/350/276/221/344/270/216/346/226/260/345/242/236.md +39 -7
  40. package/docs/manual/12-/346/240/207/347/255/276/344/270/216/350/243/205/351/245/260.md +13 -4
  41. package/docs/manual/13-/345/212/250/346/225/210/344/270/216/345/233/276/345/205/203.md +3 -0
  42. package/docs/manual/14-/350/247/206/345/233/276/344/270/216/347/255/233/351/200/211.md +35 -19
  43. package/docs/manual/15-/345/257/274/345/207/272/344/270/216/351/233/206/346/210/220.md +5 -0
  44. package/docs/manual/16-/345/256/236/344/276/213/346/226/271/346/263/225/346/200/273/350/241/250.md +12 -6
  45. package/docs/manual/17-/345/217/202/346/225/260/351/200/237/346/237/245.md +39 -22
  46. package/docs/manual/18-/351/200/220/346/226/271/346/263/225/347/244/272/344/276/213.md +52 -13
  47. package/docs/manual/20-/346/200/247/350/203/275/344/270/216/345/244/247/346/225/260/346/215/256.md +25 -1
  48. 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 +5 -5
  49. package/docs/steps/README.md +17 -11
  50. package/docs/steps/selftest-actions.html +3 -5
  51. package/docs/steps/selftest-demo.html +27 -18
  52. package/docs/steps/selftest-demos.html +387 -27
  53. package/docs/steps/selftest-docs.html +4 -4
  54. package/docs/steps/selftest-effects.html +200 -0
  55. package/docs/steps/selftest-features.html +512 -11
  56. package/docs/steps/selftest-labels.html +74 -0
  57. package/docs/steps/selftest-perf.html +293 -20
  58. package/docs/steps/selftest-scale.html +52 -29
  59. package/docs/steps/step-10-full.js +1 -1
  60. package/docs/steps/step-12-events.js +1 -1
  61. package/package.json +1 -1
@@ -168,9 +168,14 @@ view.zoomTo(12)
168
168
  view.zoomToBounds([0, 0, 400, 200], 40)
169
169
  view.zoomToBounds({ minX: 116, minY: 39, maxX: 117, maxY: 40 })
170
170
 
171
- // focusOn(target, options?):对准一个或多个目标(id / 下标 / 坐标 / 矩形 / 通道写法 / 搜索命中 / 一批)
171
+ // focusOn(target, options?):对准一个或多个目标(id / 显式类型 / 坐标 / 矩形 / 搜索命中 / 一批)
172
172
  view.focusOn('a')
173
173
  view.focusOn(['a', 'b'], { padding: 60, maxZoom: 8, select: true })
174
+ view.focusOn({ type: 'point', id: 'a' }) // 节点与线路同 id 时显式消歧
175
+ view.focusOn({ type: 'line', id: 'a' })
176
+ view.focusOn({ type: 'channel', channelId: 'c1' }) // 通道也可以直接写 { channelId: 'c1' }
177
+ view.focusOn({ type: 'group', channelId: 'c1', groupId: 'c1::2' })
178
+ view.focusOn({ type: 'group', groupId: 'global-group' }) // groupId 全局唯一时可省略通道
174
179
  view.focusOn([120, 80], { center: true }) // 只居中,比例不动
175
180
  view.focusOn({ lngLat: [116.4, 39.9] })
176
181
 
@@ -185,8 +190,7 @@ view.setCamera({ scale: 10 })
185
190
  view.setCamera({ x: 200, y: 120 })
186
191
  const camera = view.getCamera()
187
192
 
188
- // zoomScaleAuto / getZoomBaseScale() / setZoomBaseScale(scale?):自适应比例(节点大小与通道间距按同一比例变)
189
- view.updateOptions({ zoomScaleAuto: true }) // 没写联动指数的那几项按 1 跟随地图(显式写过的仍以显式值为准)
193
+ // getZoomBaseScale() / setZoomBaseScale(scale?):缩放联动的基准比例(配合 pointZoomScale / lineZoomScale)
190
194
  const zoomBase = view.getZoomBaseScale() // 基准(缺省首次适应视图时的 camera.scale)
191
195
  view.setZoomBaseScale() // 以当下这一屏为「原样大小」,之后放大缩小都从它算倍数
192
196
  view.setZoomBaseScale(2.5) // 也可以直接给一个比例
@@ -301,6 +305,13 @@ const pointStyle = view.getPointStyle(0)
301
305
  const listedPoints = view.listPoints({ limit: 5, withScreen: true })
302
306
  const listedLines = view.listLines({ inView: true, limit: 200 })
303
307
 
308
+ // getClusters() / getPointCluster():读当前点聚合展示组与配置
309
+ view.updateOptions({ pointCluster: { radius: 64, minPoints: 3, maxZoom: 2.5 } })
310
+ const clusterGroups = view.getClusters()
311
+ const clusteredOnly = clusterGroups.filter(group => group.count > 1)
312
+ const clusterState = view.getPointCluster() // { enabled, radius, minPoints, maxZoom, active }
313
+ view.updateOptions({ pointCluster: false }) // 关掉聚合,原始点全部恢复
314
+
304
315
  // forEachPointInView(rect, visit):只遍历视口里的节点,不建中间数组;回调返回 false 提前结束
305
316
  view.forEachPointInView(view.getViewRect(), (index, item, x, y, radius) => {
306
317
  readout.push(`${item.name}:半径 ${radius.toFixed(1)}px`)
@@ -327,8 +338,17 @@ view.selectFeatures('a') // 选中一个
327
338
  view.selectFeatures(['a', 'b']) // 一批
328
339
  view.selectFeatures('a', { mode: 'add' }) // mode:replace(缺省)/ add / toggle / remove
329
340
  view.selectFeatures([{ type: 'line', index: 0 }], { mode: 'toggle' })
341
+ view.selectFeatures({ type: 'channel', channelId: 'c1' }) // 整条通道的全部回线
342
+ view.selectFeatures({ type: 'group', channelId: 'c1', groupId: 'c1::2' })
343
+ view.selectFeatures({ type: 'group', groupId: 'global-group' }) // groupId 全局唯一时可省略通道
330
344
  view.selectFeatures(null) // 清空(空数组同义)
331
345
 
346
+ // selectWaypoint(lineRef, pathIndex? / { lineIndex | lineId, pathIndex }, options?):选中拐点并带选线路
347
+ view.selectWaypoint('l1', 2) // pathIndex 从 getWaypoints() 取
348
+ view.selectWaypoint({ lineIndex: 0, pathIndex: 1 }, { mode: 'add' })
349
+ view.selectWaypoint({ lineId: 'l1', pathIndex: 2 })
350
+ view.selectWaypoint(null) // 只取消拐点选中,不改要素选中集
351
+
332
352
  // deselect(refs?):取消选中(写法与 selectFeatures 一致;不传 / null 全清,空数组什么都不做)
333
353
  view.deselect('a')
334
354
  view.deselect(['a', 'b'])
@@ -339,6 +359,19 @@ const picks = view.getSelections()
339
359
  const selection = view.getSelectionData() // { ids, count, points, lines, channels, bounds, … }
340
360
  const selected = view.isSelected('a')
341
361
 
362
+ // getCascadeHighlight():读点击级联高亮(与选中集分开;没有高亮时为 null)
363
+ const cascade = view.getCascadeHighlight() // { origin, scope, points, lines, pointIndexes, lineIndexes }
364
+ view.updateOptions({
365
+ cascadeHighlight: {
366
+ scope: 'points',
367
+ triggerOnClick: true,
368
+ triggerOnMultiSelect: false,
369
+ triggerOnBoxSelect: true,
370
+ clearOnEmptyClick: true
371
+ }
372
+ })
373
+ view.on('cascadeHighlight', payload => console.log(payload?.pointIndexes, payload?.lineIndexes))
374
+
342
375
  // selectInBox(box, options?):用矩形框选(画布 CSS 像素,与手动框选同一套判定)
343
376
  view.selectInBox({ x: 40, y: 40, width: 320, height: 200 })
344
377
  view.selectInBox({ x: 40, y: 40, width: 320, height: 200 }, { selectionKinds: ['line', 'channel'] })
@@ -349,11 +382,13 @@ view.updateOptions({ selectionKinds: 'channel' }) // 只收通道回
349
382
  view.updateOptions({ selectionKinds: ['point', 'line', 'channel'] })
350
383
 
351
384
  // beginCopyPoint(options) / getPointCopy() / updateCopyPoint(patch) / commitPointCopy() / cancelPointCopy()
352
- view.beginCopyPoint({ id: 'a' }) // 出复制预览(跟着鼠标走)
353
- view.beginCopyPoint() // 先点按钮再选节点(点中下一个节点当来源)
354
- view.updateCopyPoint({ name: '新节点' }) // 改预览节点的数据
355
- view.commitPointCopy() // 落地
356
- view.cancelPointCopy() // 取消
385
+ view.updateOptions({ copy: { enabled: true, autoCommit: false } }) // API 复制入口要先开 copy 配置
386
+ view.beginCopyPoint({ id: 'a', coordinate: [40, 30] }) // 出预览并钉在世界坐标 [40, 30]
387
+ view.beginCopyPoint({ id: 'a', follow: true }) // 预览跟着指针走(autoCommit 缺省停住)
388
+ view.beginCopyPoint() // 先点按钮再选节点(点中下一个节点当来源)
389
+ view.updateCopyPoint({ name: '新节点' }) // 字段级改预览节点(落点仍由 coordinate / 指针决定)
390
+ const copyEnd = view.commitPointCopy() // 落地:返回 end 载荷;没有预览时为 null
391
+ view.cancelPointCopy() // 取消:预览撤掉,数据与历史不动
357
392
  const copyState = view.getPointCopy() // 当前预览 / 选点态(没有就是 null)
358
393
 
359
394
  // copyPoint(source?) / copyPoints(sources?, options?) / copySelectedPoints(options?) / copyPointToClipboard(sources?, options?) / copyPointsToClipboard(sources?, options?) / pastePoint(options?) / pastePoints(options?) / getPointClipboard()
@@ -407,15 +442,15 @@ view.removePoints('all') // 全删
407
442
  view.removePoints('a', { reason: '误建' }) // 原因会带进数据变更载荷
408
443
 
409
444
  // removeSelected(options?):删「当前选中的东西」(节点 / 线路 / 拐点按各自口径)
410
- view.removeSelected()
411
- view.removeSelected({ reason: '按 Delete 确认' })
445
+ const removed = view.removeSelected() // 返回 pointIds / lineIds / removedChannels 等;删完清空选中
446
+ view.removeSelected({ reason: '按 Delete 确认' }) // 整次只记一条历史、只发一条 removeSelected 数据变更
412
447
 
413
448
  // requestRemove(options?) / getRemoveRequest() / resolveRemove(scope?) / cancelRemove():先出载荷、再裁决
414
- const removePayload = view.requestRemove() // 发一条 phase: 'pending' 的 onRemove 载荷
449
+ const removePayload = view.requestRemove() // 只生成 phase: 'pending' 的载荷,数据不动
415
450
  const pendingRemove = view.getRemoveRequest() // 从外面拿这一份(没有就是 null)
416
- view.resolveRemove('channels') // 按范围确认:'lines' / 'groups' / 'channels'
451
+ view.resolveRemove('channels') // 按范围确认:'lines' / 'groups' / 'channels';无请求时返回 null
417
452
  view.resolveRemove() // 按载荷里的 scope 确认
418
- view.cancelRemove() // 取消(数据一个字节都不动)
453
+ view.cancelRemove() // 取消(数据一个字节都不动;无请求时返回 null)
419
454
  ```
420
455
 
421
456
  #### 拐点与通道
@@ -508,6 +543,7 @@ view.beginAddChannel({
508
543
  circuitSpacing: 12, opacity: 0.9
509
544
  }
510
545
  }) // 点两个节点:第一个起点、第二个终点;通道 3 回时预览也画 3 条平行回线
546
+ view.beginAddChannel({ from: 'station-a' }) // 预指定起点:接下来只点终点
511
547
  const draft = view.getAddChannelDraft() // 草稿读数(跟随光标的预览、两个节点、方案表与实际 style)
512
548
  view.updateAddChannelStyle({ arrowPositions: [0.25, 0.75], arrowReverse: true }) // 字段级合并,只改箭头
513
549
  view.updateAddChannelStyle(null) // 回到全局 channelDraftStyle
@@ -785,6 +821,7 @@ const parallelData = view.exportData({ format: 'test', parts: ['waypoint'], all:
785
821
  // exportImage(options?):导出图片(Promise<dataURL>;type: 'svg' 走矢量导出)
786
822
  const imageUrl = await view.exportImage({ scale: 2, background: '#ffffff' })
787
823
  await view.exportImage({ type: 'image/jpeg', quality: 0.9, download: '场景.jpg' }) // 也可以直接下载
824
+ await view.exportImage({ scope: 'all', padding: 24, includeLabels: false }) // 全部内容(导完恢复相机)
788
825
 
789
826
  // exportSelection(options?) / exportData({ scope: 'selection' }):导出选中的那几个
790
827
  const exportedSelection = await view.exportSelection({ withLines: true, scale: 2 })
@@ -889,4 +926,6 @@ const gl = view.gl
889
926
 
890
927
  ---
891
928
 
929
+ **相关章节**:[08 选中与拐点](08-选中与拐点.md) · [16 实例方法总表](16-实例方法总表.md) · [17 参数速查(逐个方法)](17-参数速查.md)
930
+
892
931
  [← 17 参数速查(逐个方法)](17-参数速查.md) · [手册目录](../GisView.md) · [19 读数与拖拽预览 →](19-读数与拖拽预览.md)
@@ -30,17 +30,41 @@ view.updateOptions({ denseMode: 'off' }) // 手动退出
30
30
 
31
31
  `reason` 的取值:`manual-on` / `manual-off`(强制档)、`total`(总量越线)、`visible`(可见节点数越线)、`density`(屏幕密度越线)、`below-threshold`(auto 档已退出)、`checking`(引擎还没建好或正在等首轮布局)。`visiblePoints` 只数到触发阈值就停,因此可能正好等于阈值;按总量触发时是 `null`。PerfDemo 的面板可以直接切 auto / on / off,并在读数里看到这一份状态。
32
32
 
33
+ ## 点聚合(pointCluster)
34
+
35
+ `pointCluster` 面向「点很密、单个符号已经没有辨认价值」的场景:按屏幕半径把邻近点合成一个圆,圆里显示数量;点击圆只发 `clusterClick`,不会误选内部节点。它只改变展示与命中,不改业务数据、选中集与 `getData()`:
36
+
37
+ ```js
38
+ const view = createGisView(container, {
39
+ points,
40
+ pointCluster: { radius: 64, minPoints: 3, maxZoom: 2.5 }
41
+ })
42
+
43
+ const groups = view.getClusters() // 含 count: 1 的单成员
44
+ const state = view.getPointCluster() // { enabled, radius, minPoints, maxZoom, active }
45
+ const stats = view.getStats() // clusterCount / clusterPointCount / clusterMs
46
+ ```
47
+
48
+ - **配置**:写 `true` 用缺省档(半径 60px、至少 2 点、不限缩放);`radius` 夹在 8 ~ 500 CSS 像素,`minPoints` 最小 2,`maxZoom: 0` 表示不限。`maxZoom` 以 `zoomBaseScale`(或首次适应视图比例)为基准,相机比例超过它之后 `active` 变 `false`,原始点逐点展开。
49
+ - **算法开销**:`cluster.js` 用固定网格把邻近点串成组,每个点只查相邻 9 格,单次建组按总量 O(n);成员用 `Int32Array` 链表保存,稀疏场景不为每个单成员常驻组对象。平移不重算聚合,缩放、数据、配置、选中集、拖动、合并与复制预览变化才标脏。
50
+ - **绘制与拾取**:聚合圆单独一批绘制,原始点批次过滤掉被收起的成员;标签、涟漪、节点装饰、框选、最近点查询与 `listPoints({ visibleOnly: true })` 都按同一份隐藏集合跳过内部点。`getClusters()` 为需要全量展示组的场景补上单成员,绘制与数量文字只遍历多成员圆。
51
+ - **隔离规则**:选中点、拖动成员、待确认合并的源 / 目标以及复制预览强制保持单成员,避免用户点不到正在操作的点;聚合开启时发散初始化与追加式补点暂停,保证成员关系按整份数据一次算清。
52
+ - **和 denseMode 的关系**:两者可以同时开。`denseMode` 管标签避让 / 截断与动画降级,`pointCluster` 管点本身的展示密度;只希望更快但仍要看每个符号时可关掉聚合,只保留密集档。十万点档在 `?demo=cluster` 可现场切 2 千 / 1.2 万 / 6 万 / 10 万,并观察聚合耗时与展示项数。
53
+
33
54
  ## 大数据量
34
55
 
35
56
  10 万点 + 10 万线(含通道)在 `docs/demos/GisViewDemo.vue` 里可以直接切到,交互路径上没有按总量线性增长的循环:
36
57
 
37
- - **高密度纯文本标签自动画布化**:强制显示档(`labelOptimize` 未开、`denseMode` 未激活)下,节点层或线路层里能画布的纯文本标签达到 **1024** 条时,默认 `labelRender: 'auto'` 会让该层改用一张 `.gis-view__label-canvas`,不再为每条标签建 `div`。HTML、函数返回元素、可拖拽 / 可编辑及超出 Canvas 样式能力的标签仍留在同层 DOM,因此画布与少量 DOM 可以混排;`labelRender: 'dom'` 可强制回退逐条 DOM。优化档 / 密集档仍走 DOM(避让、截断与命中判定要真实量尺寸),这一步不是它们的替代品。`selftest-labels.html` 用 1100 条纯文本验证了点标签层只剩一张画布、混入一条 `html: true` 时只保留那一条 DOM,并覆盖切回 `dom`、再切回 `auto` 的节点回收与 SVG 导出。
58
+ - **高密度纯文本标签画布化**:强制显示档(`labelOptimize` 未开、`denseMode` 未激活)下,节点层或线路层里能画布的纯文本标签达到 **1024** 条时,默认 `labelRender: 'auto'` 会让该层改用一张 `.gis-view__label-canvas`,不再为每条标签建 `div`。要求优化档也走画布时显式写 `labelRender: 'canvas'`:不设数量阈值,`maxCount` / `when` / `scanLimit` 仍照常截断,避让、换锚点与沿线滑动直接使用画布尺寸,不创建也不测量 DOM。HTML、函数返回元素、可拖拽 / 可编辑及超出 Canvas 样式能力的标签始终留在同层 DOM,因此画布与少量 DOM 可以混排;`labelRender: 'dom'` 可强制回退逐条 DOM。画布路径另有文字测量缓存(同字体 / 样式 / 文案只量一次)、交互期 `requestAnimationFrame` 合并重绘与屏外标签裁剪,连续平移 / 缩放时不会因多次指针事件重复清画布。`selftest-labels.html` 用 1100 条纯文本验证字宽只测一次、连续 5 次缩放只重绘 1 次、显式画布在优化档遵守 `maxCount: 200`,以及线路标签 6 条全部进画布;另覆盖混入 `html: true` 时只保留那一条 DOM、切回 `dom`、再切回 `auto` 的节点回收与 SVG 导出。
38
59
  - 拾取与框选共用网格索引:点走按数据范围重建的均匀网格,线走随线段重建的 `createSegmentGrid`,每次交互只遍历光标附近的候选,耗时与总量基本脱钩,`getStats().pickMs` 是随手可读的实测值(10 万规模下热态约 0.2 ~ 0.3 ms / 次)。线段按格线步进登记进它真正穿过的格子(早期按包围盒填格,一条斜跨全图的线会占掉上万格、只好整条丢进「超长线」单,而十万条线里绝大多数都是这种,于是每次拾取都要重算十万条的距离);剩下的长线只记一条包围盒,查询时按盒子先筛一遍。10 万点 / 10 万线的合成数据里(随机配对、长线极密),500 次 `pick` 从 3530 ms 降到 164 ms(0.3 ms / 次)。
39
60
  - 拖动时「哪些线跟着这个点走」按需建索引:`collectDragLinks` 从前每次都把全部线路的端点与组内成员逐个比一遍,10 万点 / 12 万线下这一步占总 CPU 的 27%,是拖动路径上唯一还按总量线性付费的循环。现在线路超过 `LINE_BIND_TOLERANCE_SCAN_LIMIT`(2048)条时改用一张线路归属表(按 `from` / `to` 的 id 与端点坐标建,整份换数据 / 增删线 / 改绑定才重建,只改坐标就地把那一条换掉),只对命中的候选逐字段判定 —— 十万级拖动降到 CPU 的 1% 以下,单条 `updatePoint`(改名这类不动几何的改动)55 → 3.3 ms(稳态 0.02 ms)。线路不多时保留原来的浮点容差扫描(行为与从前逐字不差);索引那条路只认 id 绑定与坐标精确相等 —— 这个量级坐标都是同一份来源算出来的,容差是给手写数据留的余量。
40
61
  - 视口里的线路改走线段网格:线路标签排版这类「只看屏幕里这一片」的路径(`forEachLineInView`)原先要把每条线的包围盒过一遍,10 万级 + 标签的 CPU profile 里它占 47 ~ 58%。现在线路超过 `LINE_VIEW_GRID_MIN_LINES`(2048)条、且估计出来的候选线段只占总数的 1/32 以内(`LINE_VIEW_GRID_COST_SHARE`)时改查拾取用的那张线段网格,再逐条按包围盒确认;候选排回要素下标顺序,脏线与没有线段的退化线单独补一遍,访问顺序与判定口径与整份扫描逐行相同。网格脏着(正在拖线 / 改线)或线路不多时照旧整份扫描。
41
62
  - 视口里的节点遍历也先查点网格:`forEachPointInView` 在节点超过 `POINT_VIEW_GRID_MIN_POINTS`(2048)、点网格不脏且估计候选不足总数的 1/32(`POINT_VIEW_GRID_COST_SHARE`)时,只查视口覆盖的格子、把候选排回数据顺序再逐个精确判定;网格脏着(刚改过数据 / 拖拽中)或视口几乎铺满时仍走整份扫描,避免为了省一趟遍历先付出建网格和排序的成本。`createPointGrid.estimate(..., stopAfter)` 与查询共用同一套格子口径,传 `stopAfter` 时一旦超过阈值立即返回,只判断候选是否过密时不必扫完视口里的全部格子;框选线路则复用已有的 `forEachLineInView`,不再为每条候选线复制路径 / 包围盒 / 颜色快照。
42
63
  - 悬停、选中、拖拽只做增量更新:选中集合用点 / 线两张下标 `Set` 维护,样式变化按要素下标重新解析,顶点与实例数据就地改写后按「实例槽位」上传,不重建整批缓冲。
43
64
  - 状态样式就地更新:悬停 / 选中样式只改颜色、大小、描边时按实例槽位覆盖,不重建整批;符号换成 / 换出贴图、阴影条数变化这类批次结构变化才回退整批重排。哪些要素当前真的按状态样式画过另有一份记账(`stateStyledSignatures`),整体重建后立刻重算,取消选中时按它精确还原本体样式,不会留下残留。
65
+ - 动效只维护真正的动画线路:`lineEffectRuns` 让每帧的动点循环只遍历配了 `effect` 的线路,路径就地变化时只重算这一条的总长与包围盒;普通线路再多,也不会进入动画的逐帧扫描。
66
+ - 热力图的要素采样与密度烘焙分开缓存:坐标 / 权重 / 显隐变化才重新采集,核参数 / 色带变化复用采样重烤,`density.opacity` 与 `layer` 只改绘制;拖动期间延迟到停手统一补烤。
67
+ - 鹰眼图的线路层单独使用离屏 canvas:缓存键覆盖尺寸、范围、投影、倍率与样式,线路包围盒完全落在小图视野外时直接跳过;布局尺寸没变时不再重设画布尺寸,补画不会清空线路缓存。
44
68
  - 拐点只处理当前显示的那些:`waypointVisibility: 'selected'` 下没有选中线路时,上万条线的拐点解析与上传都省掉了。
45
69
  - 通道回线的偏移在骨架变化时统一推导一次,拖动过程中只重算这一条通道的成员线。
46
70
  - 配置变更走一次合并:`updateOptions({ points, lines, channels, background, ... })` 只在需要时重建场景;demo 里的通道宽度、内收距离用 `v-model.lazy` 提交,避免拖动滑块时每帧重排。
@@ -22,10 +22,10 @@
22
22
  ## 常见问题
23
23
 
24
24
  **拖也拖不动、点了没反应,是不是坏了?**
25
- 不是 —— 功能开关**缺省全关**(见「功能开关」),什么都不写时组件只剩画布与数据。查看类要手动打开:详细配置写 `interactive: true`(平移 / 缩放 / 右键回调)与 `clickable: true`(悬停 / 选中),或一步写 `features: 'view'`;改数据的交互各有自己的开关 —— `boxSelect: true` / `draggable: true` / `waypointEditable: true` / `copy: {…}` / `merge: {…}` / `remove: {…}` / `features: { channelDraft: true, channelMenu: true }`,`features: 'edit'` 一次全开。标签 / 提示 / 鹰眼图不在开关表里:配了 `label` / `lineLabel` / `tooltip` / `minimap` 就等于打开。
25
+ 不是 —— 功能开关**缺省全关**(见「功能开关」),什么都不写时组件只剩画布与数据。查看类要手动打开:详细配置写 `interactive: true`(平移 / 缩放 / 右键回调)与 `clickable: true`(悬停 / 选中),或一步写 `features: 'view'`;改数据的交互各有自己的开关 —— `boxSelect: true` / `draggable: true` / `waypointEditable: true` / `copy: {…}` / `merge: {…}` / `remove: {…}` / `features: { channelDraft: true }`,`features: 'edit'` 一次全开。标签 / 提示 / 鹰眼图不在开关表里:配了 `label` / `lineLabel` / `tooltip` / `minimap` 就等于打开。
26
26
 
27
27
  **怎么一次关闭 / 打开组件的所有功能?**
28
- `features: false` 或 `features: 'static'` 会把全部 18 个功能开关关掉(`'static'` 连已配置的标签 / 提示 / 鹰眼图也一起关;`false` 与缺省全关同一口径);`features: true` 或 `features: 'edit'` 全部打开。运行时用 `view.setFeatures(...)` 换、`view.getFeatures()` 读 18 项有效值,完整清单见「功能开关」一节的表。
28
+ `features: false` 或 `features: 'static'` 会把全部 17 个功能开关关掉(`'static'` 连已配置的标签 / 提示 / 鹰眼图也一起关;`false` 与缺省全关同一口径);`features: true` 或 `features: 'edit'` 全部打开。运行时用 `view.setFeatures(...)` 换、`view.getFeatures()` 读 17 项有效值,完整清单见「功能开关」一节的表。
29
29
 
30
30
  **怎么完全关闭拐点的新增、移动和删除?**
31
31
  写 `features: { waypoint: false }` 或 `waypointEditable: false`。`waypoint` 就是拐点编辑的唯一总闸:新增(Ctrl + 点击、`addWaypoint` / `addWaypoints` / `addChannelGroupWaypoint`、通道右键框「加拐点」)、移动已有拐点、双击 / Delete 删除会同时停掉,没有只关其中一项的细分开关。已有拐点仍可显示和选中,数据与历史都不动。
@@ -166,7 +166,7 @@
166
166
  | 分类 | 类型 |
167
167
  | --- | --- |
168
168
  | 入口 | `GisViewOptions`、`GisViewInstance`、`GisViewEngineOptions`、`GisViewEngine`、`createGisView`、`createGisViewEngine` |
169
- | 插件与初始视图 | `GisViewPlugin`、`GisViewInitialView` |
169
+ | 插件与初始视图 | `GisViewPlugin`、`GisViewInitialView`、`GisViewFitSpacingOptions` |
170
170
  | 数据 | `GisViewPoint`、`GisViewLine`、`GisViewChannel`、`GisViewChannelGroup`、`GisViewData`、`GisViewCoordinate`、`GisViewBounds`、`GisViewRect` |
171
171
  | 样式 | `GisViewPointStyle`、`GisViewLineStyle`、`GisViewShadow`、`GisViewSelectionBoxStyle`、`GisViewCursorStyle`、`GisViewCursorValue`、`GisViewCursorTarget` |
172
172
  | 线型 | `GisViewLineType`、`GisViewLineDash`、`GisViewLineDashValues`、`GisViewLineDashObject` |
@@ -176,8 +176,8 @@
176
176
  | 覆盖层 | `GisViewLabelOptions`、`GisViewLineLabelOptions`、`GisViewItemLabel`、`GisViewLabelState`、`GisViewLabelPatch`、`GisViewSetLabelContentOptions`、`GisViewLabelListItem`、`GisViewItemLabelsInfo`、`GisViewRemoveLabelResult`、`GisViewSetLabelStatesResult`、`GisViewLabelChangePayload`、`GisViewLabelDragPayload`、`GisViewLabelClickPayload`、`GisViewLabelPointerPayload`、`GisViewLabelRef`、`GisViewLabelTarget`、`GisViewLabelBatchEntry`、`GisViewLabelRemoveEntry`、`GisViewLabelBatchSkip`、`GisViewAddLabelsResult`、`GisViewUpdateLabelsResult`、`GisViewRemoveLabelsResult`、`GisViewLineDecoration`、`GisViewTooltipOptions`、`GisViewLabelPayload`、`GisViewLineLabelPayload` |
177
177
  | 筛选 | `GisViewFilter`、`GisViewFilterResult`、`GisViewFilterCascade`、`GisViewFilterWorkerOptions` |
178
178
  | 大数据展示模式 | `GisViewDenseMode`、`GisViewDenseModeOptions`、`GisViewDenseModeInput`、`GisViewDenseState` |
179
- | 通道 | `GisViewChannelInput`、`GisViewChannelBuildOptions`、`GisViewChannelBuild`、`GisViewChannelInfo`、`GisViewChannelGroupInfo`、`GisViewChannelGroupWaypoints`、`GisViewChannelGroupLineWaypoints`、`GisViewChannelMember`、`GisViewChannelContext`、`GisViewChannelContextPayload`、`GisViewChannelAddPlan`、`GisViewChannelBetween`、`GisViewChannelAddOption`、`GisViewChannelAddInput`、`GisViewChannelAddResult`、`GisViewChannelDraftOptions`、`GisViewChannelDraftPayload`、`GisViewChannelLineMoveTarget`、`GisViewChannelLineMoveResult`、`GisViewRemoveLineResult`、`GisViewRemoveChannelResult`、`GisViewRemoveLinesResult`、`GisViewChannelInView`、`GisViewChannelAction`、`GisViewChannelActionId`、`GisViewChannelDraftStyle`、`GisViewChannelWaypointDragMode`、`createChannels`、`CHANNEL_DEFAULT_COLORS` |
180
- | 事件载荷 | `GisViewPickResult`、`GisViewPickAtResult`、`GisViewRectQueryResult`、`GisViewSelectionInfo`、`GisViewSelectedItems`、`GisViewSelectPayload`、`GisViewClickPayload`、`GisViewPointerPayload`、`GisViewPointerAction`、`GisViewPointerActionType`、`GisViewPointerTarget`、`GisViewPointerModifiers`、`GisViewWaypointActionData`、`GisViewLabelActionData`、`GisViewHoverPayload`、`GisViewCursorPayload`、`GisViewPointDragPayload`、`GisViewDragPointChange`、`GisViewDragLineChange`、`GisViewPointCopyPayload`、`GisViewPointCopyState`、`GisViewPointMergePayload`、`GisViewMergeImpact`、`GisViewMergeResult`、`GisViewMergeFailure`、`GisViewWaypointPayload`、`GisViewWaypointInfo`、`GisViewWaypointKey`、`GisViewParallelGroup`、`GisViewReadyPayload`、`GisViewEngineStats`、`GisViewEngineHeatDensity`、`GisViewDataChangeType`、`GisViewDataChangePayload`、`GisViewSelectChangePayload`、`GisViewShortcutPayload`、`GisViewRenderPayload`、`GisViewAddItemsResult`、`GisViewAddPointResult` |
179
+ | 通道 | `GisViewChannelInput`、`GisViewChannelBuildOptions`、`GisViewChannelBuild`、`GisViewChannelInfo`、`GisViewChannelGroupInfo`、`GisViewChannelGroupWaypoints`、`GisViewChannelGroupLineWaypoints`、`GisViewChannelMember`、`GisViewChannelContext`、`GisViewChannelAddPlan`、`GisViewChannelBetween`、`GisViewChannelAddOption`、`GisViewChannelAddInput`、`GisViewChannelAddResult`、`GisViewChannelDraftOptions`、`GisViewChannelDraftPayload`、`GisViewChannelLineMoveTarget`、`GisViewChannelLineMoveResult`、`GisViewRemoveLineResult`、`GisViewRemoveChannelResult`、`GisViewRemoveLinesResult`、`GisViewChannelInView`、`GisViewChannelAction`、`GisViewChannelActionId`、`GisViewChannelDraftStyle`、`GisViewChannelWaypointDragMode`、`createChannels`、`CHANNEL_DEFAULT_COLORS` |
180
+ | 事件载荷 | `GisViewPickResult`、`GisViewPickAtResult`、`GisViewRectQueryResult`、`GisViewSelectionInfo`、`GisViewSelectedItems`、`GisViewSelectPayload`、`GisViewClickPayload`、`GisViewPointerPayload、GisViewCascadeHighlightScope、GisViewCascadeHighlightOptions、GisViewCascadeHighlightInput、GisViewCascadeHighlightEntry、GisViewCascadeHighlightOrigin、GisViewCascadeHighlightInfo`、`GisViewPointerAction`、`GisViewPointerActionType`、`GisViewPointerTarget`、`GisViewPointerModifiers`、`GisViewWaypointActionData`、`GisViewLabelActionData`、`GisViewHoverPayload`、`GisViewCursorPayload`、`GisViewPointDragPayload`、`GisViewDragPointChange`、`GisViewDragLineChange`、`GisViewPointCopyPayload`、`GisViewPointCopyState`、`GisViewPointMergePayload`、`GisViewMergeImpact`、`GisViewMergeResult`、`GisViewMergeFailure`、`GisViewWaypointPayload`、`GisViewWaypointInfo`、`GisViewWaypointKey`、`GisViewParallelGroup`、`GisViewReadyPayload`、`GisViewEngineStats`、`GisViewEngineHeatDensity`、`GisViewDataChangeType`、`GisViewDataChangePayload`、`GisViewSelectChangePayload`、`GisViewShortcutPayload`、`GisViewRenderPayload`、`GisViewAddItemsResult`、`GisViewAddPointResult` |
181
181
  | 快捷键与复制 / 合并 / 删除 | `GisViewShortcuts`、`GisViewShortcut`、`GisViewShortcutAction`、`GisViewCopyOptions`、`GisViewCopyContext`、`GisViewCopyStartOptions`、`GisViewPasteOptions`、`GisViewCopyItem`、`GisViewClipboardItem`、`GisViewMergeOptions`、`GisViewMergeContext`、`GisViewPointStyleSnapshot`、`GisViewPointRef`、`GisViewMergeResult`、`GisViewMergeFailure`、`GisViewMergeOutcome`、`GisViewRemovePointsOutcome`、`GisViewRemoveSelectedOutcome`、`GisViewRemoveOptions`、`GisViewRemoveScope`、`GisViewRemovePayload`、`GisViewRemoveResult`、`GisViewRemoveOutcome`、`GisViewRemoveFailure`、`GisViewRemoveAction`、`GisViewRemovePoint`、`GisViewRemoveLine`、`GisViewRemoveWaypoint`、`GisViewRemoveGroup`、`GisViewRemoveChannel`、`GisViewRemoveCounts`、`GisViewChannelSelectMode`、`GisViewUpdateItemsOutcome`、`GisViewUpdateItemEntry` |
182
182
  | 相机与视图 | `GisViewCamera`、`GisViewViewport`、`GisViewFocusOptions`、`GisViewFocusTarget`、`GisViewExportImageOptions`、`GisViewExportSelectionOptions`、`GisViewExportSelectionResult` |
183
183
  | 导出数据 | `GisViewExportScope`、`GisViewExportPart`、`GisViewExportDataOptions`、`GisViewExportDataResult`、`GisViewFeatureRef`、`GisViewExportFilterOptions`、`GisViewExportMeta`、`GisViewExportedLabel`、`GisViewStationRecord`、`GisViewPointRecord`、`GisViewLineRecord`、`GisViewChannelRecord`、`GisViewExportFormat`、`GisViewBuiltinExportFormat`、`GisViewExportFormatContext`、`GisViewExportFormatAdapter` |
@@ -16,11 +16,11 @@ npm run dev # 或 npx vite
16
16
  控制台里 `__gisSteps.view` 就是当前这一步的视图,`__gisSteps.DATA` 是那份园区数据,可以随手调 API。
17
17
  地址栏里带步骤号(`index.html#7`)可以直接打开某一步,也能收藏 / 分享。
18
18
 
19
- 自测页就在同一个目录下(`selftest.html` / `selftest-demo.html` / `selftest-demos.html` / `selftest-labels.html` / `selftest-select.html` / `selftest-actions.html` / `selftest-scale.html` / `selftest-perf.html` / `selftest-features.html` / `selftest-full.html` / `selftest-effects.html` / `selftest-data.html` / `selftest-decoration.html` / `selftest-docs.html`):打开就跑,标题报 `PASS` / `FAIL`,页面上的清单是逐条断言。**无头环境下 `requestAnimationFrame` 基本不推进**,动画类断言要用真时间(CDP 或直接开浏览器)跑 —— `selftest-effects.html` 因此自己装了一个「帧泵」(把 `requestAnimationFrame` 收进 Map、手动按帧推进),涟漪与动点的断言才测得动。另有 docs/index.html(文档中心:把仓库里的 markdown 读成 HTML)、docs/api/index.html(API 速查:249 个实例成员逐个一张卡,参数 / 返回 / 说明 / 示例与复制,搜索 + `#成员名` 深链、左侧分组跟随与深浅主题)、docs/guide/sample-document.html(sample-document.json 的交互图解)与 docs/steps/selftest-docs.html(上面这三个页面的自测:清单 / 渲染 / 点选标注一页跑完)。
19
+ 自测页就在同一个目录下(`selftest.html` / `selftest-demo.html` / `selftest-demos.html` / `selftest-labels.html` / `selftest-select.html` / `selftest-actions.html` / `selftest-scale.html` / `selftest-perf.html` / `selftest-features.html` / `selftest-full.html` / `selftest-effects.html` / `selftest-data.html` / `selftest-decoration.html` / `selftest-docs.html`):打开就跑,标题报 `PASS` / `FAIL`,页面上的清单是逐条断言。**无头环境下 `requestAnimationFrame` 基本不推进**,动画类断言要用真时间(CDP 或直接开浏览器)跑 —— `selftest-effects.html` 因此自己装了一个「帧泵」(把 `requestAnimationFrame` 收进 Map、手动按帧推进),涟漪与动点的断言才测得动。另有 docs/index.html(文档中心:把仓库里的 markdown 读成 HTML)、docs/api/index.html(API 速查:253 个实例成员逐个一张卡,参数 / 返回 / 说明 / 示例与复制,搜索 + `#成员名` 深链、左侧分组跟随与深浅主题)、docs/guide/sample-document.html(sample-document.json 的交互图解)与 docs/steps/selftest-docs.html(上面这三个页面的自测:清单 / 渲染 / 点选标注一页跑完)。
20
20
 
21
21
  ## 怎么查全部属性与方法
22
22
 
23
- 组件的完整 API 是同一份清单的五个去处(当前:实例成员 **249** 个 = 246 个方法 + `el` / `canvas` / `gl` 三个属性,`options` **92** 个键,数据字段 **245** 个):
23
+ 组件的完整 API 是同一份清单的五个去处(当前:实例成员 **253** 个 = 250 个方法 + `el` / `canvas` / `gl` 三个属性,`options` **95** 个键,数据字段 **249** 个):
24
24
 
25
25
  - **手册里查**:`docs/manual/` 把整本手册拆成 21 章(索引页 `docs/GisView.md` 有目录与按任务的快速跳转)—— 找方法先看 [`16 实例方法总表`](../manual/16-实例方法总表.md)、要把每个成员逐个列全(参数写「名字: 类型 = 默认值」、返回值只写类型)看 [`17 参数速查`](../manual/17-参数速查.md)、要能直接复制的调用看 [`18 逐方法示例`](../manual/18-逐方法示例.md);`options` 表与数据字段表分别在 [`03 配置项(options)`](../manual/03-配置项.md) 与 [`02 坐标与数据`](../manual/02-坐标与数据.md)。同名的接口在 `src/gis-view/types.d.ts`。
26
26
  - **页面上查**:`docs/api/index.html`(API 速查)—— 左栏按 16 个分组列出全部成员,带搜索(Ctrl+K)与方法 / 属性 / options 三档开关,当前读到的分组会跟着高亮;右栏每个成员一张卡,参数 / 返回 / 说明之外,示例块直接来自 [`18 逐方法示例`](../manual/18-逐方法示例.md),带高亮与「复制名字 / 复制代码」,旁边还有「在调试页跑 →」;地址栏带 `#updateLine` 这样的 hash 直接跳到那一条。右上角主题按钮与文档中心共用 `gisDocs.reading`,跟随系统 / 浅色 / 深色三档换页不会各记一套;深链落点还留了 `scroll-margin-top`,不会被吸顶栏盖住。页面数据来自 `docs/demos/api-index.js`(生成物,禁止手改),自测页 `docs/steps/selftest-docs.html` 盯着它别掉队。
@@ -106,23 +106,29 @@ API 名字与前面几步是一一对应的;第 11 步再补两件「数据之
106
106
  - `selftest.html`:手写版第 1 ~ 9 步的断言(拾取、草稿的 step 1 / 2、落地、移除、连续新增、平移……);
107
107
  - `selftest-full.html`:真组件上的同一套断言,外加把 `index.html` 的十三个步骤逐个挂载一遍(第 13 步再点一遍画质 / 批量 / 撤销 / 重做 / 功能预设五个按钮);
108
108
  这一轮又补了四条:小图上的 GeoJSON 底图在主视图放大 3 倍前后红色描边像素数不变(容差 20%)、小图放大 6 倍时 `pointZoomScale` 指数 0 / 1 的像素对比(指数 1 至少 4 倍面积、指数 0 不长大,且倍数回到 1 时指数 1 也不再放大)、以及 stub host 上「拖节点没有结束事件、空闲计时器也会把 `hideOnInteract` 收起的标签层放出来」。
109
- - `selftest-features.html`(611 条):真组件上的线型 / 线路装饰 / 光标 / 表现型配置断言 —— 把画面用 `exportImage` 读回 2D 画布数像素(实线 > 虚线 > 点线的墨量、`dc_line` 中点装饰的像素差、`showGrid` 只重画一帧),光标则用合成的指针事件与 Ctrl 键读出 `canvas.style.cursor`;装饰那几条还会量「几何变了装饰跟不跟着走」—— 写回新路径、拖绑定的节点、加 / 删拐点之后,装饰的紫色像素质心都要落在新中点上(容差 3px);另外两条量的是**固定档**:`anchor: 'fixed'` + `at` 先把装饰钉在写死的世界坐标上,再把线从水平拖成斜的,装饰必须留在原坐标、不能跑到新中点。线路标签在单独一张画布上验:默认挂在中点、内容取 `name`,`position: 0` 落到起点,`hideOnInteract` 在缩放期间给整层加 `--hidden`、停手恢复,关掉 `lineLabel` 之后标签消失。装饰缺省 `auto` 的那一串也在这里量:没有拐点时拖端点装饰保持同一个比例(一直挂在中点)、加上拐点之后钉在那一刻的坐标上(改折线也不动)、把拐点删回直线又回到比例跟随、`anchor: 'follow'` 则一直跟到折线的新中点;`parallel` 用一条横条符号验(默认包围盒 26×6,平行之后转成 4×26),方形符号走的是 gl.POINTS 那条路(旋转后外接盒从 24×24 撑成 32×32)。通道里「每条回线各显示自己的标签」也在这里验:没有逐条配置时会被 32px 屏幕小格去重(只显示 1 ~ 2 条),给每条回线写上自己的 `lineLabelPosition` 之后 8 条全都在、位置各不相同,斜线上的标签按线路方向转、朝左的自动翻半圈(rotate = 0)。线路的层序(`zIndex`)也在这里验:画布正中一横一竖两条粗线交叉,默认后写的竖线在上(中心像素蓝),规则里给横线写 `zIndex: 5` 就换成红,`updateLine` 给竖线写 `zIndex: 9` 又换回蓝,写回 0 再回到红。这一轮另外验了插件上下文:`options.plugins` 与 `view.use()` 拿到的第二个参数是同一个冻结对象(`apiVersion === 1`、`view` 指回实例),旧的一参插件写法照常可用。这一轮另补 3 条合并触发时机断言(center 差 10px 不触发、继续拖入目标区域触发并带 trigger、collision 同距离照旧触发)。这一轮再补 1 条:13 个旧快捷方法已从实例删除,`updatePoint` / `updateLine` / `updateChannel` / `updateOptions` / `setFilter` / `setDropPreview` 仍在。对账组另补 8 条:`getChangedChannels({ ignoreGroups })` 忽略分组增删、已有分组拐点仍报、`reasons` 收窄与线路字段 / 路径 / 显隐变化,以及删除通道 / 删除线路 / 无通道场景同步清账。
109
+ - `selftest-features.html`(662 条):真组件上的线型 / 线路装饰 / 光标 / 表现型配置断言 —— 把画面用 `exportImage` 读回 2D 画布数像素(实线 > 虚线 > 点线的墨量、`dc_line` 中点装饰的像素差、`showGrid` 只重画一帧),光标则用合成的指针事件与 Ctrl 键读出 `canvas.style.cursor`;装饰那几条还会量「几何变了装饰跟不跟着走」—— 写回新路径、拖绑定的节点、加 / 删拐点之后,装饰的紫色像素质心都要落在新中点上(容差 3px);另外两条量的是**固定档**:`anchor: 'fixed'` + `at` 先把装饰钉在写死的世界坐标上,再把线从水平拖成斜的,装饰必须留在原坐标、不能跑到新中点。线路标签在单独一张画布上验:默认挂在中点、内容取 `name`,`position: 0` 落到起点,`hideOnInteract` 在缩放期间给整层加 `--hidden`、停手恢复,关掉 `lineLabel` 之后标签消失。装饰缺省 `auto` 的那一串也在这里量:没有拐点时拖端点装饰保持同一个比例(一直挂在中点)、加上拐点之后钉在那一刻的坐标上(改折线也不动)、把拐点删回直线又回到比例跟随、`anchor: 'follow'` 则一直跟到折线的新中点;`parallel` 用一条横条符号验(默认包围盒 26×6,平行之后转成 4×26),方形符号走的是 gl.POINTS 那条路(旋转后外接盒从 24×24 撑成 32×32)。通道里「每条回线各显示自己的标签」也在这里验:没有逐条配置时会被 32px 屏幕小格去重(只显示 1 ~ 2 条),给每条回线写上自己的 `lineLabelPosition` 之后 8 条全都在、位置各不相同,斜线上的标签按线路方向转、朝左的自动翻半圈(rotate = 0)。线路的层序(`zIndex`)也在这里验:画布正中一横一竖两条粗线交叉,默认后写的竖线在上(中心像素蓝),规则里给横线写 `zIndex: 5` 就换成红,`updateLine` 给竖线写 `zIndex: 9` 又换回蓝,写回 0 再回到红。这一轮另外验了插件上下文:`options.plugins` 与 `view.use()` 拿到的第二个参数是同一个冻结对象(`apiVersion === 1`、`view` 指回实例),旧的一参插件写法照常可用。这一轮另补 3 条合并触发时机断言(center 差 10px 不触发、继续拖入目标区域触发并带 trigger、collision 同距离照旧触发)。这一轮再补 1 条:13 个旧快捷方法已从实例删除,`updatePoint` / `updateLine` / `updateChannel` / `updateOptions` / `setFilter` / `setDropPreview` 仍在。对账组另补 8 条:`getChangedChannels({ ignoreGroups })` 忽略分组增删、已有分组拐点仍报、`reasons` 收窄与线路字段 / 路径 / 显隐变化,以及删除通道 / 删除线路 / 无通道场景同步清账。本轮再补 16 条:筛选恢复后通道宽度与回线车道、`focusOn` 显式 `point` / `line` / `channel` / `group` 与统一缩放口径、`selectFeatures` 分组写法、`selectWaypoint` 带选线路,以及 `lineFollowsNode` 的隐藏 / 筛选 / 删除 / 撤销与动态切换。
110
110
  - `selftest-decoration.html`(75 条):动态装饰器草稿专项自测 —— 开启后的 `start` 载荷、默认 14px 触发半径与每条线路不限量、指针在线路上 `hover`(命中线 / 自身 `decoration` / 平行角)、同一位置重复移动不重复发事件、移出目标 `leave`、在线路与空白处 `pointerup` 的 `ready` 差异、按下 / 抬起 `action.type` 都是 `decorationDraft`、单线 / 分组 / 整条通道三档作用域与非通道回退、`maxPerLine` 满额时悬浮与松手都不触发且提交前复核不写数据、`allowedLines` / `allowedChannels` / `allowedGroups` 三种白名单在悬浮与提交两处都拦截、`update` 改图标 / 尺寸 / 方向、`commit` 一次 `batch` 写四条且只发一条 `dataChange`、`cancel` 不改数据、重复 `begin` 替换旧草稿、整份换数据后过期草稿静默清掉。节点侧另有 32 条:`target: 'point'` 命中与只展开一个节点目标、方向 / 距离换算成屏幕偏移、相对偏移 / 固定世界坐标 / 鼠标位置三种位置来源、连接线样式随提交保存、`allowedPoints` 与 `maxPerPoint` 在悬浮 / 提交两处拦截、`both` 等距时节点优先且最近节点被白名单拦住时回退线路、节点与线路的重复 `reject` / `offset` 两种策略、节点装饰 get / update / remove 与整批提交、度转角与连接线进入矢量快照。这一轮另补 8 条节点断言:既有的 2 条「装饰随移动」仍保留;新增 6 条覆盖 `pointer + follow`、`fixed + follow`、`offset + fixed` 三种跨组合的预览与提交结果,确认位置来源和落地关系解耦后分别写成 `offset` 或 `at`。
111
111
  - `selftest-demo.html`:demo 级自测 —— 面板上的「定位直流线路」按当前数据集的 `dc_line` 条数走(默认数据集 `new.json` 里现在自带 5 条,点一下直接选中并定位到第一条;一条都没有的数据集才切到样例数据、定位到「直流送电」,中点挂着 `PATH_DC_LINE` 装饰),断言按数据算出来的条数写、不写死数字,读数与选中面板都跟着更新;打开时说明段落默认收起、面板按组折叠,节点标签只给最高电压那一档、线路标签默认整个不显示(面板上勾「显示」再按「显示范围」展开),面板上有 PATH_DC_LINE 的「标记定位」下拉(自动 / 一直跟随 / 写死坐标三档)。「通道回线各自显示」这一档从数据层核对(勾上之后 3669 条回线都带上了自己的 `lineLabelPosition`),再放大三步后勾「与线路平行」,画出来的标签 transform 里要出现 `rotate(...)`。这一轮又补了「数据映射」下拉(raw / lngLat / fit / scale)与「节点 / geojson 比例」滑块:切到 fit 后 320 个站点都要落进 GeoJSON 画出来的范围、读数里给出换算比例;「经纬度 → 屏幕」「屏幕 → 经纬度」两个按钮来回换算一致(屏幕读数保留 1 位小数,允许 0.01° 的回差);在画布上挪一下鼠标,面板上就要出现屏幕像素与经纬度两行读数。
112
- - `selftest-perf.html`(136 条):大数据量与画质档位的性能自测 —— 20000 点 / 8000 线的建场景与整份重排耗时、500 次拾取、逐个改 500 个节点对 `view.batch()`、四档画质与 `'auto'` 的自动升降;前面几轮补过 8 条交互期细线的断言(`getStats()` 给出 `antialias` / `software` / `thinLines` 三个读数;交互期 1px 实线整批走 `gl.LINES` 快路径、停手回到四边形展开;`interactionThinLines: false`、线宽 8px、整批带虚线这三种情况下都不走快路径,改回细实线又走);上一轮补过 7 条线路标签候选复用的断言(复用态与整层重排逐字一致,改线路内容 / 挪节点 / 相机变化之后缓存都作废)与 1 条连续更新的耗时 DIAG(2 万点 / 1 万线,中位 0.30 ms / 次);**这一轮再补 3 条标签避让盒的断言**(4 万点 + 4 万条长线放大后连续整层重排:第 2 轮起一次布局都不读、同一轮里没有哪条标签被量第二次)与 1 条量测读数 DIAG(逐轮 1427 / 0 / 0 / 0 / 0 次,旧写法 2662 / 0 … 次、第 1 轮重复量 1235 条;**上一轮把「没有哪条标签被量第二次」从按 DOM 节点去重改成按标签身份(`labelKind` # `labelIndex` # `labelLabel`)去重** —— 标签节点是池化复用的,按节点去重会把「同一格换了条标签」误报成重复量,旧代码实测第 1 轮 1235 条、新代码 0 条;**这一轮再补 3 条线路批次视口裁剪的断言**(20000 点 / 8000 线放大到 `scale: 10`:`lineInstances` 8000 → 27、相机挪回来回到 8000、来回一趟整幅像素差 0 / 241164)与 1 条读数 DIAG。这一轮再补 2 条点视口网格的断言(网格可用时枚举顺序与整份筛出来的下标序列一致、回调返回 `false` 最多只访问一个);页面每段之间让出一帧,边跑边出结果。
113
- - `selftest-scale.html`(107 条):大数据交互这一轮的自测 —— 2000 点 / 2000 线、筛选开着:`batch()` 里改 200 个节点只发 1 条 `onFilter` 与 1 条 `onDataChange`(对照组逐个调用是 200 条);改字段 / 改坐标 / 改线路之后「增量补写的隐藏集」与「从零建一个同样条件的视图」逐位一致;点挪到被筛掉的坐标上,按 `coordinate` 联动的线路当场隐藏、挪回去又出来;线段网格的步进登记与多层粗格(沿线查得到、线不经过的角落不进候选、跨层只返回一次、2 万条长线密集时小范围查询的候选远少于总条数、整图估计值与总条数同量级);点 / 线 / 空白的拾取;搜索文本缓存的命中与「改过字段之后按新值搜得到、旧值搜不到」。最底下几条 DIAG 是耗时读数(`Date.now()`),无头浏览器里虚拟时钟不往前走、显示 0.0 ms,带界面的浏览器里才是真实数字。;这一轮又补了 8 条:三万点的发散渲染(超过 2 万点走分批铺开)铺完之后装饰 400 个一条不少、挪一条带装饰的线的端点只动那一条线的装饰且落在新路径中点上、贴图符号进的是贴图批次、选中的节点抬到最上层、整份重排之后装饰与动点不翻倍,以及「节点自己配的涟漪数得清」(这一条顺带抓出了发散渲染期间涟漪补不齐的 bug)。这一轮再补一条 `createPointGrid.estimate` 与 `query` 同口径、带 `stopAfter` 提前封顶的断言(不建候选数组也能判断值不值得走网格)。
112
+ - `selftest-perf.html`(162 条):大数据量与画质档位的性能自测 —— 20000 点 / 8000 线的建场景与整份重排耗时、500 次拾取、逐个改 500 个节点对 `view.batch()`、四档画质与 `'auto'` 的自动升降;前面几轮补过 8 条交互期细线的断言(`getStats()` 给出 `antialias` / `software` / `thinLines` 三个读数;交互期 1px 实线整批走 `gl.LINES` 快路径、停手回到四边形展开;`interactionThinLines: false`、线宽 8px、整批带虚线这三种情况下都不走快路径,改回细实线又走);上一轮补过 7 条线路标签候选复用的断言(复用态与整层重排逐字一致,改线路内容 / 挪节点 / 相机变化之后缓存都作废)与 1 条连续更新的耗时 DIAG(2 万点 / 1 万线,中位 0.30 ms / 次);**这一轮再补 3 条标签避让盒的断言**(4 万点 + 4 万条长线放大后连续整层重排:第 2 轮起一次布局都不读、同一轮里没有哪条标签被量第二次)与 1 条量测读数 DIAG(逐轮 1427 / 0 / 0 / 0 / 0 次,旧写法 2662 / 0 … 次、第 1 轮重复量 1235 条;**上一轮把「没有哪条标签被量第二次」从按 DOM 节点去重改成按标签身份(`labelKind` # `labelIndex` # `labelLabel`)去重** —— 标签节点是池化复用的,按节点去重会把「同一格换了条标签」误报成重复量,旧代码实测第 1 轮 1235 条、新代码 0 条;**这一轮再补 3 条线路批次视口裁剪的断言**(20000 点 / 8000 线放大到 `scale: 10`:`lineInstances` 8000 → 27、相机挪回来回到 8000、来回一趟整幅像素差 0 / 241164)与 1 条读数 DIAG。这一轮再补 2 条点视口网格的断言(网格可用时枚举顺序与整份筛出来的下标序列一致、回调返回 `false` 最多只访问一个)。这一轮再补 19 条点聚合断言(归一化、1.2 万点建组与统计、原始数据不变、`visibleOnly` 排除成员、拾取 `type=cluster`、点击语义、`maxZoom` 展开、关闭恢复),页面每段之间让出一帧,边跑边出结果。这一轮另补 7 条热力图缓存断言:`styleKey` / `weightKey` 的语义、只改 `density.opacity` 不重采不重烤、换色带与定向改要素值会重烤。
113
+ - `selftest-scale.html`(115 条:109 PASS + 6 DIAG):大数据交互这一轮的自测 —— 2000 点 / 2000 线、筛选开着:`batch()` 里改 200 个节点只发 1 条 `onFilter` 与 1 条 `onDataChange`(对照组逐个调用是 200 条);改字段 / 改坐标 / 改线路之后「增量补写的隐藏集」与「从零建一个同样条件的视图」逐位一致;点挪到被筛掉的坐标上,按 `coordinate` 联动的线路当场隐藏、挪回去又出来;线段网格的步进登记与多层粗格(沿线查得到、线不经过的角落不进候选、跨层只返回一次、2 万条长线密集时小范围查询的候选远少于总条数、整图估计值与总条数同量级);点 / 线 / 空白的拾取;搜索文本缓存的命中与「改过字段之后按新值搜得到、旧值搜不到」。最底下几条 DIAG 是耗时读数(`Date.now()`),无头浏览器里虚拟时钟不往前走、显示 0.0 ms,带界面的浏览器里才是真实数字。;这一轮又补了 8 条:三万点的发散渲染(超过 2 万点走分批铺开)铺完之后装饰 400 个一条不少、挪一条带装饰的线的端点只动那一条线的装饰且落在新路径中点上、贴图符号进的是贴图批次、选中的节点抬到最上层、整份重排之后装饰与动点不翻倍,以及「节点自己配的涟漪数得清」(这一条顺带抓出了发散渲染期间涟漪补不齐的 bug)。这一轮再补一条 `createPointGrid.estimate` 与 `query` 同口径、带 `stopAfter` 提前封顶的断言(不建候选数组也能判断值不值得走网格)。
114
114
  - `bench-filter.html`:基准页(不是自测,只会打印读数)—— 20000 点 / 20000 线、`cascade: 'binding'` 的筛选开着(`filterWorker: false`),量「连换 10 次筛选条件」「`batch()` 里改 500 个节点(筛选开着 / 关掉)」「单个 `updatePoint`」「默认全字段 `search` 跑两次」这几档,每档先热身一遍再计时(`Date.now()`)。标题从 `bench running` 变成 `bench done` 表示跑完;别用 `--virtual-time-budget` 打开它 —— 虚拟时钟下同步脚本里的 `Date.now()` 不往前走,量出来全是 0。
115
115
  - `bench-scale.html`:百万级基准页(同样只打印读数)—— 点 / 线规模与视口走 URL 参数,`window.__bench.run(options)` 量生成数据、建场景与发散渲染、平移 / 缩放 / 放大 / 拾取、`refresh` / `updatePoint` / `updateOptions`、内存账与销毁回收;另有 `prepare`(只建场景)、`__bench.op(name)`(单步)与 `__bench.micro(count)`(原语微基准)。配 `scripts/bench-scale.mjs` 用(CDP 驱动,`--points=1000000 --gpu --port=93xx`,另有 `--profile` 抓 CPU profile、`--heap` 抓堆快照);跑全部自测页用 `scripts/run-selftests.mjs`(`node scripts/run-selftests.mjs [页面名…]`,无头 Chrome + 真时间),两个脚本都要先起 dev server。
116
- - `selftest-labels.html`(161 条):标签拖动与编辑、HTML 语义、高密度画布、导出分项多选的专项自测 —— 按住标签拖 90 px 之后标签真的挪了那么多、同时画布不平移,`labelDrag` 的 `start` / `move` / `end` 三态与 `offset` / `delta` 对得上,偏移进得了 `getLabelStates()`、再拖动节点时偏移照旧叠在节点位移上,`resetLabel` 之后回到默认锚点;`updateLabel` 改内容 / 字号 / 颜色(`style` 是增量合并,只改字号不动颜色)、`resetLabels` 一次清干净;点一下标签发 `labelClick` 并把这一条交给面板;线路标签的拖动与恢复默认同一条路;导出侧验「选什么导什么」(选场站只出场站、选通道自动带上拐点、不认识的分项记一条提示),`exportData({ scope: 'selection' })` 验场站 + 通道 / 只出线路 / 没选中时的 `ok: false` 三个分支。HTML 专项验全局 / 逐条 `true` / `false`、模板与 `field` / 默认名称转义、`updateLabel` 双向切换、线路标签富文本、点击载荷保留 `html` 与原标记、`getLabels()` 报运行时偏移、只改样式不替换 HTML 子节点、导出 → 回灌保持逐条语义,以及 120 条同内容标签首轮布局读取 ≤ 4 次的缓存断言。高密度专项用 1100 条纯文本验缺省 `auto` 只留一张画布、混入一条 `html: true` 仍保留那一条 DOM、`labelRender: 'dom'` 回退逐条 DOM 并清空画布、切回 `auto` 回收上一轮 div、SVG 导出包含画布标签与缩放只推一次。另验 GeoJSON 区域文字同一相机重复刷新 0 次 `style` 写入、只改字号复用同一 DOM 节点、离屏时改样式再回到视野仍能恢复显示、只改锚点位置也会重新投影,以及同一帧 12 次相机变化只做 1 次重投影。设备像素专项把 `devicePixelRatio` 临时设为 1.25,验标签落点与拖动期整层位移都落在物理像素网格上,并确认逐条标签不再各自声明 `will-change`。
116
+ - `selftest-labels.html`(165 条):标签拖动与编辑、HTML 语义、高密度画布、导出分项多选的专项自测 —— 按住标签拖 90 px 之后标签真的挪了那么多、同时画布不平移,`labelDrag` 的 `start` / `move` / `end` 三态与 `offset` / `delta` 对得上,偏移进得了 `getLabelStates()`、再拖动节点时偏移照旧叠在节点位移上,`resetLabel` 之后回到默认锚点;`updateLabel` 改内容 / 字号 / 颜色(`style` 是增量合并,只改字号不动颜色)、`resetLabels` 一次清干净;点一下标签发 `labelClick` 并把这一条交给面板;线路标签的拖动与恢复默认同一条路;导出侧验「选什么导什么」(选场站只出场站、选通道自动带上拐点、不认识的分项记一条提示),`exportData({ scope: 'selection' })` 验场站 + 通道 / 只出线路 / 没选中时的 `ok: false` 三个分支。HTML 专项验全局 / 逐条 `true` / `false`、模板与 `field` / 默认名称转义、`updateLabel` 双向切换、线路标签富文本、点击载荷保留 `html` 与原标记、`getLabels()` 报运行时偏移、只改样式不替换 HTML 子节点、导出 → 回灌保持逐条语义,以及 120 条同内容标签首轮布局读取 ≤ 4 次的缓存断言。高密度专项用 1100 条纯文本验缺省 `auto` 只留一张画布、混入一条 `html: true` 仍保留那一条 DOM、`labelRender: 'dom'` 回退逐条 DOM 并清空画布、切回 `auto` 回收上一轮 div、SVG 导出包含画布标签与缩放只推一次;新增断言还验 1100 条同文案只调用 `measureText` ≤ 2 次、同一任务连续 5 次缩放只重绘 1 ~ 2 次、显式 `canvas` 在优化档遵守 `maxCount: 200`,以及 6 条线路标签在优化档全部进画布且 DOM 为 0。另验 GeoJSON 区域文字同一相机重复刷新 0 次 `style` 写入、只改字号复用同一 DOM 节点、离屏时改样式再回到视野仍能恢复显示、只改锚点位置也会重新投影,以及同一帧 12 次相机变化只做 1 次重投影。设备像素专项把 `devicePixelRatio` 临时设为 1.25,验标签落点与拖动期整层位移都落在物理像素网格上,并确认逐条标签不再各自声明 `will-change`。
117
117
  - `selftest-actions.html`:事件判读与只读接口的专项自测 —— 左右键、拖动、框选、拐点与标签的 `action` 三件套(type / target / data)、`button` / `modifiers` / `moved` / `double`、Shift 框选(含没框到东西的空框)与 Ctrl 取反在 `selectChange.action.type` 上的分辨、`pickAt` 的客户端坐标与只读性、`getFeaturesInRect`(含宽高为负的矩形)与 `getChannelsInView`、`addPoints` / `addPoint` / `addLines` / `removeLines` 的返回与 `dataChange` 类型;最后一段把第 12 步 `step-12-events.js` 挂起来跑一遍(判读读数、`probeAt` / `probeRect`、两条走廊)。页面底部流式打印 PASS / FAIL。
118
- - `selftest-select.html`:选择与框选的专项自测(59 条)—— 点选 / 悬停 / Ctrl 取反 / 多选开关的像素级状态样式与恢复,`select` / `deselect` / 通道写法 / 变更来源的接口断言,以及 SVG 状态贴图的预热与不同尺寸配方;这一轮补了 `selectionKinds: 'all'` 下 `selectInBox` 一次收下节点、独立线路与按 `channelSelectMode` 摊开的通道回线。
119
- - `selftest-demos.html`:演示外壳与 20 个页签(19 个功能 + 综合,按基础 / 交互 / 样式 / 数据 / 性能 / 综合六组排列)的自测(297 条)—— 逐个用 `?demo=<id>` 打开基础视图、标签交互、标签显示、鹰眼图配置、样式与规则、悬停与提示、通道与数据、编辑与保存、事件与方法、批量复制与合并、拖拽落点、可视化连线、动态装饰器、批量新增、搜索与筛选、扩展能力、性能与质量、动画效果、扩展图形与综合示例,画布与右栏读数都在、切页签会把 `?demo=` 写回地址栏、切走之后上一个实例被销毁(不留画布)、缺省页签是综合示例、页签总数与注册表一致、顶部六个分组按钮一次只展开一组且组名可见;鹰眼图配置页另验 `layers` 的自定义 `draw` 真被调用、`placement` 切到数据前 / 后时 `drawInfo.stage` 跟着变、框拖动触发 `onInteraction` 并带相机、尺寸切换走 `updateOptions` 重铺画布、DPR 2 下 `maxPixelRatio` 的 1× / 2× 备份尺寸、关掉扩展层与 `zoomMinimapBy` / `setMinimapZoom`;批量复制 / 合并、通道拐点 / 分组两页另有假接口断言(Shift 框选、整份请求体、标准响应包与资源级结果、版本号 / opId、200 / 网络失败 / 500 / 409、原 opId 重试与幂等重放不推进版本、409 rebase、本地回滚);假后端严格契约另有一组直接断言(请求头与 body 不一致返回 400、baseVersion 非有限数字返回 400、同 opId 换 body 返回 409 `IDEMPOTENCY_CONFLICT` 且版本不前进),通道页还验了自定义格式按钮注册 → 导出 → 注销;扩展能力页另验自定义运算符 / 图元 / `view.use` 插件 / `defineExportFormat` 的注册、切换、卸载与业务 DTO 结构,并验同名重新注册图元当场换画笔;可视化连线页另验初始 `channels` 为空、正交折线拐点、预览箭头、取消不写数据,以及默认提交 / `addLines` / `addChannels` 三种落地;动态装饰器页另验吸附方式只有「跟随 / 固定」、两档下位置都有「相对节点 / 固定位置 / 鼠标位置」三档,固定 + 相对节点只改 `anchor`,固定 / 跟随 + 鼠标位置分别映射为 `placement: pointer` + 对应 `anchor` 且不带 `at`,跟随 + 固定位置仍传 `at`;通道与数据页另验「线路分组」弹框:分组卡片与「场站对标标识」都在,行内「确认移入」只改本地草稿、组件数据不动,取消 / Esc / 遮罩关闭不写数据,「确定分组」只调用一次 `splitChannelGroups` 把整份 groups 一次落地。页面底部流式打印 PASS / FAIL。
120
- - `selftest-effects.html`:线路 / 节点动画与贴图预热的专项自测(83 条,页面自带「帧泵」—— 无头环境里 `requestAnimationFrame` 几乎不推进,它把 rAF 收进 Map、手动推帧,动画类断言才测得动):纯函数组(归一化、三层「关」的语义、按弧长采样、拐点连续、相位打散)、引擎组(铺批次 / 关掉一层 / 恢复 / 逐要素覆盖 / `getLineEffects` 全程贴线)、像素组(画布上真的有一个红点在两帧之间挪了位置)、**涟漪跟手组**(开着动画拖节点:拖动前圆心在节点坐标上、拖动中跟着走且是就地改实例缓冲、松手后整份重建也对齐、再拖回原位跟着回来,另有一条像素级断言量绿圈质心落在新坐标上)、**预热组**(全排列条数、第二次命中缓存、`getSymbolCombos()` 的去重与线路侧那一条、照着它配的一份**零张新画布**、反向一条「没画过的组合会当场烤、画布 +1」)、提醒组(`effects.line` 里写 `show` 被点名)。
118
+ - `selftest-select.html`:选择与框选的专项自测(60 条)—— 点选 / 悬停 / Ctrl 取反 / 多选开关的像素级状态样式与恢复,`select` / `deselect` / 通道写法 / 变更来源的接口断言,以及 SVG 状态贴图的预热与不同尺寸配方;这一轮补了 `selectionKinds: 'all'` 下 `selectInBox` 一次收下节点、独立线路与按 `channelSelectMode` 摊开的通道回线。
119
+ - `selftest-demos.html`:演示外壳与 22 个页签(21 个功能 + 综合,按基础 / 交互 / 样式 / 数据 / 性能 / 综合六组排列)的自测(333 条)—— 逐个用 `?demo=<id>` 打开基础视图、标签交互、标签显示、鹰眼图配置、样式与规则、悬停与提示、通道与数据、编辑与保存、事件与方法、接口能力、批量复制与合并、拖拽落点、可视化连线、动态装饰器、批量新增、搜索与筛选、扩展能力、性能与质量、点聚合、动画效果、扩展图形与综合示例,画布与右栏读数都在、切页签会把 `?demo=` 写回地址栏、切走之后上一个实例被销毁(不留画布)、缺省页签是综合示例、页签总数与注册表一致、顶部六个分组按钮一次只展开一组且组名可见;鹰眼图配置页另验 `layers` 的自定义 `draw` 真被调用、`placement` 切到数据前 / 后时 `drawInfo.stage` 跟着变、框拖动触发 `onInteraction` 并带相机、尺寸切换走 `updateOptions` 重铺画布、DPR 2 下 `maxPixelRatio` 的 1× / 2× 备份尺寸、关掉扩展层与 `zoomMinimapBy` / `setMinimapZoom`;批量复制 / 合并、通道拐点 / 分组两页另有假接口断言(Shift 框选、整份请求体、标准响应包与资源级结果、版本号 / opId、200 / 网络失败 / 500 / 409、原 opId 重试与幂等重放不推进版本、409 rebase、本地回滚);假后端严格契约另有一组直接断言(请求头与 body 不一致返回 400、baseVersion 非有限数字返回 400、同 opId 换 body 返回 409 `IDEMPOTENCY_CONFLICT` 且版本不前进),通道页还验了自定义格式按钮注册 → 导出 → 注销;扩展能力页另验自定义运算符 / 图元 / `view.use` 插件 / `defineExportFormat` 的注册、切换、卸载与业务 DTO 结构,并验同名重新注册图元当场换画笔;可视化连线页另验初始 `channels` 为空、正交折线拐点、预览箭头、取消不写数据,以及默认提交 / `addLines` / `addChannels` 三种落地;动态装饰器页另验吸附方式只有「跟随 / 固定」、两档下位置都有「相对节点 / 固定位置 / 鼠标位置」三档,固定 + 相对节点只改 `anchor`,固定 / 跟随 + 鼠标位置分别映射为 `placement: pointer` + 对应 `anchor` 且不带 `at`,跟随 + 固定位置仍传 `at`;通道与数据页另验「线路分组」弹框:分组卡片与「场站对标标识」都在,行内「确认移入」只改本地草稿、组件数据不动,取消 / Esc / 遮罩关闭不写数据,「确定分组」只调用一次 `splitChannelGroups` 把整份 groups 一次落地。点聚合页另验配置控件、聚合点击只增聚合计数、`maxZoom` 展开与适应全部后恢复、半径 / 重建 / 关闭统计;接口能力页另验三种级联触发独立开关、通道 / 分组 / 拐点程序化选中、通道 `focusOn`、`lineFollowsNode` 联动,以及样式字段合并、节点 / 线路装饰 CRUD、通道线宽与线路对账、GeoJSON 背景投影、历史 / 复制 / 删除收尾、`once`、WebGL 层清理、注册表注销与初始化 options 读数;页面底部流式打印 PASS / FAIL。这一轮另补 3 条鹰眼图线路层断言:首帧只铺固定范围里的线路、同一份数据补画命中缓存、小图缩放只重铺一次且仍裁剪范围外线路。
120
+ - `selftest-effects.html`:线路 / 节点动画与贴图预热的专项自测(96 条,页面自带「帧泵」—— 无头环境里 `requestAnimationFrame` 几乎不推进,它把 rAF 收进 Map、手动推帧,动画类断言才测得动):纯函数组(归一化、三层「关」的语义、按弧长采样、拐点连续、相位打散)、引擎组(铺批次 / 关掉一层 / 恢复 / 逐要素覆盖 / `getLineEffects` 全程贴线)、像素组(画布上真的有一个红点在两帧之间挪了位置)、**涟漪跟手组**(开着动画拖节点:拖动前圆心在节点坐标上、拖动中跟着走且是就地改实例缓冲、松手后整份重建也对齐、再拖回原位跟着回来,另有一条像素级断言量绿圈质心落在新坐标上)、**预热组**(全排列条数、第二次命中缓存、`getSymbolCombos()` 的去重与线路侧那一条、照着它配的一份**零张新画布**、反向一条「没画过的组合会当场烤、画布 +1」)、提醒组(`effects.line` 里写 `show` 被点名);这一轮另补 4 条动点缓存断言(5000 条普通线 + 2 条动点线:每帧只遍历动画线路,路径换短 / 换长后总长跟着更新)。
121
121
  - `selftest-data.html`:标准数据文档与渲染后端开关的自测(103 条)—— `normalizeDocument` 的标准写法与 JSON 安全(无效日期写 `null` 并告警、类实例不静默变成空对象)、整份文档直接当 `createGisView` 入参、`exportData({ format: 'full' })` 导出 → 重建 → 再导出逐字节相同、`importData` 的整份与分列、`renderer` 三档与能力探测、网格顶点缓存窗口,以及自定义导出格式的注册 / 上下文 DTO 映射 / 深拷贝 / 视口导出 / 注销后失效;断言全在接口层,不读像素。
122
122
  - `check-sample-fields.mjs`:不是页面、是对账脚本(`node docs/steps/check-sample-fields.mjs`)—— 核对 `sample-document.json` 自己就是标准写法(`normalizeDocument` 后逐字节相同、0 条提醒)、`docs/sample-document字段详解.md` 里的读数与 `documentCounts()` 一致、以及它的「字段索引」与从文件里摘出来的字段路径集合一个不多一个不少(当前 154 个);改完样本数据跑 `--write` 重写那两段机器块(幂等)。
123
- - `selftest-docs.html`:三个文档页 + 调试页的自测(130 条)—— 文档中心(`docs/index.html`)的清单 92 份 md(10 个根 + 82 份子文档,含新的 `接口协议.md`;树形侧栏按 `parent` / `groupLabel` 分成 6 组 `details`)、首屏 README 渲染、markdown 渲染器(表格 / 带语言代码块 / 两级列表 / 引用 / 行内代码 / md 链接 / 目录 / 首条摘要卡片 / 首段导语)、元信息里的阅读时长、索引页的子文档链接、换一份子文档、代码块复制按钮、跨文档搜索与深链、跨文档锚点链接(`文件.md#小节` 原地切文档并闪一下)、能直接跑的围栏才有的「在调试页跑」与元信息行的「这一份的调试页(N 段示例)」、390px 窄屏首屏 / 深链 / 横向溢出回归、阅读百分比与「回到顶部」的联动;调试演示页(`docs/playground/index.html`)的一个示例下拉(内置示例 + 「这一份文档」两组 optgroup)、`?doc=` 读文档抽示例、片段标注与来源子栏、顶层 await 运行、`channels` 快照、光标行列、草稿自动保存与恢复、`Ctrl+S` 运行、复制 / 清空输出、状态胶囊;API 速查页(`docs/api/index.html`)的 `__gisApi` 钩子、六张统计卡、249 张成员卡都带签名网格与示例、92 个 options 四列网格、搜索过滤与命中读数、`select(名字)` 高亮、类型开关能单独收起属性、左侧当前分组高亮、深浅主题切换与阅读设置共用;图解页(`docs/guide/sample-document.html`)的 47 条数据列表、点线路 / 分组 / 标签状态的图上标注、`点击时居中` 与 `显示编号` 开关、清空标注;三个页面都跑在 iframe 里,要用 dev server 打开。
123
+ - `selftest-docs.html`:三个文档页 + 调试页的自测(130 条)—— 文档中心(`docs/index.html`)的清单 92 份 md(10 个根 + 82 份子文档,含新的 `接口协议.md`;树形侧栏按 `parent` / `groupLabel` 分成 6 组 `details`)、首屏 README 渲染、markdown 渲染器(表格 / 带语言代码块 / 两级列表 / 引用 / 行内代码 / md 链接 / 目录 / 首条摘要卡片 / 首段导语)、元信息里的阅读时长、索引页的子文档链接、换一份子文档、代码块复制按钮、跨文档搜索与深链、跨文档锚点链接(`文件.md#小节` 原地切文档并闪一下)、能直接跑的围栏才有的「在调试页跑」与元信息行的「这一份的调试页(N 段示例)」、390px 窄屏首屏 / 深链 / 横向溢出回归、阅读百分比与「回到顶部」的联动;调试演示页(`docs/playground/index.html`)的一个示例下拉(内置示例 + 「这一份文档」两组 optgroup)、`?doc=` 读文档抽示例、片段标注与来源子栏、顶层 await 运行、`channels` 快照、光标行列、草稿自动保存与恢复、`Ctrl+S` 运行、复制 / 清空输出、状态胶囊;API 速查页(`docs/api/index.html`)的 `__gisApi` 钩子、六张统计卡、253 张成员卡都带签名网格与示例、95 个 options 四列网格、搜索过滤与命中读数、`select(名字)` 高亮、类型开关能单独收起属性、左侧当前分组高亮、深浅主题切换与阅读设置共用;图解页(`docs/guide/sample-document.html`)的 47 条数据列表、点线路 / 分组 / 标签状态的图上标注、`点击时居中` 与 `显示编号` 开关、清空标注;三个页面都跑在 iframe 里,要用 dev server 打开。
124
124
 
125
- 本轮补上的两组断言:`selftest-features.html` 新增 8 条 —— 3 条验 `channelSelectMode` 在 Ctrl + 点击加点后的高亮范围(`line` / `group` / `channel`),5 条验分组 `channelWaypointDragMode: 'vertex'`(`{ x, y }` 偏移归一化并参与实际路由、同组同序号拐点按世界增量平移、相邻拐点与其它分组不动、只写 `waypointOffsets` 不改公共 `path`、`parallel_groups.group_route` 带刚性偏移)。`selftest-demos.html` 新增 1 条,验通道演示页可切到 `vertex`,且「准备两分组拐点」会拆成两组、每组两个拐点。
125
+ 上一轮补上的两组断言:`selftest-features.html` 新增 8 条 —— 3 条验 `channelSelectMode` 在 Ctrl + 点击加点后的高亮范围(`line` / `group` / `channel`),5 条验分组 `channelWaypointDragMode: 'vertex'`(`{ x, y }` 偏移归一化并参与实际路由、同组同序号拐点按世界增量平移、相邻拐点与其它分组不动、只写 `waypointOffsets` 不改公共 `path`、`parallel_groups.group_route` 带刚性偏移)。`selftest-demos.html` 新增 1 条,验通道演示页可切到 `vertex`,且「准备两分组拐点」会拆成两组、每组两个拐点。
126
+
127
+ 这一轮补上的三组断言:`selftest-effects.html` 新增 4 条(5000 条普通线 + 2 条动点线,验证每帧只遍历动画线路、路径变化后总长与采样跟着更新);`selftest-perf.html` 新增 7 条(热力图 `styleKey` / `weightKey` 语义、纯透明度更新不重烤、换色带 / 换要素值会重烤);`selftest-demos.html` 新增 3 条(鹰眼图线路层首帧裁剪、补画缓存命中、小图缩放只重铺一次)。
128
+
129
+ 这一轮补上的断言:`selftest-features.html` 新增 4 条,覆盖 `setFilter` 隐藏一端 / 两端锚点时通道回线宽度保持不变、清除筛选后恢复原值;`selftest-demos.html` 新增 5 条,覆盖综合示例里的级联高亮开关、范围切换、关闭与读数,以及「合并样式」说明与 `merge.createItem` 改写入口。
130
+
131
+ 本轮补上的断言:`selftest-demos.html` 321 → **333** 条,新增「接口能力」页的 12 条 —— 三种级联触发默认值与单独关闭、通道 / 分组 / 拐点 `selectFeatures`、`selectWaypoint` 补点并选中线路、`lineFollowsNode` 隐藏端点对照、通道 `focusOn`,以及样式字段合并、装饰 CRUD、通道线宽 / 线路对账、GeoJSON 背景投影、历史 / 复制 / 删除收尾、`once` / WebGL / 注册表 / 初始化 options 读数。`selftest-scale.html` 保持 109 PASS + 6 DIAG:`fitSpacing` 三条断言改为验证「比例提高但中心保持普通适应中心」,无筛选的 `visible: false` 联动对照组显式写 `lineFollowsNode: false`。
126
132
 
127
133
  这些页面都要用 dev server 打开(第 10 ~ 13 步与 demo 自测要加载 `src/gis-view` 的 CSS;`selftest-demo.html` 会把 7 MB 的 `new.json` 与样例数据都加载一遍),页面底部会打印 PASS / FAIL。
128
134
 
@@ -74,8 +74,7 @@
74
74
  hover: true,
75
75
  select: true,
76
76
  contextMenu: true,
77
- keyboard: true,
78
- channelMenu: true
77
+ keyboard: true
79
78
  },
80
79
  label: { enabled: true, content: '{name}', editable: true, draggable: true, maxCount: 8 },
81
80
  lineLabel: { enabled: true, content: '{name}', editable: true, draggable: true, maxCount: 8 },
@@ -84,7 +83,6 @@
84
83
  onClick: payload => push('click', payload),
85
84
  onDblClick: payload => push('dblclick', payload),
86
85
  onContextMenu: payload => push('contextmenu', payload),
87
- onChannelContextMenu: payload => push('channelmenu', payload),
88
86
  onSelectChange: payload => push('selectChange', payload),
89
87
  onSelect: payload => push('select', payload),
90
88
  onWaypointChange: payload => push('waypoint', payload),
@@ -218,8 +216,8 @@
218
216
  fire(canvas, 'contextmenu', channelAt, { button: 2, buttons: 2 })
219
217
  }
220
218
  await settle(40)
221
- const menuChannel = last('channelmenu')
222
- check('通道回线右键:onChannelContextMenu 的 action.type / target 是 contextMenu / channel', Boolean(channelAt) && menuChannel?.action?.type === 'contextMenu' && menuChannel?.action?.target === 'channel', JSON.stringify(menuChannel?.action))
219
+ const menuChannel = last('contextmenu')
220
+ check('通道回线右键:同一份 onContextMenu 载荷的 action.type / target 是 contextMenu / channel', Boolean(channelAt) && menuChannel?.action?.type === 'contextMenu' && menuChannel?.action?.target === 'channel' && menuChannel?.channelContext?.channelId === 'c1', JSON.stringify({ action: menuChannel?.action, channel: menuChannel?.channelContext?.channelId }))
223
221
  check('通道回线右键:action.data 是通道定义(通道回线按通道处理)', menuChannel?.action?.data === menuChannel?.channel && menuChannel?.action?.data?.id === 'c1' && Array.isArray(menuChannel?.action?.data?.lineIds), String(menuChannel?.action?.data?.id))
224
222
  check('通道回线右键:action.button 与通用右键一个口径(right)', menuChannel?.action?.button === 'right', String(menuChannel?.action?.button))
225
223
 
@@ -573,11 +573,11 @@
573
573
  const shapeRefreshed = await until(() => String(demoState()?.dataShapeReadout ?? '').startsWith('数据格式:'), 4000)
574
574
  check('数据格式读数也在这一页上给出(点 / 线 / 通道)', Boolean(shapeRefreshed), String(demoState()?.dataShapeReadout ?? ''))
575
575
  }
576
- // 这一轮把面板收成「一排按钮 + 弹层」:点按钮 / 右键按钮都能弹,Esc、点别处、点画布都能收;
577
- // 点画布那一下不会被吞掉(画布照样收到事件)。面板内容常驻 DOM(v-show),按文本查询照旧可用
576
+ // 这一轮把面板收成「右栏两列按钮 + 下方内联面板」:点按钮 / 右键按钮都能展开,再点按钮或按 Esc 收起;
577
+ // 面板不再 Teleport 到 body,点画布也不会把面板收掉(画布事件同样不会被吞)。内容常驻 DOM(v-show),按文本查询照旧可用
578
578
  const panelButtons = () => [...document.querySelectorAll('.demo-panel-buttons button')]
579
579
  const popoverVisible = () => {
580
- const node = document.querySelector('.demo-popover')
580
+ const node = document.querySelector('.demo-panel-inline')
581
581
  return Boolean(node) && getComputedStyle(node).display !== 'none' && node.getBoundingClientRect().height > 0
582
582
  }
583
583
  const openTab = (label) => {
@@ -587,7 +587,7 @@
587
587
  }
588
588
  return tab
589
589
  }
590
- check('面板不再是平铺的 details,弹层默认收着', document.querySelectorAll('details.demo-panel').length === 0 && !popoverVisible(), `${document.querySelectorAll('.demo-panel-body').length} 块面板内容 · ${panelButtons().length} 个按钮`)
590
+ check('面板不再是平铺的 details,内联面板默认收着', document.querySelectorAll('details.demo-panel').length === 0 && !popoverVisible(), `${document.querySelectorAll('.demo-panel-body').length} 块面板内容 · ${panelButtons().length} 个按钮`)
591
591
  if (openTab('视图 · 交互')) {
592
592
  await wait(150)
593
593
  const viewLabels = panelButtons().map(item => item.textContent.trim())
@@ -602,7 +602,13 @@
602
602
  if (probeButton) {
603
603
  probeButton.click()
604
604
  await wait(150)
605
- check('点一下按钮弹出面板', popoverVisible(), probeButton.textContent.trim())
605
+ const inlinePanel = document.querySelector('.demo-panel-inline')
606
+ const buttonGroup = document.querySelector('.demo-panel-buttons')
607
+ const panelStack = document.querySelector('.demo-panel-stack')
608
+ const panelBox = inlinePanel?.getBoundingClientRect()
609
+ const buttonBox = buttonGroup?.getBoundingClientRect()
610
+ const stackBox = panelStack?.getBoundingClientRect()
611
+ check('点一下按钮:面板直接在按钮组下方展开,宽度跟右栏一致,位置在文档流里', popoverVisible() && Boolean(panelBox) && Boolean(buttonBox) && Boolean(stackBox) && panelBox.top >= buttonBox.bottom - 1 && Math.abs(panelBox.width - stackBox.width) <= 2 && getComputedStyle(inlinePanel).position !== 'fixed' && Boolean(inlinePanel.closest('.demo-panel-stack')), `${panelBox?.top ?? 0}px / 按钮底 ${buttonBox?.bottom ?? 0}px · 宽 ${panelBox?.width ?? 0} / ${stackBox?.width ?? 0} · ${getComputedStyle(inlinePanel).position}`)
606
612
  probeButton.click()
607
613
  await wait(150)
608
614
  check('再点同一个按钮把面板收回去', !popoverVisible())
@@ -612,8 +618,8 @@
612
618
  check('右键按钮同样能弹出面板', popoverVisible(), probeButton.textContent.trim())
613
619
  window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
614
620
  await wait(150)
615
- check('Esc 收掉弹层', !popoverVisible())
616
- // 再弹一次,然后点画布:面板收掉,同时那一下画布照样收到(关面板不吞事件)
621
+ check('Esc 收掉内联面板', !popoverVisible())
622
+ // 再展开一次,然后点画布:面板保持展开,同时那一下画布照样收到
617
623
  probeButton.click()
618
624
  await wait(150)
619
625
  const canvasNode = document.querySelector('.gis-view__canvas')
@@ -628,7 +634,10 @@
628
634
  canvasNode.removeEventListener('pointerdown', markCanvas)
629
635
  }
630
636
  await wait(150)
631
- check('点画布:面板收掉,那一下画布照样收到事件(不吞事件)', !popoverVisible() && canvasGotPointer, canvasGotPointer ? '画布收到了 pointerdown' : '画布没收到 pointerdown')
637
+ check('点画布:面板保持展开,那一下画布照样收到事件(不再互斥)', popoverVisible() && canvasGotPointer, canvasGotPointer ? '画布收到了 pointerdown' : '画布没收到 pointerdown')
638
+ openTab('样式 · 标签')
639
+ await wait(150)
640
+ check('切页签自动收起旧面板,不会把上一页的内容留在下面', !popoverVisible())
632
641
  }
633
642
  // 右键空白处弹的是「画布菜单」:一行一个公开方法,点一行真的执行并收起
634
643
  const menuCanvas = document.querySelector('.gis-view__canvas')
@@ -648,17 +657,17 @@
648
657
  }
649
658
  }
650
659
  }
651
- // 这一轮新加的三件事:面板上的缩放限制真的夹相机、右键框与面板弹层互斥(Esc / 点空白一起收)、
660
+ // 这一轮新加的三件事:面板上的缩放限制真的夹相机、右键菜单与内联面板可同时存在(Esc 一起收)、
652
661
  // 「线路实时跟随」的读数说清两档差别(liveDrag 开关本身要下发到组件)
653
662
  const overlayDemo = demoState()
654
663
  const overlayView = overlayDemo?.viewRef
655
- // 面板弹层 Teleport 到 body、position: fixed —— offsetParent 恒为 null,可见性只看 display 与高度
664
+ // 内联面板就在右栏文档流里,可见性仍按 display 与高度判断(v-show 收起时不占位)
656
665
  const popoverShown = () => {
657
- const node = document.querySelector('.demo-popover')
666
+ const node = document.querySelector('.demo-panel-inline')
658
667
  return Boolean(node) && getComputedStyle(node).display !== 'none' && node.getBoundingClientRect().height > 0
659
668
  }
660
669
  const menuShown = () => [...document.querySelectorAll('.demo-channel-menu')].some(item => item.offsetParent !== null && item.getBoundingClientRect().height > 0)
661
- const popoverButton = (text) => [...document.querySelectorAll('.demo-popover button')].find(item => item.textContent.includes(text))
670
+ const popoverButton = (text) => [...document.querySelectorAll('.demo-panel-inline button')].find(item => item.textContent.includes(text))
662
671
  if (menuCanvas) {
663
672
  openTab('视图 · 交互')
664
673
  await wait(200)
@@ -668,10 +677,10 @@
668
677
  const menuBox = menuCanvas.getBoundingClientRect()
669
678
  menuCanvas.dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, cancelable: true, button: 2, clientX: menuBox.left + menuBox.width / 2, clientY: menuBox.top + 10 }))
670
679
  await wait(200)
671
- check('右键弹菜单时面板弹层自动收掉(同一时刻只留一份)', popoverBeforeMenu && !popoverShown() && menuShown(), `弹出前 ${popoverBeforeMenu} · 弹出后 ${popoverShown()} / 菜单 ${menuShown()}`)
680
+ check('右键弹菜单时右侧面板保持展开(调参不用先把面板收掉)', popoverBeforeMenu && popoverShown() && menuShown(), `弹出前 ${popoverBeforeMenu} · 弹出后 ${popoverShown()} / 菜单 ${menuShown()}`)
672
681
  window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
673
682
  await wait(200)
674
- check('Esc 收掉右键菜单(与收面板同一条口径)', !menuShown())
683
+ check('Esc 同时收掉右键菜单与右侧面板(同一条口径)', !menuShown() && !popoverShown())
675
684
  menuCanvas.dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, cancelable: true, button: 2, clientX: menuBox.left + menuBox.width / 2, clientY: menuBox.top + 10 }))
676
685
  await wait(200)
677
686
  const menuReopened = menuShown()
@@ -685,8 +694,8 @@
685
694
  await wait(200)
686
695
  panelButtons().find(item => item.textContent.includes('初始视图'))?.click()
687
696
  await wait(200)
688
- const minBox = [...document.querySelectorAll('.demo-popover .demo-slider')].find(item => item.textContent.includes('最小比例'))?.querySelector('input')
689
- const maxBox = [...document.querySelectorAll('.demo-popover .demo-slider')].find(item => item.textContent.includes('最大比例'))?.querySelector('input')
697
+ const minBox = [...document.querySelectorAll('.demo-panel-inline .demo-slider')].find(item => item.textContent.includes('最小比例'))?.querySelector('input')
698
+ const maxBox = [...document.querySelectorAll('.demo-panel-inline .demo-slider')].find(item => item.textContent.includes('最大比例'))?.querySelector('input')
690
699
  check('面板上有「最小比例 / 最大比例」两个输入框,初值就是组件缺省的两条线', Boolean(minBox) && Boolean(maxBox) && Number(minBox.value) === 0.002 && Number(maxBox.value) === 20000, `${minBox?.value ?? '没找到'} / ${maxBox?.value ?? '没找到'}`)
691
700
  if (overlayView && minBox && maxBox) {
692
701
  const atLimit = (scale, limit) => Math.abs(scale - limit) < limit * 1e-6
@@ -707,7 +716,7 @@
707
716
  await wait(200)
708
717
  check('「解除限制」回到组件缺省的两条线', Number(minBox.value) === 0.002 && Number(maxBox.value) === 20000, `${minBox.value} ~ ${maxBox.value}`)
709
718
  }
710
- const liveRow = [...document.querySelectorAll('.demo-popover label')].find(item => item.textContent.includes('线路实时跟随'))
719
+ const liveRow = [...document.querySelectorAll('.demo-panel-inline label')].find(item => item.textContent.includes('线路实时跟随'))
711
720
  const liveBox = liveRow?.querySelector('input')
712
721
  check('交互面板上有「线路实时跟随」开关与两档读数', Boolean(liveBox) && /实时/.test(String(overlayDemo?.liveDragHint ?? '')), String(overlayDemo?.liveDragHint ?? '没找到'))
713
722
  if (liveBox && overlayView) {
@@ -721,7 +730,7 @@
721
730
  }
722
731
  // 宿主缩放(父元素 CSS zoom):滑杆改的是 .demo-stage 的 --demo-host-zoom,组件按布局像素重新量出 displayScale;
723
732
  // 读数分两处:面板上的 hostScaleText(onViewport 节流回写)与 viewRef.getViewport().displayScale
724
- const hostRow = [...document.querySelectorAll('.demo-popover label')].find(item => item.textContent.includes('宿主缩放'))
733
+ const hostRow = [...document.querySelectorAll('.demo-panel-inline label')].find(item => item.textContent.includes('宿主缩放'))
725
734
  const hostBox = hostRow?.querySelector('input')
726
735
  check('交互面板上有「宿主缩放 · 父元素 zoom」滑杆', Boolean(hostBox) && hostBox.type === 'range', hostBox ? `${hostBox.min} ~ ${hostBox.max},当前 ${hostBox.value}` : '没找到')
727
736
  if (hostBox && overlayView) {