gis-view-webgl2 0.1.0 → 0.1.2

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 (87) hide show
  1. package/README.md +40 -8
  2. package/dist/gis-view.css +1 -1
  3. package/dist/index.cjs +1 -1
  4. package/dist/index.d.ts +1036 -249
  5. package/dist/index.js +1 -1
  6. package/docs/AI/345/257/271/350/257/235/346/261/207/346/200/273.md +194 -0
  7. package/docs/GisView.md +9 -8
  8. package/docs/GisView/345/256/236/347/216/260/345/216/237/347/220/206.md +2 -2
  9. 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 +172 -0
  10. package/docs/api/index.html +545 -59
  11. package/docs/demos/BackendSyncDemo.vue +10 -8
  12. package/docs/demos/BasicDemo.vue +101 -101
  13. package/docs/demos/BatchAddDemo.vue +5 -5
  14. package/docs/demos/ChannelDemo.vue +617 -10
  15. package/docs/demos/ConnectDemo.vue +392 -0
  16. package/docs/demos/CopyMergeDemo.vue +980 -929
  17. package/docs/demos/DecorationDraftDemo.vue +1300 -0
  18. package/docs/demos/DemoShell.vue +569 -240
  19. package/docs/demos/DragDropDemo.vue +11 -11
  20. package/docs/demos/EffectsDemo.vue +6 -6
  21. package/docs/demos/EventDemo.vue +327 -327
  22. package/docs/demos/ExtensionDemo.vue +458 -0
  23. package/docs/demos/GisViewDemo.vue +61 -48
  24. package/docs/demos/GraphicDemo.vue +338 -0
  25. package/docs/demos/LabelDemo.vue +553 -497
  26. package/docs/demos/MinimapDemo.vue +358 -0
  27. package/docs/demos/PerfDemo.vue +29 -0
  28. package/docs/demos/SearchFilterDemo.vue +412 -0
  29. package/docs/demos/StyleDemo.vue +121 -121
  30. package/docs/demos/api-index.js +98 -77
  31. package/docs/demos/demos.js +159 -135
  32. package/docs/demos/shared/demoData.js +107 -107
  33. package/docs/demos/shared/demoStyle.css +249 -249
  34. package/docs/demos/shared/fakeBackend.js +93 -6
  35. package/docs/format/03-/346/213/220/347/202/271/350/243/205/351/245/260/344/270/216/346/240/267/345/274/217.md +28 -8
  36. package/docs/format/04-/351/200/232/351/201/223/344/270/216/346/240/207/347/255/276.md +5 -4
  37. package/docs/format/06-/345/257/274/345/205/245/345/257/274/345/207/272/344/270/216/346/240/207/345/207/206/346/226/207/346/241/243.md +8 -6
  38. 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 +6 -6
  39. package/docs/guide/sample-document.html +4 -4
  40. package/docs/index.html +180 -9
  41. package/docs/internals/11-/350/246/206/347/233/226/345/261/202/346/240/207/347/255/276/344/270/216/346/217/220/347/244/272.md +1 -1
  42. package/docs/internals/13-/351/271/260/347/234/274/345/233/276.md +3 -2
  43. package/docs/internals/15-03-/351/200/220/350/275/256/350/256/260/345/275/225-/346/200/247/350/203/275/344/270/216/344/277/256/345/244/215-/344/270/213.md +16 -0
  44. 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 -0
  45. package/docs/internals/17-/350/260/203/350/257/225/344/270/216/346/211/251/345/261/225/345/273/272/350/256/256.md +1 -1
  46. package/docs/manual/02-/345/235/220/346/240/207/344/270/216/346/225/260/346/215/256.md +18 -4
  47. package/docs/manual/03-/351/205/215/347/275/256/351/241/271.md +110 -25
  48. package/docs/manual/04-/344/272/213/344/273/266/344/270/216/345/233/236/350/260/203.md +13 -8
  49. package/docs/manual/06-/346/225/260/346/215/256/344/270/216/345/234/272/346/231/257/346/233/264/346/226/260.md +111 -10
  50. 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 +47 -9
  51. package/docs/manual/08-/351/200/211/344/270/255/344/270/216/346/213/220/347/202/271.md +17 -15
  52. package/docs/manual/09-/351/200/232/351/201/223/344/270/216/345/244/232/345/233/236/347/272/277.md +61 -11
  53. 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 +74 -7
  54. package/docs/manual/11-/346/240/267/345/274/217/344/270/216/350/247/204/345/210/231.md +3 -2
  55. package/docs/manual/12-/346/240/207/347/255/276/344/270/216/350/243/205/351/245/260.md +260 -3
  56. package/docs/manual/13-/345/212/250/346/225/210/344/270/216/345/233/276/345/205/203.md +6 -5
  57. package/docs/manual/14-/350/247/206/345/233/276/344/270/216/347/255/233/351/200/211.md +27 -14
  58. package/docs/manual/15-/345/257/274/345/207/272/344/270/216/351/233/206/346/210/220.md +142 -49
  59. package/docs/manual/16-/345/256/236/344/276/213/346/226/271/346/263/225/346/200/273/350/241/250.md +31 -29
  60. package/docs/manual/17-/345/217/202/346/225/260/351/200/237/346/237/245.md +55 -39
  61. package/docs/manual/18-/351/200/220/346/226/271/346/263/225/347/244/272/344/276/213.md +189 -64
  62. 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 -4
  63. package/docs/manual/20-/346/200/247/350/203/275/344/270/216/345/244/247/346/225/260/346/215/256.md +42 -2
  64. 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 +22 -6
  65. package/docs/playground/index.html +5 -5
  66. package/docs/steps/README.md +14 -10
  67. package/docs/steps/check-doc-truth.mjs +1 -1
  68. package/docs/steps/selftest-actions.html +10 -10
  69. package/docs/steps/selftest-data.html +96 -14
  70. package/docs/steps/selftest-decoration.html +577 -0
  71. package/docs/steps/selftest-demo.html +6 -5
  72. package/docs/steps/selftest-demos.html +2201 -1612
  73. package/docs/steps/selftest-docs.html +89 -16
  74. package/docs/steps/selftest-effects.html +5 -5
  75. package/docs/steps/selftest-features.html +757 -86
  76. package/docs/steps/selftest-full.html +583 -583
  77. package/docs/steps/selftest-labels.html +1034 -652
  78. package/docs/steps/selftest-perf.html +1899 -1844
  79. package/docs/steps/selftest-scale.html +117 -7
  80. package/docs/steps/selftest-select.html +24 -17
  81. package/docs/steps/step-10-full.js +1 -1
  82. package/docs//346/216/245/345/217/243/345/215/217/350/256/256.md +167 -17
  83. package/docs//346/225/260/346/215/256/346/240/274/345/274/217.md +1 -1
  84. package/package.json +43 -43
  85. package/docs/demos/HistoryDemo.vue +0 -230
  86. package/docs/demos/InterfaceDemo.vue +0 -309
  87. package/docs/demos/SampleDemo.vue +0 -283
@@ -717,7 +717,7 @@ const handleQualityChange = (state) => {
717
717
  }
718
718
  /**
719
719
  * 动画效果(effects):线路上的跑动图标(ECharts 的 lines.effect 那一类)与节点上的扩散涟漪(effectScatter 那一类)。
720
- * 两种写法:全局缺省(setEffects / options.effects)与逐要素(数据里的 points[].effect / lines[].effect)。
720
+ * 两种写法:全局缺省(updateOptions({ effects }) / options.effects)与逐要素(数据里的 points[].effect / lines[].effect)。
721
721
  * 三种「关」是四件事:全局一个都不配(只有逐条配的动)/ effects: false 一处全关(连逐条配的也关掉)/
722
722
  * effects.line: false 只关全局那一份 / 逐条 effect: false 只关这一条。
723
723
  * 读数抄的是 getEffects()(归一化之后的配置,enabled 是「这次要不要动」的运行时口径)与 getStats()
@@ -790,14 +790,14 @@ const applyEffects = () => {
790
790
  if (!view) {
791
791
  return
792
792
  }
793
- view.setEffects({
793
+ view.updateOptions({ effects: {
794
794
  line: effectLineOn.value ? buildEffectLine() : null,
795
795
  point: effectPointOn.value ? buildEffectPoint() : null
796
- })
796
+ } })
797
797
  syncEffectsReadout()
798
798
  // 这一帧的读数要等引擎画完才准(动点 / 涟漪数是上一帧统计出来的):下一帧再抄一遍
799
799
  requestAnimationFrame(() => syncEffectsReadout())
800
- lastEvent.value = `setEffects:线路 ${effectLineOn.value ? '开' : '关'} / 节点 ${effectPointOn.value ? '开' : '关'} —— 与 options.effects 是同一份配置,区别只是这份在运行时换`
800
+ lastEvent.value = `updateOptions({ effects }):线路 ${effectLineOn.value ? '开' : '关'} / 节点 ${effectPointOn.value ? '开' : '关'} —— 与 options.effects 是同一份配置,区别只是这份在运行时换`
801
801
  }
802
802
  /** 逐条覆盖:前几条要素把 effect 写进数据(数据副本带 effect,导出不受影响),其余照旧走全局那一份 */
803
803
  const PER_ITEM_EFFECT_COUNT = 3
@@ -1135,7 +1135,7 @@ const PANEL_ITEMS = [
1135
1135
  { id: 'view-export', tab: 'view', label: '导出图片与选中样式', hint: 'exportImage / exportSelection 与状态样式' },
1136
1136
  { id: 'view-events', tab: 'view', label: '事件 · 选中', hint: 'onClick / onSelectChange 与 getSelectionData' },
1137
1137
  { id: 'style-heat', tab: 'style', label: '热力图', hint: '色带 / 密度场 / 光晕与作用对象' },
1138
- { id: 'style-effect', tab: 'style', label: '动画效果与预热', hint: '全局 setEffects / 逐条 data.effect 与 prebake 预热' },
1138
+ { id: 'style-effect', tab: 'style', label: '动画效果与预热', hint: '全局 updateOptions({ effects }) / 逐条 data.effect 与 prebake 预热' },
1139
1139
  { id: 'style-line', tab: 'style', label: '线型、标记与尺寸', hint: 'lineStyle、虚线、拐点与缩放联动' },
1140
1140
  { id: 'style-rules', tab: 'style', label: '样式、符号与规则', hint: 'pointStyle / pointRules 与符号表' },
1141
1141
  { id: 'style-label', tab: 'style', label: '标签与提示', hint: '节点 / 线路标签与悬停提示' },
@@ -1365,7 +1365,7 @@ const runApiEval = () => {
1365
1365
  const pluginReadout = ref('当前没有运行时插件(options.plugins 里的 focusSelectionPlugin 照常挂着)')
1366
1366
  let pluginDispose = null
1367
1367
  /**
1368
- * 运行时挂一个插件:写法与 options.plugins 里的一项完全一致(`view => hook`)——
1368
+ * 运行时挂一个插件:写法与 options.plugins 里的一项完全一致(`(view, context) => hook`)——
1369
1369
  * use() 立即执行、返回卸载函数。这个示例插件自己订阅 selectChange 记账,
1370
1370
  * 解绑写在它自己的 destroy 里:面板的「卸载」与 view.destroy() 走的都是那一个 destroy。
1371
1371
  */
@@ -1439,8 +1439,8 @@ const canvasMenuRows = computed(() => {
1439
1439
  { id: 'fit', label: '适应视图(fitView)', enabled: true, reason: '' },
1440
1440
  { id: 'inspect', label: '读一下选中集(getSelectionData)', enabled: count > 0, reason: '选中集是空的' },
1441
1441
  { id: 'exportImage', label: '导出选中的图(exportSelection)', enabled: count > 0, reason: '选中集是空的' },
1442
- { id: 'exportData', label: '导出选中的数据(exportSelectionData)', enabled: count > 0, reason: '选中集是空的' },
1443
- { id: 'clear', label: '清空选中(select(null) / deselect())', enabled: count > 0, reason: '本来就是空的' },
1442
+ { id: 'exportData', label: '导出选中的数据(exportData · scope: selection)', enabled: count > 0, reason: '选中集是空的' },
1443
+ { id: 'clear', label: '清空选中(selectFeatures(null) / deselect())', enabled: count > 0, reason: '本来就是空的' },
1444
1444
  { id: 'background', label: `切底图(当前:${backgroundModeLabel.value})`, enabled: true, reason: '' }
1445
1445
  ]
1446
1446
  })
@@ -1473,13 +1473,13 @@ const runCanvasMenuAction = (row) => {
1473
1473
  exportSelectionImage()
1474
1474
  }
1475
1475
  else if (row.id === 'exportData') {
1476
- exportSelectionData()
1476
+ exportSelectedRecords()
1477
1477
  }
1478
1478
  else if (row.id === 'clear') {
1479
- view.select(null)
1479
+ view.selectFeatures(null)
1480
1480
  syncSelectionFromView()
1481
1481
  syncVisibleCount()
1482
- lastEvent.value = '画布菜单:select(null) 清空选中'
1482
+ lastEvent.value = '画布菜单:selectFeatures(null) 清空选中'
1483
1483
  }
1484
1484
  else if (row.id === 'background') {
1485
1485
  const order = BACKGROUND_MODES.map(item => item.value)
@@ -1549,7 +1549,7 @@ const confirmStationDialog = () => {
1549
1549
  return
1550
1550
  }
1551
1551
  // 新场站直接选上:面板读数与后续编辑都跟着它(与「程序化新增线路」的新回线整批选中同一手感)
1552
- view.select(result.item.id)
1552
+ view.selectFeatures(result.item.id)
1553
1553
  syncSelectionFromView()
1554
1554
  syncVisibleCount()
1555
1555
  lastEvent.value = `画布菜单:空白处右键新增场站 → view.addPoint({ id: '${id}', name: '${name}', x: ${x}, y: ${y}, vn_kv: ${vn} }) · 落在 points[${result.index}],已选中`
@@ -1746,6 +1746,12 @@ const buildCopyOptions = () => ({
1746
1746
  /** 载荷里的节点名:name / label / id 依次回落(复制与合并的读数都用它) */
1747
1747
  const pointLabel = item => String(item?.name ?? item?.label ?? item?.id ?? '节点')
1748
1748
  const mergeEnabled = ref(true)
1749
+ /** 合并触发时机:碰撞(半径相碰)/ 中心点进入目标节点区域 */
1750
+ const mergeTrigger = ref('collision')
1751
+ const MERGE_TRIGGERS = [
1752
+ { value: 'collision', label: '碰撞(两个节点半径相碰)' },
1753
+ { value: 'center', label: '中心进入目标节点区域' }
1754
+ ]
1749
1755
  /** 碰撞容差(CSS 像素):两个节点画在屏幕上的半径之和再加它 */
1750
1756
  const mergeDistance = ref(0)
1751
1757
  /**
@@ -1791,6 +1797,7 @@ const confirmMergeByApi = payload => new Promise((resolve) => {
1791
1797
  const buildMergeOptions = () => {
1792
1798
  const options = {
1793
1799
  enabled: mergeEnabled.value,
1800
+ trigger: mergeTrigger.value,
1794
1801
  distance: mergeDistance.value,
1795
1802
  channelGroups: mergeChannelGroups.value
1796
1803
  }
@@ -2697,10 +2704,10 @@ const applyChannelNodeVisibility = () => {
2697
2704
  const anchors = activeChannelAnchorIds()
2698
2705
  const hidden = new Set(anchors)
2699
2706
  const hide = hideChannelNodes.value
2700
- view.updatePoints(view.getData().points.map(point => ({
2707
+ view.updateData({ points: view.getData().points.map(point => ({
2701
2708
  ...point,
2702
2709
  visible: hide ? !hidden.has(String(point.id)) : true
2703
- })))
2710
+ })) })
2704
2711
  syncChannelLabel()
2705
2712
  lastEvent.value = hide
2706
2713
  ? `通道两端的 ${anchors.length} 个节点已隐藏:两端必须有节点,${activeChannels().length} 条通道与回线都不再显示`
@@ -2782,7 +2789,7 @@ const handleContextMenu = (payload) => {
2782
2789
  // 为 false 就先把命中的这个选上再弹面板(右键落在哪、菜单就针对谁)
2783
2790
  if (payload.type === 'point') {
2784
2791
  if (!payload.wasSelected && payload.item?.id !== undefined && payload.item?.id !== null) {
2785
- viewRef.value?.select(payload.item.id)
2792
+ viewRef.value?.selectFeatures(payload.item.id)
2786
2793
  }
2787
2794
  // 选完再取一次选中集读数:面板上的「把选中的 N 个并拢」按的是点下去那一刻的实际选中集
2788
2795
  pointMenu.value = { ...payload, kept: payload.wasSelected === true, selection: viewRef.value?.getSelectionData() ?? payload.selection }
@@ -2886,10 +2893,10 @@ const handlePointMerge = (payload) => {
2886
2893
  target: `目标 ${target}`,
2887
2894
  detail: `并进目标之后:尺寸取双方最大、图标优先目标;撞在一起的通道${mergeChannelGroups.value === 'merge' ? '合成一个分组' : '各自保留分组'}`
2888
2895
  }
2889
- lastEvent.value = `节点相撞:${source} 拖到了 ${target} 上 · 组件只给载荷与 confirm() / cancel(),合不合并由 demo 自己写的这个弹层裁决`
2896
+ lastEvent.value = `节点合并:${source} 拖到了 ${target} 上(${payload.trigger === 'center' ? '中心进入目标区域' : '碰撞'})· 组件只给载荷与 confirm() / cancel(),合不合并由 demo 自己写的这个弹层裁决`
2890
2897
  return
2891
2898
  }
2892
- lastEvent.value = `节点相撞:${source} 拖到了 ${target} 上 · 裁决交给 options.merge.confirm(面板上切「直接合并 / 先调接口」)`
2899
+ lastEvent.value = `节点合并:${source} 拖到了 ${target} 上(${payload.trigger === 'center' ? '中心进入目标区域' : '碰撞'})· 裁决交给 options.merge.confirm(面板上切「直接合并 / 先调接口」)`
2893
2900
  return
2894
2901
  }
2895
2902
  if (payload.phase === 'confirm') {
@@ -2990,7 +2997,7 @@ const exportSelectionImage = async () => {
2990
2997
  lastEvent.value = `导出选中为图片(${imageKind}):${result.counts.points} 个节点${result.counts.lines ? ` + ${result.counts.lines} 条线路` : ''}已下载 · 出图时组件把别的要素临时藏起来、相机对到导出范围,出完自动还原`
2991
2998
  }
2992
2999
  /** 导出选中为数据:points / lines 是拷贝(改它不影响组件内部那一份),直接存成 JSON */
2993
- const exportSelectionData = async () => {
3000
+ const exportSelectedRecords = async () => {
2994
3001
  const view = viewRef.value
2995
3002
  if (!view) {
2996
3003
  return
@@ -3014,7 +3021,7 @@ const exportSelectionData = async () => {
3014
3021
  /* -------------------- 导出数据:节点 / 线路 / 通道 / 拐点分组分开取 -------------------- */
3015
3022
  /** 导出整份时的口径:四份分开(缺省)/ new.json / test.json —— 与面板上的下拉一一对应 */
3016
3023
  const exportDataFormat = ref('view')
3017
- /** 拐点要不要连没编辑过的分组一起列(exportParallelGroups 的 all) */
3024
+ /** 拐点要不要连没编辑过的分组一起列(exportData({ parts: ['waypoint'] }) 的 all) */
3018
3025
  const exportDataAll = ref(false)
3019
3026
  /**
3020
3027
  * 导出分项(多选):选什么就导什么,默认场站 + 通道。
@@ -3045,11 +3052,11 @@ const exportPartsOption = () => (exportParts.value.length ? [...exportParts.valu
3045
3052
  /** 分项导出:一次只取一份,节点 / 线路 / 通道 / 拐点各是各的口径 */
3046
3053
  const exportPart = ref('channels')
3047
3054
  const EXPORT_PARTS = [
3048
- { value: 'station', label: '节点 · station_data(new.json)', file: 'gis-station-data', run: (view, options) => view.exportStationData(options) },
3049
- { value: 'points', label: '节点 · point_data(test.json)', file: 'gis-point-data', run: (view, options) => view.exportPointData(options) },
3050
- { value: 'lines', label: '线路 · line_data(test.json)', file: 'gis-line-data', run: (view, options) => view.exportLineData(options) },
3051
- { value: 'channels', label: '通道 · channel_data(new.json)', file: 'gis-channel-data', run: (view, options) => view.exportChannelData(options) },
3052
- { value: 'groups', label: '拐点 · parallel_groups(test.json)', file: 'gis-parallel-groups', run: (view, options) => view.exportParallelGroups(options) }
3055
+ { value: 'station', label: '节点 · station_data(new.json)', file: 'gis-station-data', run: (view, options) => view.exportData({ ...options, format: 'new', parts: ['station'] }) },
3056
+ { value: 'points', label: '节点 · point_data(test.json)', file: 'gis-point-data', run: (view, options) => view.exportData({ ...options, format: 'test', parts: ['station'] }) },
3057
+ { value: 'lines', label: '线路 · line_data(test.json)', file: 'gis-line-data', run: (view, options) => view.exportData({ ...options, format: 'test', parts: ['line'] }) },
3058
+ { value: 'channels', label: '通道 · channel_data(new.json)', file: 'gis-channel-data', run: (view, options) => view.exportData({ ...options, format: 'new', parts: ['channel'] }) },
3059
+ { value: 'groups', label: '拐点 · parallel_groups(test.json)', file: 'gis-parallel-groups', run: (view, options) => view.exportData({ ...options, format: 'test', parts: ['waypoint'] }) }
3053
3060
  ]
3054
3061
  const exportDataReadout = ref('导出数据:—')
3055
3062
  const exportDataSample = ref('')
@@ -3086,7 +3093,7 @@ const exportDataToFile = () => {
3086
3093
  refreshExportSample()
3087
3094
  }
3088
3095
  /**
3089
- * 导出「选中的这一撮」的业务表数据(exportSelectionData):选中口径与「导出选中为数据」一样,
3096
+ * 导出「选中的这一撮」的业务表数据(exportData · scope: selection):选中口径与「导出选中为数据」一样,
3090
3097
  * 出的却是分项记录(场站 / 通道 / 线路 / 拐点),同样跟着上面的分项勾选走。
3091
3098
  */
3092
3099
  const exportSelectionPartsToFile = () => {
@@ -3094,7 +3101,7 @@ const exportSelectionPartsToFile = () => {
3094
3101
  if (!view) {
3095
3102
  return
3096
3103
  }
3097
- const result = view.exportSelectionData({ parts: exportPartsOption(), all: exportDataAll.value })
3104
+ const result = view.exportData({ scope: 'selection', parts: exportPartsOption(), all: exportDataAll.value })
3098
3105
  if (!result.ok) {
3099
3106
  exportReadout.value = `导出选中分项:${result.reason}`
3100
3107
  lastEvent.value = `导出选中分项没成功:${result.reason}`
@@ -3180,7 +3187,7 @@ const refreshDataShape = () => {
3180
3187
  const labelChannelId = (memberChannel ?? channels[0])?.channelId ?? null
3181
3188
  const labelChannelRow = labelChannelId === null || labelChannelId === undefined
3182
3189
  ? null
3183
- : (view.exportChannelData({ channels: [labelChannelId], labels: true }).channel_data?.[0] ?? null)
3190
+ : (view.exportData({ format: 'new', parts: ['channel'], channels: [labelChannelId], labels: true }).channel_data?.[0] ?? null)
3184
3191
  // 面板上只摆带标签的那几条回线(一条都没有就摆前几条),完整结构走「打印完整结构到控制台」
3185
3192
  const labelChannelLines = labelChannelRow
3186
3193
  ? (labelChannelRow.line_data.some(row => row.labels?.length)
@@ -3356,8 +3363,8 @@ const syncParallelGroups = () => {
3356
3363
  : '新增拐点集合:—'
3357
3364
  parallelGroupsJson.value = groups.length ? `${JSON.stringify(groups.slice(0, 2), null, 1)}${groups.length > 2 ? `\n… 共 ${groups.length} 组` : ''}` : ''
3358
3365
  }
3359
- /** 导出拐点集合:结构就是 test.json 的 parallel_groups,可以直接回写业务数据 */
3360
- const exportParallelGroups = async () => {
3366
+ /** 复制拐点集合:结构就是 test.json 的 parallel_groups,可以直接回写业务数据 */
3367
+ const copyParallelGroupRecords = async () => {
3361
3368
  if (!parallelGroups.value.length) {
3362
3369
  lastEvent.value = '还没有新增拐点:Ctrl + 点击线路加点、拖动拐点之后,通道与线路会各成一组'
3363
3370
  return
@@ -4287,7 +4294,7 @@ watch([shortcutBoxSelect, shortcutMultiSelect, shortcutAddWaypoint, shortcutDupl
4287
4294
  })
4288
4295
  // 复制的开关与落点偏移、合并的开关 / 容差 / 确认方式 / 通道分组:只改「怎么响应」,数据不动
4289
4296
  watch([copyEnabled, copyOffsetMode], () => viewRef.value?.updateOptions({ copy: buildCopyOptions() }))
4290
- watch([mergeEnabled, mergeDistance, mergeConfirmMode, mergeChannelGroups], () => viewRef.value?.updateOptions({ merge: buildMergeOptions() }))
4297
+ watch([mergeEnabled, mergeTrigger, mergeDistance, mergeConfirmMode, mergeChannelGroups], () => viewRef.value?.updateOptions({ merge: buildMergeOptions() }))
4291
4298
  // 通道选中模式:只影响下一次点选怎么摊开,updateOptions 不用重排场景
4292
4299
  watch(channelSelectMode, value => viewRef.value?.updateOptions({ channelSelectMode: value }))
4293
4300
  // 选中抬层:改了立刻整份重排一次(这类选项只影响绘制顺序)
@@ -4896,7 +4903,7 @@ const applyPerPointSymbol = () => {
4896
4903
  }
4897
4904
  const { points } = view.getData()
4898
4905
  const next = points.map((point, index) => perPointSymbol.value ? (index % 24 === 0 ? { ...point, symbol: PER_POINT_SYMBOL } : point) : point.symbol === undefined ? point : { ...point, symbol: undefined })
4899
- view.updatePoints(next)
4906
+ view.updateData({ points: next })
4900
4907
  lastEvent.value = perPointSymbol.value
4901
4908
  ? `已给 ${Math.ceil(next.length / 24)} 个节点单独配置符号(数据里写 symbol,优先级最高)`
4902
4909
  : '已清掉逐点符号,节点回到规则表的样子'
@@ -4906,7 +4913,7 @@ const toggleDraggable = (value) => {
4906
4913
  const view = viewRef.value
4907
4914
  if (view) {
4908
4915
  const { points } = view.getData()
4909
- view.updatePoints(points.map((point, index) => ({ ...point, draggable: index % 2 === 0 ? value : false })))
4916
+ view.updateData({ points: points.map((point, index) => ({ ...point, draggable: index % 2 === 0 ? value : false })) })
4910
4917
  }
4911
4918
  }
4912
4919
  /** 选中读数:交互造成的选中走 onSelect,通过接口设置的选中要自己同步一份 */
@@ -4937,7 +4944,7 @@ const syncSelectionFromView = () => {
4937
4944
  }
4938
4945
  }
4939
4946
  const selectFirstLine = () => {
4940
- viewRef.value?.select('line-0')
4947
+ viewRef.value?.selectFeatures('line-0')
4941
4948
  syncSelectionFromView()
4942
4949
  }
4943
4950
  /** 选中回线最多的那条通道的中间回线:拐点属于通道,整条通道的拐点都会显示 */
@@ -4946,13 +4953,13 @@ const selectChannelLine = () => {
4946
4953
  const channel = [...(view?.getChannels() ?? [])].sort((left, right) => right.lineIndexes.length - left.lineIndexes.length)[0]
4947
4954
  const line = channel ? view?.getData().lines[channel.lineIndexes[Math.floor(channel.lineIndexes.length / 2)]] : undefined
4948
4955
  if (view && line) {
4949
- view.select(String(line.id))
4956
+ view.selectFeatures(String(line.id))
4950
4957
  }
4951
4958
  syncSelectionFromView()
4952
4959
  }
4953
4960
  /** Ctrl 点击多个节点等价于框选:多选之后拖动任意一个,整组一起移动 */
4954
4961
  const selectSeveralNodes = () => {
4955
- viewRef.value?.select(['node-1', 'node-2', 'node-3', 'node-4'])
4962
+ viewRef.value?.selectFeatures(['node-1', 'node-2', 'node-3', 'node-4'])
4956
4963
  syncSelectionFromView()
4957
4964
  }
4958
4965
  /** 框选接口也可以直接调用:给个覆盖左上角 1/4 画布的矩形 */
@@ -5097,10 +5104,10 @@ const mergeSelectedChannel = () => {
5097
5104
  return
5098
5105
  }
5099
5106
  const name = channelRenameText.value.trim()
5100
- view.setChannelName(id, name === '' ? null : name)
5107
+ view.updateChannel(id, { name: name === '' ? null : name })
5101
5108
  lastEvent.value = name === ''
5102
- ? `通道 ${String(id)} 摘掉名称(setChannelName(id, null))`
5103
- : `通道 ${String(id)} 改名「${name}」(setChannelName)—— 悬停提示的 {name} 与搜索都跟着变`
5109
+ ? `通道 ${String(id)} 摘掉名称(updateChannel(id, { name: null }))`
5110
+ : `通道 ${String(id)} 改名「${name}」(updateChannel)—— 悬停提示的 {name} 与搜索都跟着变`
5104
5111
  syncChannelLabel()
5105
5112
  }
5106
5113
  /** 加一条回线到通道:挑第一条还没挂通道的线路(认不出 / 已在别的通道里的由 addChannelLines 挡下并说明) */
@@ -5336,7 +5343,7 @@ const readSelectionByApi = () => {
5336
5343
  * 节点右键面板:组件本身不弹任何菜单,右键载荷里就已经给全了 ——
5337
5344
  * wasSelected(命中前它在不在选中集里)+ selection(整份选中集读数)。
5338
5345
  * 这里的规矩是常见的那一套:右键已经选中的节点保留整份选中集,右键没选中的节点先把它选上再弹面板;
5339
- * 面板上的按钮各走一个公开方法(focusOn / copyPoint / mergeSelectedPoints / removePoints / select(null))。
5346
+ * 面板上的按钮各走一个公开方法(focusOn / copyPoint / mergeSelectedPoints / removePoints / selectFeatures(null))。
5340
5347
  */
5341
5348
  const pointMenu = ref(null)
5342
5349
  const closePointMenu = () => {
@@ -5413,7 +5420,7 @@ const pointMenuRows = computed(() => {
5413
5420
  { id: 'merge', label: `把选中的 ${count} 个并拢(mergeSelectedPoints)`, enabled: count >= 2, reason: '选中两个以上节点才谈得上合并' },
5414
5421
  { id: 'remove', label: '删掉这个节点(removePoints)', enabled: true, reason: '' },
5415
5422
  { id: 'deselect', label: '取消它的选中(deselect)', enabled: true, reason: '' },
5416
- { id: 'clear', label: '清空全部选中(select(null))', enabled: count > 0, reason: '本来就是空的' }
5423
+ { id: 'clear', label: '清空全部选中(selectFeatures(null))', enabled: count > 0, reason: '本来就是空的' }
5417
5424
  ]
5418
5425
  })
5419
5426
  const runPointMenuAction = (row) => {
@@ -5445,7 +5452,7 @@ const runPointMenuAction = (row) => {
5445
5452
  lastEvent.value = `右键面板:deselect(${JSON.stringify(ref)}) —— ${formatFeature(payload)} 的选中已取消,其余选中不受影响`
5446
5453
  }
5447
5454
  else if (row.id === 'clear') {
5448
- view.select(null)
5455
+ view.selectFeatures(null)
5449
5456
  lastEvent.value = '右键面板:清空选中'
5450
5457
  }
5451
5458
  closePointMenu()
@@ -6249,6 +6256,12 @@ const deselectFirstSelected = () => {
6249
6256
  <span class="demo-control-hint">{{ copyReadout }}</span>
6250
6257
  <label><input v-model="copyRollback" type="checkbox" /> 复制后模拟「接口失败」:立刻 payload.revert() 回滚</label>
6251
6258
  <label><input v-model="mergeEnabled" type="checkbox" /> 可合并节点(拖动一个节点撞上另一个)</label>
6259
+ <label class="demo-slider">
6260
+ 合并触发时机
6261
+ <select v-model="mergeTrigger">
6262
+ <option v-for="item in MERGE_TRIGGERS" :key="item.value" :value="item.value">{{ item.label }}</option>
6263
+ </select>
6264
+ </label>
6252
6265
  <label class="demo-slider">
6253
6266
  合并容差
6254
6267
  <input v-model.number="mergeDistance" type="range" min="0" max="40" step="1" />
@@ -6311,7 +6324,7 @@ const deselectFirstSelected = () => {
6311
6324
  <span class="demo-control-hint">导出的是当前这一帧:格式与背景对「整幅」和「选中」两边都管用</span>
6312
6325
  <label><input v-model="exportWithLines" type="checkbox" /> 导出选中时携带线路(两端都在选中集里的)</label>
6313
6326
  <button type="button" @click="exportSelectionImage()">导出选中为图片</button>
6314
- <button type="button" @click="exportSelectionData()">导出选中为数据</button>
6327
+ <button type="button" @click="exportSelectedRecords()">导出选中为数据</button>
6315
6328
  <span class="demo-control-hint">{{ exportReadout }}</span>
6316
6329
  <label class="demo-slider">
6317
6330
  选中点样式
@@ -6653,7 +6666,7 @@ const deselectFirstSelected = () => {
6653
6666
  </div>
6654
6667
  <div class="demo-controls demo-controls--shape">
6655
6668
  <label class="demo-slider">
6656
- 单份(老接口)
6669
+ 单份
6657
6670
  <select v-model="exportPart" title="一次只取一份:节点 / 线路 / 通道 / 拐点各是各的口径,互不混在一起" @change="refreshExportSample()">
6658
6671
  <option v-for="item in EXPORT_PARTS" :key="item.value" :value="item.value">{{ item.label }}</option>
6659
6672
  </select>
@@ -6661,7 +6674,7 @@ const deselectFirstSelected = () => {
6661
6674
  <button type="button" @click="exportPartToFile()">导出这一份(下载 JSON)</button>
6662
6675
  </div>
6663
6676
  <div class="demo-controls demo-controls--shape">
6664
- <span class="demo-control-hint">分项多选:选什么就导什么,默认场站 + 通道;勾了通道或线路会自动带上拐点(通道的路由与线路的折线都靠拐点说话),一个都不勾就是四份全要。节点与通道走 new.json 那套列名(station_data / channel_data:通道的 line_data 里是回线、coords 是整条通道的路由),线路与拐点走 test.json 那套(line_data / parallel_groups:拐点只记在集合里,不散进每条回线);业务列(max_loading_rate 这类)原样透传,warnings 会说哪个 id 没找到。「导出选中数据的这几份」走 exportSelectionData —— 选中口径与「导出选中为数据」一样(框选 / 多选出来的那批节点,线路取两端都在里面的),出的却是分项业务表。下面这一段是「只取头两个节点 / 两条线 / 一条通道」的样例</span>
6677
+ <span class="demo-control-hint">分项多选:选什么就导什么,默认场站 + 通道;勾了通道或线路会自动带上拐点(通道的路由与线路的折线都靠拐点说话),一个都不勾就是四份全要。节点与通道走 new.json 那套列名(station_data / channel_data:通道的 line_data 里是回线、coords 是整条通道的路由),线路与拐点走 test.json 那套(line_data / parallel_groups:拐点只记在集合里,不散进每条回线);业务列(max_loading_rate 这类)原样透传,warnings 会说哪个 id 没找到。「导出选中数据的这几份」统一走 exportData({ scope: 'selection' }) —— 选中口径与「导出选中为数据」一样(框选 / 多选出来的那批节点,线路取两端都在里面的),出的却是分项业务表。下面这一段是「只取头两个节点 / 两条线 / 一条通道」的样例</span>
6665
6678
  <pre class="demo-groups">{{ exportDataSample || '点「刷新样例」看当前口径下的导出结构' }}</pre>
6666
6679
  </div>
6667
6680
  </div>
@@ -6719,7 +6732,7 @@ const deselectFirstSelected = () => {
6719
6732
  <button type="button" @click="mergeSelectedChannel">合并分组</button>
6720
6733
  <button type="button" @click="separateSelectedChannelGroups">加拐点分开分组</button>
6721
6734
  <button type="button" @click="clearSeparatedChannelGroups">去掉分开的拐点</button>
6722
- <input v-model="channelRenameText" type="text" placeholder="通道名称(留空 = 摘掉)" title="setChannelName:悬停提示的 {name}、标签模板与搜索都取它" />
6735
+ <input v-model="channelRenameText" type="text" placeholder="通道名称(留空 = 摘掉)" title="updateChannel({ name }):悬停提示的 {name}、标签模板与搜索都取它" />
6723
6736
  <button type="button" @click="renameSelectedChannel">改名</button>
6724
6737
  <button type="button" @click="addLineToSelectedChannel">加一条回线</button>
6725
6738
  <button type="button" @click="removeLineFromSelectedChannel">摘一条回线</button>
@@ -7116,7 +7129,7 @@ const deselectFirstSelected = () => {
7116
7129
  <button type="button" @click="selectQuarterBox">选中左上 1/4</button>
7117
7130
  <button type="button" @click="addWaypointToFirstLine">给首条线路加点</button>
7118
7131
  <button type="button" @click="addWaypointToChannel">给通道加点</button>
7119
- <button type="button" @click="exportParallelGroups">导出拐点集合</button>
7132
+ <button type="button" @click="copyParallelGroupRecords">复制拐点集合</button>
7120
7133
  <button type="button" @click="deselectFirstSelected">取消选中一个(deselect)</button>
7121
7134
  <button type="button" @click="clearSelection">清空选中(deselect)</button>
7122
7135
  <span class="demo-control-hint">{{ selectedLabel }}</span>
@@ -7145,7 +7158,7 @@ const deselectFirstSelected = () => {
7145
7158
  <div class="demo-controls">
7146
7159
  <label class="demo-slider"><input v-model="effectLineOn" type="checkbox" @change="applyEffects" />线路动点</label>
7147
7160
  <label class="demo-slider"><input v-model="effectPointOn" type="checkbox" @change="applyEffects" />节点涟漪</label>
7148
- <button type="button" @click="applyEffects">下发全局配置(setEffects)</button>
7161
+ <button type="button" @click="applyEffects">下发全局配置(updateOptions)</button>
7149
7162
  <span class="demo-control-hint">动画效果:{{ effectText }}</span>
7150
7163
  </div>
7151
7164
  <div class="demo-controls">
@@ -7171,7 +7184,7 @@ const deselectFirstSelected = () => {
7171
7184
  <span class="demo-control-hint">预热(prebake):{{ prebakeText }}</span>
7172
7185
  </div>
7173
7186
  <div class="demo-controls">
7174
- <details class="demo-hint-more"><summary>说明</summary><span class="demo-control-hint">效果分两层:全局缺省(<code>setEffects</code> 或 <code>options.effects</code>)与逐要素(数据里的 <code>points[].effect</code> / <code>lines[].effect</code>)。上面那三个「逐条覆盖」按钮演示的就是逐要素的三种写法:写一份自己的、<code>true</code> 交回全局、<code>false</code> 这几条不动 —— 只在数据里逐条配也会动,全局一个都不配也行。三层「关」是四件事:什么都不配 / <code>effects: false</code> 一处关掉(谁都别动)/ <code>effects.line: false</code> 只关全局那一份 / 逐条 <code>effect: false</code>。线路的图标按弧长跑(拐点上连着走、朝向跟着所在那一段),节点涟漪每帧只改一个 <code>u_time</code>,两者都只铺视口里的、按「画多少」付费;页签切到后台或系统要求减少动态效果时直接停帧(<code>respectReducedMotion</code> 缺省尊重它,想照跑就关掉)。预热不改变画面:它把「会画到的那些贴图」在首帧之前烤好,省掉第一次画到时的解码 + 光栅化,稳态帧与它无关,再烤一遍命中的是缓存。收组合有两种写法:<strong>按实际组合(精确)</strong>走 <code>getSymbolCombos()</code>,把这份数据真正会用到的组合收出来(真实数据实测 <strong>26 条</strong>:照着它配预热零张新画布,逐条命中);<strong>按规则表全排列(粗)</strong>是笛卡尔积,颜色与尺寸由同一个字段决定时(都跟着电压走那种)会把用不到的组合也烤一遍 —— 同一份数据收出 <strong>1741 条</strong>、多烤 21 倍画布,无头下首帧从 29 ms 掉到 963 ms。<strong>「提前预知」的正确用法是清单要准,不是越多越好。</strong></span></details>
7187
+ <details class="demo-hint-more"><summary>说明</summary><span class="demo-control-hint">效果分两层:全局缺省(<code>updateOptions({ effects })</code> 或 <code>options.effects</code>)与逐要素(数据里的 <code>points[].effect</code> / <code>lines[].effect</code>)。上面那三个「逐条覆盖」按钮演示的就是逐要素的三种写法:写一份自己的、<code>true</code> 交回全局、<code>false</code> 这几条不动 —— 只在数据里逐条配也会动,全局一个都不配也行。三层「关」是四件事:什么都不配 / <code>effects: false</code> 一处关掉(谁都别动)/ <code>effects.line: false</code> 只关全局那一份 / 逐条 <code>effect: false</code>。线路的图标按弧长跑(拐点上连着走、朝向跟着所在那一段),节点涟漪每帧只改一个 <code>u_time</code>,两者都只铺视口里的、按「画多少」付费;页签切到后台或系统要求减少动态效果时直接停帧(<code>respectReducedMotion</code> 缺省尊重它,想照跑就关掉)。预热不改变画面:它把「会画到的那些贴图」在首帧之前烤好,省掉第一次画到时的解码 + 光栅化,稳态帧与它无关,再烤一遍命中的是缓存。收组合有两种写法:<strong>按实际组合(精确)</strong>走 <code>getSymbolCombos()</code>,把这份数据真正会用到的组合收出来(真实数据实测 <strong>26 条</strong>:照着它配预热零张新画布,逐条命中);<strong>按规则表全排列(粗)</strong>是笛卡尔积,颜色与尺寸由同一个字段决定时(都跟着电压走那种)会把用不到的组合也烤一遍 —— 同一份数据收出 <strong>1741 条</strong>、多烤 21 倍画布,无头下首帧从 29 ms 掉到 963 ms。<strong>「提前预知」的正确用法是清单要准,不是越多越好。</strong></span></details>
7175
7188
  </div>
7176
7189
  </div>
7177
7190
  <div v-show="activePanel === 'ops-quality'" class="demo-panel-body">