gis-view-webgl2 0.1.7 → 0.1.9

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 (70) hide show
  1. package/README.md +14 -11
  2. package/dist/gis-view.css +1 -1
  3. package/dist/index.cjs +1 -1
  4. package/dist/index.d.ts +445 -241
  5. package/dist/index.js +1 -1
  6. package/docs/AI/345/257/271/350/257/235/346/261/207/346/200/273.md +65 -498
  7. package/docs/GisView.md +1 -1
  8. package/docs/GisView/345/256/236/347/216/260/345/216/237/347/220/206.md +1 -1
  9. package/docs/ai/06-/344/274/232/350/257/235/347/264/242/345/274/225/344/270/216/345/244/215/346/237/245/346/226/271/346/263/225.md +114 -478
  10. package/docs/demos/BasicDemo.vue +144 -27
  11. package/docs/demos/BatchAddDemo.vue +133 -7
  12. package/docs/demos/ChannelDemo.vue +61 -0
  13. package/docs/demos/ConnectDemo.vue +164 -33
  14. package/docs/demos/CopyMergeDemo.vue +144 -42
  15. package/docs/demos/EventDemo.vue +29 -24
  16. package/docs/demos/LabelDemo.vue +5 -5
  17. package/docs/demos/api-index.js +53 -41
  18. package/docs/demos/gis-view-demo/GisViewDemoCardHeader.vue +1 -1
  19. package/docs/demos/gis-view-demo/GisViewDemoStage.vue +1 -1
  20. package/docs/demos/gis-view-demo/panels/DataPanels.vue +1 -1
  21. package/docs/demos/gis-view-demo/useGisViewDemo.js +31 -33
  22. package/docs/format/02-/350/212/202/347/202/271/344/270/216/347/272/277/350/267/257.md +1 -0
  23. package/docs/format/04-/351/200/232/351/201/223/344/270/216/346/240/207/347/255/276.md +3 -0
  24. 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
  25. package/docs/from0/05-/347/233/270/346/234/272/344/270/216/344/272/244/344/272/222.md +1 -1
  26. 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
  27. package/docs/index.html +136 -13
  28. package/docs/internals/15-04-/351/200/220/350/275/256/350/256/260/345/275/225-/347/203/255/345/212/233/345/233/276/344/270/216/346/200/247/350/203/275.md +10 -0
  29. package/docs/manual/02-/345/235/220/346/240/207/344/270/216/346/225/260/346/215/256.md +1 -1
  30. package/docs/manual/03-/351/205/215/347/275/256/351/241/271.md +82 -14
  31. package/docs/manual/04-/344/272/213/344/273/266/344/270/216/345/233/236/350/260/203.md +23 -17
  32. package/docs/manual/05-/347/224/273/350/264/250/344/270/216/346/211/271/351/207/217/346/233/264/346/226/260.md +65 -0
  33. 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
  34. 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 +100 -40
  35. package/docs/manual/08-/351/200/211/344/270/255/344/270/216/346/213/220/347/202/271.md +11 -7
  36. package/docs/manual/09-/351/200/232/351/201/223/344/270/216/345/244/232/345/233/236/347/272/277.md +66 -6
  37. 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
  38. package/docs/manual/12-/346/240/207/347/255/276/344/270/216/350/243/205/351/245/260.md +28 -5
  39. package/docs/manual/14-/350/247/206/345/233/276/344/270/216/347/255/233/351/200/211.md +46 -14
  40. package/docs/manual/15-/345/257/274/345/207/272/344/270/216/351/233/206/346/210/220.md +5 -2
  41. package/docs/manual/16-/345/256/236/344/276/213/346/226/271/346/263/225/346/200/273/350/241/250.md +34 -29
  42. package/docs/manual/17-/345/217/202/346/225/260/351/200/237/346/237/245.md +30 -22
  43. package/docs/manual/18-/351/200/220/346/226/271/346/263/225/347/244/272/344/276/213.md +168 -81
  44. 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
  45. package/docs/manual/20-/346/200/247/350/203/275/344/270/216/345/244/247/346/225/260/346/215/256.md +5 -3
  46. 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 +9 -6
  47. package/docs/steps/README.md +201 -120
  48. package/docs/steps/bench-scale.html +1 -1
  49. package/docs/steps/index.html +222 -6
  50. package/docs/steps/selftest-actions.html +26 -25
  51. package/docs/steps/selftest-data.html +1 -1
  52. package/docs/steps/selftest-demos.html +290 -16
  53. package/docs/steps/selftest-docs.html +17 -6
  54. package/docs/steps/selftest-features.html +594 -54
  55. package/docs/steps/selftest-full.html +3 -3
  56. package/docs/steps/selftest-labels.html +82 -1
  57. package/docs/steps/selftest-perf.html +62 -7
  58. package/docs/steps/selftest-select.html +71 -6
  59. package/docs/steps/step-05-pick.js +4 -8
  60. package/docs/steps/step-07-channels.js +3 -5
  61. package/docs/steps/step-08-labels.js +3 -3
  62. package/docs/steps/step-10-full.js +3 -3
  63. package/docs/steps/step-12-events.js +41 -24
  64. package/docs//344/273/2160/345/210/260/345/256/214/346/225/264/347/232/204/345/256/236/347/216/260.md +1 -0
  65. package/package.json +1 -1
  66. package/docs/ai/01-/350/265/267/346/272/220/344/270/216/350/277/231/344/273/275/346/261/207/346/200/273/347/232/204/347/224/261/346/235/245.md +0 -29
  67. package/docs/ai/02-/346/214/211/345/206/205/345/256/271/345/210/206/347/261/273-/350/277/231/344/270/200/350/267/257/351/203/275/345/234/250/346/224/271/344/273/200/344/271/210.md +0 -66
  68. package/docs/ai/03-/351/207/215/350/246/201/351/227/256/347/255/224/351/200/211/357/274/210/344/270/200/357/274/211/357/274/232/351/200/211/345/236/213/351/200/232/351/201/223/344/270/216/346/240/267/345/274/217.md +0 -178
  69. package/docs/ai/04-/351/207/215/350/246/201/351/227/256/347/255/224/351/200/211/357/274/210/344/272/214/357/274/211/357/274/232/344/272/244/344/272/222/346/200/247/350/203/275/344/270/216/346/225/260/346/215/256.md +0 -140
  70. package/docs/ai/05-/351/207/215/350/246/201/351/227/256/347/255/224/351/200/211/357/274/210/344/270/211/357/274/211/357/274/232/345/256/277/344/270/273/346/227/201/346/224/257/344/270/216/346/224/266/345/217/243.md +0 -43
@@ -21,7 +21,7 @@ const {
21
21
  <p>Ctrl + 点击线路或通道加点之后,拐点不会分散写进每条回线,而是归到一个新的拐点集合里:一个分组一组(没拆分过就是一条通道一组),组里有 member_line_ids(哪些回线共用)与 group_route(两端 + 拐点),结构对齐 test.json 的 parallel_groups;「导出拐点集合」按钮会把这份集合复制到剪贴板,可以直接回写业务数据。</p>
22
22
  <p>这一轮补上三块:① 通道可以分组了——默认一条通道一组,「拆分分组」按每组几条回线把它拆开(第一组沿用当前拐点、新分组的线路不带拐点),「合并分组」把其余分组并进第一组(被并入的线路跟随第一组的拐点,第一组没有拐点就一条都不带),加点 / 拖点只作用于同一分组,分组声明写回 channels[].groups;② 鹰眼图也支持规则——「规则沿用主图」只搬颜色与层级(主图的像素尺寸不会搬过来),「按电压单独配色」给小图自己配一份(半径缩到 1 ~ 2.6px、高电压用方块、zIndex 沿用电压分档),「分层」控制分层叠画、层级下拉只画某几层,「放大节点 / 放大线路」让节点与线路跟着小图自身的倍数放大(滚轮或 - / + 都算);③ 定向更新——updatePoint / updateLine / updateChannel 改一个节点、线路或通道就重渲染相关的视图,只重解析受影响的那一小片,其余要素与缓存一个字节都不动,鹰眼图与标签覆盖层跟着重画。</p>
23
23
  <p>热力图这一轮把节点那一层补上了:合成数据的节点与线路都按一张「热区场」随机生成 max_loading_rate(在数据范围里撒 4 ~ 6 个热区,节点取自己坐标处的场值、线路取两端中点处的场值 —— 画出来是几团往外化开的色斑,而不是均匀噪点),作用对象因此可以切「节点 + 线路 + 通道」/「只按节点上色」/「线路 + 通道回线」/「只按通道自己的值」四档;面板上的「重新随机一份热力数据」能在任意数据集上现生成一份(换种子、按当前节点范围重建场,只改字段值,坐标与拓扑一动不动),真实数据 new.json 的节点本来没有这一列、线路那一列又恒为 1.5,点一下就能立刻看到热力。作用对象带上节点时图标不再跟着换:图标符号写了 tint(逐实例着色),填充按白色烘焙、由热力色逐实例染上,描边写 currentStrokeColor 不参与染色 —— 形状与描边还是那张图标,只是填充跟着值走;贴图缓存键里因此不再有逐点的热力色,900 节点时绘制调用仍然在 36 上下(以前一个节点一张贴图时会涨到约 1500,10 万级直接把主线程烤住),想看「完全不参与热力图」的效果就把节点从作用对象里去掉 —— 图标本身不会变成内置圆 / 方块。</p>
24
- <p>这一轮补上六件事:① <b>节点复制</b>——按住 Alt(面板上可以改成别的键)点一个节点,复制品立刻压在源节点上跟着鼠标走,松开左键(不再看 Alt 什么时候松)或再点一下落地;也可以点面板上的「先点按钮再选节点」再点一个节点(复制品落在它旁边),或者选中节点按 Ctrl+C、把鼠标移到目标位置按 Ctrl+V。复制全程走 <b>onPointCopy</b>:start / move / commit / end / cancel(复制到剪贴板另发一条 clipboard),落地前的 commit 阶段可以 payload.cancel() 撤掉、payload.update() / setItem() 改数据,id 与整份数据由 copy.createId / copy.createItem 定;落地之后的 end 阶段还能 payload.revert() 整份回滚(面板上「复制后模拟接口失败」那个勾就是它)。② <b>节点合并</b>——把一个节点拖到另一个节点上(碰撞按两个节点画在屏幕上的半径之和 + merge.distance 判,整组拖动不参与),<b>组件不弹任何对话框</b>:pending 停在原地等使用方裁决,面板上的「合并确认」三档分别是「停住(由这个自写弹层来确认)」/ false 直接合并 / 函数返回 Promise(先调接口再决定,resolve(false) 取消);确认会先推进到 confirm 阶段并把合并结果给你(可以改,也可以在这一步因为「接口失败」整份撤掉),确认之后来源节点的数据并进目标(缺省 { ...来源, ...目标 },可用 merge.createItem 整份接管)、<b>尺寸取双方最大、图标优先目标</b>、绑在它上面的线路改绑、合并出来的自环与零长线删掉;两端因此撞成同一条的通道按 merge.channelGroups 处理(各自保留分组 / 合成一个分组,自环的整条删),读数里会带上改端点的通道数与并掉的通道数。取消或接口失败则把拖动的节点与跟随它的线路还原回原位,并补发一条拖拽结束。③ <b>所有快捷键都能改</b>——options.shortcuts 按动作配(框选 / 多选 / 加点 / 复制 / 复制到剪贴板 / 粘贴 / 取消 / 删除 / 适应视图),写法是纯修饰键(ctrl / meta / shift / alt / mod)、组合键(ctrl+c)、单键(escape / 0)或数组,写 false 表示不绑定;面板「视图 · 交互」里那四个下拉就是改框选 / 多选 / 加点 / 复制的修饰键——比如把框选改成 Shift,之后 Shift + 拖动才出选框,按住 Shift 时连光标变体也跟着走。④ <b>选中抬层</b>——selectionOnTop(缺省开)把选中的节点、拖动中的整组、待确认合并的目标节点与选中线路所在的整条通道画在所有要素之上:选中了就一定看得见,与 zIndex 无关;关掉就完全按 zIndex 与数据顺序画。⑤ <b>删除(Delete)</b>——选中拐点 / 线路 / 节点之后按 Delete(或面板上的「删除选中」)先出一份删除载荷:要删什么、连带什么、能按哪些范围删。<b>组件不弹界面</b>:pending 停在原地等 <b>confirm(scope)</b> / cancel(),Esc 也能取消;范围三档是「只删选中的线路」/「删除整个分组」/「删除整条通道」(后两档只在删的是通道回线、且这条通道分过组时才有),选中的拐点与节点一律一起删 —— <b>删节点会连挂在它身上的线路、以及拿它当端点的整条通道一起删</b>,载荷里的 lineCount / channelCount 就是这份连带读数;确认之后整段合成一条历史,撤销一步全回来;开关是 features.remove,快捷键照样走 shortcuts.remove(可以单独配成 shift+delete 之类)。⑥ <b>通道选中模式</b>——channelSelectMode 决定点中一条通道回线时选中什么:line(缺省,只选这一条)/ group(连它所在分组一起)/ channel(整条通道一起);在 group / channel 下 Ctrl + 点击是<b>整批取反</b>,通道以外的线路永远只选它自己。它只影响「下一次点选怎么摊开」,切换走 updateOptions,场景一个字节都不用重排。面板「视图 · 复制合并」里那两项就是它们。</p>
24
+ <p>这一轮补上六件事:① <b>节点复制</b>——按住 Alt(面板上可以改成别的键)点一个节点,复制品立刻压在源节点上跟着鼠标走,松开左键(不再看 Alt 什么时候松)或再点一下落地;也可以点面板上的「先点按钮再选节点」再点一个节点(复制品落在它旁边),或者选中节点按 Ctrl+C、把鼠标移到目标位置按 Ctrl+V。复制全程走 <b>onPointCopy</b>:start / move / commit / end / cancel(复制到剪贴板另发一条 clipboard),落地前的 commit 阶段可以 payload.cancel() 撤掉、payload.update() 改数据,id 与整份数据由 copy.createId / copy.createItem 定;落地之后的 end 阶段还能 payload.revert() 整份回滚(面板上「复制后模拟接口失败」那个勾就是它)。② <b>节点合并</b>——把一个节点拖到另一个节点上(碰撞按两个节点画在屏幕上的半径之和 + merge.distance 判,整组拖动不参与),<b>组件不弹任何对话框</b>:pending 停在原地等使用方裁决,面板上的「合并确认」三档分别是「停住(由这个自写弹层来确认)」/ false 直接合并 / 函数返回 Promise(先调接口再决定,resolve(false) 取消);确认会先推进到 confirm 阶段并把合并结果给你(可以改,也可以在这一步因为「接口失败」整份撤掉),确认之后来源节点的数据并进目标(缺省 { ...来源, ...目标 },可用 merge.createItem 整份接管)、<b>尺寸取双方最大、图标优先目标</b>、绑在它上面的线路改绑、合并出来的自环与零长线删掉;两端因此撞成同一条的通道按 merge.channelGroups 处理(各自保留分组 / 合成一个分组,自环的整条删),读数里会带上改端点的通道数与并掉的通道数。取消或接口失败则把拖动的节点与跟随它的线路还原回原位,并补发一条拖拽结束。③ <b>所有快捷键都能改</b>——options.shortcuts 按动作配(框选 / 多选 / 加点 / 复制 / 复制到剪贴板 / 粘贴 / 取消 / 删除 / 适应视图),写法是纯修饰键(ctrl / meta / shift / alt / mod)、组合键(ctrl+c)、单键(escape / 0)或数组,写 false 表示不绑定;面板「视图 · 交互」里那四个下拉就是改框选 / 多选 / 加点 / 复制的修饰键——比如把框选改成 Shift,之后 Shift + 拖动才出选框,按住 Shift 时连光标变体也跟着走。④ <b>选中抬层</b>——selectionOnTop(缺省开)把选中的节点、拖动中的整组、待确认合并的目标节点与选中线路所在的整条通道画在所有要素之上:选中了就一定看得见,与 zIndex 无关;关掉就完全按 zIndex 与数据顺序画。⑤ <b>删除(Delete)</b>——选中拐点 / 线路 / 节点之后按 Delete(或面板上的「删除选中」)先出一份删除载荷:要删什么、连带什么、能按哪些范围删。<b>组件不弹界面</b>:pending 停在原地等 <b>confirm(scope)</b> / cancel(),Esc 也能取消;范围三档是「只删选中的线路」/「删除整个分组」/「删除整条通道」(后两档只在删的是通道回线、且这条通道分过组时才有),选中的拐点与节点一律一起删 —— <b>删节点会连挂在它身上的线路、以及拿它当端点的整条通道一起删</b>,载荷里的 lineCount / channelCount 就是这份连带读数;确认之后整段合成一条历史,撤销一步全回来;开关是 features.remove,快捷键照样走 shortcuts.remove(可以单独配成 shift+delete 之类)。⑥ <b>通道选中模式</b>——channelSelectMode 决定点中一条通道回线时选中什么:line(缺省,只选这一条)/ group(连它所在分组一起)/ channel(整条通道一起);在 group / channel 下 Ctrl + 点击是<b>整批取反</b>,通道以外的线路永远只选它自己。它只影响「下一次点选怎么摊开」,切换走 updateOptions,场景一个字节都不用重排。面板「视图 · 复制合并」里那两项就是它们。</p>
25
25
  <p>定位口径这一轮统一了:<code>focusOn(target)</code> 缺省只把中心移到目标上,比例一点不动;要按目标范围适配必须显式写 <code>{ zoom: true }</code>,可再配 <code>padding</code> / <code>maxZoom</code>。节点、线路、通道与分组走同一条规则,搜索面板与画布菜单里需要适配的按钮都已经写成显式开启。</p>
26
26
  </div>
27
27
  <span class="demo-badge">WebGL2 · 无内置 GUI</span>
@@ -67,7 +67,7 @@ const {
67
67
  <span>world ({{ canvasMenu.world[0].toFixed(2) }}, {{ canvasMenu.world[1].toFixed(2) }}) · 选中 {{ canvasMenu.selection?.count ?? 0 }} 个</span>
68
68
  <button type="button" @click="closeCanvasMenu">×</button>
69
69
  </div>
70
- <p class="demo-channel-menu-hint">右键载荷(onContextMenu)里就带着 x / y、world、命中的要素与 selection 读数:这份菜单整个写在 demo 里,组件不带任何界面。每一行都是一个公开方法;落在空白处先 preventDefault 掉浏览器自带菜单。空白处多一行「新增场站」:弹一份 demo 自己的表单,收集字段后走 addPoint,落点默认就是这一下右键的 world 坐标。</p>
70
+ <p class="demo-channel-menu-hint">右键载荷(onContextMenu)里带着命中的要素、world 与 originalEvent,选中集读数由 view.getSelectionInfo() / getSelectionData() 取:这份菜单整个写在 demo 里,组件不带任何界面。每一行都是一个公开方法;落在空白处先对 originalEvent 调 preventDefault 收掉浏览器自带菜单。空白处多一行「新增场站」:弹一份 demo 自己的表单,收集字段后走 addPoint,落点默认就是这一下右键的 world 坐标。</p>
71
71
  <div class="demo-channel-menu-actions">
72
72
  <button v-for="row in canvasMenuRows" :key="row.id" type="button" :data-demo="'canvas-menu-' + row.id" :disabled="!row.enabled" :title="row.reason" @click="runCanvasMenuAction(row)">{{ row.label }}</button>
73
73
  </div>
@@ -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()
@@ -15,6 +15,7 @@
15
15
  | `shadow` | `GisViewShadow` | 无(不画阴影) | 阴影:`{ color, blur, offsetX, offsetY }`,偏移与模糊都是 CSS 像素 |
16
16
  | `visible` | `boolean` | `true` | `false` 时这个节点不画(等价于被筛选隐藏,但数据还在);被它锚定的通道与回线一起消失 |
17
17
  | `clickable` / `draggable` / `copyable` / `mergeable` | `boolean` | 前两个跟随 `options.clickable` / `options.draggable`(**`options.draggable` 缺省关**:不写它就没有节点可拖,逐条的 `draggable: true` 也得等这个总开关或者 `features.drag` 打开才生效);`copyable` / `mergeable` 缺省 `true` | 逐条覆盖 `options` 上的同名开关 |
18
+ | `mergeInfo` | `GisViewMergeInfo \| null` | 无 | `mergePoints` / `mergeSelectedPoints` 写入的合并追溯(版本 `1`):保留节点保存 `targetPoint`、`sourceIds`、来源 `points` 快照与受影响 `lines` / `channels` 快照;`splitPoint(source)` 省略 `parts` 时据此自动解除合并。可 JSON 序列化,随数据导出;保留字段,业务数据不要占用。字段详解见 [manual 07 的「合并追溯」](../manual/07-%E6%8C%87%E9%92%88%E4%B8%8E%E8%A6%81%E7%B4%A0%E7%BC%96%E8%BE%91.md#%E5%90%88%E5%B9%B6%E8%BF%BD%E6%BA%AFmergeinfo) |
18
19
  | `cursor` / `ctrlCursor` | `GisViewCursorValue` | 跟随 `options.cursorStyle.point` / `cursorStyle.ctrl.point` | 悬停这个点时的光标(按住 Ctrl / ⌘ 时用后者),写法见 GisView.md 的「光标」 |
19
20
  | `hoverStyle` / `selectStyle` / `multiSelectStyle` / `boxSelectStyle` | `GisViewPointStyle` | 分别跟随 `options.pointHoverStyle` / `pointSelectStyle` / `pointMultiSelectStyle` / `pointBoxSelectStyle`(后两个缺省回落到选中样式) | 逐条覆盖悬停 / 选中 / Ctrl 多选 / 框选样式,按字段覆盖自身样式 |
20
21
  | `labels` | `GisViewItemLabel \| GisViewItemLabel[]` | 无(不写且没配 `label` 就不画标签) | 这个点上的标签清单(可以多条),见下文「标签」;`labelVisible` / `labelVisibility` / `labelPosition` / `labelOffset` 是旧式单条写法 |
@@ -11,6 +11,7 @@
11
11
  | `from` / `to` | `string \| number` | **必填** | 两端节点 id,节点不存在时整条通道与回线一起不显示;两端都要落在节点上,只声明一端不行 |
12
12
  | `circuits` | `number` | `1` | 回线数量(`createChannels` 用它展开回线) |
13
13
  | `lineIds` | `Array<string \| number>` | 无(`createChannels` 生成的那一份就是它) | 组成这条通道的线路 id,**顺序即回线顺序**(沿骨架左法线从负到正) |
14
+ | `mergeInfo` | `GisViewMergeInfo \| null` | 无 | 合并后仍存在的受影响通道带轻量追溯:`{ version, targetId, sourceIds, originalIds }`;`originalIds` 是参与合并、被改端点或被并掉的原始通道 id,便于后端对账。可 JSON 序列化;保留字段,业务数据不要占用。完整快照在保留节点的 `mergeInfo` 上 |
14
15
  | `path` | `GisViewCoordinate[]` | 无(两端直连) | 骨架的**中间拐点**(不含两端,两端由节点坐标补齐):与线路的 `path` 相反,这里只写中间 |
15
16
  | `groups` | `GisViewChannelGroup[]` | 无(= 一个通道一组) | 分组声明:`[{ id, lineIds, path, waypointOffsets? }]`,一组一条走廊;拆分 / 合并的结果写在这里,见「分组」 |
16
17
  | `width` / `offsetMode` / `inset` / `tangentInset` | `number` / `'tangent' \| 'inset'` / `number` / `number \| null` | 分别回落到 `options.channelWidth`(`0.02`)/ `'tangent'` / `options.channelInset`(`0`)/ `options.channelTangentInset`(`null` = 按回线线宽收) | 通道外观:宽度(`tangent` 下是最外侧两条回线的中心距)、偏移方式、内收比例(负数为外收)、两端切点内收 |
@@ -19,6 +20,8 @@
19
20
  | `lineProps` / `linePropsByMember` | `Partial<GisViewLine>` / 数组 | 无 | 回线上透传的业务字段(`lineProps` 全体共用、`linePropsByMember[i]` 只作用第 i 回,后者优先) |
20
21
  | `channelProps` | `Partial<GisViewChannel>` | 无 | 通道上额外透传的字段(原样带到通道声明里) |
21
22
 
23
+ `channelSimplify` 是视图级渲染选项(`options.channelSimplify`),不是通道数据字段:它可以让分组在回线数超过阈值时只画一条代表线,但不写入 `channels`、不改 `lineIds` / `groups`,也不会出现在 `exportData()` 的通道字段里。
24
+
22
25
  ### 通道分组(拐点分组 / parallel_groups)
23
26
 
24
27
  - **一组**(默认):`lineIds` 是全部成员、`path` 是这一组的拐点;`getChannelGroups(id)` 返回一组。
@@ -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