gis-view-webgl2 0.1.7 → 0.1.8

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 (54) hide show
  1. package/README.md +9 -8
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.d.ts +232 -228
  4. package/dist/index.js +1 -1
  5. package/docs/AI/345/257/271/350/257/235/346/261/207/346/200/273.md +49 -0
  6. package/docs/GisView.md +1 -1
  7. package/docs/ai/06-/344/274/232/350/257/235/347/264/242/345/274/225/344/270/216/345/244/215/346/237/245/346/226/271/346/263/225.md +48 -0
  8. package/docs/demos/BasicDemo.vue +2 -2
  9. package/docs/demos/BatchAddDemo.vue +6 -6
  10. package/docs/demos/ConnectDemo.vue +164 -33
  11. package/docs/demos/CopyMergeDemo.vue +27 -14
  12. package/docs/demos/EventDemo.vue +17 -20
  13. package/docs/demos/LabelDemo.vue +5 -5
  14. package/docs/demos/api-index.js +31 -29
  15. package/docs/demos/gis-view-demo/GisViewDemoCardHeader.vue +1 -1
  16. package/docs/demos/gis-view-demo/GisViewDemoStage.vue +1 -1
  17. package/docs/demos/gis-view-demo/panels/DataPanels.vue +1 -1
  18. package/docs/demos/gis-view-demo/useGisViewDemo.js +31 -33
  19. 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 +8 -7
  20. package/docs/from0/05-/347/233/270/346/234/272/344/270/216/344/272/244/344/272/222.md +1 -1
  21. package/docs/from0/07-/351/200/232/351/201/223/345/210/206/347/273/204/344/270/216/345/244/232/345/233/236/347/272/277.md +26 -10
  22. package/docs/manual/02-/345/235/220/346/240/207/344/270/216/346/225/260/346/215/256.md +1 -1
  23. package/docs/manual/04-/344/272/213/344/273/266/344/270/216/345/233/236/350/260/203.md +21 -16
  24. 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 +9 -10
  25. 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 +54 -31
  26. package/docs/manual/08-/351/200/211/344/270/255/344/270/216/346/213/220/347/202/271.md +3 -3
  27. package/docs/manual/09-/351/200/232/351/201/223/344/270/216/345/244/232/345/233/236/347/272/277.md +9 -6
  28. 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 +226 -493
  29. package/docs/manual/12-/346/240/207/347/255/276/344/270/216/350/243/205/351/245/260.md +2 -2
  30. package/docs/manual/14-/350/247/206/345/233/276/344/270/216/347/255/233/351/200/211.md +12 -13
  31. package/docs/manual/16-/345/256/236/344/276/213/346/226/271/346/263/225/346/200/273/350/241/250.md +23 -21
  32. package/docs/manual/17-/345/217/202/346/225/260/351/200/237/346/237/245.md +19 -17
  33. package/docs/manual/18-/351/200/220/346/226/271/346/263/225/347/244/272/344/276/213.md +125 -78
  34. 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 +1 -1
  35. package/docs/manual/20-/346/200/247/350/203/275/344/270/216/345/244/247/346/225/260/346/215/256.md +2 -2
  36. 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 +4 -4
  37. package/docs/steps/README.md +9 -7
  38. package/docs/steps/bench-scale.html +1 -1
  39. package/docs/steps/index.html +6 -6
  40. package/docs/steps/selftest-actions.html +26 -25
  41. package/docs/steps/selftest-data.html +1 -1
  42. package/docs/steps/selftest-demos.html +102 -6
  43. package/docs/steps/selftest-docs.html +3 -3
  44. package/docs/steps/selftest-features.html +283 -51
  45. package/docs/steps/selftest-full.html +3 -3
  46. package/docs/steps/selftest-labels.html +1 -1
  47. package/docs/steps/selftest-perf.html +31 -4
  48. package/docs/steps/selftest-select.html +20 -5
  49. package/docs/steps/step-05-pick.js +4 -8
  50. package/docs/steps/step-07-channels.js +3 -5
  51. package/docs/steps/step-08-labels.js +3 -3
  52. package/docs/steps/step-10-full.js +3 -3
  53. package/docs/steps/step-12-events.js +41 -24
  54. package/package.json +1 -1
@@ -179,7 +179,7 @@ const {
179
179
  <span class="demo-control-hint">{{ channelDraftReadout }}</span>
180
180
  </div>
181
181
  <div class="demo-controls">
182
- <details class="demo-hint-more"><summary>说明</summary><span class="demo-control-hint">新建通道(kind: 'channel'):两个节点之间没有通道就建一条,已有通道就变成新分组 / 并入某一组;「一次加几回」决定这一次往通道里加几条回线 —— 新通道 / 新分组 / 并入分组的余量都按它算,加不下的方案置灰(上限是 options.channelMaxGroups / channelGroupMaxLines)。新建线路(kind: 'line'):只加线路、不挂任何通道,两个节点相同才不允许。两者的草稿交互是同一套,onChannelDraft 载荷里的 kind 与 circuits 说明这一段在加什么,「继续新增」时还能换(continueAddChannel({ kind, circuits }))。勾上「交接模式」之后两个节点选完就先收起草稿(假通道消失),由 demo 自己在 onChannelDraft 的 ready 回调里调 addChannel 落地(面板的逐条属性走第二段 lineItems;与「选完两个节点直接落地」同时勾时后者先落地)。选完两个节点后,结束事件(ready)的读数(hasChannel / hasGroup / groupCount / groups / group / channel / existingChannelId)摆到下面「结束事件」那一行;旁边的「收掉假通道不新建」演示 endAddChannel:静默收掉示例假通道、数据不动、不补 cancel 事件</span></details>
182
+ <details class="demo-hint-more"><summary>说明</summary><span class="demo-control-hint">新建通道(beginAddChannel):两个节点之间没有通道就建一条,已有通道就变成新分组 / 并入某一组;「一次加几回」决定这一次往通道里加几条回线 —— 新通道 / 新分组 / 并入分组的余量都按它算,加不下的方案置灰(上限是 options.channelMaxGroups / channelGroupMaxLines)。新建线路(beginAddLine):只加线路、不挂任何通道,两个节点相同才不允许。两个入口各自固定语义,不能靠 continueAddChannel 中途切换;要换入口请点对应的新增按钮。勾上「交接模式」之后两个节点选完就先收起草稿(假通道消失),由 demo 自己在 onChannelDraft 的 ready 回调里调 addChannel 落地(面板的逐条属性走第二段 lineData;与「选完两个节点直接落地」同时勾时后者先落地)。选完两个节点后,结束事件(ready)的读数(hasChannel / hasGroup / groupCount / groups / group / channel / existingChannelId)摆到下面「结束事件」那一行;旁边的「收掉假通道不新建」演示 endAddChannel:静默收掉示例假通道、数据不动、不补 cancel 事件</span></details>
183
183
  </div>
184
184
  <div class="demo-controls">
185
185
  <span class="demo-control-hint">onChannelDraftClick(草稿里每点一次都发):{{ draftClickText }}</span>
@@ -1426,7 +1426,7 @@ export const useGisViewDemo = () => {
1426
1426
  if (!payload) {
1427
1427
  return []
1428
1428
  }
1429
- const count = payload.selection?.count ?? 0
1429
+ const count = viewRef.value?.getSelectionInfo().count ?? 0
1430
1430
  return [
1431
1431
  { id: 'addStation', label: `新增场站(addPoint)· 落点 (${payload.world[0].toFixed(1)}, ${payload.world[1].toFixed(1)})`, enabled: !payload.hit, reason: '这一下命中了要素:在空白处右键才能新增场站' },
1432
1432
  { id: 'locate', label: `定位并适配选中(focusOn)${count ? ` · ${count} 个` : ''}`, enabled: count > 0, reason: '先在画布上选一个 / 一批要素' },
@@ -2806,7 +2806,7 @@ export const useGisViewDemo = () => {
2806
2806
  selectionReadout.value = `左键载荷:${describeSelectionPayload(payload)}`
2807
2807
  const pickedChannel = payload?.channelId === undefined ? '' : ` · 通道 ${String(payload.channelId)}${payload.groupId === undefined ? '' : ` / 分组 ${String(payload.groupId)}`}`
2808
2808
  lastEvent.value = payload
2809
- ? `单击 ${formatFeature(payload)}${pickedChannel} · 世界 (${payload.coordinate[0].toFixed(4)}, ${payload.coordinate[1].toFixed(4)}) · ${payload.selected ? '已选中' : '未选中'}(点之前${payload.wasSelected ? '已选中' : '未选中'})· 选中集 ${payload.selection?.count ?? 0} 个`
2809
+ ? `单击 ${formatFeature(payload)}${pickedChannel} · 世界 (${payload.coordinate[0].toFixed(4)}, ${payload.coordinate[1].toFixed(4)}) · ${payload.selected ? '已选中' : '未选中'}(点之前${payload.wasSelected ? '已选中' : '未选中'})· 选中集 ${viewRef.value?.getSelectionInfo().count ?? 0} 个`
2810
2810
  : '单击空白处,已清空选中。'
2811
2811
  }
2812
2812
  const handleDblClick = (payload) => {
@@ -2820,7 +2820,7 @@ export const useGisViewDemo = () => {
2820
2820
  const target = payload.hit ? formatFeature(payload) : '空白处'
2821
2821
  const channel = payload.channelId === undefined ? '' : ` · 通道 ${String(payload.channelId)}${payload.groupId === undefined ? '' : ` / 分组 ${String(payload.groupId)}`}`
2822
2822
  const state = payload.selected === undefined ? '' : ` · ${payload.selected ? '已选中' : '未选中'}`
2823
- return `${target}${channel}${state} · world (${payload.world[0].toFixed(4)}, ${payload.world[1].toFixed(4)}) · button ${payload.button}`
2823
+ return `${target}${channel}${state} · world (${payload.world[0].toFixed(4)}, ${payload.world[1].toFixed(4)}) · button ${payload.originalEvent?.button ?? '-'}`
2824
2824
  }
2825
2825
  const handlePointerDown = (payload) => {
2826
2826
  writePointerText(`按下 ${describePointer(payload)}`)
@@ -2839,7 +2839,7 @@ export const useGisViewDemo = () => {
2839
2839
  viewRef.value?.selectFeatures(payload.item.id)
2840
2840
  }
2841
2841
  // 选完再取一次选中集读数:面板上的「把选中的 N 个并拢」按的是点下去那一刻的实际选中集
2842
- pointMenu.value = { ...payload, kept: payload.wasSelected === true, selection: viewRef.value?.getSelectionData() ?? payload.selection }
2842
+ pointMenu.value = { ...payload, kept: payload.wasSelected === true }
2843
2843
  }
2844
2844
  else {
2845
2845
  pointMenu.value = null
@@ -2852,12 +2852,12 @@ export const useGisViewDemo = () => {
2852
2852
  else {
2853
2853
  closeChannelMenu()
2854
2854
  // 空白处与「普通线路」在这里弹画布菜单
2855
- canvasMenu.value = { ...payload, selection: viewRef.value?.getSelectionData() ?? payload.selection }
2855
+ canvasMenu.value = payload
2856
2856
  }
2857
2857
  // 组件只发回调、不碰浏览器默认行为:右键菜单由使用方决定,这里先把自带的那份收掉
2858
2858
  payload.originalEvent?.preventDefault?.()
2859
2859
  }
2860
- lastEvent.value = `右键 ${describePointer(payload)} · ${payload.type === 'point' ? (payload.wasSelected ? '命中已经选中的那一个:保留整份选中集' : '命中的没在选中集里:先把它选上再弹面板') : payload.channelContext ? '命中通道回线:同一份载荷的 channelContext 直接弹通道右键框' : '落在空白处 / 普通线路上:弹画布菜单(每一行都是一个公开方法)'} · 选中集 ${payload.selection?.count ?? 0} 个 · onContextMenu 每个右键都发(点 / 线 / 通道 / 空白),要自己弹菜单就在回调里 event.preventDefault()`
2860
+ lastEvent.value = `右键 ${describePointer(payload)} · ${payload.type === 'point' ? (payload.wasSelected ? '命中已经选中的那一个:保留整份选中集' : '命中的没在选中集里:先把它选上再弹面板') : payload.channelContext ? '命中通道回线:同一份载荷的 channelContext 直接弹通道右键框' : '落在空白处 / 普通线路上:弹画布菜单(每一行都是一个公开方法)'} · 选中集 ${viewRef.value?.getSelectionInfo().count ?? 0} 个 · onContextMenu 每个右键都发(点 / 线 / 通道 / 空白),要自己弹菜单就在回调里 payload.originalEvent.preventDefault()`
2861
2861
  }
2862
2862
  const handleDrag = (payload) => {
2863
2863
  const phase = payload.phase === 'start' ? '开始拖拽' : payload.phase === 'drag' ? '拖拽中' : '拖拽结束'
@@ -5397,8 +5397,8 @@ export const useGisViewDemo = () => {
5397
5397
  const selectedItemsJson = ref('')
5398
5398
  /**
5399
5399
  * 指针载荷里的选中信息:selected(现在在不在选中集里)/ wasSelected(onClick 给「点之前」的那一份,
5400
- * 右键与按下 / 松开就是当下)与整份选中集读数 selection(ids / count / 点 / 线 / 通道条数)。
5401
- * 左键右键要自己接一套操作(自建面板、按命中分支)时,看这几项就够了。
5400
+ * 右键与按下 / 松开就是当下);整份选中集读数改调 view.getSelectionInfo()。
5401
+ * 左键右键要自己接一套操作(自建面板、按命中分支)时,看快照 + 这一次读数就够了。
5402
5402
  * Ctrl + 点击(多选取反)与 Ctrl + 拖动(框选)不走 onClick,那一下的结果看 onSelectChange。
5403
5403
  */
5404
5404
  const describeSelectionPayload = (payload) => {
@@ -5409,7 +5409,7 @@ export const useGisViewDemo = () => {
5409
5409
  const target = payload.type ? formatFeature(payload) : '空白处'
5410
5410
  const state = payload.selected === true ? '现在已选中' : '现在未选中'
5411
5411
  const before = payload.wasSelected === undefined ? '' : ` · ${payload.wasSelected ? '在这之前已选中' : '在这之前未选中'}`
5412
- const info = payload.selection
5412
+ const info = viewRef.value?.getSelectionInfo()
5413
5413
  const bag = info ? ` · 选中集 ${info.count} 个(点 ${info.points} / 线 ${info.lines} / 通道 ${info.channels})${info.ids.length ? ` · ids ${info.ids.slice(0, 3).map(String).join(', ')}${info.ids.length > 3 ? '…' : ''}` : ''}` : ''
5414
5414
  return `${target} · ${state}${before}${bag}`
5415
5415
  }
@@ -5434,11 +5434,11 @@ export const useGisViewDemo = () => {
5434
5434
  lines: data.lines.slice(0, 3).map((line, slot) => line.name ?? line.id ?? `lines[${data.lineIndexes[slot]}]`),
5435
5435
  channels: data.channels.slice(0, 3).map(channel => channel.name ?? channel.id ?? '(无 id)')
5436
5436
  }, null, 1)
5437
- lastEvent.value = '选中信息:getSelectionData() 一份取全(ids / count / 点线通道条数 / 要素本身 / 下标 / bounds),与指针载荷里的 selection 是同一份 state'
5437
+ lastEvent.value = '选中信息:getSelectionData() 一份取全(ids / count / 点线通道条数 / 要素本身 / 下标 / bounds),指针 / 右键载荷不再挂这份读数,需要时直接调 getSelectionInfo()'
5438
5438
  }
5439
5439
  /**
5440
5440
  * 节点右键面板:组件本身不弹任何菜单,右键载荷里就已经给全了 ——
5441
- * wasSelected(命中前它在不在选中集里)+ selection(整份选中集读数)。
5441
+ * wasSelected(命中前它在不在选中集里),整份选中集读数调 view.getSelectionInfo()。
5442
5442
  * 这里的规矩是常见的那一套:右键已经选中的节点保留整份选中集,右键没选中的节点先把它选上再弹面板;
5443
5443
  * 面板上的按钮各走一个公开方法(focusOn / copyPoint / mergeSelectedPoints / removePoints / selectFeatures(null))。
5444
5444
  */
@@ -5507,7 +5507,7 @@ export const useGisViewDemo = () => {
5507
5507
  if (!payload) {
5508
5508
  return []
5509
5509
  }
5510
- const count = payload.selection?.count ?? 1
5510
+ const count = viewRef.value?.getSelectionInfo().count ?? 0
5511
5511
  return [
5512
5512
  { id: 'focus', label: '聚焦到它(focusOn)', enabled: true, reason: '' },
5513
5513
  { id: 'copy', label: '复制一份(copyPoint)', enabled: true, reason: '' },
@@ -5733,13 +5733,13 @@ export const useGisViewDemo = () => {
5733
5733
  const view = viewRef.value
5734
5734
  const lineOnly = payload.kind === 'line'
5735
5735
  // 面板上的逐条属性(一条 / 一批)就是第二段:按每项 id 建回线,这些 id 自动组成通道的 lineIds
5736
- const members = (payload.lineItems ?? payload.linePropsByMember ?? []).filter(Boolean)
5736
+ const members = (payload.lineData ?? payload.linePropsByMember ?? []).filter(Boolean)
5737
5737
  const baseProps = payload.lineProps ?? {}
5738
5738
  const lineData = members.length
5739
5739
  ? members.map(item => ({ ...baseProps, ...item }))
5740
5740
  : Array.from({ length: Math.max(payload.circuits ?? 1, 1) }, () => ({ ...baseProps }))
5741
5741
  if (lineOnly) {
5742
- const lineResult = view.addLine({ from: payload.from.id, to: payload.to.id, name: payload.name, lineProps: payload.lineProps }, lineData)
5742
+ const lineResult = view.addLine({ from: payload.from.id, to: payload.to.id, name: payload.name, lineProps: payload.lineProps }, lineData, { select: true })
5743
5743
  if (lineResult.ok) {
5744
5744
  lastAddedChannel.value = { lineIds: lineResult.lineIds ?? [], lineId: lineResult.lineId, channelId: null, resolution: lineResult.resolution }
5745
5745
  syncChannelLabel()
@@ -5750,9 +5750,9 @@ export const useGisViewDemo = () => {
5750
5750
  }
5751
5751
  const result = payload.hasChannel && payload.channel
5752
5752
  ? (payload.hasGroup
5753
- ? view.joinChannelGroupLines(payload.channel.id, undefined, lineData)
5754
- : view.addChannelGroupLines(payload.channel.id, lineData))
5755
- : view.addChannel({ from: payload.from.id, to: payload.to.id, name: payload.name }, lineData)
5753
+ ? view.joinChannelGroupLines(payload.channel.id, undefined, lineData, { select: true })
5754
+ : view.addChannelGroupLines(payload.channel.id, lineData, undefined, { select: true }))
5755
+ : view.addChannel({ from: payload.from.id, to: payload.to.id, name: payload.name }, lineData, undefined, { select: true })
5756
5756
  if (result.ok) {
5757
5757
  lastAddedChannel.value = { lineIds: result.lineIds ?? [], lineId: result.lineId, channelId: result.channelId, resolution: result.resolution }
5758
5758
  syncChannelLabel()
@@ -5828,9 +5828,9 @@ export const useGisViewDemo = () => {
5828
5828
  channelDraft.value = null
5829
5829
  draftClickText.value = '—'
5830
5830
  const lineOnly = kind === 'line'
5831
- view.beginAddChannel({
5832
- // kind 为 'line' 时不挂通道(新建线路);circuits 是新建通道时一次加几回
5833
- kind: lineOnly ? 'line' : 'channel',
5831
+ const begin = lineOnly ? view.beginAddLine : view.beginAddChannel
5832
+ begin.call(view, {
5833
+ // circuits 是这一段一次加几回
5834
5834
  circuits: addChannelCircuits.value,
5835
5835
  autoCommit: addChannelAutoCommit.value,
5836
5836
  // 连续新增:落地之后自动开下一段;'chain' 再以上一段终点当下一段起点
@@ -5849,16 +5849,15 @@ export const useGisViewDemo = () => {
5849
5849
  ? `新建线路:点两个节点,第二个点完直接落到图上(不挂通道${addChannelCircuits.value > 1 ? `,一次 ${addChannelCircuits.value} 条` : ''})`
5850
5850
  : `新建通道(一次 ${addChannelCircuits.value} 回):点第一个节点当起点,再点第二个节点出方案${addChannelAutoCommit.value ? ',第二个点完直接落地' : '(下面的按钮选怎么加)'}${addChannelContinuous.value ? ';落地之后草稿自动重开,可以接着加下一条' : ''}`
5851
5851
  }
5852
- /** 手动开下一段草稿:连续模式落地时是自动的,这里演示「草稿退出之后接着加」,顺便换这一段加什么 */
5853
- const continueAddChannel = (kind = 'channel') => {
5852
+ /** 手动开下一段草稿:连续模式落地时是自动的,这里演示「草稿退出之后接着加」 */
5853
+ const continueAddChannel = () => {
5854
5854
  const view = viewRef.value
5855
5855
  if (!view) {
5856
5856
  return
5857
5857
  }
5858
- const lineOnly = kind === 'line'
5858
+ const lineOnly = channelDraft.value?.kind === 'line'
5859
5859
  view.continueAddChannel({
5860
- // 重开这一段时把 kind / circuits 一起传下去:想换成「新建线路」就再点一次上面的按钮
5861
- kind: lineOnly ? 'line' : 'channel',
5860
+ // 入口语义由本次草稿决定;想切换通道 / 独立线路请点上面的两个新增按钮
5862
5861
  circuits: addChannelCircuits.value,
5863
5862
  autoCommit: addChannelAutoCommit.value,
5864
5863
  continuous: addChannelContinuous.value ? (addChannelChain.value ? 'chain' : true) : false,
@@ -5936,7 +5935,7 @@ export const useGisViewDemo = () => {
5936
5935
  if (!view || !draft) {
5937
5936
  return
5938
5937
  }
5939
- const result = view.commitAddChannel(option ? { type: option.type, groupId: option.groupId } : undefined)
5938
+ const result = view.commitAddChannel(option ? { resolution: { type: option.type, groupId: option.groupId } } : undefined, { select: true })
5940
5939
  channelDraft.value = null
5941
5940
  if (result.ok) {
5942
5941
  lastAddedChannel.value = { lineIds: result.lineIds ?? [], lineId: result.lineId, channelId: result.channelId, resolution: result.resolution }
@@ -5967,7 +5966,7 @@ export const useGisViewDemo = () => {
5967
5966
  from: from.id,
5968
5967
  to: target.id,
5969
5968
  name: `新增通道 ${String(from.id)} ↔ ${String(target.id)}`
5970
- }, Array.from({ length: Math.max(Math.trunc(Number(addChannelCircuits.value)) || 1, 1) }, (_item, index) => ({ color: '#f4a259', width: 3, ...(buildLinePropsByMember()?.[index] ?? {}) })))
5969
+ }, Array.from({ length: Math.max(Math.trunc(Number(addChannelCircuits.value)) || 1, 1) }, (_item, index) => ({ color: '#f4a259', width: 3, ...(buildLinePropsByMember()?.[index] ?? {}) })), undefined, { select: true })
5971
5970
  if (result.ok) {
5972
5971
  lastAddedChannel.value = { lineIds: result.lineIds ?? [], lineId: result.lineId, channelId: result.channelId, resolution: result.resolution }
5973
5972
  }
@@ -5978,7 +5977,7 @@ export const useGisViewDemo = () => {
5978
5977
  syncChannelGroupPanel()
5979
5978
  syncParallelGroups()
5980
5979
  }
5981
- /** 程序化新增独立线路:不挂通道(等价于 kind: 'line'),一次加几回由「一次加几回」下拉决定 */
5980
+ /** 程序化新增独立线路:不挂通道,一次加几回由「一次加几回」下拉决定 */
5982
5981
  const addLineProgrammatic = () => {
5983
5982
  const view = viewRef.value
5984
5983
  if (!view) {
@@ -5991,19 +5990,18 @@ export const useGisViewDemo = () => {
5991
5990
  lastEvent.value = '程序化新增线路:至少要有两个节点'
5992
5991
  return
5993
5992
  }
5993
+ const lineData = Array.from({ length: Math.max(Math.trunc(Number(addChannelCircuits.value)) || 1, 1) }, (_item, index) => ({ ...(buildLinePropsByMember()?.[index] ?? {}) }))
5994
5994
  const result = view.addLine({
5995
5995
  from: from.id,
5996
5996
  to: target.id,
5997
- circuits: addChannelCircuits.value,
5998
5997
  name: `独立线路 ${String(from.id)} ↔ ${String(target.id)}`,
5999
- lineProps: { color: '#25c2a0', width: 2.5 },
6000
- linePropsByMember: buildLinePropsByMember()
6001
- })
5998
+ lineProps: { color: '#25c2a0', width: 2.5 }
5999
+ }, lineData, { select: true })
6002
6000
  if (result.ok) {
6003
6001
  lastAddedChannel.value = { lineIds: result.lineIds ?? [], lineId: result.lineId, channelId: null, resolution: result.resolution }
6004
6002
  }
6005
6003
  lastEvent.value = result.ok
6006
- ? `程序化新增独立线路(一次 ${result.circuits} 条):${describeChannelAdd(result)}(kind: 'line',不挂通道,通道声明里不会有它)`
6004
+ ? `程序化新增独立线路(一次 ${result.circuits} 条):${describeChannelAdd(result)}(不挂通道,通道声明里不会有它)`
6007
6005
  : `没能新增:${result.reason}`
6008
6006
  syncChannelLabel()
6009
6007
  syncChannelGroupPanel()
@@ -11,17 +11,17 @@
11
11
  | `type` | 命中的要素类型:`'point'` / `'line'`(通道回线也是 `'line'`) |
12
12
  | `index` / `item` | 在 `points` / `lines` 里的下标与数据对象(空白处没有这两项) |
13
13
  | `screen` / `coordinate` / `distance` | 命中点:画布 CSS 像素、世界坐标、拾取距离(像素) |
14
- | `selection` | 选中集读数 `{ ids, count, points, lines, channels }`(通道回线按通道算一个) |
15
- | `selected` / `wasSelected` | 这一刻 / 点之前在不在选中集里 |
14
+ | `selected` / `wasSelected` | 这一刻 / 点之前在不在选中集里(事件快照;整份计数用 `getSelectionInfo()`) |
16
15
  | `channelIndex` / `channel` / `groupId` / `group` | 命中通道回线时多给的通道读数(`channel` 是通道定义,`group` 是分组读数) |
17
16
  | `action` | 这次操作的判读(见下) |
18
17
  | `lngLat` | 配了 GeoJSON 底图时的经纬度 |
19
18
 
20
19
  `action` 是同一套写法:`type` 是动作(`select` / `clearSelection` / `click` / `hover` / `contextMenu` /
21
20
  `dragPoint` / `boxSelect` / `pan` / `selectLabel` / `dragLabel` / `dblClick`…)、`target` 是目标
22
- (`point` / `line` / `channel` / `empty` / `label` / `selection`)、`data` 就是那一份数据
23
- (点 / 线是数据对象、通道回线是通道定义、标签是 `{ label, parent }`)、`button` / `modifiers` / `moved` /
24
- `double` / `press` / `duration` 说明按了什么键、拖没拖、是不是双击的第二下。
21
+ (`point` / `line` / `channel` / `empty` / `label` / `selection`)、`moved` / `double` / `press` / `duration`
22
+ 说明拖没拖、是不是双击的第二下、按了多久。命中的数据在载荷顶层:点 / 线读 `item`,通道回线读
23
+ `channel` / `channelContext`,标签读 `label` / `parent`,拐点读 `lineIndex` / `pathIndex` / `coordinate` / `line`。
24
+ 鼠标键、修饰键与 clientX / clientY 读 `payload.originalEvent`;整份选中集读数用 `view.getSelectionInfo()`。
25
25
 
26
26
  ### 选中集
27
27
 
@@ -41,8 +41,8 @@
41
41
  | `onLabelClick` / `onLabelDrag` / `onLabelContextMenu` / `onLabelDblClick` | `kind` / `index` / `id` / `labelId` / `item` / `label`(这一条标签的 id、内容、来源、合并后的样式)/ `parent`(挂在哪条要素上,线路在通道里时带通道 / 分组)/ `state` |
42
42
  | `onQualityChange` | `{ mode, name, level, glow, ornament, scale, budgetMs, frameMs }` |
43
43
 
44
- 三个事件的总线载荷是 `{ payload, event }`(要原样带回 DOM 事件):`pointerDown` / `pointerUp` / `contextMenu`,
45
- 回调那一侧仍是 `(payload, event)` 两个参数,`view.on` 订阅时记得拆包;通道回线的右键上下文在同一份
44
+ `pointerDown` / `pointerUp` / `contextMenu` 与其余事件一样是「回调一份载荷、总线同一份载荷」,不再额外包
45
+ `{ payload, event }`;要原始 DOM 事件就读 `payload.originalEvent`。通道回线的右键上下文在同一份
46
46
  `contextMenu` 载荷的 `channelContext` 上。
47
47
 
48
48
  ### 自建面板常用的读数
@@ -53,6 +53,7 @@
53
53
  | `listPoints(options)` / `listLines(options)` | 带 `index` / `item` / 几何 / 颜色的快照,`withScreen: true` 直接给屏幕坐标 |
54
54
  | `getLineChannel(ref)` | 这条回线属于哪条通道、哪一组(`{ lineIndex, channelId, channelIndex, channel, groupId, group }`) |
55
55
  | `isSelected(ref)` | 这个要素在不在选中集里 |
56
+ | `getSelectionInfo()` | 选中集轻量计数:`{ ids, count, points, lines, channels }`;指针 / 点击 / 悬停载荷不再重复挂这一份 |
56
57
  | `getSelectionData()` | 选中集一次取全:读数(`ids` / `count` / `pointCount` / `lineCount` / `channelCount`)/ 要素本身(`points` / `lines` / `channels`)/ `items` / 下标 / 世界包围盒 `bounds` |
57
58
 
58
59
  ## 示例数据
@@ -152,7 +152,7 @@ export const DEFAULT_SHORTCUTS = {
152
152
 
153
153
  ② **不能被拾取**。预览跟着鼠标走,光标十有八九正压在它上面;如果 `pick()` 会命中它,鼠标一移过去就会「停在预览上」。所以 `pick` 与 `pickDraggablePoint` 都跳过 `pointCopy.index`,合并的候选里也要排除它。
154
154
 
155
- ③ **要能改**。使用方可能想在落地前改复制品的名字、颜色、业务字段 —— 事件载荷里给 `update(patch)` / `setItem(patch)`,内部只重解析这一个节点(`setCopyPreviewItem` 复用 `updatePoint` 那条增量路径),坐标仍以预览落点为准。
155
+ ③ **要能改**。使用方可能想在落地前改复制品的名字、颜色、业务字段 —— 事件载荷里给 `update(patch)`,内部只重解析这一个节点(`setCopyPreviewItem` 复用 `updatePoint` 那条增量路径),坐标仍以预览落点为准(`payload.item.x` / `payload.item.y`)。
156
156
 
157
157
  落地分两步,中间留了一个可以反悔的口子:
158
158
 
@@ -274,24 +274,40 @@ const shift = ordinal * (span + gap) // 第 1 组原地不动(ordinal 为 0
274
274
  - `setCurrentChannelGroup(channelId, groupId)` 记下「当前分组」并把它整组选中,右键框与分组列表共用这一份状态;
275
275
  - `runChannelAction(actionId, options)` 把界面的动作转成引擎调用,返回执行之后的新上下文。
276
276
 
277
- 新增通道走同一条思路:`beginAddChannel()` 进入草稿态 → 只认节点(点空白继续等)→ 第二个节点点完给方案表
277
+ 新增流程走同一条思路,入口按语义拆成 `beginAddChannel()`(新建通道)与 `beginAddLine()`(新建线路):
278
+ 两者都进入草稿态 → 只认节点(点空白继续等)→ 第二个节点点完给方案表
278
279
  (`newChannel` / `newGroup` / `joinGroup`,按 `channelMaxGroups` / `channelGroupMaxLines` 标 `enabled`),
279
280
  `applyChannelAdd`(实例层)把方案落到数据副本:新回线 id 防撞、`groups` 声明按引擎现状重写、新组不带拐点、
280
281
  并入就塞进目标组。草稿期间数据整份换过要在 `validateChannelDraft` 里作废,否则会拿着旧下标去落点。
281
282
 
282
- 一次加几回、加不加通道,是同一段代码的两个分支(`kind` / `circuits`):
283
+ 一次加几回、加不加通道,由入口语义与 `circuits` / `lineData` 决定(草稿载荷里的 `kind` 只读):
283
284
 
284
285
  - **余量按「这一次加几回」算**:`count` 传进 `buildChannelAddOptions` 之后,`newGroup` 要「还能新分组」且
285
286
  `count <= channelGroupMaxLines`,`joinGroup` 要这一组剩下的余量 `>= count`(不可用时 `reason` 写清
286
287
  「只剩 R 回余量,本次要加 M 回」)。一条条加的时候这些判断都等价于「还能不能加一条」,所以旧代码不用改,
287
288
  只是把「1」换成调用方给的数。
288
289
  - **落地一次生成一批**:`applyChannelAdd` 按 `count` 循环,每条 id 都查一次重(`uniqueLineId`)、名字加 `#序号`,
289
- `newChannel` 把这一批 `lineIds` 整体写进新通道,`newGroup` / `joinGroup` 整体并进目标分组,落地后整批选中。
290
+ `newChannel` 把这一批 `lineIds` 整体写进新通道,`newGroup` / `joinGroup` 整体并进目标分组。落地后缺省不改变
291
+ 选中集;需要选中整批新回线时,由 `commitAddChannel(..., { select: true })` 或程序化新增的 `options.select` 打开。
290
292
  结果里 `lines` / `lineIds` 是这一批(`line` / `lineId` 是第一条),界面「移除上一条新增」按它整批摘。
291
- - **新建线路不碰通道**:`kind: 'line'` 时方案表留空(独立线路不挑方案),`applyChannelAdd` 只往 `lines` 里加,
292
- `channels` 一个字节都不动;入口上就是 `addLine({ from, to, circuits })`(等价于
293
- `addChannel({ ...input, kind: 'line' })`)。草稿里 `kind` / `circuits` 跟着载荷走,
294
- `continueAddChannel({ kind, circuits })` 因此能在下一段换一种加法。
293
+ - **新建线路默认不碰通道**:`beginAddLine()` 的 `kind` 恒为 `line`,不写 `existingChannel` 时方案表留空,
294
+ `applyChannelAdd` 只往 `lines` 里加,`channels` 一个字节都不动;程序化落地就是
295
+ `addLine({ from, to }, lineData)`(一次多回由 `lineData` 条数决定)。两端已有通道且显式写
296
+ `existingChannel: 'newGroup' / 'joinGroup' / 'auto'` 时,方案表才会改走通道分组,把这批新回线挂进已有通道。
297
+ `beginAddChannel({ kind: 'line' })` 已移除;`continueAddChannel(...)` 只续配置,不能在下一段切换通道 / 线路入口。
298
+
299
+ **已有通道策略**由草稿的 `singleChannel` / `existingChannel` 决定,方案表仍负责计算「能不能加」:
300
+
301
+ - `singleChannel: true` 只对 `beginAddChannel` 生效,等价于缺省 `existingChannel: 'block'`;两端已有通道时
302
+ `ready` 载荷带 `blocked: true`,提交返回失败并发 `phase: 'blocked'`,不会自动新建分组。
303
+ - `existingChannel: 'newGroup'` 固定新加一组;`'joinGroup'` 用 `groupId` / `groupIndex`(从 0 起)指定分组;
304
+ `'auto'` 有新分组余量时优先新分组,否则并进可用分组;`'block'` 直接失败。
305
+ - 没有已有通道时,这些策略不改变「新建通道」;`beginAddLine` 在没有通道时始终落成独立线路。
306
+ - 阻断只影响**缺省提交**:使用方显式传一个可用的 `resolution`,仍可把这次新增改道到新分组 / 指定分组。
307
+
308
+ 解除合并(`splitPoint`)和通道分组共用同一个「成员不能拆散」的约束:`options.autoLines: 'alternate'`
309
+ 把同一条通道的成员当成一个分配单位、独立线路各算一个单位,轮流给负载最少的新节点;返回的
310
+ `autoAssignedLines` 与 `lineAssignments` 让界面直接显示每份接到哪些回线、通道是否仍成组。
295
311
 
296
312
  ### 6.13 每点一次都要能拿到信息:草稿回调与连续新增
297
313
 
@@ -315,9 +331,9 @@ const shift = ordinal * (span + gap) // 第 1 组原地不动(ordinal 为 0
315
331
 
316
332
  同理,鼠标的每一种动作都应当有一条平行的回调,且载荷里直接带上「当前是哪个元素」:
317
333
  `onHover`(悬停)、`onClick`(单击)、`onDblClick`(双击)、`onPointerDown` / `onPointerUp`(左 / 中 / 右键
318
- 按下与抬起)、`onContextMenu`(右键)。后三条统一用 `buildPointerPayload(pick(x, y), point, event)`:
319
- 把拾取结果摊平(`type` / `index` / `item` 与通道字段)、补上 `button` / `buttons`、光标的世界坐标与
320
- 画布像素、以及 `originalEvent`;命中空白处 `hit` 为 false。要自己弹右键菜单就在回调里 `preventDefault()`,
334
+ 按下与抬起)、`onContextMenu`(右键)。后三条都只收一个 `payload`,统一由 `buildPointerPayload(pick(x, y), point, event)` 造:
335
+ 把拾取结果摊平(`type` / `index` / `item` 与通道字段),光标位置放 `world` / `x` / `y`;按键、修饰键与 `clientX` / `clientY`
336
+ 读 `payload.originalEvent`,选中集读数调 `getSelectionInfo()`。命中空白处 `hit` 为 false。要自己弹右键菜单就在回调里 `payload.originalEvent.preventDefault()`,
321
337
  引擎只负责「告诉你点到了什么」,不替你决定界面长什么样。
322
338
 
323
339
 
@@ -5,7 +5,7 @@
5
5
  ## 坐标、相机与像素
6
6
 
7
7
  - **世界坐标**:`x` / `y` 就是数据里的数值(经纬度、平面坐标都行),组件不做投影、不做坐标轴翻转。数据范围由节点与线路顶点共同决定(`getBounds()`)。
8
- - **相机**:`{ center: [x, y], scale }`,`scale` 的单位是**屏幕 CSS 像素 / 世界单位**(`getCamera()` / `setCamera()`)。`fitView(padding)` 适应整份数据,`zoomToBounds(bounds, padding)` 适应一段范围,`zoomTo(scale, origin)` 直接设比例,`zoomBy(factor, origin)` 按锚点缩放,`focusOn(target, { center: true })` 只挪中心。
8
+ - **相机**:`{ center: [x, y], scale }`,`scale` 的单位是**屏幕 CSS 像素 / 世界单位**(`getCamera()` / `setCamera()`)。`fitView(padding)` 适应整份数据,`zoomToBounds(bounds, padding)` 适应一段范围,`zoomTo(scale, origin)` 直接设比例,`zoomBy(factor, origin)` 按锚点缩放,`focusOn(target)` 缺省只挪中心。
9
9
  - **滚轮缩放**:以光标为锚点,`deltaY` 100 对应 1.12 倍 —— 缩放量按 `deltaY` 的大小算(`deltaMode` 是行 / 页时先折算成像素),触摸板的连续小位移因此是平滑的小步长,鼠标滚轮的一格仍是原来那一步,一次甩得再大也有上限(`deltaY` 夹在 ±240,免得某个设备一次甩出十倍)。**缩放缺省是关的**(`interactive` / `zoom` 都没写时滚轮完全不接),写 `interactive: true` 或 `features: { zoom: true }` / `features: 'view'` 才接;接着的时候一律 `preventDefault()`(含 Ctrl + 滚轮的浏览器缩放)。
10
10
  - **三档坐标口径**(`world` 不多余:数据 `x` / `y` 就是世界坐标,事件载荷里叫 `world` 只是标口径;三档之间是「客户端坐标 ↔ 画布 CSS 像素 ↔ 世界坐标」的换算链):
11
11
  - **世界坐标(`x` / `y`)**:数据、相机、几何读数、`focusOn` 与拖拽落点用的都是这一套,没有第二套;
@@ -7,20 +7,20 @@
7
7
  | 字段 | 载荷 | 说明 |
8
8
  | --- | --- | --- |
9
9
  | `onReady` | `{ canvas, gl }` | WebGL2 上下文与缓冲区就绪 |
10
- | `onSelect` | `GisViewSelectPayload \| null` | 选中操作回调:回答「这一下选了什么」。真实单击 / Ctrl / `multiSelect` 取反 / 框选 / `selectAt()` 每次成功操作都发(清空与「取反取没了」的载荷是 `null`);`selectFeatures()` / `deselect()` / `focusOn({ select })` 等按引用接口只在选中集实际变化时发。载荷带 `items` / `box` / `originalEvent` / `mode`;单通道时顶层还有 `channelId` / `channelIndex` / `channel` / `groupId` / `groupIndex` / `group` / `groups`,多通道看 `channelContexts`,`items[]` 里的回线只带 `channelId` / `channelIndex` / `groupId` / `groupIndex` 标量归属,不重复挂 `group` / `groups` |
11
- | `onClick` | `GisViewClickPayload \| null` | 单击要素或空白处(空白处为 `null`):`type` / `item` / `index` / `screen` / `coordinate` / `button` / `selection` / `originalEvent` 之外还有 `action`(`type` 是 `select` / `toggleSelect` / `clearSelection` / `click`,`double` 说明这一下是不是双击里的第二下,`press` / `duration` / `longPress` 说明这一次按下想做什么、按了多久、算不算长按),见「指针事件」。`selectTrigger: 'dblClick'` 时第一下只发 `action.type: 'click'`,第二下才应用选中并带 `double: true` |
12
- | `onClusterClick` | `GisViewClusterClickPayload` | 点中多成员聚合圆:带 `id` / `count` / `pointIndexes` / `pointIds` / `bounds` / `radius` / `coordinate` / `screen` / `selection` / `action`(`action.type` 恒为 `clusterClick`、`action.target` 为 `cluster`)。这一条**不会同时发 `onClick`**,也不会改动选中集;聚合圆内部的原始节点不参与普通点击 |
10
+ | `onSelect` | `GisViewSelectPayload \| null` | 选中操作回调:回答「这一下选了什么」。真实单击 / Ctrl / `multiSelect` 取反 / 框选 / `selectAt()` 每次成功操作都发(清空与「取反取没了」的载荷是 `null`);`selectFeatures()` / `deselect()` / `focusOn({ select })` 等按引用接口只在选中集实际变化时发。载荷带 `items` / `box` / `originalEvent` / `mode`;单通道时顶层还有 `channelId` / `channelIndex` / `channel` / `groupId` / `groupIndex` / `group` / `groups` / `lineIndexes` / `lineIds`,多通道看 `channelContexts`。`items[]` 里的回线不挂任何通道字段,归属统一从外层映射或 `getLineChannel()` 取 |
11
+ | `onClick` | `GisViewClickPayload \| null` | 单击要素或空白处(空白处为 `null`):命中数据读 `type` / `index` / `item` / `screen` / `coordinate`,`selected` / `wasSelected` 是点完之后 / 点之前的选中态快照;按键、修饰键与指针位置读 `originalEvent`,整份选中集读数用 `view.getSelectionInfo()`(回调里就是 `payload.item` 所在视图的那一份)。另有 `action`(`type` 是 `select` / `toggleSelect` / `clearSelection` / `click`,`double` 说明这一下是不是双击里的第二下,`press` / `duration` / `longPress` 说明这一次按下想做什么、按了多久、算不算长按),见「指针事件」。`selectTrigger: 'dblClick'` 时第一下只发 `action.type: 'click'`,第二下才应用选中并带 `double: true` |
12
+ | `onClusterClick` | `GisViewClusterClickPayload` | 点中多成员聚合圆:带 `id` / `count` / `pointIndexes` / `pointIds` / `bounds` / `radius` / `coordinate` / `screen` / `originalEvent` / `action`(`action.type` 恒为 `clusterClick`、`action.target` 为 `cluster`)。这一条**不会同时发 `onClick`**,也不会改动选中集;聚合圆内部的原始节点不参与普通点击。选中集读数用 `getSelectionInfo()` |
13
13
  | `onDblClick` | `GisViewClickPayload \| null` | 双击要素或空白处(320ms / 6px 内连续两次单击):`action.type` 是 `dblClick`、`action.double` 为 `true`。普通双击在两种 `selectTrigger` 下都会于第二下派发;`dblClick` 模式下选中 / 清空 / 多选取反也在这一下应用,`click` 模式下第一下已经应用过。`click` 模式的 Ctrl / `multiSelect` 取反快捷路保持原口径,不额外发 `onClick` / `onDblClick` |
14
- | `onPointerDown` | `GisViewPointerPayload` | 左键 / 中键 / 右键按下:命中的要素、`button` / `buttons` 与光标位置;`action.type` 是这一次按下的**意图**(`dragPoint` / `dragWaypoint` / `boxSelect` / `pan` / `copy` / `channelDraft` / `contextMenu`…),`action.target` 说明点的是什么、`action.data` 直接给到那份数据,见「指针事件」 |
14
+ | `onPointerDown` | `GisViewPointerPayload` | 左键 / 中键 / 右键按下:命中的要素与光标位置在载荷顶层;`action.type` 是这一次按下的**意图**(`dragPoint` / `dragWaypoint` / `boxSelect` / `pan` / `copy` / `channelDraft` / `contextMenu`…),`action.target` 说明点的是什么。点 / 线读 `item`,通道回线读 `channel` / `channelContext`,鼠标键、修饰键与 clientX / clientY 读 `originalEvent`,见「指针事件」 |
15
15
  | `onPointerUp` | `GisViewPointerPayload` | 左键 / 中键 / 右键抬起,与 `onPointerDown` 成对:`action.type` 是**刚做完的那件事**(拖过就是 `dragPoint` / `dragWaypoint` / `boxSelect` / `pan`,没拖就是 `click`),`action.moved` 说明这一次是拖还是点,另带 `action.press`(按下时的意图)/ `duration`(按了多久)/ `longPress`(长按) |
16
16
  | `onContextMenu` | `GisViewPointerPayload` | 每一次右键:点、线、通道、空白都只发这一条(要素信息在载荷里);`action.type` 固定是 `contextMenu`、`action.target` 说明点的是点 / 线 / 通道 / 空白。命中通道回线时,同一载荷另带 `channelContext`(几组 / 当前哪组 / 可用交互),不再有单独的通道右键回调。要自己弹菜单就在回调里渲染。**右键回调缺省关**(`contextMenu` / `interactive` 都没写时组件不接右键),写 `interactive: true` / `contextMenu: true` 或 `features: 'view'` 才发;接住右键时系统菜单默认已被收掉(`preventContextMenu` 缺省 `true`),想看系统菜单写 `preventContextMenu: false` |
17
17
  | `onCascadeHighlight` | `GisViewCascadeHighlightInfo \| null` | 点击级联高亮变了:载荷带来源 `origin`(节点 / 线路 / 通道回线)、范围 `scope` 与亮着的 `points` / `lines` / `pointIndexes` / `lineIndexes`,与 `getCascadeHighlight()` 同一份;点空白或关掉开关时是 `null`。这条高亮不改选中集,事件总线名为 `cascadeHighlight` |
18
- | `onHover` | `GisViewHoverPayload \| null` | 悬停要素变化(空白处 / 移出画布是 `null`):与拾取结果同一份读数(`type` / `index` / `item` + 通道字段,另有光标位置 `x` / `y` / `world` / `clientX` / `clientY`、选中集 `selection` 与 `lngLat`)+ `action`(`type` 恒为 `hover`、`target` 说明停在什么上面、`button` 是 `none`、`modifiers` 是当下的修饰键);程序化悬停 `hoverAt()` 也走同一条 |
18
+ | `onHover` | `GisViewHoverPayload \| null` | 悬停要素变化(空白处 / 移出画布是 `null`):与拾取结果同一份读数(`type` / `index` / `item` + 通道字段,另有光标位置 `x` / `y` / `world` 与 `lngLat`)+ `action`(`type` 恒为 `hover`、`target` 说明停在什么上面);指针正好停在最近一次指针事件上时 `originalEvent` 是那一次事件,按键、修饰键与 clientX / clientY 从它读。选中集总读数用 `getSelectionInfo()`;程序化悬停 `hoverAt()` 也走同一条 |
19
19
  | `onFilter` | `GisViewFilterResult` | 筛选完成(含 Worker 异步返回):隐藏的点 / 线下标、剩余可见数量、是否用了 Worker 与计算耗时。`filter.mode: 'remove'` 时删除完成后只发最终一次,`hiddenPoints` / `hiddenLines` 为空 |
20
20
  | `onCursor` | `[number, number] \| null` | 光标世界坐标,移出画布时为 `null` |
21
21
  | `onPointer` | `GisViewPointerState` | 光标每次移动(含移出画布)都发:`{ inside, screen, world, lngLat }`,屏幕坐标是画布 CSS 像素、`lngLat` 要有 GeoJSON 背景才有;`getPointer()` 随时能读同一份,移出时 `inside: false` |
22
22
  | `onPointDrag` | `GisViewPointDragPayload` | 拖拽点:`phase` 为 `start` / `drag` / `end`,载荷带最新坐标、本次移动的全部节点 `points` 与跟随移动的线路 `lines` |
23
- | `onPointCopy` | `GisViewPointCopyPayload \| GisViewBatchCopyPayload` | 复制节点:`phase` 为 `start` / `move` / `commit` / `end` / `cancel`(复制到剪贴板另有 `clipboard`,批量复制 / 整组粘贴是 `batch`),`commit` 阶段可以 `payload.cancel()` 取消、`payload.update()` / `setItem()` 改数据;`clipboard` 载荷带整组 `sources` / `items` / `lines` / `channels` / `count` / `anchor`,`batch` 载荷另带 `indexes` / `lineSources` / `channelSources` / `revert()`,见「节点复制」 |
23
+ | `onPointCopy` | `GisViewPointCopyPayload \| GisViewBatchCopyPayload` | 复制节点:`phase` 为 `start` / `move` / `commit` / `end` / `cancel`(复制到剪贴板另有 `clipboard`,批量复制 / 整组粘贴是 `batch`),`commit` 阶段可以 `payload.cancel()` 取消、`payload.update()` 改数据;落点与最新节点都读 `payload.item`(批量看 `items[0]`),不再另挂 `point` / `coordinate`。`clipboard` 载荷带整组 `sources` / `items` / `lines` / `channels` / `count` / `anchor`,`batch` 载荷另带 `indexes` / `lineSources` / `channelSources` / `revert()`,见「节点复制」 |
24
24
  | `onPointMerge` | `GisViewPointMergePayload \| GisViewMergeResult \| GisViewMergeFailure \| GisViewMergeSelectedPayload` | 合并节点:拖到另一个节点上时先发 `pending`(组件不弹任何界面,在回调里调 `payload.confirm()` / `cancel()` 裁决,或存下来自己接界面),之后发 `confirm`(载荷里的 `item` 是合并结果,可改 / 可取消)、`cancel`,数据并完 / 并失败再发一条 `merge`(`ok: true` / `ok: false`);拖拽入口的载荷带 `trigger`(`'collision'` / `'center'`),说明这一次按哪种时机命中;框选 / 多选之后调 `mergeSelectedPoints()` 时**逐对发 `merge`**,收尾再发一条 `phase: 'mergeSelected'` 的汇总(`merged` 逐对列出、`skipped` 说明谁没并进去),见「节点合并」 |
25
25
  | `onRemove` | `GisViewRemovePayload` | 删除请求(Delete 键 / `requestRemove()`):`pending` 时给一份载荷说明要删什么、连带什么、能按哪些范围删(`points` / `lines` / `waypoints` / `groups` / `channels` / `actions` / `counts`),确认之后发 `phase: 'remove'`(`result` 是结果)、取消发 `phase: 'cancel'`;组件不弹任何界面,弹不弹、怎么弹都由使用方决定(在回调里调 `payload.confirm(scope)` / `payload.cancel()`,或存下来自己接界面),见「删除(remove)」 |
26
26
  | `onWaypointChange` | `GisViewWaypointPayload` | 拐点新增 / 选中 / 删除与拖动,`phase` 为 `add` / `select` / `remove` 或拖动时的 `start` / `move` / `end`,载荷带最新线路数据与所属集合的 `group_id`;通道内的回线共用骨架,载荷是**被操作的那条回线**的推导结果。同步后端只接改变数据的 `add` / `remove` / `end`,并用 `getChannelGroupWaypoints().path` 提交整份骨架,见「通道与多回线」 |
@@ -30,7 +30,7 @@
30
30
  | `onChannelGroupChange` | `GisViewChannelGroupChangePayload` | 分组变化:拆分 / 合并 / 挪线 / 新建组(`action.type` 为 `splitGroups` / `mergeGroups` / `moveLine` / `addGroup`)与整份声明跟着补丁换(`setGroups`,`updateChannel` 写 `groups` / `lineIds`);`action` 带这一笔的 `groupId` / `groupIds` / `lineIds`(`updateLine` 顺带挪组时 `action.via` 为 `'updateLine'`),`item` / `groups` 是与 `dataChange('channelGroups')` 同一份的最新读数。同步后端时用 `getChannelGroups(channelId)` 提交整份声明,不按 action 猜 delta,见「通道分组」 |
31
31
  | `onLabelDrag` | `GisViewLabelDragPayload` | 标签被拖走(`draggable`):`phase` 为 `start` / `move` / `end`,载荷带 `kind`(`point` / `line`)、`index` / `id` / `item`、当前偏移 `offset`、起手时的 `startOffset` 与本次位移 `delta`(CSS 像素),见「标签的拖动与编辑」 |
32
32
  | `onLabelClick` | `GisViewLabelClickPayload` | 点了一下标签(`editable`,位移小于 3 px 才算点选):载荷带 `kind` / `index` / `id` / `item`、当前 `offset` 与这一条的状态读数 `state`,使用方按它选中这一条 |
33
- | `onLabelContextMenu` | `GisViewLabelPointerPayload` | 在标签上按右键(`draggable` / `editable` 任开一个):载荷与 `onLabelClick` 同一份读数,另带 `clientX` / `clientY`(菜单按它定位)与原始 `contextmenu` 事件;系统菜单按 `preventContextMenu` 收(缺省收),要弹自己的菜单就接这一条 |
33
+ | `onLabelContextMenu` | `GisViewLabelPointerPayload` | 在标签上按右键(`draggable` / `editable` 任开一个):载荷与 `onLabelClick` 同一份读数,菜单定位与按键读 `payload.originalEvent.clientX` / `clientY`;系统菜单按 `preventContextMenu` 收(缺省收),要弹自己的菜单就接这一条 |
34
34
  | `onLabelDblClick` | `GisViewLabelPointerPayload` | 双击标签(开关同上):载荷与 `onLabelContextMenu` 一致(原始事件是 `dblclick`),用它开自己的编辑框 |
35
35
  | `onLabelChange` | `GisViewLabelChangePayload` | 标签状态变了:`action` 为 `update`(`updateLabel`)/ `reset`(`resetLabel`)/ `resetAll`(`resetLabels`)/ `add`(`addLabel`)/ `remove`(`removeLabel`)/ `restore`(`setLabelStates`),批量的三条各发 `addAll` / `updateAll` / `removeAll`;载荷带 `kind` / `index` / `id` 与 `patch` / `state` / `removed`,批量那三条另带 `count` 与 `added` / `updated` / `items` / `skipped`;面板按它回显 |
36
36
  | `onCameraChange` | `GisViewCamera` | 平移或缩放后触发 |
@@ -108,19 +108,24 @@ const view = createGisView('#map', {
108
108
  | `selectTrigger: 'dblClick'` 的第二下 | `onClick → onSelect → onSelectChange`(有变化时)`→ onDblClick` |
109
109
 
110
110
  `onSelect` 与 `onSelectChange` 带同一份通道上下文:选中集只涉及**一条通道**时,载荷顶层平铺
111
- `channelId` / `channelIndex` / `channel` / `groupId` / `groupIndex` / `group` / `groups`;`group` 是当前组
112
- (真实单击 / `selectAt` 是命中的那一组,框选与按引用接口是选中集里第一条通道回线所在的组),`groups`
113
- 是整条通道的全部分组。选中集涉及**多条通道**时顶层不再摊平,改看 `channelContexts`(每项带这组字段与
114
- 本次选中的 `lineIndexes` / `lineIds`)。单通道不会再额外造一份 `channelContexts`。两个事件共享同一批
115
- `items` 与分组数组引用,不会深拷贝;`items[]` 里每条回线只保留标量归属字段,需要分组读数从顶层
116
- 或 `channelContexts` 查,不再给每条回线重复挂 `group` / `groups`。纯节点或空选中没有这些通道字段。
111
+ `channelId` / `channelIndex` / `channel` / `groupId` / `groupIndex` / `group` / `groups` 与本次选中的
112
+ `lineIndexes` / `lineIds`;`group` 是当前组(真实单击 / `selectAt` 是命中的那一组,框选与按引用接口是
113
+ 选中集里第一条通道回线所在的组),`groups` 是整条通道的全部分组。选中集涉及**多条通道**时顶层不再摊平,
114
+ 改看 `channelContexts`(每项带这组字段与本次选中的 `lineIndexes` / `lineIds`)。单通道不会再额外造一份
115
+ `channelContexts`。两个事件共享同一批 `items`、分组对象与数组引用,不会深拷贝。
116
+
117
+ `items[]` 里的通道回线**不再挂任何通道字段**(`channelId` / `channelIndex` / `channel` / `groupId` /
118
+ `groupIndex` / `group` / `groups` / `lineIndexes` / `lineIds`)。要用 `items` 反查归属,按选中的线路下标
119
+ 查外层的 `lineIndexes` / `lineIds`,再对应顶层上下文或 `channelContexts`;只问一条线路时也可以直接调
120
+ `getLineChannel(index)`。`groups[].lineIds` 是整组成员,外层 `lineIndexes` / `lineIds` 是本次选中子集,
121
+ 两者语义不同。纯节点或空选中没有这些通道字段。
117
122
 
118
123
  `cause` 取 `click`(指针点击)/ `boxSelect`(框选)/ `keyboard`(Esc 清空)/ `api`(接口调用)。
119
124
 
120
125
  `action` 是更细的一份判读(与指针事件同一套写法):`action.type` 是 `select`(点中一个或接口选中)/
121
126
  `toggleSelect`(Ctrl 点击取反,取反取没了也是它)/ `boxSelect`(框选,没框到东西也是它)/
122
- `clearSelection`(点空白、Esc 或接口清空),`action.target` 是 `selection` / `empty`,`action.data` 就是
123
- 这一批 `items`,`action.modifiers` 是这一次的修饰键。单击触发模式下,Ctrl 取反与框选这两条路**不经过 `onClick`**;
127
+ `clearSelection`(点空白、Esc 或接口清空),`action.target` 是 `selection` / `empty`;这一批数据直接读载荷的
128
+ `items`,修饰键读 `originalEvent`。单击触发模式下,Ctrl 取反与框选这两条路**不经过 `onClick`**;
124
129
  点空白仍会发 `onClick`(载荷为 `null`),顺序同样是 `onClick → onSelect → onSelectChange(有变化时)`。`selectTrigger: 'dblClick'`
125
130
  时第一下仍会发 `onClick`,第二下应用选中 / 取反 / 清空后按 `onClick → onSelect → onSelectChange(有变化时)→ onDblClick` 派发。
126
131
  要按操作分支的面板认这一个字段:
@@ -184,7 +189,7 @@ view.off('*') // 不传 handler = 摘掉这个事件的全部订阅,返回摘
184
189
  | 约定 | 说明 |
185
190
  | --- | --- |
186
191
  | 事件名与回调一一对应 | `dataChange` 就是 `onDataChange`、`selectChange` 就是 `onSelectChange`,载荷完全同一份;`GisViewEventMap` 是事件名到载荷的类型表,`EVENT_NAMES` 是运行时的名字数组 |
187
- | 要 DOM 事件的三个 | `pointerDown` / `pointerUp` / `contextMenu` 的载荷是 `{ payload, event }`(与回调的两个参数一一对应),其余事件的载荷与回调相同 |
192
+ | 指针类事件只有一个参数 | `pointerDown` / `pointerUp` / `contextMenu` 的回调收一份载荷,事件总线订阅时也是同一份直接载荷;原始 DOM 事件读 `payload.originalEvent`,不再额外包 `{ payload, event }` |
188
193
  | 回调抛错不扩散 | 订阅者抛错只走 `onError`:不打断其它订阅者,也不影响组件自己的流程 |
189
194
  | 高频事件不白算 | 组件内部按「有没有人订阅」短路(`count(name) \|\| count('*')`):没人听的时候不构造载荷,`pointer` / `hover` / `render` 这些每帧级别的事件也可以直接订阅 |
190
195
  | 订阅时机 | 创建时的 `ready` 与首次 `dataChange` 发生在 `createGisView` 返回之前,这两条要在 options 里写回调(`onReady` 等) |
@@ -44,7 +44,7 @@ view.updateData({ channels: nextChannels })
44
44
  view.addPoint({ id: 's-1', x: 116.4, y: 39.9, name: '甲站' })
45
45
  view.addPoints(stations) // 返回 { ok, count, indexes, items }
46
46
 
47
- // 通道(新签名:通道信息 + 线路数组 line_data,见「新增通道」;旧写法 { from, to, circuits } 照样认)
47
+ // 通道(通道信息 + 线路数组 lineData,见「新增通道」)
48
48
  view.addChannel({ from: 's-1', to: 's-2', name: '甲通道' }, [{ id: 'c-1' }, { id: 'c-2' }])
49
49
  view.addChannels(channelDeclarations) // 批量版:逐条落地,只重排一次、只记一步历史、只发一条 addChannels
50
50
  ```
@@ -52,7 +52,7 @@ view.addChannels(channelDeclarations) // 批量版:逐条落地,只
52
52
  两条新增路线的分工:`addPoint` / `addPoints` / `addLines` 追加的是「现成的数据对象」;
53
53
  `addChannel` / `addChannels` / `addLine` 是按两端节点新建(通道声明一起改)。`addChannels` 的返回里
54
54
  `results` 与传入顺序一一对应(失败的条目也在里面,按各自的 `reason` 读),`lineIds` / `channelIds`
55
- 只列成功那几条;批量落地后新回线整批选中,与单条 `addChannel` 的手感一致。十万级数据下批量新增只付
55
+ 只列成功那几条;新增类方法缺省都不改选中集,需要选中时显式传 `options.select: true`。十万级数据下批量新增只付
56
56
  一次场景重排,比逐条 `addChannel` 少几十上百次整份重建。新增类方法的完整清单(节点 / 线路 / 通道 / **分组** / **拐点**)与返回口径见「按方法新增要素」。
57
57
 
58
58
  补点 / 补线的代价与「场景铺到哪一步」无关:发散渲染还没铺完时又来一批(「刚删完紧接着补」这种最常见的
@@ -321,12 +321,11 @@ const geometryOnly = view.getChangedLines({ reasons: ['path'] })
321
321
  | --- | --- | --- | --- |
322
322
  | 节点(一条 / 一批) | `addPoint(item, options?)` / `addPoints(items, options?)` | 与数据里的 `points[]` 同一份字段(`id` / `x` / `y` / 样式 / 业务字段) | 单条是 `{ ok, count, indexes, items, index, item }`、批量去掉 `index` / `item`;追加进 `points`,走引擎的增量追加(见「动态换数据与加载完成」) |
323
323
  | 线路(现成数据,一条 / 一批) | `addLines(items, options?)` | 与数据里的 `lines[]` 同一份字段(`id` / `from` / `to` / `path`…),**不挂通道** | `{ ok, count, indexes, items }`;追加进 `lines` |
324
- | 独立线路(按两端节点新建) | `addLine(input, lineData?)` | `from` / `to` 必填,`circuits` 一次几回,`lineProps` / `lineId` / `name`…;第二段与 `addChannel` 的线路数组同一份写法(每项可以只写一个 id:裸字符串 / 数字按 `{ id }` 收) | 与 `addChannel` 同一份结果(`kind: 'line'`),落地后新回线自动选中 |
325
- | 通道(新签名,只新建) | `addChannel(spec, lineData?, group?)` | 第一段 `{ from, to, name?, id?, …通道字段 }`;第二段线路数组 line_data(一条或一批,字符串 / 数字按 id 认,对象浅拷)—— **按每项 id 建 `lineIds`**;第三段可选分组信息 `{ id?, lineIds?, path? }`(单个 / 一组一个);两个节点之间已有通道时整条失败(**第二段 / 第三段显式给了**才算新签名,只写 `{ from, to }` 的照旧按方案表分流),见「新增通道」 | 与下面旧写法同一份结果(`channelLineIds` 是落地后通道的完整成员表);通道声明与回线一起落地,自动选中 |
326
- | 通道(旧写法:按方案表) | `addChannel(input)` | `from` / `to` 必填;`circuits`、`resolution`(`'newChannel'` / `{ type: 'newGroup' }` / `{ type: 'joinGroup', groupId }`)或顶层简写 `channelId` / `groupId` / `groupIndex`(并进已有通道,见「新增通道」)、`channelProps` / `lineProps` / `linePropsByMember` / `name` / `path` | `{ ok, reason?, plan, resolution, kind, circuits, line, lineId, lineIds, lines, lineIndex, channelId, groupId, channelLineIds, channelIndex }`(`channelLineIds` 是落地后通道的完整成员表 —— 并进已有通道时就是更新过的 `lineIds`);通道声明与回线一起落地,自动选中 |
327
- | 通道(已有通道:新加一组 / 并进某一组) | `addChannelGroupLines(channelId, lineData, groupInfo?)` / `joinChannelGroupLines(channelId, groupId, lineData)` | 第二段都是线路数组 line_data(按每项 id 建回线);新加一组可用 `groupInfo` 给组 id 与骨架,并进某一组用 `groupId`(认分组 id 也认序号,从 0 起,不传就挑第一个还能并的) | 与 `addChannel` 同一份结果;`channelLineIds` 是更新后的完整成员表 |
328
- | 通道(一批) | `addChannels(items, options?)` | 每一项与 `addChannel` 同一套写法 | `{ ok, count, results, lineIds, channelIds, items }`:`results` 与传入顺序一一对应,失败的在各自 `reason` 里;只重排一次、只记一步历史 |
329
- | 通道(已有通道:一次加多组) | `addChannelGroups(channelId, groups, options?)` | `groups` 每项 `{ id?, path?, lineData \| lines \| lineItems }`,线路数据与 `addChannel` 第二段同一份写法 | `{ ok, count, results, lineIds, groupIds, groups }`:逐组落地但只记一步历史、只发一条 `addChannelGroups` 数据变更;`options.emit: false` 只落地不通知 |
324
+ | 独立线路(按两端节点新建) | `addLine(spec, lineData?, options?)` | `from` / `to` 必填;`spec` 可写 `name?` / `id?` / `lineProps?` / `path?` / `routing?` / `lineData?`,第二段 `lineData` 一条或一批(每项可以只写一个 id:裸字符串 / 数字按 `{ id }` 收);`options?: { emit?, select? }` | 与 `addChannel` 同一份结果结构(`kind: 'line'`);缺省不改变选中集,`select: true` 才选中新回线 |
325
+ | 通道(只新建) | `addChannel(spec, lineData?, group?, options?)` | 第一段 `{ from, to, name?, id?, channelProps?, path?, routing?, lineData?, group? }`;第二段线路数组 lineData(一条或一批)—— **按每项 id 建 `lineIds`**;第三段可选分组信息 `{ id?, lineIds?, path? }`;`options?: { emit?, select? }`。两个节点之间已有通道时整条失败,见「新增通道」 | `{ ok, reason?, plan, resolution, kind, circuits, line, lineId, lineIds, lines, lineIndex, channelId, groupId, channelLineIds, channelIndex }`;缺省不改变选中集,`select: true` 才选中新回线 |
326
+ | 通道(已有通道:新加一组 / 并进某一组) | `addChannelGroupLines(channelId, lineData?, groupInfo?, options?)` / `joinChannelGroupLines(channelId, groupId?, lineData?, options?)` | 第二段都是线路数组 lineData(按每项 id 建回线);新加一组可用 `groupInfo` 给组 id 与骨架,并进某一组用 `groupId`(认分组 id 也认序号,从 0 起,不传就挑第一个还能并的);`options?: { emit?, select? }` | 与 `addChannel` 同一份结果;`channelLineIds` 是更新后的完整成员表;缺省不选中,`select: true` 才选中新回线 |
327
+ | 通道(一批) | `addChannels(items, options?)` | 每一项与 `addChannel` 第一段同一套写法,可在项内写 `lineData` / `group`;`options?: { emit?, select? }` | `{ ok, count, results, lineIds, channelIds, items }`:`results` 与传入顺序一一对应,失败的在各自 `reason` 里;只重排一次、只记一步历史;缺省不选中,`select: true` 才选中全部成功的新回线 |
328
+ | 通道(已有通道:一次加多组) | `addChannelGroups(channelId, groups, options?)` | `groups` 每项 `{ id?, groupId?, path?, lineData? \| lines? }`,线路数据与 `addChannel` 第二段同一份写法;`lineItems` 已移除;`options?: { emit?, label?, select? }` | `{ ok, count, results, lineIds, groupIds, groups }`:逐组落地但只记一步历史、只发一条 `addChannelGroups` 数据变更;缺省不选中,`select: true` 才选中新回线 |
330
329
  | 分组(把回线拆成新组) | `addChannelGroup(channelId, lineIds, options?)` | 要拆出去的回线 `lineIds`;`options.path` 给新组骨架(缺省沿用来源组、`path: []` 要一条直连);`options.id` 指定组 id,撞上这条通道已有分组时拒绝(不会写出两个同名分组) | `{ ok, reason, channelId, lineIds, groupId, groupIndex, groups, skipped }`;到组数 / 回数上限时 `ok: false` + `reason`,显式 `id` 重名同样 `ok: false` + `reason` |
331
330
  | 分组(整份重拆 / 合并 / 单条改派) | `splitChannelGroups(channelId, [{ id, lineIds, path? }…])` / `mergeChannelGroups(channelId, groupIds, targetGroupId)` / `moveChannelLine(channelId, lineId, { type: 'newGroup' } \| { type: 'joinGroup', groupId })` | 前两个整份换分组声明(第一个组沿用当前拐点);`moveChannelLine` 挪一条回线,也接受 `'newGroup'` 或直接给分组 id | 都把最终声明写回数据副本、记一条「通道分组」历史;`moveChannelLine` 返回 `{ ok, channelId, lineId, groupId, groupIndex, groups }` |
332
331
  | 分组(删一个:把一组并进相邻组) | `removeChannelGroup(channelId, groupId, options?)` | `channelId` / `groupId` 目标通道与要删的组;`options.targetGroupId` 指名并到哪一组(缺省前一组,第一组并后一组) | `{ ok, reason, channelId, groupId, targetGroupId, lineIds, groups }`;骨架 / 拐点沿用目标组,并完只剩一组时声明回到「整条通道一组」;单组通道 `ok: false`(删整条用 `removeChannel`) |
@@ -352,7 +351,7 @@ const added = view.addChannel({ from: 's-1', to: 's-2', path: [[116.7, 40.0]] },
352
351
  console.log(added.channelId, added.lineIds, added.groupId)
353
352
  // 多条通道一次建:每项与 addChannel 同一套写法,只重排一次、只记一步历史
354
353
  const many = view.addChannels([
355
- { from: 's-2', to: 's-3', lineItems: [{ id: 'line-c' }, { id: 'line-d' }] },
354
+ { from: 's-2', to: 's-3', lineData: [{ id: 'line-c' }, { id: 'line-d' }] },
356
355
  { from: 's-3', to: 's-4', name: '三号通道', lineData: [{ id: 'line-e' }] }
357
356
  ])
358
357
 
@@ -380,7 +379,7 @@ view.getParallelGroups()
380
379
  ```
381
380
 
382
381
  四条口径:
383
- - **新增时的线路 / 通道 `path` 口径**:`addLine` / `addChannel(..., kind: 'line')` 的 `routing: 'orthogonal'` 会自动补一个先横后纵的 90° 拐点;输入里的 `path` 一律按**中间拐点**解释,独立线路落地成 `[起点, ...中间拐点, 终点]` 的完整 `path`,新建通道 / 分组只把中间拐点写进骨架。`beginAddChannel` 草稿用同一份解析,预览与落地一致。
382
+ - **新增时的线路 / 通道 `path` 口径**:`addLine` / `addChannel` 的 `routing: 'orthogonal'` 会自动补一个先横后纵的 90° 拐点;输入里的 `path` 一律按**中间拐点**解释,独立线路落地成 `[起点, ...中间拐点, 终点]` 的完整 `path`,新建通道 / 分组只把中间拐点写进骨架。`beginAddChannel` / `beginAddLine` 草稿用同一份解析,预览与落地一致。
384
383
 
385
384
  - **返回里带下标**:`addPoint` / `addPoints` / `addLines` 的 `indexes` 是追加后在数组里的位置(老数据一个都不动),后续 `updatePoint` / `updateLine` 直接拿它当 id 用;`addChannel` 给的是 `lineIndex` / `channelIndex` / `groupId`。
386
385
  - **事件与历史**:新增发 `dataChange`(`addPoints` / `addLines` / `addLine` / `addChannel` / `addChannels` / `addChannelGroups`),分组改动另发 `channelGroups`(`onChannelGroupChange`,见「通道分组」);`addPoints` / `addLines` / `addChannels` / `addChannelGroups` / `addWaypoints` 的批量写法都只记一步历史、只发一条数据变更,`options.emit: false` 只落地不通知。开着历史时每一步都可撤销。