gis-view-webgl2 0.1.6 → 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 (55) hide show
  1. package/README.md +9 -8
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.d.ts +316 -233
  4. package/dist/index.js +1 -1
  5. package/docs/AI/345/257/271/350/257/235/346/261/207/346/200/273.md +106 -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 +110 -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 +7 -7
  14. package/docs/demos/StyleDemo.vue +1 -1
  15. package/docs/demos/api-index.js +34 -31
  16. package/docs/demos/gis-view-demo/GisViewDemoCardHeader.vue +1 -1
  17. package/docs/demos/gis-view-demo/GisViewDemoStage.vue +1 -1
  18. package/docs/demos/gis-view-demo/panels/DataPanels.vue +1 -1
  19. package/docs/demos/gis-view-demo/useGisViewDemo.js +33 -35
  20. 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
  21. package/docs/from0/05-/347/233/270/346/234/272/344/270/216/344/272/244/344/272/222.md +1 -1
  22. 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
  23. package/docs/manual/02-/345/235/220/346/240/207/344/270/216/346/225/260/346/215/256.md +1 -1
  24. package/docs/manual/04-/344/272/213/344/273/266/344/270/216/345/233/236/350/260/203.md +49 -16
  25. 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
  26. 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 +60 -36
  27. package/docs/manual/08-/351/200/211/344/270/255/344/270/216/346/213/220/347/202/271.md +60 -4
  28. package/docs/manual/09-/351/200/232/351/201/223/344/270/216/345/244/232/345/233/236/347/272/277.md +17 -1
  29. 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
  30. package/docs/manual/12-/346/240/207/347/255/276/344/270/216/350/243/205/351/245/260.md +2 -2
  31. package/docs/manual/14-/350/247/206/345/233/276/344/270/216/347/255/233/351/200/211.md +12 -13
  32. package/docs/manual/16-/345/256/236/344/276/213/346/226/271/346/263/225/346/200/273/350/241/250.md +26 -23
  33. package/docs/manual/17-/345/217/202/346/225/260/351/200/237/346/237/245.md +23 -20
  34. package/docs/manual/18-/351/200/220/346/226/271/346/263/225/347/244/272/344/276/213.md +132 -79
  35. 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
  36. package/docs/manual/20-/346/200/247/350/203/275/344/270/216/345/244/247/346/225/260/346/215/256.md +2 -1
  37. 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
  38. package/docs/steps/README.md +9 -7
  39. package/docs/steps/bench-scale.html +1 -1
  40. package/docs/steps/index.html +6 -6
  41. package/docs/steps/selftest-actions.html +26 -25
  42. package/docs/steps/selftest-data.html +1 -1
  43. package/docs/steps/selftest-demos.html +102 -6
  44. package/docs/steps/selftest-docs.html +3 -3
  45. package/docs/steps/selftest-features.html +284 -52
  46. package/docs/steps/selftest-full.html +3 -3
  47. package/docs/steps/selftest-labels.html +1 -1
  48. package/docs/steps/selftest-perf.html +31 -4
  49. package/docs/steps/selftest-select.html +183 -1
  50. package/docs/steps/step-05-pick.js +4 -8
  51. package/docs/steps/step-07-channels.js +3 -5
  52. package/docs/steps/step-08-labels.js +3 -3
  53. package/docs/steps/step-10-full.js +3 -3
  54. package/docs/steps/step-12-events.js +41 -24
  55. package/package.json +1 -1
@@ -65,25 +65,45 @@ view.updateOptions({ cascadeHighlight: false }) // 关掉并收掉当前高亮
65
65
 
66
66
  ## 指针事件
67
67
 
68
- 鼠标的每一种动作都有回调,载荷里都能拿到「当前是哪个元素」:`onHover`(悬停)、`onClick`(单击左键)、`onDblClick`(双击)、`onPointerDown` / `onPointerUp`(按下与抬起,含左 / 中 / 右键)、`onContextMenu`(右键)。悬停是 `GisViewHoverPayload`、点击与双击是 `GisViewClickPayload` —— 都是在 `GisViewPickResult`(点 / 线 / 通道回线的 `type`、`index`、`item`、`screen`、`coordinate` 与通道字段)上再补光标位置与一份 `action`;按下 / 抬起 / 右键统一发 `GisViewPointerPayload`(同样是拾取结果 + `action`)。
68
+ 鼠标的每一种动作都有回调,载荷里都能拿到「当前是哪个元素」:`onHover`(悬停)、`onClick`(单击左键)、`onDblClick`(双击)、`onPointerDown` / `onPointerUp`(按下与抬起,含左 / 中 / 右键)、`onContextMenu`(右键)。悬停是 `GisViewHoverPayload`、点击与双击是 `GisViewClickPayload` —— 都是在 `GisViewPickResult`(点 / 线 / 通道回线的 `type`、`index`、`item`、`screen`、`coordinate`,以及通道回线的 `group` / `groups`)上再补光标位置与一份 `action`;按下 / 抬起 / 右键统一发 `GisViewPointerPayload`(同样是拾取结果 + `action`)。
69
69
 
70
70
  ### action:一次操作的判读
71
71
 
72
- 指针事件、拐点、标签与选中集事件都带一份 `action`,回答三个问题:**这是什么操作**(`action.type`)、**点在了什么上面**(`action.target`)、**能拿到什么数据**(`action.data`):
72
+ 指针事件、拐点、标签与选中集事件都带一份 `action`,回答两个问题:**这是什么操作**(`action.type`)、**点在了什么上面**(`action.target`)。命中的数据不重复打包,直接在载荷顶层读:
73
73
 
74
74
  | 字段 | 取值 | 说明 |
75
75
  | --- | --- | --- |
76
76
  | `action.type` | 动作名 | 按下时是**意图**(`dragPoint` 拖节点 / `dragWaypoint` 拖拐点 / `boxSelect` 框选 / `pan` 平移 / `copy` 复制 / `channelDraft` 点两次加通道 / `decorationDraft` 动态装饰器草稿 / `contextMenu` 右键 / `click` 普通左键),松开时是**刚做完的那件事**(没拖就是 `click`),点击时是**真正发生的那一件**(`select` 选中 / `clearSelection` 点空白 / `click` 没开选中时的点击);拐点是 `addWaypoint` / `removeWaypoint` / `selectWaypoint` / `dragWaypoint`,标签是 `selectLabel` / `dragLabel` / `contextMenu` / `dblClick`,选中集是 `select` / `toggleSelect` / `boxSelect` / `clearSelection`,悬停固定是 `hover` |
77
77
  | `action.target` | `point` / `line` / `channel` / `waypoint` / `label` / `selection` / `empty` | 点在了什么上面:通道回线的 `type` 也是 `line`,但要按通道处理,所以 `target` 单独给 `channel`;点空白是 `empty`;拐点与标签各有自己的 `waypoint` / `label` |
78
- | `action.data` | 对象 / `null` | 按 `target` 直接取到的那份数据:节点 / 线路是数据里那个对象(与 `getData()` 同一份引用)、通道回线是通道定义、选中集是这一批 `items`、拐点是 `{ lineIndex, pathIndex, coordinate, line }`、标签是 `{ label, parent }`、空白处是 `null` |
79
- | `action.button` | `left` / `middle` / `right` / `none` | 鼠标键名,与载荷里的 `button` 数字一一对应;悬停没有按键,固定是 `'none'` |
80
- | `action.modifiers` | `{ ctrl, meta, shift, alt }` | 修饰键,与原生事件一一对应 —— 「按住 Shift 是另一套菜单」这类判断不用再读 `originalEvent` |
81
- | `action.left` / `action.right` | 布尔 | 左键 / 右键的一眼判定(与 `action.button` 同一份:`left` 就是 `button === 'left'`),比比对字符串少写一点;中键两边都是 `false` |
78
+ | 命中数据 | 载荷顶层 | 点 / 线读 `item`(与数据里同一份对象);通道回线读 `channel` / `channelContext`;选中集变更读 `items`;拐点读 `lineIndex` / `pathIndex` / `coordinate` / `line`;标签读 `label` / `parent`;聚合读 `id` / `count` / `pointIds` / `bounds` |
79
+ | 原始输入 | `originalEvent` | 鼠标键(`button` / `buttons`)、修饰键(`ctrlKey` / `metaKey` / `shiftKey` / `altKey`)与 `clientX` / `clientY` 都从原始事件读;程序化 `hoverAt()` 没有这一份 |
82
80
  | `action.moved` | 布尔 | 这一次按下之后有没有真的拖动过:按下恒为 `false`,松开时说明是「拖」还是「点」,拐点拖动是 `start` 为 `false`、`move` / `end` 为 `true` |
83
81
  | `action.double` | 布尔 | 只在点击载荷里出现:`true` 说明这一下是双击里的第二下(单双击要做两件事的界面按它推迟一下) |
84
82
  | `action.press` | 动作名 / `null` | 只在松开 / 点击载荷里出现:**按下时的意图**(与那一刻 `onPointerDown` 的 `action.type` 同一份)——「按下想拖、松开成了点」这类对照看它;没有配对的按下事件时是 `null` |
85
83
  | `action.duration` | 毫秒 | 只在松开 / 点击载荷里出现:这一次按下按了多久(四舍五入),没有配对的按下事件时是 `0` |
86
- | `action.longPress` | 布尔 | 只在松开 / 点击载荷里出现:这一次算不算长按(超过 500 ms 且中间没拖动过 —— 触摸 / 触控笔按它出菜单,`action.button` 仍然是 `'left'`) |
84
+ | `action.longPress` | 布尔 | 只在松开 / 点击载荷里出现:这一次算不算长按(超过 500 ms 且中间没拖动过 —— 触摸 / 触控笔按它出菜单;鼠标键仍看 `originalEvent.button`) |
85
+
86
+ ### 载荷迁移:旧字段 → 新入口
87
+
88
+ 这一版把能从别处取得的字段从回调载荷里删掉了。旧代码按下表改一行即可;`selected` / `wasSelected` 是事件发生时的状态快照,保留:
89
+
90
+ | 旧字段 / 写法 | 现在从哪取 |
91
+ | --- | --- |
92
+ | `payload.selection` | `view.getSelectionInfo()`(`{ ids, count, points, lines, channels }`,结果按选中集引用缓存) |
93
+ | `action.data`(点 / 线) | `payload.item` |
94
+ | `action.data`(通道回线) | `payload.channel` / `payload.channelContext` |
95
+ | `action.data`(选中集) | `payload.items` |
96
+ | `action.data`(拐点) | `payload.lineIndex` / `pathIndex` / `coordinate` / `line` |
97
+ | `action.data`(标签) | `payload.label` / `payload.parent` |
98
+ | `action.data`(聚合) | 载荷顶层 `id` / `count` / `pointIndexes` / `pointIds` / `bounds` / `radius` |
99
+ | `action.button` / `action.modifiers` / `action.left` / `action.right` | `payload.originalEvent.button` / `buttons` / `ctrlKey` / `metaKey` / `shiftKey` / `altKey` |
100
+ | `payload.button` / `payload.buttons` | `payload.originalEvent.button` / `buttons` |
101
+ | `payload.clientX` / `payload.clientY` | `payload.originalEvent.clientX` / `clientY` |
102
+ | `payload.lineIndex` / `payload.lineId`(右键线路) | `payload.index` / `payload.item.id`;通道归属用 `view.getLineChannel(payload.index)` |
103
+ | 复制载荷 `payload.point` | `payload.item`;批量看 `payload.items[0]` |
104
+ | 复制载荷 `payload.coordinate` | `payload.item.x` / `payload.item.y`;批量看 `payload.items[0].x` / `y` |
105
+ | 复制载荷 `payload.setItem(patch)` | `payload.update(patch)` |
106
+ | 回调 `(payload, event)` 两个参数 | 回调只收 `(payload)`,DOM 事件在 `payload.originalEvent`;事件总线载荷也不再包成 `{ payload, event }` |
87
107
 
88
108
  ```js
89
109
  createGisView(container, {
@@ -91,19 +111,19 @@ createGisView(container, {
91
111
  // 一套分支处理全部指针操作:先看是什么操作,再看点在什么上面,最后直接取数据
92
112
  onPointerUp: payload => {
93
113
  if (payload.action.moved) return // 拖过的不当点击处理
94
- if (payload.action.type === 'click' && payload.action.target === 'point') openPoint(payload.action.data)
95
- if (payload.action.type === 'click' && payload.action.target === 'channel') openChannel(payload.action.data)
114
+ if (payload.action.type === 'click' && payload.action.target === 'point') openPoint(payload.item)
115
+ if (payload.action.type === 'click' && payload.action.target === 'channel') openChannel(payload.channel)
96
116
  if (payload.action.target === 'empty') closePanel()
97
117
  },
98
118
  onClick: payload => {
99
119
  if (!payload) return // 空白处的点击载荷是 null(选中已经清空)
100
120
  if (payload.action.double) return // 双击的第二下:单双击要做不同的事时按它推迟
101
- openDetail(payload.item, payload.action.type, payload.action.modifiers)
121
+ openDetail(payload.item, payload.action.type, payload.originalEvent)
102
122
  },
103
- onContextMenu: (payload, event) => {
104
- event.preventDefault()
123
+ onContextMenu: payload => {
124
+ payload.originalEvent.preventDefault()
105
125
  // 右键只有一条意思:action.type 恒为 contextMenu,action.target 说明点的是什么
106
- openMenu(payload.action.target, payload.action.data, payload.world)
126
+ openMenu(payload.action.target, payload, payload.world)
107
127
  }
108
128
  })
109
129
  ```
@@ -112,32 +132,33 @@ createGisView(container, {
112
132
  createGisView(container, {
113
133
  points, lines, channels,
114
134
  // 左键按下:按下那一刻光标下的要素
115
- onPointerDown: (payload, event) => {
116
- console.log(payload.button, payload.buttons) // 0 左键、1 中键、2 右键
135
+ onPointerDown: payload => {
136
+ console.log(payload.originalEvent.button, payload.originalEvent.buttons) // 0 左键、1 中键、2 右键
117
137
  console.log(payload.hit, payload.type, payload.item) // 空白处 hit 为 false、type 为 null
118
138
  console.log(payload.world, payload.x, payload.y) // 光标处的世界坐标与画布 CSS 像素
119
139
  },
120
140
  onPointerUp: payload => console.log('抬起', payload.type, payload.item),
121
- // 右键:点 / 线 / 空白都发,命中通道回线时载荷里还有 channelId / groupId / channelIndex
122
- onContextMenu: (payload, event) => {
141
+ // 右键:点 / 线 / 空白都发,命中通道回线时载荷里还有 channelId / groupId / channelIndex / group / groups
142
+ onContextMenu: payload => {
123
143
  if (!payload.hit) return // 空白处也可以在这里收掉自己的菜单
124
- event.preventDefault() // 想自己弹菜单就拦住浏览器默认菜单
144
+ payload.originalEvent.preventDefault() // 想自己弹菜单就拦住浏览器默认菜单
125
145
  openMenu(payload.item, payload.world, payload.type)
126
146
  },
127
147
  onHover: payload => console.log('悬停', payload?.item ?? '空白处')
128
148
  })
129
149
  ```
130
150
 
131
- - 载荷形状一致,取信息只需认三处:`type`(`'point'` / `'line'` / `null`)、`item`(要素原始数据,可读业务字段)、`world` / `coordinate`(前者是光标处的世界坐标,后者是要素自身的坐标)。命中通道回线时,`channelIndex` / `channelId` / `groupId` / `groupIndex` / `channel` 一并给出;右键载荷还会在 `channelContext` 上带整份分组上下文。
151
+ - 载荷形状一致,取信息只需认三处:`type`(`'point'` / `'line'` / `null`)、`item`(要素原始数据,可读业务字段)、`world` / `coordinate`(前者是光标处的世界坐标,后者是要素自身的坐标)。命中通道回线时,`channelIndex` / `channelId` / `groupId` / `groupIndex` / `channel` 一并给出;`group` 是**当前命中的这一组**,`groups` 是**同一通道的全部分组**(按声明顺序,每项带自己的 `groupIndex`),所以不用再查一次 `getChannelGroups()` 就能画分组切换。
152
+ - 右键载荷还会在 `channelContext` 上带整份分组上下文(当前组、组数、可用动作);`groups` 是各鼠标载荷共有的纯读数,`channelContext.actions` 只在右键里给。
132
153
  - 配了 GeoJSON 背景时,以上载荷都多一个 `lngLat`:按光标的世界坐标(要素命中用 `coordinate`,悬停用最近一次光标位置)反算出来的真实经纬度,点 / 线 / 通道都一样;想自己换算就用 `worldToLngLat` / `screenToLngLat`,光标级别的屏幕坐标 + 世界坐标 + 经纬度看 `onPointer` 与 `getPointer()`。
133
- - `onPointerDown` / `onPointerUp` 不改变组件行为:选中、拖拽、框选、拐点编辑照旧。左键的一次「点」(没有拖动)仍然走 `onClick` / `onDblClick`,两者是并列的,不会互相吞掉 —— 载荷里的 `action.type` 各说各话:按下是**意图**、松开是**刚做完的那件事**(`action.moved` 说明是拖还是点)、点击是**真正发生的那一件**。
154
+ - `onPointerDown` / `onPointerUp` 不改变组件行为:选中、拖拽、框选、拐点编辑照旧。左键的一次「点」(没有拖动)仍然走 `onClick` / `onDblClick`,两者是并列的,不会互相吞掉 —— 载荷里的 `action.type` 各说各话:按下是**意图**、松开是**刚做完的那件事**(`action.moved` 说明是拖还是点)、点击是**真正发生的那一件**。这一下如果同时改了选中,顺序固定是 `onClick → onSelect → onSelectChange`;`selectTrigger: 'dblClick'` 的第二下再在最后补 `onDblClick`。
134
155
  - 点空白处照发:`onPointerDown` / `onPointerUp` 的 `action.target` 是 `empty`(开了平移时按下是 `pan` 的意图),`onClick` / `onDblClick` 的载荷是 `null`(选中在那一刻已经清空)——「点了空白」这件事也可以看 `onSelectChange` 的 `action.type: 'clearSelection'`。
135
- - **中键按住就是临时平移**(`pan` 开关开着时):`onPointerDown` 的 `action.type` 是 `pan`、`action.button` 是 `'middle'`,按住拖动平移画布、松开结束(`onPointerUp` 的 `action.type` 也是 `pan`);这一路**不经过** `onClick`,中键也不会改选中 —— 想自己接中键(中键菜单 / 快捷操作)就按 `action.button === 'middle'` 认它。长按(触摸 / 触控笔)与中键一样有明确的读数:看松开 / 点击载荷的 `action.longPress` 与 `action.press` / `action.duration`。
136
- - `onContextMenu` 每次都只发这一条;命中通道回线时,分组上下文挂在同一份载荷的 `channelContext` 上(见「通道右键交互框」),不再额外发第二个回调。**右键缺省整个不接**(`contextMenu` 开关没开时系统菜单照弹、回调也不发);接住之后一律 `preventDefault()` 收掉系统菜单(点 / 线 / 空白 / 通道都一样),回调照常发。想自己弹菜单就写 `interactive: true` / `contextMenu: true` / `features: 'view'`,想让系统菜单自己弹出来就写 `preventContextMenu: false`。
156
+ - **中键按住就是临时平移**(`pan` 开关开着时):`onPointerDown` 的 `action.type` 是 `pan`、`originalEvent.button === 1`,按住拖动平移画布、松开结束(`onPointerUp` 的 `action.type` 也是 `pan`);这一路**不经过** `onClick`,中键也不会改选中 —— 想自己接中键(中键菜单 / 快捷操作)就按 `originalEvent.button` 认它。长按(触摸 / 触控笔)与中键一样有明确的读数:看松开 / 点击载荷的 `action.longPress` 与 `action.press` / `action.duration`。
157
+ - `onContextMenu` 每次都只发这一条;命中通道回线时,分组读数在同一份载荷的 `group` / `groups` 上,可执行动作挂在 `channelContext` 上(见「通道右键交互框」),不再额外发第二个回调。**右键缺省整个不接**(`contextMenu` 开关没开时系统菜单照弹、回调也不发);接住之后一律 `preventDefault()` 收掉系统菜单(点 / 线 / 空白 / 通道都一样),回调照常发。想自己弹菜单就写 `interactive: true` / `contextMenu: true` / `features: 'view'`,想让系统菜单自己弹出来就写 `preventContextMenu: false`。
137
158
  - 命中的层级是**点 → 线 → 通道**:右键落在节点上时载荷是 `type: 'point'`,没有 `channelContext` —— 节点压在通道回线上(端点最常见)不会被当成通道右键;点空白处同样没有 `channelContext`,可以直接用它收框。
138
159
  - 悬停是高频率回调:大数据量下建议只做节流的读数或轻量样式切换(demo 里的读数就是这么做的)。
139
160
 
140
- - 命中的要素**在不在选中集里**、以及整份选中集有多少,载荷里直接给:`selected`(这一刻在不在选中集里)、`wasSelected`(`onClick` 给的是「点之前」的那一份,双击同理;右键与按下 / 松开都是「当下」,此时与 `selected` 相同)、`selection`(整份选中集的读数 `{ ids, count, points, lines, channels }`,通道回线按通道算一个)。左键 / 右键要自己接一套操作(按命中分支、弹自己的面板、决定要不要保留多选)时,看这三项就不用再去比对 `getSelectionData().ids`:
161
+ - 命中的要素**在不在选中集里**用载荷里的 `selected`(这一刻)/ `wasSelected`(`onClick` 给的是「点之前」的那一份,双击同理;右键与按下 / 松开都是「当下」,此时与 `selected` 相同)。整份选中集有多少改用 `view.getSelectionInfo()`:`{ ids, count, points, lines, channels }`,通道回线按通道算一个。左键 / 右键要自己接一套操作(按命中分支、弹自己的面板、决定要不要保留多选)时,这一快照加一次读数就够:
141
162
 
142
163
  ```js
143
164
  createGisView(container, {
@@ -146,23 +167,24 @@ createGisView(container, {
146
167
  if (!payload) return // 空白处:选中已经清空
147
168
  // payload.selected:「点完之后」在不在选中集里;payload.wasSelected:「点之前」的那一份
148
169
  if (payload.selected && payload.wasSelected) openDetail(payload.item)
149
- console.log(payload.selection.count, payload.selection.channels)
170
+ const info = view.getSelectionInfo()
171
+ console.log(info.count, info.channels)
150
172
  },
151
- onContextMenu: (payload, event) => {
152
- event.preventDefault() // 自己弹菜单
173
+ onContextMenu: payload => {
174
+ payload.originalEvent.preventDefault() // 自己弹菜单
153
175
  if (payload.type === 'point' && !payload.wasSelected) {
154
176
  // 右键落在没选中的节点上:先把它选上,再按「选中集」弹面板(落在已选中的节点上就保留整份多选)
155
177
  view.selectFeatures(payload.item.id)
156
178
  }
157
- openMenu({ target: payload.item, selection: payload.selection, coordinate: payload.world })
179
+ openMenu({ target: payload.item, selection: view.getSelectionInfo(), coordinate: payload.world })
158
180
  }
159
181
  })
160
182
  ```
161
183
 
162
- - Ctrl + 点击(多选取反)与 Shift + 拖动(默认框选键,可用 `shortcuts.boxSelect` 改)**不经过** `onClick`:这两下都走 `onSelectChange` —— 取反的 `action.type` 是 `toggleSelect`(取反取没了也是它,此时 `action.target` 是 `empty`)、框选是 `boxSelect`(没框到东西也是它)、点空白清空是 `clearSelection`;载荷里的 `items` 是操作之后的选中集、`count` 是新的条数。想在这条路里知道「点下去的那一刻它是不是已选中的」,看 `onPointerDown` / `onPointerUp` 载荷的 `selected`,或者在回调里读一次 `getSelectionData()`。
184
+ - Ctrl + 点击(多选取反)与 Shift + 拖动(默认框选键,可用 `shortcuts.boxSelect` 改)**不经过** `onClick`:顺序是 `onSelect → onSelectChange`。取反的 `action.type` 是 `toggleSelect`(取反取没了也是它,此时 `action.target` 是 `empty`)、框选是 `boxSelect`(没框到东西也是它)、点空白清空是 `clearSelection`;载荷里的 `items` 是操作之后的选中集、`count` 是新的条数。想在这条路里知道「点下去的那一刻它是不是已选中的」,看 `onPointerDown` / `onPointerUp` 载荷的 `selected`,或者在回调里读一次 `getSelectionData()`。选中事件里通道回线的 `group` / `groups` 与本次选中的 `lineIndexes` / `lineIds` 看单通道载荷顶层;多通道一起选中时看 `channelContexts`。`items[]` 里的回线不重复挂任何通道字段,需要单条归属时用外层 `lineIndexes` / `lineIds` 映射或调 `getLineChannel(index)`。
163
185
  - 不订阅事件、只想知道「这儿是什么」时用 `pickAt(clientX, clientY)`:按鼠标事件里的客户端坐标拾取,返回与指针载荷同一份结果(另加 `target` 与 `lngLat`),空白处返回 `null`;只读 —— 不改选中、不动光标,自建浮层 / 自定义菜单 / 拖拽放下时用它。框内有什么(自建框选界面、框内统计)用 `getFeaturesInRect(rect)`、当前画面里有哪几条通道用 `getChannelsInView()`,三个方法都在「实例方法」里。
164
186
  - 悬停也可以**程序化驱动**:`hoverAt(clientX, clientY)` 让指针「停」到某个位置(悬停高亮、光标与 `onHover` 一起更新,返回与 `onHover` 同一份读数),`clearHover()` 撤掉并补发一条 `onHover(null)`,`getHover()` 随时读当前这一份 —— 外部列表 / 表格 hover 到某一行时联动高亮图上的要素用它,不必自己派发指针事件;不需要了就不用订阅 `onHover`。
165
- - 选中集整个拿走用 `getSelectionData()`,一份就够:读数(`ids` / `count` / `pointCount` / `lineCount` / `channelCount`)加**要素本身**(`points` / `lines` / `channels`(按通道去重))与 `items`(与 `getSelections()` 同一份)、`pointIndexes` / `lineIndexes`、`bounds`(世界包围盒,空选中时 `null`):
187
+ - 选中集整个拿走用 `getSelectionData()`,一份就够:读数(`ids` / `count` / `pointCount` / `lineCount` / `channelCount`)加**要素本身**(`points` / `lines` / `channels`(按通道去重))与 `items`(与 `getSelections()` 同一份,通道回线不带通道字段)、`pointIndexes` / `lineIndexes`、`bounds`(世界包围盒,空选中时 `null`):
166
188
 
167
189
  ```js
168
190
  const data = view.getSelectionData()
@@ -343,13 +365,13 @@ if (!copied.ok) console.warn(copied.reason)
343
365
 
344
366
  | phase | 什么时候 | 能做什么 |
345
367
  | --- | --- | --- |
346
- | `start` | 预览刚出现 | `payload.update({...})` / `setItem({...})` 改复制品,`payload.cancel()` 直接取消 |
368
+ | `start` | 预览刚出现 | `payload.update({...})` 改复制品,`payload.cancel()` 直接取消 |
347
369
  | `move` | 预览跟着鼠标动(`follow: true`) | 同 `start` |
348
370
  | `commit` | 落地之前(`commitPointCopy()` 里发) | `payload.cancel()` 可以拦住这一次复制,改数据也在这里生效 |
349
371
  | `end` | 已经落地 | 读 `payload.index` / `payload.item` 同步业务数据 |
350
372
  | `cancel` | 复制被取消 | 清理自建预览 |
351
373
  | `clipboard` | 复制到剪贴板(Ctrl+C) | `payload.cancel()` 表示不要这次剪贴板 |
352
- | `batch` | 批量复制 / 整组粘贴落地 | 读 `payload.sources`(来源快照)、`payload.items` / `indexes`(新节点)、`payload.lines` / `channels`(一起复制出来的内部线路 / 通道);要撤掉整批用 `payload.revert()` / `cancel()`,要改落地后的节点用 `view.updatePoint(payload.item.id, patch)`(批量载荷不提供预览期的 `update` / `setItem` 改写) |
374
+ | `batch` | 批量复制 / 整组粘贴落地 | 读 `payload.sources`(来源快照)、`payload.items` / `indexes`(新节点)、`payload.lines` / `channels`(一起复制出来的内部线路 / 通道);要撤掉整批用 `payload.revert()` / `cancel()`,要改落地后的节点用 `view.updatePoint(payload.item.id, patch)`(批量载荷不提供预览期的 `update` 改写) |
353
375
 
354
376
  ```js
355
377
  createGisView(container, {
@@ -637,14 +659,16 @@ const split = view.splitPoint('hub', [
637
659
  { name: '枢纽(主)' }, // 第一份:沿用原下标与 id
638
660
  { x: 116.42, y: 39.91, name: '枢纽(东)', lines: ['line-east'] },
639
661
  { x: 116.38, y: 39.89, name: '枢纽(西)', lines: ['line-west'], channelTo: ['corridor-1'] }
640
- ], { label: '拆分枢纽' })
662
+ ], { autoLines: 'alternate', label: '解除合并' })
641
663
 
642
664
  if (split.ok) {
643
- console.log(split.pointIds, split.linesChanged, split.channelsChanged)
665
+ console.log(split.pointIds, split.autoAssignedLines, split.lineAssignments)
644
666
  api.post('/gis/points/split', split.changes) // 拆分后的节点、受影响的线路与通道端点,以及删除项
645
667
  }
646
668
  ```
647
669
 
670
+ `options.autoLines` 决定**没写进 `lines` 的线路**怎么分:缺省 `'first'` 全跟第一份(兼容旧行为);`'alternate'` 按「同一条通道的成员成一组、独立线路各算一条」为单位,轮流分给当前负载最少的一份。显式写进 `lines` 的线路不参与自动分配,但会计入负载;同一条通道的成员因此不会被拆到两份上。解除合并时推荐 `'alternate'`,可以让四回线的通道整体去 A、两条独立线路分别去 A / B,而不是把通道回线拆散。
671
+
648
672
  每份节点定义可以直接写节点字段(`id` / `x` / `y` / `name` / 样式 / 业务字段),也可以放进 `item`;另外三项是从节点字段里摘出来的分配指令,不会写进节点数据:
649
673
 
650
674
  | 字段 | 作用 |
@@ -657,14 +681,14 @@ if (split.ok) {
657
681
 
658
682
  - **第一份接在原下标上**,缺省沿用来源节点的 id;其余几份追加到 `points` 末尾。`options.keepSourceId: false` 时第一份也生成 `${来源 id}-split-1`。没写 id 的其余份依次生成 `${来源 id}-split-2` / `-3`…,撞上现有 id 时自动加序号;显式写了重复 id 会整次失败。
659
683
  - **节点字段从来源继承,再按这一份的字段覆盖**;坐标缺省继承来源。至少给两份,坐标必须是有效数字,否则 `ok: false` 且 `reason` 说明原因。
660
- - **线路默认跟第一份**:没写进任何一份 `lines`、但连在来源节点上的线路,全部改挂到第一份;同一条线路显式分给两份会整次失败。认不出 / 没连在来源上的引用进 `skipped.lines`,不影响其余条目。
684
+ - **线路默认跟第一份**:没写进任何一份 `lines`、但连在来源节点上的线路,`autoLines: 'first'`(缺省)时全部改挂到第一份;写 `autoLines: 'alternate'` 时按上面的「通道成组 / 独立线路单条」轮流分配。同一条线路显式分给两份会整次失败。认不出 / 没连在来源上的引用进 `skipped.lines`,不影响其余条目。
661
685
  - **通道端点默认跟第一条被分配过的成员回线**:例如通道里唯一一条回线分给了第二份,通道的 `from` 会自动改到第二份节点,而不是留在第一份;成员都没显式分配时才回落到第一份。显式写了 `channels` / `channelFrom` / `channelTo` 的按显式分配来。
662
686
  - **几何一起改**:线路的 `from` / `to` 改到对应新节点;没有 id 绑定、但 `start` / `end` / `path` 顶点与来源坐标重合的端点 / 顶点也改到新坐标。通道端点按新声明重新锚定,成员回线由通道骨架推导,不用逐条改。
663
687
  - **拆完清理退化数据**:两端落到同一份、变自环或零长度的线路删掉;两端因此相同的通道删掉;成员回线全被删空的通道也删掉。删除项分别进 `removedLineIds` / `removedLineIndexes` 与 `removedChannelIds`。
664
- - **返回的 `channelsChanged` 只数端点被改写的通道**;被删掉的通道单列在 `removedChannels` / `removedChannelIds`。`changes` 给 `{ removedPoint, points, lines, removedLines, channels, removedChannels }`:`points` 是拆分后的全部节点,`lines` / `channels` 是仍存在且端点被改写的线路 / 通道,删除项单列;可直接作为后端事务输入。`skipped` 说明哪些线路 / 通道引用没认出来。
688
+ - **返回的 `channelsChanged` 只数端点被改写的通道**;被删掉的通道单列在 `removedChannels` / `removedChannelIds`。`autoLines` 是本次采用的口径,`autoAssignedLines` 只数自动分配(没在 `parts[].lines` 里显式写)的线路,`lineAssignments` 按份给出 `{ index, pointId, lineIds, lineCount }`,含显式与自动分配后的最终结果。`changes` 给 `{ removedPoint, points, lines, removedLines, channels, removedChannels }`:`points` 是拆分后的全部节点,`lines` / `channels` 是仍存在且端点被改写的线路 / 通道,删除项单列;可直接作为后端事务输入。`skipped` 说明哪些线路 / 通道引用没认出来。
665
689
  - **撤销**:不用逐条回滚,`view.undo()` 一步回到拆分前;重做一步恢复拆分结果。
666
690
 
667
- `CopyMergeDemo.vue` 的「拆分节点」一节可以直接点按钮看效果:选中一个节点,按两份拆开,读数会列出新节点 id、改绑的线路与通道端点,以及这一步的变更集。
691
+ `CopyMergeDemo.vue` 的「解除合并」一节可以直接点按钮看效果:选中一个节点,按两份拆开,调用 `splitPoint(source.id, [A, B], { autoLines: 'alternate', label: '解除合并' })`;读数会列出新节点 id、自动分线数、每份接到多少条线路、通道成员是否仍成组,以及改绑 / 删除的线路与通道。
668
692
 
669
693
  ## 删除(remove)
670
694
 
@@ -2,6 +2,33 @@
2
2
 
3
3
  > 选中与拖动的层级、框选、接口选中、线路拐点的增删改。
4
4
 
5
+ ## 选中与取消选中速查
6
+
7
+ 选中与取消选中共用同一份 state:鼠标点击、Ctrl / ⌘ 多选、框选和接口调用改的都是这一份集合,读回统一用 `isSelected()` / `getSelections()` / `getSelectionData()`。
8
+
9
+ | 想做什么 | 怎么做 |
10
+ | --- | --- |
11
+ | 选中一个 | 单击节点 / 线路;或 `view.selectFeatures(ref)` |
12
+ | 按坐标像单击一样选中 | `view.selectAt(clientX, clientY)`(通道回线按 `channelSelectMode` 摊开) |
13
+ | 换一份选中集 | `view.selectFeatures(refs)`(缺省 `mode: 'replace'`) |
14
+ | 追加 / 取反 / 移除 | `view.selectFeatures(refs, { mode: 'add' \| 'toggle' \| 'remove' })` |
15
+ | 多选 | 打开 `multiSelect` 后单击,或按住 Ctrl / ⌘ 单击;框选先开 `boxSelect`,再按住 Shift 拖动 |
16
+ | 取消指定项 | `view.deselect(refs)`,等价于 `view.selectFeatures(refs, { mode: 'remove' })` |
17
+ | 取消全部 | 单击空白处、无其它编辑态时按 Esc、`view.deselect()`、`view.selectFeatures(null)` 或 `view.selectFeatures([])` |
18
+ | 读回结果 | `view.isSelected(ref)` / `view.getSelections()` / `view.getSelectionData()` |
19
+
20
+ ```js
21
+ view.selectFeatures(['station-1', { type: 'point', index: 3 }]) // 换一份选中集
22
+ view.selectFeatures('station-2', { mode: 'add' }) // 加选
23
+ view.selectFeatures('station-1', { mode: 'toggle' }) // 已选中就取消
24
+ view.deselect('station-2') // 只取消这一项
25
+ view.deselect() // 取消全部
26
+ ```
27
+
28
+ `refs` 可以是节点 / 线路 id、`{ type, index }`、通道 / 分组写法、拾取载荷或搜索命中。公开实例没有 `select()`,旧代码统一改用 `selectFeatures()`;`deselect([])` 是「没有目标」,不会改选中集。上面按缺省 `selectTrigger: 'click'` 写;改成 `'dblClick'` 后,选中与点空白清空都由第二下完成,接口调用不受影响。
29
+
30
+ **选中状态只有一份**:`selectFeatures()` / `deselect()` 与单击、Ctrl 多选、框选改的是同一份选中 state,样式、`isSelected()` 与 `getSelectionData()` 都立即更新。选中集实际变化时,它们与点击一样会按 `onSelect → onSelectChange` 发回调(接口来源 `cause: 'api'`);区别只在入口和通知口径,不在“算不算选中”。`onSelect` 是**本次操作**回调(真实点击 / 框选 / `selectAt` 每次成功操作都发),`onSelectChange` 是**集合状态**回调(只在集合真的变了时发,适合同步面板)。选中通道回线时两条回调都在外层带 `channelId` / `group` / `groups` / 本次选中的 `lineIndexes` / `lineIds`;多通道看 `channelContexts`,`items[]` 不再逐条挂通道字段,见 [04 事件与回调](04-事件与回调.md#回调)。
31
+
5
32
  ## 选中 / 拖动的层级(selectionOnTop)
6
33
 
7
34
  `selectionOnTop`(默认 `true`)把「选中的节点、拖动中的整组、待确认合并的目标节点」以及「选中线路所在的整条通道」画在所有要素之上,与 `zIndex` 无关 —— 选中了就一定看得见。写 `selectionOnTop: false` 就完全按 `zIndex` 与数据顺序绘制。抬层只在选中 / 拖动 / 合并状态变化时整份重排一次,悬停这类高频路径不参与,十万级数据下不会拖慢交互。
@@ -54,7 +81,7 @@ createGisView(container, {
54
81
  view.updateOptions({ selectTrigger: 'click' }) // 运行时切回单击选中
55
82
  ```
56
83
 
57
- 运行时切换 `selectTrigger` 会清掉上一次点击的双击判定状态,切换后的第一下不会被算成旧点击序列的第二下。`selectFeatures()` / `select()` 这类接口选中不受触发方式影响。
84
+ 运行时切换 `selectTrigger` 会清掉上一次点击的双击判定状态,切换后的第一下不会被算成旧点击序列的第二下。`selectFeatures()` 这类接口选中不受触发方式影响。
58
85
 
59
86
  ### 选择类型过滤(selectionKinds)
60
87
 
@@ -81,7 +108,7 @@ view.selectInBox({ x: 40, y: 40, width: 320, height: 200 }, { selectionKinds: ['
81
108
 
82
109
  ## 接口选中(selectFeatures)与读回选中
83
110
 
84
- 点选、Ctrl 多选、框选与接口选中落在**同一份选中 state** 上:任何一种选上,`getSelections()` / `getSelectionData()` 与 `onSelectChange` 读到的都是它(接口选中会补一条 `cause: 'api'` 的选中集变更,所以一个回调就能覆盖所有来源)。
111
+ 点选、Ctrl 多选、框选与接口选中落在**同一份选中 state** 上:任何一种选上,`getSelections()` / `getSelectionData()` 读到的都是它。真实操作(点击 / Ctrl 取反 / 框选 / `selectAt`)每次成功操作都发 `onSelect`;按引用接口(`selectFeatures` / `deselect` / `focusOn({ select })`)只在实际改变选中集时发。`onSelectChange` 覆盖所有来源,并且只在集合真的变化时发;两条都在时顺序固定为 `onSelect → onSelectChange`(真实单击最前面还有 `onClick`)。
85
112
 
86
113
  `selectFeatures(refs, options?)` 认这些写法(传 `null` / 空数组等于清空):
87
114
 
@@ -112,6 +139,35 @@ view.selectFeatures({ type: 'channel', channelId: 'channel-a' }, { mode: 'remove
112
139
 
113
140
  拾取载荷(`pickAt` 或指针事件里的那一份)与搜索命中里可能带着 `channelId` / `channelIndex` / `groupId` —— 那是「这条回线属于哪条通道」的上下文,不是「要选整条通道」:它们的 `type` / `kind` 写着 `line` / `point`,落地选中的就是那一条要素自己(与在画布上点它一个结果)。要整条通道,显式写 `{ type: 'channel', channelId: 'channel-a' }`。
114
141
 
142
+ ### 按坐标单击选中(selectAt)
143
+
144
+ 如果手上只有鼠标坐标、又希望组件像用户单击一样选中,用 `selectAt(clientX, clientY)`:命中顺序、通道回线摊开(`channelSelectMode`)与真实点击一致,但不需要先自己拆拾取载荷。命中空白或聚合圆时返回 `null`,不改原选中集。
145
+
146
+ 它立即生效,不受 `selectTrigger: 'dblClick'` 等待影响,也不派发 `onClick` / `onDblClick`;每次成功命中都发 `onSelect`(模拟一次单击),`onSelectChange` 只在选中集实际变化时发,`cause` 都是 `'api'`。选中通道回线时两条回调都在外层带当前命中的 `group`、整条通道的 `groups`、顶层 `channelId` 与本次选中的 `lineIndexes` / `lineIds`;多通道看 `channelContexts`。要取消指定项或全部,继续用 `deselect()`。
147
+
148
+ `selectAt()` 与 `selectFeatures()` 最终落在同一份选中 state,选中样式和读回结果没有区别;区别在入口、是否经过坐标命中与返回值:
149
+
150
+ | | `selectAt(clientX, clientY)` | `selectFeatures(refs, options?)` |
151
+ | --- | --- | --- |
152
+ | 输入 | 浏览器客户端坐标 | id / 下标 / 通道 / 分组等引用 |
153
+ | 命中方式 | 与真实单击同一套 `pick()`,通道回线按 `channelSelectMode` 摊开 | 按引用直接解析;整条通道要显式写 `{ type: 'channel', channelId }` |
154
+ | 怎么改选中集 | 始终像普通单击一样换一份 | `mode` 可 `'replace'` / `'add'` / `'toggle'` / `'remove'` |
155
+ | 空白 | 返回 `null`,原选中集不动 | `null` / 空数组清空;认不出的条目逐条跳过 |
156
+ | 回调 | 每次成功命中都发 `onSelect`;选中集实际变化时再发 `onSelectChange`。顺序 `onSelect → onSelectChange`(`cause: 'api'`),不发 `onClick` / `onDblClick` | 选中集实际变化时发 `onSelect → onSelectChange`(`cause: 'api'`),不发 `onClick` / `onDblClick` |
157
+ | 返回 | 本次选中载荷或 `null` | 实例本身,可链式调用 |
158
+
159
+ ```js
160
+ const at = view.worldToClient(120, 60)
161
+ const selected = view.selectAt(at[0], at[1]) // 节点 / 线路 / 通道回线,命中什么选什么
162
+
163
+ view.updateOptions({ channelSelectMode: 'channel' })
164
+ view.selectAt(at[0], at[1]) // 命中通道里任一回线 = 选中整条通道
165
+ view.selectAt(-1000, -1000) // 空白:返回 null,原选中集不动
166
+ ```
167
+
168
+ 已经知道节点 id 时直接用 `view.selectFeatures('station-1024')`,不必先求坐标;`selectAt` 留给「只有鼠标 / 触摸坐标」
169
+ 的场景(画布外点击转发、命中测试、浮层把坐标交给组件)。它不是选中节点的必经入口,也不是 `selectFeatures` 的替代品。
170
+
115
171
  聚焦同一套目标写法交给 `focusOn`,显式 `type` 还能解决节点与线路同 id 的歧义;整条通道可以写 `{ channelId }`,分组写 `{ type: 'group', channelId?, groupId }`(`groupId` 全局唯一时可省略 `channelId`)。节点、线路、通道与分组的取范围与缩放都走 `zoomToBounds`,不会因为类型不同换一套放大倍率:
116
172
 
117
173
  ```js
@@ -133,7 +189,7 @@ selection.channelCount // 这些回线涉及的通道数(按通道去重)
133
189
  selection.points // 选中的节点数据(引用,与 getData() 同一份)
134
190
  selection.lines // 选中的线路数据(通道回线也算一条)
135
191
  selection.channels // 选中回线所属的通道(按通道去重,同一条通道的几回线算一条)
136
- selection.items // 与 getSelections() 同一份:带下标、屏幕坐标与通道上下文
192
+ selection.items // 与 getSelections() 同一份:带下标与屏幕坐标;通道回线不带通道字段
137
193
  selection.bounds // 世界包围盒(空选中时 null),直接交给 zoomToBounds / focusOn
138
194
  view.focusOn(selection.items) // 把视角中心挪到这一批(比例不动)
139
195
  view.focusOn(selection.items, { zoom: true }) // 按这一批的整体范围适配
@@ -144,7 +200,7 @@ view.focusOn(selection.items, { select: true, zoom: true }) // 反过来也行
144
200
 
145
201
  ### 取消选中(deselect / selectFeatures(null))
146
202
 
147
- 取消某一个 / 某一类 / 全部都是**一句话**(`deselect()` 与 `selectFeatures(null)` 都表示全部取消):与选中落在同一份 state 上,清完 `getSelections()` / `getSelectionData()` 立刻读得到,`onSelectChange` 补一条 `cause: 'api'` 的选中集变更。
203
+ 取消某一个 / 某一类 / 全部都是**一句话**(`deselect()` 与 `selectFeatures(null)` 都表示全部取消):与选中落在同一份 state 上,清完 `getSelections()` / `getSelectionData()` 立刻读得到;选中集实际变化时按 `onSelect → onSelectChange` 派发,`onSelect` 的载荷为 `null`、`cause` 为 `'api'`。
148
204
 
149
205
  ```js
150
206
  view.deselect('station-1024') // 取消一个节点(id,或 { type: 'point', index: 3 })
@@ -40,7 +40,23 @@ view.showFeatures('channel', 'c1')
40
40
  view.updateOptions({ channelSelectMode: 'channel' }) // 点一下回线 = 选中整条通道
41
41
  ```
42
42
 
43
- Ctrl / ⌘ + 点击在 `'group'` / `'channel'` 下是**整批取反**:这一组 / 这一条通道都在选中集里时一起取消,否则一起选上(与单击走同一套摊开规则)。按住 `shortcuts.addWaypoint`(默认也是 Ctrl / ⌘)点击通道回线加点时,加完由这一档决定选中并高亮一条、一组还是整条通道。框选、接口选中(`select` / `selectFeatures`)与「按通道写法的接口选中」不受影响。改这一项只重画一帧、不重排场景(`updateOptions({ channelSelectMode })`)。
43
+ Ctrl / ⌘ + 点击在 `'group'` / `'channel'` 下是**整批取反**:这一组 / 这一条通道都在选中集里时一起取消,否则一起选上(与单击走同一套摊开规则)。按住 `shortcuts.addWaypoint`(默认也是 Ctrl / ⌘)点击通道回线加点时,加完由这一档决定选中并高亮一条、一组还是整条通道。框选与 `selectAt()` 也按这一档摊开;`selectFeatures` 的显式通道写法按你给的目标选中(要整条通道就写 `{ type: 'channel', channelId }`),不靠这一档推断。改这一项只重画一帧、不重排场景(`updateOptions({ channelSelectMode })`)。
44
+
45
+ 鼠标载荷(`onPointerDown` / `onPointerUp` / `onClick` / `onDblClick` / `onHover` / `pickAt()`)命中通道回线时,
46
+ `group` 是当前命中的那一组,`groups` 是同一通道的全部分组(按声明顺序,每项带 `groupIndex` / `lineIds` /
47
+ 骨架与 `path`)。点击一条回线时不必先扫一遍 `getChannelGroups()` 再自己判断是哪组;`groups.length` 就是组数。
48
+
49
+ 选中回调(`onSelect` / `onSelectChange`)带同一份通道上下文:选中集只涉及一条通道时,载荷顶层平铺
50
+ `channelId` / `channelIndex` / `channel` / `groupId` / `groupIndex` / `group` / `groups` 与本次选中的
51
+ `lineIndexes` / `lineIds`。真实单击与 `selectAt()` 的 `group` 是命中的那一组;框选、`selectFeatures()`
52
+ 这类没有单次命中组的入口,取选中集里第一条通道回线所在的组。`groups` 始终是整条通道的全部分组,不是只含
53
+ 选中的组。一次选中多条通道时顶层不再摊平,改看 `channelContexts`(每项带同样的字段与本次选中的
54
+ `lineIndexes` / `lineIds`);单通道不会额外创建 `channelContexts`。
55
+
56
+ `items[]` 里每条回线**不再挂任何通道字段**,避免十万级选中时为每条线路重复写同一份归属。要用 `items`
57
+ 反查时看外层的 `lineIndexes` / `lineIds`(本次选中子集),再对应顶层上下文或 `channelContexts`;只问一条
58
+ 线路时调 `getLineChannel(index)`。`groups[].lineIds` 是整组成员,与本次选中的 `lineIds` 不是同一份数据。
59
+ `onSelect` 与 `onSelectChange` 共用同一批对象引用,两条都订阅时不会各拷一份。
44
60
 
45
61
  ### 分组拐点拖动模式(channelWaypointDragMode)
46
62