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
@@ -2264,7 +2264,7 @@
2264
2264
  check('拆分发 channelGroupChange(action: splitGroups 带新组表)', cgBus.length === 1 && cgSplit?.action?.type === 'splitGroups' && cgSplit.action.groupIds.length === 2 && cgSplit.channelId === 'ch-1' && cgSplit.groups.length === 2, JSON.stringify({ type: cgSplit?.action?.type, groupIds: cgSplit?.action?.groupIds }))
2265
2265
  check('onChannelGroupChange 回调和事件总线收到同一份载荷', cgOption.length === 1 && cgOption[0] === cgBus[0])
2266
2266
 
2267
- // 选中一条回线再合并:选中镜像里的 groupId / groupIndex 要跟着真值走(本轮修复的回归守卫)
2267
+ // 选中一条回线再合并:选中项不再挂通道字段,实时归属要跟着真值走(回归守卫)
2268
2268
  cgView.selectFeatures('c2')
2269
2269
  await settle(60)
2270
2270
  const cgTruth = () => {
@@ -2277,7 +2277,10 @@
2277
2277
  cgView.mergeChannelGroups('ch-1', [cgAfterSplit[1].id], cgAfterSplit[0].id)
2278
2278
  await settle(60)
2279
2279
  const cgMerged = cgTruth()
2280
- check('合并回线之后选中镜像的 groupId / groupIndex 一起刷新(本轮修复)', String(cgMirror()?.groupId) === cgMerged?.id && cgMirror()?.groupIndex === cgMerged?.index, `镜像 ${cgMirror()?.groupId}/${cgMirror()?.groupIndex} / 真值 ${cgMerged?.id}/${cgMerged?.index}`)
2280
+ const cgOwner = cgView.getLineChannel('c2')
2281
+ const cgOwnerGroups = cgView.getChannelGroups('ch-1')
2282
+ const cgOwnerGroupIndex = cgOwnerGroups.findIndex(group => String(group.id) === String(cgOwner?.groupId))
2283
+ check('合并回线之后 getLineChannel 的 groupId / groupIndex 一起刷新,选中项仍不挂通道字段', String(cgOwner?.groupId) === cgMerged?.id && cgOwnerGroupIndex === cgMerged?.index && cgMirror()?.groupId === undefined && cgMirror()?.groupIndex === undefined, `归属 ${cgOwner?.groupId}/${cgOwnerGroupIndex} / 真值 ${cgMerged?.id}/${cgMerged?.index} / 镜像 ${cgMirror()?.groupId}/${cgMirror()?.groupIndex}`)
2281
2284
  const cgMerge = cgBus[cgBus.length - 1]
2282
2285
  check('合并发 channelGroupChange(action: mergeGroups 带目标组与来源组)', cgMerge?.action?.type === 'mergeGroups' && String(cgMerge.action.groupId) === String(cgMerged?.id) && cgMerge.action.groupIds.length === 2, JSON.stringify({ type: cgMerge?.action?.type, groupId: cgMerge?.action?.groupId, groupIds: cgMerge?.action?.groupIds }))
2283
2286
 
@@ -2657,7 +2660,7 @@
2657
2660
  chView.destroy()
2658
2661
  chHost.remove()
2659
2662
 
2660
- /* 这一轮新增:屏幕 ↔ 真实坐标互转 / addChannel 第二段 lineItems / beginAddChannel 交接模式 */
2663
+ /* 这一轮新增:屏幕 ↔ 真实坐标互转 / addChannel 第二段 lineData / beginAddChannel 交接模式 */
2661
2664
  const hwHost = document.createElement('div')
2662
2665
  hwHost.style.cssText = 'width:420px;height:280px'
2663
2666
  document.body.appendChild(hwHost)
@@ -2701,18 +2704,18 @@
2701
2704
  const hwClientViaScreen = hwView.screenToClient(hwScreen[0], hwScreen[1])
2702
2705
  check('screenToClient(project(世界)) 与 worldToClient(同一世界) 同一结果:反向也等价', Math.abs(hwClientViaScreen[0] - hwClient[0]) < 1e-6 && Math.abs(hwClientViaScreen[1] - hwClient[1]) < 1e-6, JSON.stringify({ viaScreen: hwClientViaScreen, viaWorld: hwClient }))
2703
2706
  // ② addChannel / addLine 第二段:连同线路一起新建
2704
- const hwAdd = hwView.addChannel({ from: 'ha', to: 'hb', name: '第二段通道', circuits: 5 }, [
2707
+ const hwAdd = hwView.addChannel({ from: 'ha', to: 'hb', name: '第二段通道' }, [
2705
2708
  { id: 'hw-l1', color: '#112233', width: 4 },
2706
2709
  { id: 'hw-l2', color: '#445566' }
2707
2710
  ])
2708
2711
  const hwLine = id => hwView.getOptions().lines.find(line => String(line.id) === id)
2709
2712
  const hwChannelOf = channelId => hwView.getOptions().channels.find(channel => String(channel.id) === String(channelId))
2710
- check('addChannel(input, lineItems):给了几条建几条(circuits: 5 忽略),逐条字段各自落地', hwAdd.ok === true && hwAdd.lineIds.length === 2 && hwAdd.circuits === 2 && (hwChannelOf(hwAdd.channelId)?.lineIds ?? []).length === 2 && hwLine('hw-l1')?.color === '#112233' && hwLine('hw-l1')?.width === 4 && hwLine('hw-l2')?.color === '#445566', JSON.stringify({ ids: hwAdd.lineIds, circuits: hwAdd.circuits, l1: hwLine('hw-l1') }))
2711
- const hwAddOne = hwView.addLine({ from: 'hc', to: 'hd', circuits: 3 }, { id: 'hw-solo', color: '#abcdef' })
2713
+ check('addChannel(spec, lineData):给几条建几条,逐条字段各自落地', hwAdd.ok === true && hwAdd.lineIds.length === 2 && hwAdd.circuits === 2 && (hwChannelOf(hwAdd.channelId)?.lineIds ?? []).length === 2 && hwLine('hw-l1')?.color === '#112233' && hwLine('hw-l1')?.width === 4 && hwLine('hw-l2')?.color === '#445566', JSON.stringify({ ids: hwAdd.lineIds, circuits: hwAdd.circuits, l1: hwLine('hw-l1') }))
2714
+ const hwAddOne = hwView.addLine({ from: 'hc', to: 'hd' }, { id: 'hw-solo', color: '#abcdef' })
2712
2715
  check('第二段收单条(非数组)也认:addLine 按它建一条独立线路', hwAddOne.ok === true && hwAddOne.lineIds.length === 1 && hwAddOne.lineIds[0] === 'hw-solo' && hwAddOne.kind === 'line' && hwLine('hw-solo')?.color === '#abcdef', JSON.stringify({ ids: hwAddOne.lineIds, circuits: hwAddOne.circuits }))
2713
- const hwBatch = hwView.addChannels([{ from: 'ha', to: 'hc', lineItems: [{ id: 'hw-b1' }, { id: 'hw-b2' }, { id: 'hw-b3' }] }])
2714
- check('addChannels 的每一项也能写 lineItems:三条一起建', hwBatch.ok === true && hwBatch.lineIds.length === 3 && ['hw-b1', 'hw-b2', 'hw-b3'].every(id => hwLine(id) !== undefined), JSON.stringify({ ids: hwBatch.lineIds }))
2715
- // ③ beginAddChannel:lineItems 对齐条数、handoff 交接
2716
+ const hwBatch = hwView.addChannels([{ from: 'ha', to: 'hc', lineData: [{ id: 'hw-b1' }, { id: 'hw-b2' }, { id: 'hw-b3' }] }])
2717
+ check('addChannels 的每一项写 lineData:三条一起建', hwBatch.ok === true && hwBatch.lineIds.length === 3 && ['hw-b1', 'hw-b2', 'hw-b3'].every(id => hwLine(id) !== undefined), JSON.stringify({ ids: hwBatch.lineIds }))
2718
+ // ③ beginAddChannel:lineData 对齐条数、handoff 交接
2716
2719
  const hwClickPoint = id => {
2717
2720
  const item = hwView.getData().points.find(point => point.id === id)
2718
2721
  return hwView.project(item.x, item.y)
@@ -2748,9 +2751,13 @@
2748
2751
  const hwHandAdd = hwReady ? hwView.addChannel({ from: hwReady.from.id, to: hwReady.to.id, name: '交接通道' }) : { ok: false }
2749
2752
  check('交接之后自己走 addChannel:真实的通道与回线落地', hwHandAdd.ok === true && hwHandAdd.channelId !== null && hwView.getOptions().lines.length === hwLineBefore + 1, JSON.stringify({ ok: hwHandAdd.ok, channelId: hwHandAdd.channelId }))
2750
2753
  hwDraftEvents.length = 0
2751
- hwView.beginAddChannel({ autoCommit: true, lineItems: [{ id: 'hw-d1', color: '#0a0b0c' }, { id: 'hw-d2', color: '#0d0e0f' }] })
2752
- await settle(40)
2753
- check('beginAddChannel({ lineItems }):草稿按第二段对齐条数(circuits = 2)', hwView.getAddChannelDraft()?.circuits === 2 && (hwView.getAddChannelDraft()?.lineItems?.length ?? 0) === 2, JSON.stringify({ circuits: hwView.getAddChannelDraft()?.circuits }))
2754
+ hwView.beginAddChannel({ lineItems: [{ id: 'hw-old-d1' }, { id: 'hw-old-d2' }] })
2755
+ await settle(40)
2756
+ check('beginAddChannel 旧 lineItems 已移除:不进入草稿的 lineData、回数按缺省 1', hwView.getAddChannelDraft()?.lineData === null && hwView.getAddChannelDraft()?.circuits === 1, JSON.stringify({ circuits: hwView.getAddChannelDraft()?.circuits, lineData: hwView.getAddChannelDraft()?.lineData }))
2757
+ hwView.cancelAddChannel()
2758
+ hwView.beginAddChannel({ autoCommit: true, lineData: [{ id: 'hw-d1', color: '#0a0b0c' }, { id: 'hw-d2', color: '#0d0e0f' }] })
2759
+ await settle(40)
2760
+ check('beginAddChannel({ lineData }):草稿按第二段对齐条数(circuits = 2)', hwView.getAddChannelDraft()?.circuits === 2 && (hwView.getAddChannelDraft()?.lineData?.length ?? 0) === 2, JSON.stringify({ circuits: hwView.getAddChannelDraft()?.circuits }))
2754
2761
  hwClick(hwClickPoint('hb'))
2755
2762
  await settle(30)
2756
2763
  hwClick(hwClickPoint('hd'))
@@ -2812,17 +2819,17 @@
2812
2819
  check('位置对账:新加进来的节点以加入时坐标入账(刚加不算,之后再挪才算)', mvAfterAdd === 0 && mvMovedNew.length === 1 && mvMovedNew[0].id === 'me' && mvMovedNew[0].x === 620, JSON.stringify({ afterAdd: mvAfterAdd, afterMove: mvMovedNew.map(item => item.id) }))
2813
2820
  check('位置对账:clearMovedPoints 返回实例本身(可以链式调)', mvView.clearMovedPoints('me') === mvView && mvView.clearMovedPoints().getMovedPoints().length === 0, '')
2814
2821
 
2815
- const mvAdd1 = mvView.addChannel({ from: 'ma', to: 'mb' })
2822
+ const mvAdd1 = mvView.addChannelGroupLines('mch')
2816
2823
  const mvCh1 = mvChannel('mch')
2817
- check('addChannel 已有通道:自动方案 newGroup,通道 lineIds 与 groups 一起追加', mvAdd1.ok === true && mvAdd1.resolution?.type === 'newGroup' && mvCh1.lineIds.length === 3 && mvCh1.groups.length === 2 && mvCh1.groups[1].lineIds.length === 1, JSON.stringify({ type: mvAdd1.resolution?.type, lineIds: mvCh1.lineIds, groups: mvCh1.groups.map(group => group.lineIds) }))
2818
- check('addChannel 已有通道:结果回带更新后的通道 lineIds(channelLineIds 与声明一致)', Array.isArray(mvAdd1.channelLineIds) && mvAdd1.channelLineIds.length === 3 && JSON.stringify(mvAdd1.channelLineIds) === JSON.stringify(mvCh1.lineIds), JSON.stringify(mvAdd1.channelLineIds))
2819
- const mvAdd2 = mvView.addChannel({ from: 'ma', to: 'mb', channelId: 'mch', groupIndex: 0 })
2824
+ check('addChannelGroupLines 已有通道:新分组,通道 lineIds 与 groups 一起追加', mvAdd1.ok === true && mvAdd1.resolution?.type === 'newGroup' && mvCh1.lineIds.length === 3 && mvCh1.groups.length === 2 && mvCh1.groups[1].lineIds.length === 1, JSON.stringify({ type: mvAdd1.resolution?.type, lineIds: mvCh1.lineIds, groups: mvCh1.groups.map(group => group.lineIds) }))
2825
+ check('addChannelGroupLines 已有通道:结果回带更新后的通道 lineIds(channelLineIds 与声明一致)', Array.isArray(mvAdd1.channelLineIds) && mvAdd1.channelLineIds.length === 3 && JSON.stringify(mvAdd1.channelLineIds) === JSON.stringify(mvCh1.lineIds), JSON.stringify(mvAdd1.channelLineIds))
2826
+ const mvAdd2 = mvView.joinChannelGroupLines('mch', 0)
2820
2827
  const mvCh2 = mvChannel('mch')
2821
- check('addChannel 顶层简写:channelId + groupIndex 并进指定分组(通道 lineIds 继续追加)', mvAdd2.ok === true && mvAdd2.resolution?.type === 'joinGroup' && mvAdd2.groupId === mvCh2.groups[0].id && mvCh2.lineIds.length === 4 && mvCh2.groups[0].lineIds.length === 3, JSON.stringify({ type: mvAdd2.resolution?.type, groupId: mvAdd2.groupId, lineIds: mvCh2.lineIds, groups: mvCh2.groups.map(group => group.lineIds) }))
2822
- const mvAdd3 = mvView.addChannel({ from: 'ma', to: 'mb', channelId: 'nope' })
2823
- check('addChannel 指定了不在方案里的通道:失败并给出可读原因', mvAdd3.ok === false && typeof mvAdd3.reason === 'string' && mvAdd3.reason.includes('没有可用方案'), JSON.stringify({ ok: mvAdd3.ok, reason: mvAdd3.reason }))
2824
- const mvAdd4 = mvView.addChannel({ from: 'mb', to: 'mc', channelId: 'mch-x' })
2825
- check('addChannel 两个节点之间还没有通道:顶层 channelId 仍是新建通道的 id', mvAdd4.ok === true && mvAdd4.channelId === 'mch-x' && Array.isArray(mvAdd4.channelLineIds) && mvAdd4.channelLineIds.length === 1 && mvChannel('mch-x').lineIds.length === 1, JSON.stringify({ ok: mvAdd4.ok, channelId: mvAdd4.channelId, channelLineIds: mvAdd4.channelLineIds }))
2828
+ check('joinChannelGroupLines 按分组序号并进指定分组(通道 lineIds 继续追加)', mvAdd2.ok === true && mvAdd2.resolution?.type === 'joinGroup' && mvAdd2.groupId === mvCh2.groups[0].id && mvCh2.lineIds.length === 4 && mvCh2.groups[0].lineIds.length === 3, JSON.stringify({ type: mvAdd2.resolution?.type, groupId: mvAdd2.groupId, lineIds: mvCh2.lineIds, groups: mvCh2.groups.map(group => group.lineIds) }))
2829
+ const mvAdd3 = mvView.addChannelGroupLines('nope')
2830
+ check('分组方法指定了不存在的通道:失败并给出可读原因', mvAdd3.ok === false && typeof mvAdd3.reason === 'string' && mvAdd3.reason.includes('找不到通道'), JSON.stringify({ ok: mvAdd3.ok, reason: mvAdd3.reason }))
2831
+ const mvAdd4 = mvView.addChannel({ from: 'mb', to: 'mc', id: 'mch-x' })
2832
+ check('addChannel 两个节点之间还没有通道:id 是新建通道的 id', mvAdd4.ok === true && mvAdd4.channelId === 'mch-x' && Array.isArray(mvAdd4.channelLineIds) && mvAdd4.channelLineIds.length === 1 && mvChannel('mch-x').lineIds.length === 1, JSON.stringify({ ok: mvAdd4.ok, channelId: mvAdd4.channelId, channelLineIds: mvAdd4.channelLineIds }))
2826
2833
  mvView.destroy()
2827
2834
  mvHost.remove()
2828
2835
 
@@ -3096,8 +3103,8 @@
3096
3103
  diag('右键回线中段', `世界 ${rcMidWorld.map(value => Number(value).toFixed(2)).join(',')} · 回线折点 ${Array.isArray(rcLine?.path) ? rcLine.path.length : 0}`)
3097
3104
  await rcRightClick(rcView.project(rcMidWorld[0], rcMidWorld[1]))
3098
3105
  const rcContext = rcHits[1]?.channelContext
3099
- check('右键落在回线中段:同一份 onContextMenu 载荷里带 channelContext(组数 / 当前分组 / 可用交互)', rcHits.length === 2 && rcContext?.channelId === 'ch-rc' && rcHits[1]?.lineId === 'rc-1' && rcContext?.groupCount === 1 && (rcContext?.actions ?? []).length > 0, JSON.stringify({ count: rcHits.length, channel: rcContext?.channelId, line: rcHits[1]?.lineId, groups: rcContext?.groupCount }))
3100
- check('右键通道只发这一条(不再额外发 onChannelContextMenu),线路下标 / id 也在载荷上', rcHits[1]?.type === 'line' && rcHits[1]?.channelId === 'ch-rc' && rcHits[1]?.lineId === 'rc-1' && Number.isInteger(rcHits[1]?.lineIndex) && rcHits[1]?.action?.target === 'channel', JSON.stringify({ count: rcHits.length, type: rcHits[1]?.type, lineIndex: rcHits[1]?.lineIndex, target: rcHits[1]?.action?.target }))
3106
+ check('右键落在回线中段:同一份 onContextMenu 载荷里带 channelContext(组数 / 当前分组 / 可用交互)', rcHits.length === 2 && rcContext?.channelId === 'ch-rc' && rcHits[1]?.item?.id === 'rc-1' && rcContext?.groupCount === 1 && (rcContext?.actions ?? []).length > 0, JSON.stringify({ count: rcHits.length, channel: rcContext?.channelId, line: rcHits[1]?.item?.id, groups: rcContext?.groupCount }))
3107
+ check('右键通道只发这一条(不再额外发 onChannelContextMenu),线路下标读 index、id 读 item.id', rcHits[1]?.type === 'line' && rcHits[1]?.channelId === 'ch-rc' && rcHits[1]?.item?.id === 'rc-1' && Number.isInteger(rcHits[1]?.index) && rcHits[1]?.lineId === undefined && rcHits[1]?.lineIndex === undefined && rcHits[1]?.action?.target === 'channel', JSON.stringify({ count: rcHits.length, type: rcHits[1]?.type, index: rcHits[1]?.index, item: rcHits[1]?.item?.id, target: rcHits[1]?.action?.target }))
3101
3108
  rcView.destroy()
3102
3109
  rcHost.remove()
3103
3110
 
@@ -3519,18 +3526,19 @@
3519
3526
  await settle(60)
3520
3527
  }
3521
3528
  await rdClick(rdView.project(0, 0))
3522
- check('单击载荷:selected true / wasSelected false(点之前没选中)+ selection 读数', rdClicks[0]?.selected === true && rdClicks[0]?.wasSelected === false && rdClicks[0]?.selection?.count === 1 && rdClicks[0]?.selection?.points === 1, JSON.stringify({ selected: rdClicks[0]?.selected, was: rdClicks[0]?.wasSelected, count: rdClicks[0]?.selection?.count }))
3529
+ const rdClickInfo = rdView.getSelectionInfo()
3530
+ check('单击载荷:selected true / wasSelected false(点之前没选中),总读数走 getSelectionInfo()', rdClicks[0]?.selected === true && rdClicks[0]?.wasSelected === false && rdClicks[0]?.selection === undefined && rdClickInfo.count === 1 && rdClickInfo.points === 1, JSON.stringify({ selected: rdClicks[0]?.selected, was: rdClicks[0]?.wasSelected, count: rdClickInfo.count }))
3523
3531
  await rdClick(rdView.project(0, 0))
3524
- check('再点已经选中的节点:wasSelected 也是 true(点之前就在选中集里)', rdClicks[1]?.selected === true && rdClicks[1]?.wasSelected === true && rdClicks[1]?.selection?.count === 1, JSON.stringify({ selected: rdClicks[1]?.selected, was: rdClicks[1]?.wasSelected }))
3532
+ check('再点已经选中的节点:wasSelected 也是 true(点之前就在选中集里)', rdClicks[1]?.selected === true && rdClicks[1]?.wasSelected === true && rdClicks[1]?.selection === undefined && rdView.getSelectionInfo().count === 1, JSON.stringify({ selected: rdClicks[1]?.selected, was: rdClicks[1]?.wasSelected }))
3525
3533
  const rdSelects = []
3526
3534
  rdView.on('selectChange', payload => rdSelects.push(payload))
3527
3535
  await rdClick(rdView.project(0, 0), { ctrlKey: true })
3528
3536
  check('Ctrl + 点已选中的节点:多选取反走 onSelectChange(count 0),不经过 onClick', rdSelects.length === 1 && rdSelects[0].count === 0 && rdSelects[0].items.length === 0 && rdClicks.length === 2 && rdView.getSelectionData().count === 0, JSON.stringify({ selects: rdSelects.length, clicks: rdClicks.length, count: rdSelects[0]?.count }))
3529
3537
  rdView.selectFeatures(['rd-a', 'rd-b'])
3530
3538
  await rdRightClick(rdView.project(320, 0))
3531
- check('右键落在没选中的节点上:selected / wasSelected 都是 false,selection 照旧给整份选中集', rdMenus[0]?.type === 'point' && rdMenus[0]?.item?.id === 'rd-c' && rdMenus[0]?.selected === false && rdMenus[0]?.wasSelected === false && rdMenus[0]?.selection?.count === 2, JSON.stringify({ id: rdMenus[0]?.item?.id, selected: rdMenus[0]?.selected, count: rdMenus[0]?.selection?.count }))
3539
+ check('右键落在没选中的节点上:selected / wasSelected 都是 false,总读数走 getSelectionInfo()', rdMenus[0]?.type === 'point' && rdMenus[0]?.item?.id === 'rd-c' && rdMenus[0]?.selected === false && rdMenus[0]?.wasSelected === false && rdMenus[0]?.selection === undefined && rdView.getSelectionInfo().count === 2, JSON.stringify({ id: rdMenus[0]?.item?.id, selected: rdMenus[0]?.selected, count: rdView.getSelectionInfo().count }))
3532
3540
  await rdRightClick(rdView.project(0, 0))
3533
- check('右键落在已选中的节点上:wasSelected true(据此决定要不要保住整份多选)', rdMenus[1]?.selected === true && rdMenus[1]?.wasSelected === true && rdMenus[1]?.selection?.count === 2, JSON.stringify({ selected: rdMenus[1]?.selected, was: rdMenus[1]?.wasSelected }))
3541
+ check('右键落在已选中的节点上:wasSelected true(据此决定要不要保住整份多选)', rdMenus[1]?.selected === true && rdMenus[1]?.wasSelected === true && rdMenus[1]?.selection === undefined && rdView.getSelectionInfo().count === 2, JSON.stringify({ selected: rdMenus[1]?.selected, was: rdMenus[1]?.wasSelected }))
3534
3542
  const rdPointInfo = rdView.getSelectionData()
3535
3543
  check('getSelectionData 给读数(ids / count / 各类型条数)', rdPointInfo.count === 2 && rdPointInfo.pointCount === 2 && rdPointInfo.lineCount === 0 && rdPointInfo.channelCount === 0 && rdPointInfo.ids.join(',') === 'rd-a,rd-b', JSON.stringify(rdPointInfo))
3536
3544
  rdView.selectFeatures(['rd-1', 'rd-2'])
@@ -3982,27 +3990,27 @@
3982
3990
  const apiCorners = [[0, 0], [400, 40]].map(spot => apiView.project(spot[0], spot[1]))
3983
3991
  const apiInside = apiCorners.every(spot => spot[0] >= 0 && spot[0] <= apiStats.cssWidth && spot[1] >= 0 && spot[1] <= apiStats.cssHeight)
3984
3992
  check('focusOn(一批目标, { zoom: true }):整体包围盒居中,三个点都在视口里', Math.abs(apiBatch.center[0] - 200) < 1e-6 && Math.abs(apiBatch.center[1] - 20) < 1e-6 && apiBatch.scale > 0 && apiInside, JSON.stringify({ center: apiBatch.center, corners: apiCorners }))
3985
- // center: true 是原来 centerOn 的语义:只挪中心,比例一点不动
3986
- apiView.focusOn(['api-a', 'api-b'], { center: true })
3993
+ // 只挪中心、比例一点不动是 focusOn 的缺省语义
3994
+ apiView.focusOn(['api-a', 'api-b'])
3987
3995
  const apiCenter1 = apiView.getCamera()
3988
- check('focusOn(一批, { center: true }):中心挪到这一段的中点,比例一点不动', Math.abs(apiCenter1.center[0] - 100) < 1e-6 && Math.abs(apiCenter1.center[1] - 20) < 1e-6 && apiCenter1.scale === apiBatch.scale, JSON.stringify(apiCenter1.center))
3989
- apiView.focusOn([400, 0], { center: true })
3996
+ check('focusOn(一批):中心挪到这一段的中点,比例一点不动', Math.abs(apiCenter1.center[0] - 100) < 1e-6 && Math.abs(apiCenter1.center[1] - 20) < 1e-6 && apiCenter1.scale === apiBatch.scale, JSON.stringify(apiCenter1.center))
3997
+ apiView.focusOn([400, 0])
3990
3998
  const apiCenter2 = apiView.getCamera().center
3991
- check('focusOn([x, y], { center: true }) 与 { x, y } 写法都认', Math.abs(apiCenter2[0] - 400) < 1e-6 && Math.abs(apiCenter2[1]) < 1e-6, JSON.stringify(apiCenter2))
3992
- apiView.focusOn('api-b', { center: true })
3999
+ check('focusOn([x, y]) 与 { x, y } 写法都认', Math.abs(apiCenter2[0] - 400) < 1e-6 && Math.abs(apiCenter2[1]) < 1e-6, JSON.stringify(apiCenter2))
4000
+ apiView.focusOn('api-b')
3993
4001
  const apiCenter3 = apiView.getCamera().center
3994
- check('focusOn(id, { center: true }):中心挪到这个要素上', Math.abs(apiCenter3[0] - 200) < 1e-6 && Math.abs(apiCenter3[1] - 40) < 1e-6, JSON.stringify(apiCenter3))
4002
+ check('focusOn(id):中心挪到这个要素上', Math.abs(apiCenter3[0] - 200) < 1e-6 && Math.abs(apiCenter3[1] - 40) < 1e-6, JSON.stringify(apiCenter3))
3995
4003
  const apiChannelScale = apiView.getCamera().scale
3996
4004
  apiView.focusOn({ type: 'channel', channelId: 'api-ch' })
3997
4005
  const apiChannelCamera = apiView.getCamera()
3998
4006
  const apiCenter4 = apiChannelCamera.center
3999
4007
  check('focusOn(通道写法) 缺省只居中,线路 / 通道不再自动放大', Math.abs(apiCenter4[0] - 100) < 5 && Math.abs(apiCenter4[1] - 20) < 5 && apiChannelCamera.scale === apiChannelScale, JSON.stringify({ center: apiCenter4, scale: [apiChannelScale, apiChannelCamera.scale] }))
4000
- apiView.focusOn('没有这个 id', { center: true })
4008
+ apiView.focusOn('没有这个 id')
4001
4009
  const apiCenter5 = apiView.getCamera().center
4002
4010
  check('focusOn:认不出来的目标什么都不做(视角停在原处)', apiCenter5[0] === apiCenter4[0] && apiCenter5[1] === apiCenter4[1], JSON.stringify(apiCenter5))
4003
- apiView.focusOn({ minX: 0, minY: 0, maxX: 400, maxY: 40 }, { center: true })
4011
+ apiView.focusOn({ minX: 0, minY: 0, maxX: 400, maxY: 40 })
4004
4012
  const apiCenter6 = apiView.getCamera().center
4005
- check('focusOn(一段范围, { center: true }):取中心', Math.abs(apiCenter6[0] - 200) < 1e-6 && Math.abs(apiCenter6[1] - 20) < 1e-6, JSON.stringify(apiCenter6))
4013
+ check('focusOn(一段范围):取中心', Math.abs(apiCenter6[0] - 200) < 1e-6 && Math.abs(apiCenter6[1] - 20) < 1e-6, JSON.stringify(apiCenter6))
4006
4014
  // select: true:被聚焦的要素同时变成选中状态(与接口选中同一份 state);mode 决定怎么并进当前选中集
4007
4015
  const apiBeforeFocusSelect = apiView.getSelectionData().ids.join(',')
4008
4016
  apiView.focusOn(['api-a', 'api-b', 'api-c'], { select: true, zoom: true, padding: 20 })
@@ -4733,13 +4741,13 @@
4733
4741
  check('addChannel 新签名:lineIds 自动按线路数组每项的 id 构建', JSON.stringify(nwCh1?.lineIds) === JSON.stringify(['nl1', 'nl2']) && JSON.stringify(nwAdd.lineIds) === JSON.stringify(['nl1', 'nl2']), JSON.stringify(nwCh1?.lineIds))
4734
4742
  check('addChannel 新签名:线路数组逐条落地(自己的字段保留、from / to 固定两端)', nwLine('nl1')?.color === '#112233' && nwLine('nl1')?.from === 'na' && nwLine('nl1')?.to === 'nb' && nwAdd.circuits === 2, JSON.stringify({ l1: nwLine('nl1'), count: nwAdd.circuits }))
4735
4743
  check('addChannel 新签名:第三段分组信息写进声明(id / lineIds / path)', nwCh1?.groups?.length === 1 && nwCh1.groups[0].id === 'ng1' && JSON.stringify(nwCh1.groups[0].lineIds) === JSON.stringify(['nl1', 'nl2']) && JSON.stringify(nwCh1.groups[0].path) === JSON.stringify([[0, 30]]), JSON.stringify(nwCh1?.groups))
4736
- check('addChannel 新签名:落地后新回线整批选中', JSON.stringify(nwView.getSelectionData().ids) === JSON.stringify(['nl1', 'nl2']), JSON.stringify(nwView.getSelectionData().ids))
4744
+ check('addChannel 缺省不改变选中集(select 是显式选项)', nwView.getSelectionData().ids.length === 0, JSON.stringify(nwView.getSelectionData().ids))
4737
4745
 
4738
- // ② 旧写法继续可用(circuits / resolution 那一套)
4746
+ // ② 旧写法已移除;只写 { from, to } 是新签名,会新建一条回线
4739
4747
  const nwLegacy = nwView.addChannel({ from: 'nc', to: 'nd', circuits: 2, name: '丙丁' })
4740
- check('addChannel 旧写法(circuits 那套)继续可用:一次两回', nwLegacy.ok === true && nwLegacy.circuits === 2 && nwView.getOptions().lines.filter(item => item.from === 'nc' && item.to === 'nd').length === 2, JSON.stringify({ ok: nwLegacy.ok, ids: nwLegacy.lineIds }))
4748
+ check('addChannel 旧 circuits 写法已移除:明确失败且不写数据', nwLegacy.ok === false && String(nwLegacy.reason).includes('circuits') && nwView.getOptions().lines.filter(item => item.from === 'nc' && item.to === 'nd').length === 0, JSON.stringify({ ok: nwLegacy.ok, reason: nwLegacy.reason }))
4741
4749
  const nwBare = nwView.addChannel({ from: 'nc', to: 'nd' })
4742
- check('只写 { from, to } 的老调用照旧按方案表分流(已有通道 → newGroup,不被新签名当成「只新建」)', nwBare.ok === true && nwBare.resolution?.type === 'newGroup', JSON.stringify({ ok: nwBare.ok, type: nwBare.resolution?.type, reason: nwBare.reason }))
4750
+ check('只写 { from, to }:新建通道并自动建一条回线', nwBare.ok === true && nwBare.resolution?.type === 'newChannel' && nwBare.lineIds.length === 1, JSON.stringify({ ok: nwBare.ok, type: nwBare.resolution?.type, reason: nwBare.reason }))
4743
4751
 
4744
4752
  // ③ 两个节点之间已有通道:新签名整条失败,plan 里带分组信息(拿来给用户看「已经有几组」)
4745
4753
  const nwLinesBefore = nwView.getOptions().lines.length
@@ -4748,9 +4756,11 @@
4748
4756
  check('失败结果里的 plan 带 hasChannel / hasGroup / groups / channel / group', nwBlocked.plan?.hasChannel === true && nwBlocked.plan?.hasGroup === true && nwBlocked.plan.groups.length === 1 && nwBlocked.plan.channel?.id === 'nc1' && nwBlocked.plan.group?.id === 'ng1', JSON.stringify({ hasChannel: nwBlocked.plan?.hasChannel, groups: nwBlocked.plan?.groups?.map(item => item.id) }))
4749
4757
 
4750
4758
  // ④ 拆出来的两个方法:新加一组 / 并进某一组
4751
- const nwNewGroup = nwView.addChannelGroupLines('nc1', [{ id: 'nl3', color: '#334455' }], { id: 'ng2' })
4759
+ const nwSelectBefore = nwSelectChanges.length
4760
+ const nwNewGroup = nwView.addChannelGroupLines('nc1', [{ id: 'nl3', color: '#334455' }], { id: 'ng2' }, { select: true })
4752
4761
  const nwCh1b = nwChannel('nc1')
4753
4762
  check('addChannelGroupLines:新回线进新分组(成员表一起长)', nwNewGroup.ok === true && nwNewGroup.groupId === 'ng2' && nwNewGroup.resolution?.type === 'newGroup' && JSON.stringify(nwCh1b.lineIds) === JSON.stringify(['nl1', 'nl2', 'nl3']) && nwCh1b.groups.length === 2, JSON.stringify({ ok: nwNewGroup.ok, groupId: nwNewGroup.groupId }))
4763
+ check('addChannelGroupLines({ select: true }):落地后只选中这次的新回线,并只发一次选中变化', nwSelectChanges.length === nwSelectBefore + 1 && JSON.stringify(nwView.getSelectionData().ids) === JSON.stringify(['nl3']), JSON.stringify({ changes: nwSelectChanges.length - nwSelectBefore, ids: nwView.getSelectionData().ids }))
4754
4764
  const nwJoin = nwView.joinChannelGroupLines('nc1', 'ng1', [{ id: 'nl4' }])
4755
4765
  check('joinChannelGroupLines:按分组 id 并进某一组', nwJoin.ok === true && nwJoin.groupId === 'ng1' && JSON.stringify(nwChannel('nc1').groups.find(item => item.id === 'ng1').lineIds) === JSON.stringify(['nl1', 'nl2', 'nl4']), JSON.stringify({ groups: nwChannel('nc1')?.groups?.map(item => [item.id, item.lineIds]) }))
4756
4766
  const nwJoinIndex = nwView.joinChannelGroupLines('nc1', 1, 'nl5')
@@ -4759,8 +4769,10 @@
4759
4769
  check('通道找不到时拆出来的方法给出 reason、不抛错', nwJoinMiss.ok === false && String(nwJoinMiss.reason).includes('找不到通道'), JSON.stringify({ ok: nwJoinMiss.ok, reason: nwJoinMiss.reason }))
4760
4770
 
4761
4771
  // ⑤ addChannels 的每一项也认新签名(lineData + group 写在对象上)
4762
- const nwMany = nwView.addChannels([{ from: 'na', to: 'nc', name: '甲丙', id: 'nc2', lineData: [{ id: 'nl6' }, { id: 'nl7' }], group: { id: 'ng3' } }])
4772
+ const nwBatchSelectBefore = nwSelectChanges.length
4773
+ const nwMany = nwView.addChannels([{ from: 'na', to: 'nc', name: '甲丙', id: 'nc2', lineData: [{ id: 'nl6' }, { id: 'nl7' }], group: { id: 'ng3' } }], { select: true })
4763
4774
  check('addChannels 的每一项认新签名(lineData + group)', nwMany.ok === true && nwMany.channelIds[0] === 'nc2' && JSON.stringify(nwChannel('nc2')?.lineIds) === JSON.stringify(['nl6', 'nl7']) && nwChannel('nc2')?.groups?.[0]?.id === 'ng3', JSON.stringify({ ok: nwMany.ok, ids: nwMany.channelIds }))
4775
+ check('addChannels({ select: true }):两条回线一次选中,不逐条发选中变化', nwSelectChanges.length === nwBatchSelectBefore + 1 && JSON.stringify(nwView.getSelectionData().ids) === JSON.stringify(['nl6', 'nl7']), JSON.stringify({ changes: nwSelectChanges.length - nwBatchSelectBefore, ids: nwView.getSelectionData().ids }))
4764
4776
 
4765
4777
  // ⑥ updateChannel 的 group(单数:改一组的信息)/ groups(复数:整份换)
4766
4778
  nwView.updateChannel('nc1', { group: { id: 'ng1', path: [[7, 8]] } })
@@ -4784,7 +4796,7 @@
4784
4796
  send('pointerdown', { buttons: 1 })
4785
4797
  send('pointerup', { buttons: 0 })
4786
4798
  }
4787
- const nwSelectBefore = nwView.getSelectionData().ids
4799
+ const nwSelectedBeforeDraft = nwView.getSelectionData().ids
4788
4800
  nwEvents.length = 0
4789
4801
  nwSelectChanges.length = 0
4790
4802
  nwView.beginAddChannel({ select: false })
@@ -4794,7 +4806,7 @@
4794
4806
  await settle(40)
4795
4807
  nwClick(nwScreenOf('nb'))
4796
4808
  await settle(60)
4797
- check('select 关着:点两个节点不改变选中集(只画两个草稿点)', JSON.stringify(nwView.getSelectionData().ids) === JSON.stringify(nwSelectBefore) && nwSelectChanges.length === 0, JSON.stringify(nwView.getSelectionData().ids))
4809
+ check('select 关着:点两个节点不改变选中集(只画两个草稿点)', JSON.stringify(nwView.getSelectionData().ids) === JSON.stringify(nwSelectedBeforeDraft) && nwSelectChanges.length === 0, JSON.stringify(nwView.getSelectionData().ids))
4798
4810
  const nwReadyPlain = nwEvents.find(payload => payload.phase === 'ready')
4799
4811
  check('结束事件(ready)带两个节点与「没有分组」的读数', nwReadyPlain !== undefined && nwReadyPlain.from.id === 'nd' && nwReadyPlain.to.id === 'nb' && nwReadyPlain.finished === true && nwReadyPlain.hasChannel === false && nwReadyPlain.hasGroup === false && nwReadyPlain.groups.length === 0 && nwReadyPlain.group === null && nwReadyPlain.channel === null, JSON.stringify({ from: nwReadyPlain?.from?.id, to: nwReadyPlain?.to?.id, hasGroup: nwReadyPlain?.hasGroup }))
4800
4812
  check('beginAddChannel 只负责示例路径:ready 之后草稿还在、一条数据都没落', nwView.getAddChannelDraft()?.phase === 'ready' && nwEvents.every(payload => payload.phase !== 'commit'), JSON.stringify({ draft: nwView.getAddChannelDraft()?.phase, phases: nwEvents.map(payload => payload.phase) }))
@@ -4817,6 +4829,7 @@
4817
4829
  nwDraftMode = 'commit'
4818
4830
  nwEvents.length = 0
4819
4831
  const nwLinesBefore2 = nwView.getOptions().lines.length
4832
+ const nwSelectedBeforeInside = nwView.getSelectionData().ids
4820
4833
  nwView.beginAddChannel()
4821
4834
  await settle(40)
4822
4835
  nwClick(nwScreenOf('nd'))
@@ -4824,7 +4837,7 @@
4824
4837
  nwClick(nwScreenOf('nb'))
4825
4838
  await settle(80)
4826
4839
  check('事件里 endAddChannel + addChannel:真实通道与回线落地、假通道不留', nwInside?.ok === true && nwChannel('nc9') !== undefined && nwLine('nl9') !== undefined && nwLine('nl10') !== undefined && nwView.getOptions().lines.length === nwLinesBefore2 + 2, JSON.stringify({ ok: nwInside?.ok, lines: nwView.getOptions().lines.map(item => item.id) }))
4827
- check('事件里落地之后草稿收掉、新回线选中', nwView.getAddChannelDraft() === null && JSON.stringify(nwView.getSelectionData().ids) === JSON.stringify(['nl9', 'nl10']), JSON.stringify(nwView.getSelectionData().ids))
4840
+ check('事件里落地之后草稿收掉、缺省不改选中集', nwView.getAddChannelDraft() === null && JSON.stringify(nwView.getSelectionData().ids) === JSON.stringify(nwSelectedBeforeInside), JSON.stringify({ before: nwSelectedBeforeInside, after: nwView.getSelectionData().ids }))
4828
4841
 
4829
4842
  // ⑩ select: true 与结束事件里的分组读数;提前结束用 cancelAddChannel
4830
4843
  nwDraftMode = null
@@ -4938,10 +4951,10 @@
4938
4951
  check('addLine 不传 id:按 from-to-序号 自动编号(撞车由 uniqueLineId 往后排)',
4939
4952
  bvAuto.ok === true && String(bvAuto.lineId) === 'bb-bc-0',
4940
4953
  JSON.stringify({ ok: bvAuto.ok, lineId: bvAuto.lineId }))
4941
- const bvLegacy = bvView.addChannel({ from: 'ba', to: 'bb', circuits: 2 }, ['bare-4', 'bare-5'])
4942
- check('旧写法 addChannel 的 lineItems 同样认裸 id:给了两条就按两条建(circuits 忽略)',
4943
- bvLegacy.ok === true && JSON.stringify(bvLegacy.lineIds.map(String)) === JSON.stringify(['bare-4', 'bare-5']),
4944
- JSON.stringify({ ok: bvLegacy.ok, lineIds: bvLegacy.lineIds }))
4954
+ const bvChannel = bvView.addChannel({ from: 'ba', to: 'bb' }, ['bare-4', 'bare-5'])
4955
+ check('addChannel 第二段同样认裸 id:给了两条就建两条回线',
4956
+ bvChannel.ok === true && JSON.stringify(bvChannel.lineIds.map(String)) === JSON.stringify(['bare-4', 'bare-5']),
4957
+ JSON.stringify({ ok: bvChannel.ok, lineIds: bvChannel.lineIds }))
4945
4958
  bvView.destroy()
4946
4959
  bvHost.remove()
4947
4960
  // 容器先 display:none(量不出视口)再显示:挂起的那次落位兑现的是「请求那一刻」的范围,中途新加的远处节点不算进来
@@ -4993,7 +5006,96 @@
4993
5006
  JSON.stringify(dfView.getCamera()))
4994
5007
  dfView.destroy()
4995
5008
  dfHost.remove()
4996
-
5009
+
5010
+ /* 新增接口:选中由 options.select 显式打开;addLine 一步撤销;有通道声明时仍走追加式补线 */
5011
+ const awHost = document.createElement('div')
5012
+ awHost.style.cssText = 'width:520px;height:340px'
5013
+ document.body.appendChild(awHost)
5014
+ const awSelectChanges = []
5015
+ const awView = createGisView(awHost, {
5016
+ points: [
5017
+ { id: 'aa', x: -180, y: -100 },
5018
+ { id: 'ab', x: -90, y: -100 },
5019
+ { id: 'ac', x: 0, y: -100 },
5020
+ { id: 'ad', x: 90, y: -100 },
5021
+ { id: 'ae', x: 0, y: 100 },
5022
+ { id: 'af', x: 180, y: 100 }
5023
+ ],
5024
+ lines: [],
5025
+ channels: [],
5026
+ features: { channelDraft: true },
5027
+ history: true,
5028
+ showGrid: false,
5029
+ onSelectChange: event => awSelectChanges.push(event)
5030
+ })
5031
+ await settle(60)
5032
+ const awIds = () => awView.getSelectionData().ids
5033
+ const awLine = id => awView.getOptions().lines.find(item => String(item.id) === String(id))
5034
+ const awInitialChanges = awSelectChanges.length
5035
+ const awPointPlain = awView.addPoint({ id: 'aw-p1', x: 40, y: 30 })
5036
+ check('addPoint 缺省不改变选中集', awPointPlain.ok === true && awIds().length === 0 && awSelectChanges.length === awInitialChanges, JSON.stringify({ ids: awIds(), changes: awSelectChanges.length - awInitialChanges }))
5037
+ const awPointsSelected = awView.addPoints([{ id: 'aw-p2', x: 50, y: 35 }, { id: 'aw-p3', x: 60, y: 40 }], { select: true })
5038
+ check('addPoints({ select: true }):一次选中这批新节点', awPointsSelected.ok === true && JSON.stringify(awIds()) === JSON.stringify(['aw-p2', 'aw-p3']), JSON.stringify(awIds()))
5039
+ awView.selectFeatures(null)
5040
+ const awLinesPlain = awView.addLines([{ id: 'aw-l1', from: 'aa', to: 'ab' }])
5041
+ check('addLines 缺省不改变选中集', awLinesPlain.ok === true && awIds().length === 0, JSON.stringify(awIds()))
5042
+ const awLinesSelected = awView.addLines([{ id: 'aw-l2', from: 'ac', to: 'ad' }, { id: 'aw-l3', from: 'ae', to: 'af' }], { select: true })
5043
+ check('addLines({ select: true }):一次选中这批新线路', awLinesSelected.ok === true && JSON.stringify(awIds()) === JSON.stringify(['aw-l2', 'aw-l3']), JSON.stringify(awIds()))
5044
+ awView.selectFeatures(null)
5045
+ const awLinePlain = awView.addLine({ from: 'aa', to: 'ac', id: 'aw-l4' })
5046
+ check('addLine 缺省不改变选中集', awLinePlain.ok === true && awIds().length === 0, JSON.stringify(awIds()))
5047
+ const awLineSelected = awView.addLine({ from: 'ae', to: 'af', id: 'aw-l5' }, undefined, { select: true })
5048
+ check('addLine({ select: true }):只选中新增回线', awLineSelected.ok === true && JSON.stringify(awIds()) === JSON.stringify(['aw-l5']), JSON.stringify(awIds()))
5049
+ awView.selectFeatures(null)
5050
+ const awChannelPlain = awView.addChannel({ from: 'aa', to: 'ab', id: 'aw-c1' }, ['aw-c1-1'])
5051
+ check('addChannel 缺省不改变选中集', awChannelPlain.ok === true && awIds().length === 0, JSON.stringify(awIds()))
5052
+ const awChannelSelected = awView.addChannel({ from: 'ab', to: 'ad', id: 'aw-c2' }, ['aw-c2-1', 'aw-c2-2'], undefined, { select: true })
5053
+ check('addChannel({ select: true }):只选中这次新建的全部回线', awChannelSelected.ok === true && JSON.stringify(awIds()) === JSON.stringify(['aw-c2-1', 'aw-c2-2']), JSON.stringify(awIds()))
5054
+ const awLineLegacy = awView.addLine({ from: 'ac', to: 'af', circuits: 2 })
5055
+ check('addLine 旧 circuits 入参已移除:明确失败且不写数据', awLineLegacy.ok === false && String(awLineLegacy.reason).includes('circuits') && awLine('ac-af-0') === undefined, JSON.stringify({ ok: awLineLegacy.ok, reason: awLineLegacy.reason }))
5056
+ const awCommitLegacy = awView.commitAddChannel({ lineItems: [{ id: 'aw-old-commit' }] })
5057
+ check('commitAddChannel 旧 lineItems 入参已移除:明确失败且不写数据', awCommitLegacy.ok === false && String(awCommitLegacy.reason).includes('lineItems') && awLine('aw-old-commit') === undefined, JSON.stringify({ ok: awCommitLegacy.ok, reason: awCommitLegacy.reason }))
5058
+ const awChannelGroupsBefore = JSON.stringify(awView.getOptions().channels)
5059
+ const awLineCountBeforeGroups = awView.getOptions().lines.length
5060
+ const awGroupLegacy = awView.addChannelGroups('aw-c2', [{ lineData: [{ id: 'aw-group-new' }], lineItems: [{ id: 'aw-group-old' }] }])
5061
+ check('addChannelGroups 旧 lineItems 入参已移除:整组拒绝、不改数据', awGroupLegacy.ok === false && String(awGroupLegacy.reason).includes('lineItems') && JSON.stringify(awView.getOptions().channels) === awChannelGroupsBefore && awView.getOptions().lines.length === awLineCountBeforeGroups, JSON.stringify({ ok: awGroupLegacy.ok, reason: awGroupLegacy.reason }))
5062
+
5063
+ awView.beginAddChannel({ kind: 'line' })
5064
+ await settle(20)
5065
+ const awDraftKind = awView.getAddChannelDraft()?.kind
5066
+ awView.continueAddChannel({ kind: 'line' })
5067
+ await settle(20)
5068
+ const awContinuedKind = awView.getAddChannelDraft()?.kind
5069
+ awView.cancelAddChannel()
5070
+ awView.beginAddLine({ kind: 'channel' })
5071
+ await settle(20)
5072
+ const awLineDraftKind = awView.getAddChannelDraft()?.kind
5073
+ awView.cancelAddChannel()
5074
+ check('beginAddChannel 固定通道语义:运行时 kind: line 不再切换入口', awDraftKind === 'channel', JSON.stringify({ kind: awDraftKind }))
5075
+ check('continueAddChannel 不切换入口语义:通道草稿传 kind: line 仍是通道', awContinuedKind === 'channel', JSON.stringify({ kind: awContinuedKind }))
5076
+ check('beginAddLine 固定线路语义:运行时 kind: channel 不再切换入口', awLineDraftKind === 'line', JSON.stringify({ kind: awLineDraftKind }))
5077
+
5078
+ const awChannelsBeforeLine = JSON.stringify(awView.getOptions().channels)
5079
+ awView.clearHistory()
5080
+ const awHistoryLine = awView.addLine({ from: 'aa', to: 'af', id: 'aw-history' })
5081
+ await settle(20)
5082
+ const awCenter = awView.project(0, 0)
5083
+ const awHistoryHit = awView.pick(awCenter[0], awCenter[1])
5084
+ const awHistoryState = awView.getHistoryState()
5085
+ const awUndo = awView.undo()
5086
+ await settle(20)
5087
+ const awUndoHit = awView.pick(awCenter[0], awCenter[1])
5088
+ const awChannelsAfterUndo = JSON.stringify(awView.getOptions().channels)
5089
+ check('addLine 有通道声明时仍可落地与拾取,通道成员 / 分组不变', awHistoryLine.ok === true && awHistoryHit?.item?.id === 'aw-history' && awChannelsBeforeLine === JSON.stringify(awView.getOptions().channels), JSON.stringify({ hit: awHistoryHit?.item?.id }))
5090
+ check('addLine 的撤销只记一步:一次 undo 就摘掉这条回线', awHistoryState.undoCount === 1 && awUndo?.type === 'addLine' && awLine('aw-history') === undefined && awUndoHit?.item?.id !== 'aw-history' && awChannelsAfterUndo === awChannelsBeforeLine, JSON.stringify({ before: awHistoryState.undoCount, undo: awUndo, hit: awUndoHit?.item?.id }))
5091
+ const awRedo = awView.redo()
5092
+ await settle(20)
5093
+ const awRemovedLine = awView.removeLine('aw-history')
5094
+ check('addLine 重做后再删除:只摘回线,通道声明保持不变', awRedo?.type === 'addLine' && awRemovedLine.ok === true && awLine('aw-history') === undefined && JSON.stringify(awView.getOptions().channels) === awChannelsBeforeLine, JSON.stringify({ redo: awRedo, removed: awRemovedLine.ok }))
5095
+ check('有通道时 addLine 仍返回独立线路读数(channelId / channelLineIds 为 null)', awHistoryLine.channelId === null && awHistoryLine.channelLineIds === null && awHistoryLine.kind === 'line', JSON.stringify({ channelId: awHistoryLine.channelId, channelLineIds: awHistoryLine.channelLineIds }))
5096
+ awView.destroy()
5097
+ awHost.remove()
5098
+
4997
5099
  nwHost.remove()
4998
5100
  /* 这一轮新增:组件方法审计(getPointStyle / updateCopyPoint / setCurrentChannelGroup(null) / options 透传 / getElementBounds / 拐点中间点口径 / minimap.setZoom / off / runChannelAction 认不出的动作) */
4999
5101
  const auHost = document.createElement('div')
@@ -5475,6 +5577,104 @@
5475
5577
  clearedLayers === 1 && draftView.getWebGLLayers().length === 0 && (draftView.getOptions().webglLayers ?? []).length === 0 && draftView.requestRender() === draftView,
5476
5578
  JSON.stringify({ cleared: clearedLayers, layers: draftView.getWebGLLayers().length, options: draftView.getOptions().webglLayers?.length }))
5477
5579
 
5580
+ /* beginAddLine / beginAddChannel:单一通道阻断、已有通道新分组 / 指定分组、beginAddLine 挂回线 */
5581
+ const bdHost = document.createElement('div')
5582
+ bdHost.style.cssText = 'width:520px;height:320px'
5583
+ document.body.append(bdHost)
5584
+ const bdEvents = []
5585
+ const bdView = createGisView(bdHost, {
5586
+ points: [
5587
+ { id: 'bda', x: 0, y: 0, name: 'A' },
5588
+ { id: 'bdb', x: 80, y: 0, name: 'B' },
5589
+ { id: 'bdc', x: 0, y: 70, name: 'C' }
5590
+ ],
5591
+ lines: [],
5592
+ fitPadding: 60,
5593
+ showGrid: false,
5594
+ features: { channelDraft: true },
5595
+ onChannelDraft: payload => bdEvents.push(payload)
5596
+ })
5597
+ await settle(120)
5598
+ const bdSeed = bdView.addChannel({ id: 'bd-ch', name: '已有通道', from: 'bda', to: 'bdb' }, [
5599
+ { id: 'bd-l1' },
5600
+ { id: 'bd-l2' }
5601
+ ], [
5602
+ { id: 'bd-g1', lineIds: ['bd-l1'] },
5603
+ { id: 'bd-g2', lineIds: ['bd-l2'] }
5604
+ ])
5605
+ check('beginAddChannel 前置:已有通道带两个命名分组,供 singleChannel / newGroup / joinGroup 测试', bdSeed.ok === true && bdView.getChannelGroups('bd-ch').length === 2, JSON.stringify({ ok: bdSeed.ok, groups: bdView.getChannelGroups('bd-ch').map(group => group.id) }))
5606
+ const bdCanvas = bdView.canvas
5607
+ const bdClick = async (id) => {
5608
+ const point = bdView.getData().points.find(item => item.id === id)
5609
+ const screen = bdView.project(point.x, point.y)
5610
+ const rect = bdCanvas.getBoundingClientRect()
5611
+ for (const type of ['pointerdown', 'pointerup']) {
5612
+ bdCanvas.dispatchEvent(new PointerEvent(type, {
5613
+ bubbles: true,
5614
+ cancelable: true,
5615
+ composed: true,
5616
+ pointerId: 31,
5617
+ pointerType: 'mouse',
5618
+ isPrimary: true,
5619
+ button: 0,
5620
+ clientX: rect.left + screen[0],
5621
+ clientY: rect.top + screen[1]
5622
+ }))
5623
+ }
5624
+ await settle(50)
5625
+ }
5626
+
5627
+ bdView.beginAddLine({ from: 'bda', singleChannel: true })
5628
+ const bdLineStart = bdView.getAddChannelDraft()
5629
+ await bdClick('bdb')
5630
+ const bdLineReady = bdView.getAddChannelDraft()
5631
+ const bdLineBefore = bdView.getData().lines.length
5632
+ const bdLineResult = bdView.commitAddChannel()
5633
+ check('beginAddLine 不把 singleChannel 当成挂通道开关:两端已有通道仍出独立线路草稿', bdLineStart?.kind === 'line' && bdLineStart.singleChannel === false && bdLineStart.existingChannel === null && bdLineReady?.blocked === false, JSON.stringify({ kind: bdLineStart?.kind, single: bdLineStart?.singleChannel, existing: bdLineStart?.existingChannel, readyBlocked: bdLineReady?.blocked }))
5634
+ check('beginAddLine 提交后只加独立线路,不碰已有通道', bdLineResult.ok === true && bdLineResult.kind === 'line' && bdView.getData().lines.length === bdLineBefore + 1 && bdView.getChannelGroups('bd-ch').length === 2, JSON.stringify({ kind: bdLineResult.kind, channelId: bdLineResult.channelId, groups: bdView.getChannelGroups('bd-ch').length }))
5635
+
5636
+ bdEvents.length = 0
5637
+ const bdBlockLines = bdView.getData().lines.length
5638
+ bdView.beginAddChannel({ from: 'bda', singleChannel: true })
5639
+ await bdClick('bdb')
5640
+ const bdBlockDraft = bdView.getAddChannelDraft()
5641
+ const bdBlockResult = bdView.commitAddChannel()
5642
+ const bdBlockEvent = bdEvents.find(payload => payload.phase === 'blocked')
5643
+ check('singleChannel:两端已有通道时 ready 直接 blocked,并给出 singleChannel 原因', bdBlockDraft?.blocked === true && bdBlockDraft.existingChannel === 'block' && /singleChannel/.test(bdBlockDraft.reason ?? ''), JSON.stringify({ blocked: bdBlockDraft?.blocked, existing: bdBlockDraft?.existingChannel, reason: bdBlockDraft?.reason }))
5644
+ check('singleChannel:隐式提交发 blocked 事件且不改数据,事件仍报 channel / 当前电路数', bdBlockResult.ok === false && bdBlockEvent?.kind === 'channel' && bdBlockEvent?.singleChannel === true && bdView.getData().lines.length === bdBlockLines, JSON.stringify({ result: bdBlockResult.ok, event: bdBlockEvent?.phase, kind: bdBlockEvent?.kind, lines: bdView.getData().lines.length }))
5645
+
5646
+ bdEvents.length = 0
5647
+ bdView.beginAddChannel({ from: 'bda', singleChannel: true, existingChannel: 'newGroup' })
5648
+ await bdClick('bdb')
5649
+ const bdNewGroupDraft = bdView.getAddChannelDraft()
5650
+ const bdNewGroupResult = bdView.commitAddChannel()
5651
+ check('existingChannel: newGroup 覆盖 singleChannel:已有通道新加一组回线', bdNewGroupDraft?.blocked === false && bdNewGroupDraft.preferred === 'newGroup' && bdNewGroupResult.ok === true && bdNewGroupResult.channelId === 'bd-ch' && bdView.getChannelGroups('bd-ch').length === 3, JSON.stringify({ preferred: bdNewGroupDraft?.preferred, ok: bdNewGroupResult.ok, groupId: bdNewGroupResult.groupId, groups: bdView.getChannelGroups('bd-ch').length }))
5652
+
5653
+ bdEvents.length = 0
5654
+ bdView.beginAddChannel({ from: 'bda', existingChannel: 'joinGroup', groupId: 'bd-g1' })
5655
+ await bdClick('bdb')
5656
+ const bdJoinDraft = bdView.getAddChannelDraft()
5657
+ const bdJoinBefore = bdView.getChannelGroups('bd-ch').find(group => group.id === 'bd-g1')?.lineIndexes.length ?? 0
5658
+ const bdJoinResult = bdView.commitAddChannel()
5659
+ const bdJoinAfter = bdView.getChannelGroups('bd-ch').find(group => group.id === 'bd-g1')?.lineIndexes.length ?? 0
5660
+ check('existingChannel: joinGroup + groupId:新回线落进指定分组,读数带回 targetGroupId', bdJoinDraft?.targetGroupId === 'bd-g1' && bdJoinResult.ok === true && bdJoinResult.groupId === 'bd-g1' && bdJoinAfter === bdJoinBefore + 1, JSON.stringify({ draftTarget: bdJoinDraft?.targetGroupId, groupId: bdJoinResult.groupId, before: bdJoinBefore, after: bdJoinAfter }))
5661
+
5662
+ bdEvents.length = 0
5663
+ bdView.beginAddLine({ from: 'bda', existingChannel: 'joinGroup', groupId: 'bd-g2' })
5664
+ await bdClick('bdb')
5665
+ const bdAttachDraft = bdView.getAddChannelDraft()
5666
+ const bdAttachChannels = bdView.getOptions().channels.length
5667
+ const bdAttachResult = bdView.commitAddChannel()
5668
+ check('beginAddLine + existingChannel: joinGroup:线路入口也能把新回线挂进已有分组(不新建通道)', bdAttachDraft?.kind === 'line' && bdAttachDraft.existingChannel === 'joinGroup' && bdAttachDraft.targetGroupId === 'bd-g2' && bdAttachResult.ok === true && bdAttachResult.kind === 'channel' && bdAttachResult.groupId === 'bd-g2' && bdView.getOptions().channels.length === bdAttachChannels, JSON.stringify({ draftKind: bdAttachDraft?.kind, target: bdAttachDraft?.targetGroupId, resultKind: bdAttachResult.kind, groupId: bdAttachResult.groupId, channels: bdView.getOptions().channels.length }))
5669
+
5670
+ bdView.beginAddChannel({ from: 'bda', existingChannel: 'auto' })
5671
+ await bdClick('bdb')
5672
+ const bdAutoDraft = bdView.getAddChannelDraft()
5673
+ check('existingChannel: auto:有组余量时优先推荐新分组', bdAutoDraft?.blocked === false && bdAutoDraft.preferred === 'newGroup' && bdAutoDraft.existingChannel === 'auto', JSON.stringify({ preferred: bdAutoDraft?.preferred, existing: bdAutoDraft?.existingChannel }))
5674
+ bdView.cancelAddChannel()
5675
+ bdView.destroy()
5676
+ bdHost.remove()
5677
+
5478
5678
  /* 隐藏 / 显示、节点拆分、通道变化对账:接口级自测 */
5479
5679
  const splitHost = document.createElement('div')
5480
5680
  splitHost.style.cssText = 'width:640px;height:380px'
@@ -5591,6 +5791,38 @@
5591
5791
  lineOnlyView.removeLine('lo-ab')
5592
5792
  const lineOnlyAfterDelete = lineOnlyView.getChangedLines().find(line => line.id === 'lo-ab')
5593
5793
  check('getChangedLines:只有节点和线路时独立对账,删线路后同步清账', lineOnlyInitial && Boolean(lineOnlyChange) && lineOnlyChange.fields.includes('name') && !lineOnlyAfterDelete, JSON.stringify({ initial: lineOnlyInitial, change: lineOnlyChange?.reasons, after: lineOnlyAfterDelete ?? null }))
5794
+ const autoSplitHost = document.createElement('div')
5795
+ autoSplitHost.style.cssText = 'width:480px;height:280px'
5796
+ document.body.append(autoSplitHost)
5797
+ const autoSplitView = createGisView(autoSplitHost, {
5798
+ points: [
5799
+ { id: 'as-hub', x: 0, y: 0 },
5800
+ { id: 'as-a', x: 8, y: 0 },
5801
+ { id: 'as-b', x: 0, y: 8 }
5802
+ ],
5803
+ lines: [
5804
+ { id: 'as-c1', from: 'as-hub', to: 'as-a' },
5805
+ { id: 'as-c2', from: 'as-hub', to: 'as-a' },
5806
+ { id: 'as-i1', from: 'as-hub', to: 'as-b' },
5807
+ { id: 'as-i2', from: 'as-hub', to: 'as-b' },
5808
+ { id: 'as-i3', from: 'as-hub', to: 'as-a' }
5809
+ ],
5810
+ channels: [{ id: 'as-ch', from: 'as-hub', to: 'as-a', lineIds: ['as-c1', 'as-c2'] }],
5811
+ showGrid: false
5812
+ })
5813
+ const autoSplit = autoSplitView.splitPoint('as-hub', [
5814
+ { x: -1, y: 0, name: '解除 A' },
5815
+ { x: 1, y: 0, name: '解除 B' }
5816
+ ], { autoLines: 'alternate' })
5817
+ const autoAssignSlot = id => autoSplit.lineAssignments.find(assignment => assignment.lineIds.includes(id))?.index
5818
+ const autoChannelSlotA = autoAssignSlot('as-c1')
5819
+ const autoChannelSlotB = autoAssignSlot('as-c2')
5820
+ const autoSplitChannel = autoSplitView.getOptions().channels.find(channel => channel.id === 'as-ch')
5821
+ const autoSplitNewPoint = autoSplit.pointIds[1]
5822
+ check('splitPoint({ autoLines: alternate }):按通道成组轮流分配,两条同通道回线不会拆到不同节点', autoSplit.ok === true && autoSplit.autoLines === 'alternate' && autoSplit.autoAssignedLines === 5 && autoChannelSlotA !== undefined && autoChannelSlotA === autoChannelSlotB, JSON.stringify({ autoLines: autoSplit.autoLines, assigned: autoSplit.autoAssignedLines, slots: [autoChannelSlotA, autoChannelSlotB], assignments: autoSplit.lineAssignments.map(item => `${item.index}:${item.lineIds.join('+')}`) }))
5823
+ check('splitPoint({ autoLines: alternate }):通道端点跟随整组成员,lineAssignments 报出每一份的线路条数', autoSplit.lineAssignments.length === 2 && autoSplit.lineAssignments.reduce((sum, item) => sum + item.lineCount, 0) === 5 && String(autoSplitChannel?.from) === String(autoSplit.pointIds[autoChannelSlotA]) && String(autoSplitChannel?.to) === 'as-a' && autoSplitNewPoint !== 'as-hub', JSON.stringify({ channelFrom: autoSplitChannel?.from, newPoint: autoSplitNewPoint, counts: autoSplit.lineAssignments.map(item => item.lineCount) }))
5824
+ autoSplitView.destroy()
5825
+ autoSplitHost.remove()
5594
5826
  lineOnlyView.destroy()
5595
5827
  lineOnlyHost.remove()
5596
5828
  splitView.destroy()
@@ -42,9 +42,9 @@
42
42
  onClick: payload => push('click', payload),
43
43
  onDblClick: payload => push('dblclick', payload),
44
44
  onSelect: payload => push('select', payload),
45
- onPointerDown: (payload, event) => push('down', payload),
46
- onPointerUp: (payload, event) => push('up', payload),
47
- onContextMenu: (payload, event) => push('contextmenu', payload),
45
+ onPointerDown: payload => push('down', payload),
46
+ onPointerUp: payload => push('up', payload),
47
+ onContextMenu: payload => push('contextmenu', payload),
48
48
  onChannelDraft: payload => push('draft', payload),
49
49
  onChannelDraftClick: payload => push('draftclick', payload)
50
50
  })
@@ -536,7 +536,7 @@
536
536
  const labelMenuEvent = new MouseEvent('contextmenu', { bubbles: true, cancelable: true, button: 2, clientX: menuRect.left + 4, clientY: menuRect.top + 4 })
537
537
  firstMenu.dispatchEvent(labelMenuEvent)
538
538
  const menuPayload = menuPayloads[menuPayloads.length - 1]
539
- check('右键标签发 labelContextMenu:哪一条 / 内容 / 挂在谁 / 指针位置都在,系统菜单被拦下', menuPayload?.labelId === 'default' && menuPayload?.label?.content === '右键站' && menuPayload?.parent?.id === 'm1' && Math.abs(menuPayload.clientX - (menuRect.left + 4)) < 0.01 && labelMenuEvent.defaultPrevented === true, JSON.stringify({ labelId: menuPayload?.labelId, content: menuPayload?.label?.content, parent: menuPayload?.parent?.id, x: menuPayload?.clientX, prevented: labelMenuEvent.defaultPrevented }))
539
+ check('右键标签发 labelContextMenu:哪一条 / 内容 / 挂在谁 / 指针位置都在,系统菜单被拦下', menuPayload?.labelId === 'default' && menuPayload?.label?.content === '右键站' && menuPayload?.parent?.id === 'm1' && Math.abs(menuPayload.originalEvent.clientX - (menuRect.left + 4)) < 0.01 && menuPayload.clientX === undefined && labelMenuEvent.defaultPrevented === true, JSON.stringify({ labelId: menuPayload?.labelId, content: menuPayload?.label?.content, parent: menuPayload?.parent?.id, x: menuPayload?.originalEvent?.clientX, prevented: labelMenuEvent.defaultPrevented }))
540
540
  check('右键载荷里的 state 与偏移读数和点选同一套', menuPayload?.state === null && Array.isArray(menuPayload?.offset) && menuPayload.offset[0] === 0 && menuPayload.offset[1] === 0, JSON.stringify({ state: menuPayload?.state, offset: menuPayload?.offset }))
541
541
  }
542
542
  menuView.updateOptions({ preventContextMenu: false })