gis-view-webgl2 0.1.1 → 0.1.3

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 (66) hide show
  1. package/README.md +23 -7
  2. package/dist/gis-view.css +1 -1
  3. package/dist/index.cjs +1 -1
  4. package/dist/index.d.ts +563 -61
  5. package/dist/index.js +1 -1
  6. package/docs/AI/345/257/271/350/257/235/346/261/207/346/200/273.md +182 -0
  7. package/docs/GisView.md +2 -2
  8. 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 +165 -0
  9. package/docs/api/index.html +1 -1
  10. package/docs/demos/ApiCoverageDemo.vue +644 -0
  11. package/docs/demos/ChannelDemo.vue +580 -18
  12. package/docs/demos/ClusterDemo.vue +251 -0
  13. package/docs/demos/CopyMergeDemo.vue +60 -10
  14. package/docs/demos/EventDemo.vue +3 -3
  15. package/docs/demos/GisViewDemo.vue +64 -8809
  16. package/docs/demos/LabelDemo.vue +67 -11
  17. package/docs/demos/api-index.js +58 -41
  18. package/docs/demos/demos.js +33 -16
  19. package/docs/demos/gis-view-demo/GisViewDemoCardHeader.vue +28 -0
  20. package/docs/demos/gis-view-demo/GisViewDemoDialogs.vue +82 -0
  21. package/docs/demos/gis-view-demo/GisViewDemoFooter.vue +16 -0
  22. package/docs/demos/gis-view-demo/GisViewDemoHeading.vue +16 -0
  23. package/docs/demos/gis-view-demo/GisViewDemoPanelStack.vue +71 -0
  24. package/docs/demos/gis-view-demo/GisViewDemoStage.vue +219 -0
  25. package/docs/demos/gis-view-demo/demo.css +1230 -0
  26. package/docs/demos/gis-view-demo/demoContext.js +12 -0
  27. package/docs/demos/gis-view-demo/panels/DataPanels.vue +303 -0
  28. package/docs/demos/gis-view-demo/panels/OpsPanels.vue +174 -0
  29. package/docs/demos/gis-view-demo/panels/StylePanels.vue +561 -0
  30. package/docs/demos/gis-view-demo/panels/ViewPanels.vue +396 -0
  31. package/docs/demos/gis-view-demo/useGisViewDemo.js +6188 -0
  32. package/docs/format/03-/346/213/220/347/202/271/350/243/205/351/245/260/344/270/216/346/240/267/345/274/217.md +4 -2
  33. package/docs/format/04-/351/200/232/351/201/223/344/270/216/346/240/207/347/255/276.md +5 -4
  34. 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 +3 -2
  35. package/docs/internals/16-03-/344/274/230/345/214/226/344/270/216/345/217/226/350/210/215-/344/270/213.md +1 -1
  36. package/docs/manual/02-/345/235/220/346/240/207/344/270/216/346/225/260/346/215/256.md +10 -4
  37. package/docs/manual/03-/351/205/215/347/275/256/351/241/271.md +105 -16
  38. package/docs/manual/04-/344/272/213/344/273/266/344/270/216/345/233/236/350/260/203.md +8 -8
  39. 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 +100 -0
  40. 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 +78 -3
  41. package/docs/manual/08-/351/200/211/344/270/255/344/270/216/346/213/220/347/202/271.md +31 -5
  42. package/docs/manual/09-/351/200/232/351/201/223/344/270/216/345/244/232/345/233/236/347/272/277.md +64 -10
  43. 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 +65 -8
  44. package/docs/manual/12-/346/240/207/347/255/276/344/270/216/350/243/205/351/245/260.md +54 -1
  45. package/docs/manual/13-/345/212/250/346/225/210/344/270/216/345/233/276/345/205/203.md +3 -0
  46. package/docs/manual/14-/350/247/206/345/233/276/344/270/216/347/255/233/351/200/211.md +35 -19
  47. package/docs/manual/15-/345/257/274/345/207/272/344/270/216/351/233/206/346/210/220.md +5 -0
  48. package/docs/manual/16-/345/256/236/344/276/213/346/226/271/346/263/225/346/200/273/350/241/250.md +18 -6
  49. package/docs/manual/17-/345/217/202/346/225/260/351/200/237/346/237/245.md +47 -22
  50. package/docs/manual/18-/351/200/220/346/226/271/346/263/225/347/244/272/344/276/213.md +80 -13
  51. package/docs/manual/20-/346/200/247/350/203/275/344/270/216/345/244/247/346/225/260/346/215/256.md +27 -0
  52. 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 +5 -5
  53. package/docs/steps/README.md +18 -10
  54. package/docs/steps/selftest-actions.html +3 -5
  55. package/docs/steps/selftest-demo.html +27 -18
  56. package/docs/steps/selftest-demos.html +578 -53
  57. package/docs/steps/selftest-docs.html +7 -6
  58. package/docs/steps/selftest-effects.html +200 -0
  59. package/docs/steps/selftest-features.html +725 -12
  60. package/docs/steps/selftest-labels.html +323 -4
  61. package/docs/steps/selftest-perf.html +293 -20
  62. package/docs/steps/selftest-scale.html +52 -29
  63. package/docs/steps/step-10-full.js +1 -1
  64. package/docs/steps/step-12-events.js +1 -1
  65. package/docs//346/216/245/345/217/243/345/215/217/350/256/256.md +17 -11
  66. package/package.json +2 -2
@@ -26,19 +26,45 @@ view.addChannelGroupWaypoint()
26
26
  - 不传坐标时按「分开分组」的默认位置落点:取这一组最长的一段骨架,在它的中点沿法线往外让一档 —— 第 1 组原地不动、第 2 组让一档、第 3 组再往外一档,档距是这一组的回线跨度 + 一个回线间距,所以拉开的几组既不会重合也不会贴在一起。被拉走的组仍然按自己在整条通道里的那几条车道相对骨架平行偏移(组内照样等距),拖走、拖回来都不影响别的分组。
27
27
  - 分组拐点只挂在**这一组的骨架**上:加 / 删 / 拖都只动这一组的线路与拐点。两种加点入口的差别只在怎么给坐标:`insertWaypoint(lineIndex, ...)`(实例层 `addWaypoint(id, ...)`)从回线进去、把坐标投影到该回线所属分组的骨架上;`addChannelGroupWaypoint()` 从分组进去、缺省位置就是上面那个「分开分组」的偏移落点。`removeWaypoint(lineIndex, pathIndex)` 与 `removeChannelGroupWaypoint()` 同理,删的都是这一组骨架上的顶点。
28
28
  - 增删都会发 `onWaypointChange`,载荷带 `channelId` / `groupId` 与代表回线(这一组的第一条)的 `line`;实例层收到之后会把这一组的声明同步回 `channels[].groups`,下次 `updateOptions` 不会把分组拐点弹回整条通道。`addChannelGroupWaypoint()` 还会顺手把这一组选出来(多选,整组高亮)。
29
- - **存的是公共拐点,不是各自的**:一个分组只有一份骨架,加 / 删 / 拖都只写这一份(数据里是 `channels[].groups[].path`);每条回线「自己的」拐点不落数据 —— 它是公共骨架按这条回线所在车道偏移推出来的只读读数(与图上画的一致)。想「让某条回线单独拐」就把它拆成新分组(`moveChannelLine(id, lineId, 'newGroup')` 或 `addChannelGroup`),新的那一组才多出一份公共骨架。
29
+ - **存的是公共拐点与分组偏移**:一个分组只有一份骨架,加 / 删 / 拖都只写这一份(数据里是 `channels[].groups[].path`);`'vertex'` 模式额外把同组同序号的刚性增量记在 `channels[].groups[].waypointOffsets`。每条回线「自己的」拐点不落数据 —— 它是公共骨架按这条回线所在车道偏移、再叠分组刚性偏移推出来的只读读数(与图上画的一致)。想「让某条回线单独拐」就把它拆成新分组(`moveChannelLine(id, lineId, 'newGroup')` 或 `addChannelGroup`),新的那一组才多出一份公共骨架。
30
30
  - 区分这两层读数用 `getChannelGroupWaypoints(channelId, groupId)`:公共那一份是 `path` / `skeleton`(返回里 `scope: 'group'`,给后端 `channel.group.path` 就是它,组内不必按回线逐条发),各自那一份是 `lines[].path` / `lines[].waypoints`(`derived: true`,只读)。加 / 删 / 拖之后照公共那一份重新拼整份 `path` 覆盖即可;各自的那份不用回写。
31
31
  - 右键交互框里的「加拐点 / 删拐点」按钮走的是同一条路径:`runChannelAction('add-waypoint' | 'remove-waypoint', { channelId, groupId, coordinate?, pathIndex? })`。
32
32
 
33
+ #### 拖动模式:parallel / vertex
34
+
35
+ 拖动分组拐点时,`channelWaypointDragMode` 决定骨架怎么改(缺省 `'parallel'`;普通线路始终只动自己的拐点):
36
+
37
+ - `'parallel'`:保留现有的共用骨架 + 平行偏移逻辑。拖动骨架顶点,组内回线按各自车道重新平行推导,拐角处的相邻顶点可能随 miter 一起微调。
38
+ - `'vertex'`:只平移当前分组同一 `pathIndex` 的拐点。组内每条回线都按相同世界增量移动这一序号,相邻拐点、其它顶点与其它分组一个都不动;结果记在分组的 `waypointOffsets` 上,不重建公共 `path`。
39
+
40
+ ```js
41
+ view.updateOptions({ channelWaypointDragMode: 'vertex' })
42
+
43
+ // 拖过之后,分组声明会写成:
44
+ {
45
+ id: 'corridor-1-b',
46
+ lineIds: ['c-3', 'c-4'],
47
+ path: [[100, 80], [260, 80]],
48
+ waypointOffsets: { '1': [42, 30] }
49
+ }
50
+ ```
51
+
52
+ - `waypointOffsets` 的键是**分组骨架顶点序号**(含首尾端点,从 0 起),只接受中间拐点;值是这一组所有成员回线都要叠加的世界坐标增量,写法为 `[dx, dy]` 或 `{ x, y }`。
53
+ - 偏移按组保存。`getChannelGroups()` / `getChannelGroupWaypoints()` / `getChannels()` 都返回 `waypointOffsets`;`getParallelGroups()` 与 `exportData({ format: 'test' | 'view', parts: ['waypoint'] })` 的 `group_route` 已经叠上偏移,导出的是实际路由。
54
+ - 组内每条回线仍先按自己的车道做平行偏移,再叠这一份刚性偏移;所以同组所有回线在 `'vertex'` 拖动时一起平移同一距离,回线之间保持原来的间距。
55
+ - 模式切换只影响下一次拖动,**不会清空已有的 `waypointOffsets`**。要恢复纯平行骨架,可把偏移拖回零,或重写分组声明并带上当前 `path`、省略 `waypointOffsets`(例如整份 `updateChannel(channelId, { groups })`);只写 `id` + `lineIds` 的分组补丁会沿用当前偏移。
56
+ - 后端同步时,通道分组拐点不能只交 `path`:同时提交 `getChannelGroupWaypoints()` 返回的 `path` 与 `waypointOffsets`,否则刷新后会回到旧骨架 / 丢掉非平行拖动结果。
57
+
33
58
  ### 通道右键交互框
34
59
 
35
- 右键点中通道的回线,引擎把这条通道的上下文交给 `onChannelContextMenu`(**通道右键框缺省关**:右键本身要接得住 —— `contextMenu` / `interactive: true` / `features: 'view'` 任一 —— 再加 `features: { channelMenu: true }`):
60
+ 右键点中通道的回线,引擎把这条通道的上下文挂在**同一份 `onContextMenu` 载荷**的 `channelContext` 上(不再有单独的 `onChannelContextMenu` / `channelMenu`)。右键本身要接得住:`contextMenu` / `interactive: true` / `features: 'view'` 任一即可:
36
61
 
37
62
  ```js
38
63
  view.updateOptions({
39
- onChannelContextMenu: (context, event) => {
64
+ onContextMenu: (payload, event) => {
65
+ const context = payload.channelContext
40
66
  if (!context) {
41
- return closeMenu() // 点空白处:收到 null,用它把框收掉
67
+ return closeMenu() // 点空白或节点:没有 channelContext,用它把框收掉
42
68
  }
43
69
  // 这条通道有几组、当前是哪一组、这一组有几条回线 / 几个拐点
44
70
  console.log(context.channelId, context.groupCount, context.group.index, context.group.lineCount, context.group.waypointCount)
@@ -58,17 +84,23 @@ view.getChannelContext()
58
84
  view.getChannelContext('corridor-1', 'corridor-1::2')
59
85
  ```
60
86
 
61
- - 载荷是 `{ channelId, channelIndex, channel, groupId, groupCount, groups, group, actions, lineIndex, lineId, coordinate, x, y, clientX, clientY, originalEvent }`:`groups` 是这条通道的全部分组读数(`{ id, channelId, index, lineIds, lineIndexes, lineCount, waypointCount, edited, anchored, current, skeleton, path }`),`group` 是其中的当前分组,`groupCount` 是组数;`x` / `y` 是命中的画布 CSS 像素(`clientX` / `clientY` 是页面坐标),交互框照着摆就行。
87
+ - `payload` 仍是通用右键载荷(`action.type: 'contextMenu'`、`action.target: 'channel'`、`type: 'line'`、`channelId` / `groupId` / `lineIndex` / `lineId` / `coordinate` 等),`channelContext` 是 `{ channelId, channelIndex, channel, groupId, groupCount, groups, group, actions, lineIndex, lineId, coordinate, x, y, clientX, clientY, originalEvent }`:`groups` 是这条通道的全部分组读数(`{ id, channelId, index, lineIds, lineIndexes, lineCount, waypointCount, edited, anchored, current, skeleton, path }`;引擎的完整分组读数还带 `waypointOffsets`),`group` 是其中的当前分组,`groupCount` 是组数;`x` / `y` 是命中的画布 CSS 像素(`clientX` / `clientY` 是页面坐标),交互框照着摆就行。
62
88
  - `actions` 每项 `{ id, label, enabled, reason? }`:`enabled: false` 时 `reason` 说明原因(拐点编辑关了、这一组还没有拐点、已经是多组先合并再拆…),界面直接照着渲染按钮即可。
63
89
  - 六个动作 id:`select-group`(选中该分组)/ `deselect-group`(取消该分组的选中,等价于 `deselect({ channelId, groupId })`)、`add-waypoint` / `remove-waypoint`(给这一组加 / 删拐点,见上一节)、`split`(按 `options.size` 每组几条把通道拆成多组,只有一组且多于一条回线时可用)、`merge`(其余分组并回当前这一组)。
64
90
  - 更自由的分配走 `moveChannelLine(channelId, lineId, target)`:按**回线**粒度拆成新分组或并进别的分组 —— 两回线选「拆成新分组」就是两个分组各一条(见「通道分组」)。
65
- - 右键**落在节点上**时给的是 `null`:层级是点 → 线 → 通道,节点正压在回线端点是常态,这时该走 `onContextMenu` 的节点载荷(`type: 'point'`)。拾取到节点却弹出通道菜单是最容易被当成 bug 的一处。
66
- - 点在**空白处**(没命中通道回线)回调收到 `null`,正好用来收框;接住右键之后系统菜单一律被收掉(`preventContextMenu` 缺省 `true`,要留系统菜单就写 `false`;`contextMenu` 没开时组件根本不接右键,见「指针事件」)。右键的同时命中的那一组会被记成「当前分组」,`getChannelContext()` 不传参时取的就是它。
91
+ - 右键**落在节点上**时没有 `channelContext`:层级是点 → 线 → 通道,节点正压在回线端点是常态,这时载荷是 `type: 'point'`。拾取到节点却弹出通道菜单是最容易被当成 bug 的一处。
92
+ - 点在**空白处**(没命中通道回线)同样没有 `channelContext`,正好用来收框;接住右键之后系统菜单一律被收掉(`preventContextMenu` 缺省 `true`,要留系统菜单就写 `false`;`contextMenu` 没开时组件根本不接右键,见「指针事件」)。右键的同时命中的那一组会被记成「当前分组」,`getChannelContext()` 不传参时取的就是它。
67
93
  - 新增通道草稿期间右键等于取消草稿(发一条 `phase: 'cancel'`)。
68
94
 
69
95
  ### 新增通道
70
96
 
71
- `beginAddChannel()` 进入新增流程(**草稿缺省关**,写 `features: { channelDraft: true }` 或 `features: 'edit'` 才进得去;程序化的 `addChannel()` 不受限):点第一个节点当起点,一条草稿线跟着光标走(颜色 / 线宽 / 两端圆点由 `channelDraftStyle` 配),点第二个节点出方案表。**同一套流程用 `kind` 分两种**:`'channel'`(缺省)是**新建通道**,两个节点之间已经有通道就加一组 / 并进某一组;`'line'` 是**新建线路**,只加不挂任何通道的独立线路(`channels` 一个字节都不动,两个节点相同才不允许)。`circuits` 决定这一次加几条(通道回线 / 独立线路都一样)。草稿期间点空白处继续等(不会退出)、点起点同一个节点不算数、按住拖动仍然平移画布,Esc / 右键 / `cancelAddChannel()` 退出。
97
+ `beginAddChannel()` 进入新增流程(**草稿缺省关**,写 `features: { channelDraft: true }` 或 `features: 'edit'` 才进得去;程序化的 `addChannel()` 不受限):缺省点第一个节点当起点,一条草稿线跟着光标走(颜色 / 线宽 / 两端圆点由 `channelDraftStyle` 配),点第二个节点出方案表。**`options.from` 可以预指定起点**(节点 id / 下标 / `{ id }` / `{ index }`),调用后直接进入 `phase: 'from'`,只需再点终点;不写 `from` 就保持原来的两次点击。开始事件的 `from` 相位、`select: true` 时预置起点的选中状态都会同步给出;`from` 找不到时草稿不开始并报 warning。**同一套流程用 `kind` 分两种**:`'channel'`(缺省)是**新建通道**,两个节点之间已经有通道就加一组 / 并进某一组;`'line'` 是**新建线路**,只加不挂任何通道的独立线路(`channels` 一个字节都不动,两个节点相同才不允许)。`circuits` 决定这一次加几条(通道回线 / 独立线路都一样)。草稿期间点空白处继续等(不会退出)、点起点同一个节点不算数、按住拖动仍然平移画布,Esc / 右键 / `cancelAddChannel()` 退出。
98
+
99
+ ```js
100
+ view.beginAddChannel({ from: 'station-a' }) // 起点已经指定:接下来只点 station-b
101
+ view.beginAddChannel({ from: { id: 'station-a' }, select: true })
102
+ view.continueAddChannel({ from: 'station-c', kind: 'line' }) // 连续新增的下一段也能换指定起点
103
+ ```
72
104
 
73
105
  **直线 / 正交折线(`routing` / `path`)**:`beginAddChannel()` / `continueAddChannel()` 新增 `routing: 'straight' | 'orthogonal'` 与 `path`。`routing` 缺省 `'straight'`;写 `'orthogonal'` 且没有有效中间拐点时,组件自动补一个「先横后纵」的拐点 `[to.x, from.y]`。`path` 是**中间拐点**(不含两端),写了有效拐点就按它走,此时不会再追加自动拐点;草稿与 `commitAddChannel` / `addLine` / `addChannel` 共用同一份路由解析,预览和落地的折线完全一致。`onChannelDraft` / `onChannelDraftClick` 载荷里也带 `routing` / `path`,两端选好后 `path` 是实际会落地的中间拐点。
74
106
 
@@ -252,8 +284,33 @@ view.addLine({ from: 'station-a', to: 'station-b' }, [{ id: 'n-1' }, { id: 'n-2'
252
284
 
253
285
  **余量怎么算**:`getChannelAddOptions(from, to, count?, kind?)`(实例层按节点 id,引擎层同名方法按下标)里的 `count` 就是这一次要加的回数,每个方案的 `enabled` 都按它判 —— `newGroup` 要求「还能新分组」并且 `count <= channelGroupMaxLines`,`joinGroup` 要求目标分组的余量 `>= count`(`reason` 会写清「只剩 R 回余量,本次要加 M 回」),全都不可用才 `blocked`。`kind: 'line'` 时方案表是空的、`preferred` 为 `null`(独立线路不挑方案),只有两个节点相同才 `blocked`。
254
286
 
287
+ `beginAddChannel()` / `continueAddChannel()` 的 `GisViewChannelDraftOptions` 逐项如下:
288
+
289
+ | 字段 | 类型 | 缺省 | 说明 |
290
+ | --- | --- | --- | --- |
291
+ | `from` | `GisViewPointRef` | 无 | 预指定起点(id / 下标 / `{ id }` / `{ index }`),直接进 `from` 相位;找不到时报 warning 且不开始 |
292
+ | `autoCommit` | `boolean` | `false` | 两个节点一选完就按推荐方案自动落地,不用再调 `commitAddChannel()` |
293
+ | `continuous` | `boolean \| 'chain'` | `false` | 每段落地后自动开下一段;`'chain'` 等价于同时 `chain: true` |
294
+ | `chain` | `boolean` | `false` | 连续新增时以上一段终点当下一段起点;显式 `from` 优先于它 |
295
+ | `kind` | `'channel' \| 'line'` | `'channel'` | 新建通道 / 回线,或新建不挂通道的独立线路 |
296
+ | `circuits` | `number` | `1` | 一次新建几回;`lineItems` 给了有效项时按它的条数对齐 |
297
+ | `name` | `string` | 按两端节点生成 | 新线路名 |
298
+ | `lineId` | `string \| number` | 按通道 id 生成 | 新回线 id 的兼容别名;与 `id` 二选一 |
299
+ | `id` | `string \| number` | 无 | 新回线 id;与 `lineId` 同时给时优先 |
300
+ | `lineProps` | `Partial<GisViewLine>` | 无 | 透传给每条新回线的业务字段(颜色、宽度、电压等级…) |
301
+ | `linePropsByMember` | `Array<Partial<GisViewLine> \| undefined>` | 无 | 逐回属性,第 i 项只盖在第 i 条上;与 `createChannels` 同名同义 |
302
+ | `lineItems` | `GisViewChannelLineItems` | 无 | 连同线路一起新建的一条 / 一批;给了几条就建几条 |
303
+ | `routing` | `'straight' \| 'orthogonal'` | `'straight'` | 直连,或自动补一个先横后纵的拐点 |
304
+ | `path` | `GisViewCoordinate[]` | 无 | 中间拐点(不含两端);有效时优先于 `routing` 的自动拐点 |
305
+ | `channelDraftStyle` | `GisViewChannelDraftStyle` | 全局 `channelDraftStyle` | 本次草稿的预览样式,草稿结束即丢弃 |
306
+ | `handoff` | `boolean` | `false` | `ready` 发完就收掉假通道,由使用方自己落地;与 `autoCommit` 同给时先自动落地 |
307
+ | `select` | `boolean` | `false` | 点节点时同步进选中集(走 `onSelect` / `onSelectChange`);预指定 `from` 时起点当场选中 |
308
+ | `channelProps` | `Partial<GisViewChannel>` | 无 | 新通道透传字段(宽度、节点宽度、偏移方式…) |
309
+ | `originalEvent` | `Event \| null` | `null` | 触发这次草稿的原始事件,原样带进回调载荷 |
310
+
255
311
  ```js
256
312
  view.beginAddChannel({
313
+ from: 'station-a', // 预指定起点:接下来只点终点;不写则点两次
257
314
  autoCommit: false, // true:两个节点一选完就按推荐方案自动落地,不用再调 commitAddChannel
258
315
  name: '新增通道', // 新线路名,缺省按两端节点拼
259
316
  lineId: 'new-line-1', // 新回线 id,缺省按通道 id 生成(撞车自动加后缀)
@@ -36,7 +36,7 @@ view.updateOptions({
36
36
  | `position` | `GisViewLabelPosition` | `'right'` | 八向与居中 |
37
37
  | `offset` / `gap` | `[number, number]` / `number` | `[0, 0]` / `8` | 额外像素偏移与节点间距(`anchor: 'edge'` 时从符号外沿量起) |
38
38
  | `anchor` | `'edge' \| 'center'` | `'edge'` | 贴边方式:`edge` 让标签贴着符号外沿,节点放大后标签一起往外让;`center` 是从节点中心量 |
39
- | `html` | `boolean` | `false` | 字符串内容是否按 HTML 渲染;模板里的取值始终转义,函数返回 `HTMLElement` 时不受它影响 |
39
+ | `html` | `boolean` | `false` | 字符串 `content` 是否按 HTML 解析。逐条 `labels[].html` / `updateLabel(..., { html })` 可以覆盖全局;写 `false` 时标记按字面文本显示。模板取值、`field` 与默认名称始终转义,函数返回 `HTMLElement` 时不受它影响。**它只控制解析方式,不决定容器**:写 `false` 仍然逐条建 `.gis-view__label`,要减少高密度纯文本标签的 DOM 见「标签容器渲染(labelRender)」 |
40
40
  | `when` | `GisViewConditionInput` | 无 | 条件语言,只给命中的节点渲染标签 |
41
41
  | `maxCount` | `number` | `200` | 最多渲染多少个标签(按当前视口内可见点的顺序取,被避让丢掉的候选不计入),`0` 表示不限制;只在 `labelOptimize: true` 时生效(缺省强制显示档不截断) |
42
42
  | `className` / `style` | `string` / `Record<string, string>` | 无 | 附加类名与内联样式:**不用写 HTML 也能改外观** —— `style` 是内联样式的增量合并,`background` / `color` / `fontSize: '14px'` / `fontWeight` / `borderRadius` / `padding` 这些写什么加什么(值按 CSS 写法给字符串),逐条再叠 `updateLabel` 的 `style`;不配就是样式表里的默认(白底 88% / `#1f3550` / 12px),想清掉某一项就写 `{ 那一项: null }` |
@@ -52,6 +52,10 @@ view.updateOptions({
52
52
 
53
53
  函数形态的 `content` 会收到 `{ index, item, x, y, screen, radius }`(`radius` 是节点画在屏幕上的半径),需要自己拼 DOM 时直接返回元素即可。`anchor: 'edge'` 时标签到节点的距离始终是「符号当前的屏幕半径 + `gap`」:节点被悬停 / 选中样式放大,标签跟着往外让,缩小跟着靠近;缩放地图时标签按新相机重新投影,间距不会跟着地图一起被放大(鹰眼图放大缩小几次之后标签也不会飘出画布)。`html: true` 时字符串按 HTML 渲染:模板串里的取值仍然会转义,自己拼标记时也要记得转义业务字段。标签只渲染当前视口内的节点,相机变化后按 `requestAnimationFrame` 合并重排,移出视口的标签 DOM 回收进池子复用;单个节点可以用 `labelVisible: false`、`labelPosition`、`labelOffset` 覆盖全局配置,也可以用 `labelVisibility: 'always' | 'hidden'` 单独指定显隐策略(`labelVisible: true` 等价于 `always`)。
54
54
 
55
+ `html` 是**逐条**开关:全局 `label.html: true` 时,某条数据仍可写 `labels: [{ id: 'default', content: '<b>{name}</b>', html: false }]`,这一条就显示字面的 `<b>…</b>`;反过来全局 `false`、单条写 `true` 也会按标记渲染。运行时用 `updateLabel(kind, ref, { html: true | false })` 双向切换,写 `null` 回到全局 / 声明值。只有字符串 `content` 受它控制,函数返回 `HTMLElement` 时始终按元素插入。`getLabels()` 的清单以及 `labelClick` / `labelDrag` 载荷里的 `label` 都会带 `html`;HTML 模式下 `content` 返回 `innerHTML`,编辑面板回写时不会把 `<b>` / `<span>` 洗成纯文本。导出数据会把显式 `true` / `false` 一起写进 `labels`,导出再回灌保持原语义。
56
+
57
+ HTML 标签不等于每帧重新 `innerHTML`:覆盖层把内容、内联样式、class 与尺寸签名分开比较,只有内容真的变化才重解析标记;只改字号 / 颜色 / 位置时内容一个字节都不重写。尺寸缓存也按内容与样式共享,不同节点上的同文案标签只量一次布局。HTML 解析本身仍有成本,能靠 `style` / `className` 完成的外观优先用它们;大量富文本标签建议配合 `labelOptimize: true`、`maxCount` 与 `scanLimit`。
58
+
55
59
  排布顺序上有两层稳定化:上一轮已经显示出来的标签优先占位(平移时不会因为扫描窗口换人而成片消失),同一轮里 `always` 的排在 `auto` 前面,`auto` 抢不到位置就不画。避让用的是按 64 px 分桶的网格索引,候选再多也不会退化成两两比较;竖长拖拽时每隔 220 ms 会补一次真实排版,不然整层变换只能带着上一轮那批标签走。整层变换只剩平移:缩放时一条标签都不缩放文字 —— 512 条以内逐个按新相机重投影,超过 512 条按排版时记下的节点中心与固定像素偏移线性推,字号、清晰度与观感都保持原样。标签落点吸附到**设备像素网格**(`devicePixelRatio × 宿主显示比例`,1.25 / 1.5 倍屏上整数 CSS 像素仍可能落在半个设备像素上);拖动期整层位移也按同一网格吸附,且逐条标签不再各自写 `will-change`,只保留标签层整体的合成提示,避免多个合成层产生相位差与重复栅格化。节点标签、线路标签、提示框、底图区域文字的投影都走这一份口径。默认开启的标签避让(`avoidOverlap`)在候选和已经放下的标签重叠时按 `overlapMode` 走:`'shift'`(默认)主动换落点,节点标签绕这个节点换锚点再试(`anchorFallback`,默认最多试 4 个:先换对侧,再按左 / 上 / 下…绕一圈),线路标签沿线滑动(`slideFallback`),找不到空位才不画;`'hide'` 不做任何位置回退,碰撞就直接不显示。换向只改 class 与 transform,内容与尺寸都不重量,也没多读一次布局。底图的区域文字(`background.geoJsonStyle.label`)另占一张「软」网格:标签先绕开它,实在绕不开照画 —— 数据标签优先于底图上写死的文字;只压到区域文字不会触发 `hide`。
56
60
 
57
61
  **一个节点上多条标签**:`label` 也可以写成数组,数组里每一条各自有内容 / 位置 / 样式 / 显隐,`id` 是它与数据里的 `labels`(见「标签(labels,导入即回显)」)以及 `updateLabel` / `removeLabel` 对上号的键:
@@ -138,6 +142,30 @@ view.updateOptions({ labelOptimize: true }) // 切到优化档;false / 不写
138
142
 
139
143
  两档的差别在密集场景里是数量级:同一个视口里挤着 2 万个节点(1280×800、quality high,实测见「性能实测与 WebGPU 评估」),优化档只画得下 57 条标签、连续平移 / 缩放都在一帧 17 ms 上下(顶到刷新率);强制显示档 2 万个标签全画,同样的平移 / 缩放要 187.5 / 390.7 ms 一帧。十万级数据建议显式打开;另一头,强制显示档因为避让判定整段不走,连底图区域文字那张「软」网格都不用铺(省掉逐条量区域文字盒子的那一趟)。
140
144
 
145
+ ## 标签容器渲染(labelRender)
146
+
147
+ `html` 与顶层 `labelRender` 解决的是两件事:`html: false` 只关闭字符串的 HTML 解析,标签外壳仍是逐条 `.gis-view__label`(默认 `div`),几万条纯文本标签就会产生几万个节点;`labelRender` 决定这些纯文本标签要不要换成一张画布。
148
+
149
+ - **`auto`(缺省)**:只在**强制显示档**(`labelOptimize` 未开、`denseMode` 未激活)启用。节点层与线路层各自统计这一轮能画布的纯文本标签,某一层达到 **1024** 条,该层就改画到一张 `.gis-view__label-canvas`,不再为这些标签建 `div`;两层互不牵连。
150
+ - **`canvas`**:纯文本标签始终改画到单张 Canvas,不受 1024 条阈值限制,优化档与密集档也生效。`maxCount` / `when` / `scanLimit` / `overlapMode` 等策略照旧,不能画布的标签仍按原规则走 DOM。高密度场景建议同时打开 `labelOptimize`,让截断与避让限制画布标签数量。
151
+ - **`dom`**:始终逐条建 `.gis-view__label`,保持旧行为。数据量不大、要自己用 CSS / 事件精细控制标签时用它。
152
+ - **仍走 DOM 的标签**:`html: true`、函数返回 `HTMLElement`、可拖拽 / 可编辑,以及 Canvas 画不全的样式(背景图、阴影、`transform`、多行、字间距、非实线或四边不一致的边框等)会留在同层 DOM 里,和画布标签可以混排。画布只接管纯文本且样式能完整复刻的那部分。
153
+ - **优化档与避让**:显式 `canvas` 在 `labelOptimize: true` / 密集档下不会退回 DOM;避让碰撞框、节点换锚点与线路沿线换位都直接使用画布记录的宽高,文字宽度按「字体 + 样式 + 文案」缓存,同一批同文案标签不再逐条 `measureText`。线路内容是函数时无法保证换位后文字同步,换位回退会保守放弃,避免文字与位置错配。
154
+ - **画布与交互**:画布按视口 CSS 尺寸与设备像素比(含宿主显示比例,夹在 0.5 ~ 4)分配像素。首屏排版完成时立即出图;平移、缩放、拖节点、拖线路与悬停半径变化产生的连续重绘按 `requestAnimationFrame` 合并到下一帧,同一帧多次相机变化只画一次。绘制前会剔除视口外标签(旋转标签按外接圆,平移期把整层位移也算进去)。`exportImage({ includeLabels: true })` 会把画布标签写进 SVG。数量回落到阈值以下或切到 `dom` 时,DOM 标签会重新建回来;切换只重排标签层,不动引擎。
155
+
156
+ ```js
157
+ view.updateOptions({ labelRender: 'auto' }) // 缺省:强制显示档达到 1024 条才自动画布化
158
+ view.updateOptions({ labelRender: 'canvas' }) // 纯文本始终画布化(优化档也生效)
159
+ view.updateOptions({ labelRender: 'dom' }) // 强制逐条 DOM,保持旧行为
160
+
161
+ // 大数据下通常和优化档一起用:200 条上限、避让与换落点都保留,只是容器换成画布
162
+ view.updateOptions({
163
+ labelOptimize: true,
164
+ labelRender: 'canvas',
165
+ label: { enabled: true, content: '{name}', maxCount: 200 }
166
+ })
167
+ ```
168
+
141
169
  ## 标签的拖动与编辑
142
170
 
143
171
  节点标签与线路标签各有一对开关(`label` / `lineLabel` 上的 `draggable` 与 `editable`,都缺省 `false`),打开之后标签自己接住指针,关着时标签照旧 `pointer-events: none`,一点都不挡画布:
@@ -155,6 +183,31 @@ view.on('labelDblClick', payload => openEditor(payload)) // 双击标签:载
155
183
  view.on('labelChange', payload => {}) // { action: 'update' | 'reset' | 'resetAll' | 'add' | 'remove' | 'restore' | 'addAll' | 'updateAll' | 'removeAll', kind, index, id, labelId?, patch?, state?, removed? }
156
184
  ```
157
185
 
186
+ ### 逐条标签的交互开关
187
+
188
+ `draggable` / `editable` 不只能配在全局 `label` / `lineLabel` 上:数据里的 `labels[]`、`addLabel` / `addLabels` 的定义也可以逐条写。规则是「这一类有任意一条全局配置打开,就成为该类标签的缺省;这一条自己写了就以自己为准」:
189
+
190
+ - 没写 `draggable` / `editable` 的标签跟随该类缺省(`options.label` 或 `options.lineLabel` 里任意一条写了 `true` 就算打开)。
191
+ - 写了 `true` 只让这一条接指针;写了 `false` 可以**单独关掉这一条**,即使全局是 `true`。
192
+ - 两个都关着(或缺省就是关)时,这一条标签保持 `pointer-events: none`,不挡画布,也不发 `labelClick` / `labelDrag` / 右键 / 双击。
193
+
194
+ ```js
195
+ const points = [{
196
+ id: 'p-1', x: 0, y: 0, name: '甲站',
197
+ labels: [
198
+ { id: 'name', content: '{name}', position: 'right', draggable: true, editable: true }, // 这一条可拖可点
199
+ { id: 'note', content: '只读备注', position: 'bottom', draggable: false, editable: false } // 全局开着也单独关掉
200
+ ]
201
+ }]
202
+
203
+ const view = createGisView(container, {
204
+ points,
205
+ label: { content: '{name}', draggable: true, editable: true } // 没写交互字段的标签跟随这一档
206
+ })
207
+ ```
208
+
209
+ 拖动仍按「要素 + `labelId`」记偏移,点击仍按同一条发 `labelClick`;A 拖出来的位移不会写到 B 身上。逐条标签如果放在优化档(`labelOptimize: true`)里,还要注意避让可能把某一条挤掉;要保证同一节点上的两条都出现,可以给它们不同的 `offset`,或在定义里写 `avoidOverlap: false`(`LabelDemo` 的第一个节点就是这么让「可拖 / 可编辑」与「只读」两条同时显示的)。
210
+
158
211
  **点的是哪一条标签、挂在哪条要素上**:`labelClick` 与 `labelDrag` 的载荷里都带两份现成的读数,不用自己去 `getLabels()` 反查:
159
212
 
160
213
  - `label` —— 这一条标签自己:`{ id, content, position, className, style, source, own, added, definition }`。`content` 是当下画在屏幕上的文本(模板 / 函数算完之后的那一份),`position` / `style` 是合并之后的生效值,`source` 说明它是哪来的(`'config'` 配置里定义的 / `'data'` 数据里写的 / `'runtime'` 运行时 `addLabel` 加的),`definition` 只在运行时加的标签上有(持久化回显按它重建)。
@@ -46,6 +46,8 @@ view.updateOptions({
46
46
 
47
47
  `max` 缺省的 `'auto'` **不是拿场的峰值归一化**:密度场天生长尾(几个热点能比周围高一两个数量级),按峰值线性归一化会把九成面积挤进最冷一档、整张图糊成一片。缺省改成按分位取「膝盖」—— 90% 以下的面积铺满前 3/4 档,剩下那一截留给热点周围的色带;要老行为写 `max: 'peak'`,写数字就是按它线性归一化。`threshold`(门槛)与 `gamma`(对比度)一起决定盖住多大面积:门槛抬起来只留热点,就得到参考图那种「大片浅色 + 一圈圈色带 + 白心」。`weight: 'value'`(缺省)按要素的值域比例算比重(值高 + 密集 = 热),`weight: 'count'` 只数密度。场按世界坐标烘一次,平移不重算,缩放档变化超过 1.35 倍才重烤;要素特别多时撒点自动降采样(档边仍是细栅格的分辨率),`opacity` 只在画的时候乘、不触发重烤。
48
48
 
49
+ 密度场的缓存分两层:坐标、权重字段、显隐或通道值变化时才重新采集要素;只改密度核参数、色带或画层时复用上一份采样。`density.opacity` 与 `layer` 只改绘制、不重烤,换色带或核参数才用旧采样重算场。拖动节点 / 拐点时只记一笔「几何已变」,停手后统一补烤一次,不在每个指针事件里重算;密度场关闭期间发生的坐标 / 权重变化也会记账,下一次开启不会复用旧要素。
50
+
49
51
  **鹰眼图**跟着同一份 `heatmap` 取色带颜色与尺寸倍数,但**不画光晕、也不画密度场**:小图要的是清晰,一层放大的光晕或一张分档色块在几百像素的缩略图上只会糊成一片。
50
52
 
51
53
  **作用对象与通道**:`target` 决定哪些要素吃这份配置。`target: 'channel'` 时通道里的每条回线取**所属通道**的字段值(整条走廊一个颜色,回线不会各染一色);回线自己有值(`target` 含 `'line'`,且该回线命中字段或自己配了 `value`)时按自己的值算。逐要素 `item.heatmap` 与全局同义:`false` 不参与(保持自身配色),`true` 沿用全局那份,对象在全局配置上逐字段覆盖(只想改字段 / 值域 / 色带里的一个就写一个)。
@@ -206,6 +208,7 @@ for (const glyph of view.getLineEffects()) {
206
208
 
207
209
  - **动点进的是既有批次**:形状(圆)进圆点实例缓冲、图片 / SVG 进贴图组,与节点 / 装饰走同一条符号链(同一张烘焙好的贴图,不会为动画再烤一份)。
208
210
  - **每帧只改位置**:`updateLineEffects()` 按弧长算出这一帧的位置,按**槽位连续**攒成一段一次 `bufferSubData`(一段最多 64 个),写入量只跟视野里的动点数走;包围盒与视野不相交的线整条跳过。一帧最多算 4000 个动点,超了下一帧接着补。
211
+ - **每帧只遍历配了动点的线路**:动画线路另建 `lineEffectRuns` 清单,`updateLineEffects()` 不再逐条扫全部线路 —— 普通线路再多,动画循环也只处理真正配了 `effect` 的那几条。拖动端点 / 拐点、通道骨架变化时,路径就地换掉的同时重算该条总长与包围盒,动点按新弧长继续采样,不会按旧长度跳位;多层阴影的实例更新也合并成一次 `bufferSubData`。
209
212
  - **涟漪每帧只传一个 `u_time`**:半径、相位、周期、线宽在建实例时全部定死(相位按 `stagger` 与第几圈算好),实例缓冲一个字节都不重传,GPU 侧一次实例化绘制画完。
210
213
  - **涟漪实例只建「视口里的」**:先问点网格要视口外扩一成的候选,排一遍保证「按数据顺序」,再用与框选同一份可见判定过滤;相机在动但**拖动 / 缩放期间不重建**(涟漪记的是世界坐标,相机怎么动它们自己跟着走),停手后那一帧一次补齐。`limit`(缺省 2000)是兜底:密集数据里一屏几万个节点不会把涟漪铺成一片糊。
211
214
  - **不画的时候一个字节都不花**:动画开着才续帧 —— 视野里没有动点也没有涟漪、页签切到后台(`document.hidden`)、系统要求减少动态效果(`prefers-reduced-motion`)时直接停,静止画面照旧按需渲染;`effects` 没配(数据里也没人逐条配)时这一整层只是几个布尔判断。
@@ -4,7 +4,7 @@
4
4
 
5
5
  ## 筛选
6
6
 
7
- `setFilter` 按属性过滤点与线,点被过滤掉时绑在它身上的线路按 `cascade` 联动消失:
7
+ `setFilter` 按属性过滤点与线,点被过滤掉时绑在它身上的线路按 `cascade` 联动消失;这个默认联动还受 `lineFollowsNode` 影响(见下):
8
8
 
9
9
  ```js
10
10
  // 只看 525kV 以上的节点:两端节点没了的线路(以及通道回线)一起隐藏
@@ -47,10 +47,12 @@ normalizeFilter({ points: {}, cascade: 'both', nope: 1 }) // unknownKeys: ['nop
47
47
  | --- | --- | --- | --- |
48
48
  | `points` | `GisViewConditionInput<GisViewPoint>` | 无 | 点条件,缺省表示不过滤点 |
49
49
  | `lines` | `GisViewConditionInput<GisViewLine>` | 无 | 线条件,缺省表示不过滤线 |
50
- | `cascade` | `false \| 'binding' \| 'coordinate' \| 'both'` | `'binding'` | 点被过滤后哪些线路一起消失:按 `from` / `to` 绑定、按端点坐标(端点正好落在被过滤的节点上)、两者都算、或完全不联动 |
50
+ | `cascade` | `false \| 'binding' \| 'coordinate' \| 'both'` | `lineFollowsNode` 开启时 `'binding'`,关闭时 `false` | 点被过滤后哪些线路一起消失:按 `from` / `to` 绑定、按端点坐标(端点正好落在被过滤的节点上)、两者都算、或完全不联动;显式写出的值始终优先 |
51
51
  | `worker` | `boolean` | 跟随 `filterWorker` | 这一次筛选是否允许放进 Worker |
52
52
 
53
- 联动只影响表现,数据本身不动:`updateData` / `updateOptions` 换了数据之后按同一个条件重算。通道回线也是普通线路,跟着同一个 `cascade` 走:`binding` / `coordinate` / `both` 时绑着被过滤节点的通道回线一起收起来,`false` 时照旧画 —— 被过滤的节点仍然当锚点用(几何不跟着筛选断,回线端点还钉在它原来的坐标上),只是节点自己不显示。只有节点真的不在数据里(删掉、或者写了 `visible: false`)时,两端锚不住的通道与它的回线才整条不显示(和「两端没有节点就不显示」是同一条规则)。拖拽节点松手后,按端点坐标联动的筛选会按新坐标重算一次。需要「把某个要素找出来并居中」而不是隐藏其它要素时用 `search`,见「搜索与定位」。
53
+ 联动只影响表现,数据本身不动:`updateData` / `updateOptions` 换了数据之后按同一个条件重算。通道回线也是普通线路,跟着同一个 `cascade` 走:`binding` / `coordinate` / `both` 时绑着被过滤节点的通道回线一起收起来,`false` 时照旧画 —— 被过滤的节点仍然当锚点用(几何不跟着筛选断,回线端点还钉在它原来的坐标上),只是节点自己不显示。`cascade: false` 时仍显示的通道宽度也按被过滤节点原本的尺寸链计算,不会因为节点不画了就改按另一端尺寸或退回默认 `channelWidth`。只有节点真的不在数据里(删掉、或者写了 `visible: false`)时,两端锚不住的通道与它的回线才整条不显示(和「两端没有节点就不显示」是同一条规则)。
54
+
55
+ `lineFollowsNode: false` 把「节点退场」与「线路 / 通道退场」拆开:筛选未显式写 `cascade` 时默认不再级联,隐藏、删除节点也只动节点;线路与通道继续停在最后一次解析出的坐标、骨架与宽度。条件里显式写了 `cascade` 时仍按显式值级联。筛选条件放宽或清空、被筛掉的回线重新出现后,引擎会在线路线宽解析完成后重新排一次通道偏移,通道与回线按当前宽度回到正确车道,不会保留筛选期间的旧间距。拖拽节点松手后,按端点坐标联动的筛选会按新坐标重算一次。需要「把某个要素找出来并居中」而不是隐藏其它要素时用 `search`,见「搜索与定位」。
54
56
 
55
57
  ### 大数据下的 Worker 筛选
56
58
 
@@ -93,7 +95,12 @@ hit?.field / hit?.value // 命中的是哪个字段、字段值是什么(如 '
93
95
  // focusOn:把视角移到目标上 —— 单条、一批、只居中都由它一条搞定
94
96
  view.focusOn(hit) // 一条命中:按它的范围适配视图
95
97
  view.focusOn('station-1024', { scale: 12 }) // 直接给比例
98
+ view.focusOn({ type: 'point', id: 'station-1024' }) // 节点 / 线路同 id 时显式消歧
99
+ view.focusOn({ type: 'line', id: 'station-1024' })
96
100
  view.focusOn({ type: 'line', index: 3 })
101
+ view.focusOn({ type: 'channel', channelId: 'channel-a' }) // 通道也可以直接写 { channelId }
102
+ view.focusOn({ type: 'group', channelId: 'channel-a', groupId: 'group-2' })
103
+ view.focusOn({ type: 'group', groupId: 'group-2' }) // groupId 全局唯一时可省略通道
97
104
  view.focusOn([116.4, 39.9]) // 一个坐标点:只居中,比例不动
98
105
  view.focusOn(['station-1', 'station-2', 'station-3']) // 一批:取整体包围盒 —— 这三个点的中心当屏幕中心
99
106
  view.focusOn(['station-1', 'station-2'], { select: true }) // 顺带把它们选上(与接口选中同一份 state)
@@ -108,6 +115,8 @@ view.focusOn({ lngLat: [116.4, 39.9] }, { center: true }) // 配了 GeoJSON 底
108
115
  view.focusOn({ minX: 116, minY: 39, maxX: 117, maxY: 40 }, { center: true }) // 一段范围:取中心
109
116
  ```
110
117
 
118
+ 字符串 / 数字仍然兼容旧写法:先按节点 id 找,再按线路 id 找;同一个 id 同时出现在节点与线路里时用 `{ type: 'point' | 'line', id }` 消歧。裸字符串不表示通道;整条通道可以写 `{ channelId }`,要和通用 `id` / `index` 写法区分时再用 `{ type: 'channel', ... }`。分组必须写 `{ type: 'group', channelId?, groupId }`,`groupId` 在全部通道里唯一时可以省略通道。节点、线路、通道与分组都先解析成世界坐标范围再交给同一条 `zoomToBounds` 路径,聚焦比例口径一致,没有为某类目标额外乘放大倍率。
119
+
111
120
  | `focusOn` 的选项 | 说明 |
112
121
  | --- | --- |
113
122
  | `scale` | 直接设成这个比例,不再按范围适配 |
@@ -205,7 +214,7 @@ createGisView(container, {
205
214
  4. **按「节点间距」而不是「包围盒」算适应**:`focusOn([...几个代表性要素])` / `zoomToBounds(业务范围)`;也可以给 `minScale` 一个下界,缩到这条线就停住。
206
215
  5. **想让节点本身不跟着缩**:`pointZoomScale` / `lineZoomScale` 调小(`0` = 完全不跟随地图缩放),尺寸主要由 `pointSizeScale` / `lineWidthScale` 的常数倍数决定 —— 比例被带跑时至少节点不会一起变小。
207
216
 
208
- 6. **让整个画面的比例恒定(想省事就用它)**:打开 `zoomScaleAuto` —— 没写联动指数的那几项按 1 算,包围盒变大(相机比例变小)时节点与线宽跟着一起小,节点大小与通道间距的比例保持不变;换了一批量纲完全不同的数据、想重新校准就 `setZoomBaseScale()` 把当下这一屏钉成基准,见「自适应比例(zoomScaleAuto)」。
217
+ 6. **给节点最小间距兜底(先试它)**:打开 `fitSpacing` —— 适应视图时按可见节点最近中心距反推比例,远处孤立点撑大包围盒也不会再把主体压成一团;孤立点允许落到视口外,必要时用 `maxZoom` 限制放大倍数,见「适应视图的节点间距(fitSpacing)」。
209
218
 
210
219
  ## 缩放范围(minScale / maxScale)
211
220
 
@@ -229,30 +238,33 @@ view.updateOptions({ minScale: scale / 5, maxScale: scale * 5 })
229
238
 
230
239
  demo 的「初始视图与背景」面板里就有现成的入口(`docs/demos/GisViewDemo.vue`):两个输入框(最小比例 / 最大比例)直接写数字,改完走 `updateOptions({ minScale, maxScale })` **并顺手 `setCamera` 把当前相机夹回区间**(更新配置本身不会把相机拉回来,demo 里把这一步显式补上了);`以当前比例设限(÷5 ~ ×5)` 按当下比例算一对;`试一下下限` / `试一下上限` 各喂一个远超区间的比例给 `setCamera()`,读数里能看到相机停在哪;`解除限制` 回到组件缺省的 `0.002 ~ 20000`。自测页 `docs/steps/selftest-demo.html` 有 3 条断言盯着这一套(按当前比例设限并下发 / 越界喂值被夹在上下限 / 解除限制回到缺省)。
231
240
 
232
- ## 自适应比例(zoomScaleAuto)
241
+ ## 适应视图的节点间距(fitSpacing)
233
242
 
234
- 不同数据集 / 不同包围盒下「节点一样大、通道间距却时短时长」的根因是**两套尺子**:节点的屏幕尺寸由 `pointSizeScale`(常数倍数)定,包围盒变了也不变;节点之间的距离是世界坐标乘 `camera.scale` 得来的,包围盒一大(适应视图后 `scale` 变小)屏幕上的间距就短 —— 每换一批数据都得重新调一次尺寸系数。
243
+ 「两个本来一样的视图,只因为多了一个远处节点,节点就突然很大、很挤」——根因是自动适应读的是**数据包围盒**:远处节点把范围撑大,`fitView()` 只能把 `camera.scale` 压小,节点之间的距离随之变短;节点自己的屏幕尺寸不跟着缩,看起来就像放大了一样。
235
244
 
236
- `zoomScaleAuto` 把这两套尺子合成一套:**打开后没写联动指数的 `pointZoomScale` / `lineZoomScale` 按 1(与地图同倍率)算** —— 包围盒变大、相机比例变小时节点与线宽跟着一起小,节点大小与通道间距的比例保持不变。
245
+ `fitSpacing` 不改包围盒,而是在普通适应算完之后再做一次**节点间距兜底**:找出当前可见节点里最近的两个不同位置,按它们的世界距离算出「要让中心距达到 `minDistance` 需要多大的比例」,取普通适应与这个比例中较大的一个。它**只提高 `camera.scale`,不改变相机中心**:中心始终沿用普通适应按数据包围盒算出的那一个,所以不会因为最近节点对换了一对就突然跳到图上另一个位置。规则网格里最近距离大量并列也不会影响结果 —— 距离相同就只取同一个目标比例,没有「挑哪一对并居中」的问题。
237
246
 
238
247
  ```js
239
- // 打开自适应比例:之后换数据 / 加远处的节点,节点大小与通道间距按同一比例变
240
- const view = createGisView('#map', { points, lines, channels, zoomScaleAuto: true })
241
- view.updateOptions({ zoomScaleAuto: true }) // 运行时开关也只重画一帧(不重排场景)
248
+ // 最省事:最小中心距 24 CSS 像素,放大倍数不设相对上限(仍受 maxScale 约束)
249
+ const view = createGisView('#map', { points, lines, channels, fitSpacing: true })
250
+
251
+ // 细调:最近节点至少隔 32px;最多放大到普通适应比例的 8 倍
252
+ view.updateOptions({ fitSpacing: { minDistance: 32, maxZoom: 8 } })
253
+ view.fitView() // 改配置不会当场动相机,再适应一次才看得到
242
254
 
243
- view.getZoomBaseScale() // 基准比例:缺省是首次适应视图时的 camera.scale,等于它时要素「原样大小」
244
- view.setZoomBaseScale() // 以当下这一屏为基准(换了一批量纲完全不同的数据、想重新校准观感时调它)
245
- view.setZoomBaseScale(2.5) // 也可以直接给一个比例
255
+ view.updateOptions({ fitSpacing: false }) // 关掉,下一次 fitView 恢复普通包围盒适应
246
256
  ```
247
257
 
248
258
  几个约定:
249
259
 
250
- - 开关只给**没写**指数的那一项兜底:显式写过 `pointZoomScale` / `lineZoomScale`(哪怕写 `0`)就以显式值为准 —— 想只让线路跟随、节点恒定,就写 `zoomScaleAuto: true` + `pointZoomScale: 0`
251
- - 基准是 `zoomBaseScale`,缺省取**首次适应视图**时的 `camera.scale`:首屏观感与从前一样,之后不管范围怎么变比例都不再被带跑;`setZoomBaseScale()` 用来换一批新量纲的数据重新定「原样大小」
252
- - 跟随是幂指数:`zoomScaleAuto` 打开且没写指数时就是 1(地图放大 2 倍、要素也大 2 倍);想弱一点显式写 `pointZoomScale: 0.5`(地图放大 4 倍、节点放大 2 倍)
253
- - 节点屏幕尺寸一变,按端点尺寸取宽的通道回线位置跟着重排(`channelNodeWidth`,见「通道与多回线」),因此打开之后缩放画面时通道宽度也是稳定的
254
- - 拾取容差跟着放大系数一起放大(放大后的节点 / 粗线照样点得准),标签避让用的也是同一份半径
255
- - demo 的「样式」面板有「自适应比例」开关与「以当前比例为基准」按钮;自测页 `docs/steps/selftest-scale.html` 有 6 条断言盯着这一套(基准缺省 / 相机放大 2 倍尺寸翻倍 / 重定基准 / 显式指数优先 / 关掉不再跟随)
260
+ - **默认关**,打开后只影响「默认 fit 模式下的首次适应」与手动 `fitView()`;`zoomToBounds()` / `focusOn()` 仍按指定范围算,不会强行套节点间距。
261
+ - 只统计**筛选 / 隐藏之后仍然可见**的节点。完全重合的坐标不算有效间距(否则两个重叠点会让整套兜底失效);少于两个不同位置时不调整。
262
+ - **只放大、不跳视点**:`fitSpacing` 不重算中心,也不挑一对节点居中。需要改变中心时用 `initialView.center` / `initialView.bounds`、`focusOn()` 或 `setCamera({ center })`。
263
+ - `minDistance` 是节点**中心之间**的屏幕距离(CSS 像素),默认 `24`。想给节点本身和描边留更多空隙就调大。
264
+ - `maxZoom` 是相对普通适应当前比例的倍数上限,默认 `0` 表示不限;设成正数可避免极端数据把画面放大到只剩局部。`minScale` / `maxScale` 始终是最后一层夹取。
265
+ - 远处孤立点仍参与普通适应的包围盒与中心计算;放大后它可以落到视口外,这是「先保证节点间距」的取舍。如果孤立点连中心都不该影响,关掉 `fitSpacing` 或改用 `initialView.bounds` / 预筛数据钉住业务范围。
266
+ - 显式配了 `initialView.scale` / `initialView.zoom` 时以使用方给的为准;只有没有写死比例 / 倍数的 fit 模式才走间距兜底。
267
+ - demo 的「样式」面板有「适应节点间距」开关、最小中心距与最大放大输入;自测页 `docs/steps/selftest-scale.html` 有 8 条断言盯着这一套(配置回显 / 提高比例且忽略重合点 / 保持普通适应中心 / 不跳到最近节点对 / 屏幕间距 / `maxZoom` / 关闭恢复 / 规则网格并列时仍保持原中心)。
256
268
 
257
269
  ## 背景层
258
270
 
@@ -439,6 +451,8 @@ createGisView(container, {
439
451
 
440
452
  交互与开销:**在框上按下直接拖框**(世界位移 = 屏幕位移 ÷ 小图比例),**空白处单击不改变视角**,按住超过 `longPressDelay` 才进入「跟着拖」,拖动过程中始终跟着光标;光标停在小图上滚动滚轮按光标位置缩放主图(`wheelZoom: false` 关掉)。每个有位移的手势结束时可通过 `onInteraction` 收到一条通知,不必自己在小图 DOM 上再挂一套指针监听。底图画的是筛选之后真正画出来的线,通道骨架与回线都在内(直接问引擎要几何,不用自己再推一遍通道偏移),画法是先铺线、再把节点整层盖上去 —— 节点层级高于线路,密集底图上的节点不会被线咬掉。底图只在数据或筛选结果变化时重画(节点直接写进像素缓冲,十万级也只在重建时花一次时间),相机变化只改视口框的 `transform`,所以拖动小图不掉帧。小图还会缓存**上一次的节点像素结果**与**数据范围**:一次改动排在前后两帧各重画一次(引擎重排之后要再取一次几何),第二次与第一次的数据、范围、尺寸完全一样时直接复用那份像素,十万点下第二次重画从 ~20ms 降到 0.3ms(配了 `pointMapZoomScale` / `lineMapZoomScale` 时把「主视图倍数」也并进缓存的键,倍数变了才重铺);`extent: auto` 的取舍跟相机有关,相机挪进 / 挪出那份小范围时才补一次重画(每帧只做几次比较,判断用的范围是缓存的)。小图拖框 / 小图滚轮带来的相机变化不算主画布上的交互:配了 `hideOnInteract` 的图层不会在这时被收起来,拖动结束还会补一次真实排版 —— 不会出现「动一下鹰眼图,主视图标签全没了」。真在主画布上拖动过之后、两层标签同时收着时,收尾计时器也照样挂上 —— 覆盖层停手的收尾是无条件的,不会因为「两层都在收着、当下没东西可排」把恢复一起跳过。
441
453
 
454
+ 线路层另有自己的离屏 canvas 与缓存:缓存键包含画布尺寸、范围、投影、小图倍率与线路样式,命中时整层直接合成,不再重复投影与描边;`collectMinimapData()` 复制路径时顺路算出每条线的包围盒,完全落在小图视野外的线不建路径。只有布局尺寸实际变化时才重设 canvas 尺寸,单纯补画或移动视口框不会清空这份缓存。
455
+
442
456
  `zoomable: true` 之后小图自身也能整体放大缩小,空白处的拖动换成下面两种动作(单击不再需要长按,空白处不再走 `longPressDelay` 那条路):
443
457
 
444
458
  - **平移小图**(`dragAction: 'pan'`,默认):光标往哪拖,小图内容跟着走,主图不动;内容夹在数据范围内,拖到头就停住。主图视口框跟着内容一起挪,必要时会贴在边上指示方向。
@@ -485,4 +499,6 @@ createGisView(container, {
485
499
 
486
500
  ---
487
501
 
502
+ **相关章节**:[03 配置项(options)](03-配置项.md) · [07 指针与要素编辑](07-指针与要素编辑.md) · [08 选中与拐点](08-选中与拐点.md) · [16 实例方法总表](16-实例方法总表.md)
503
+
488
504
  [← 13 动效与图元](13-动效与图元.md) · [手册目录](../GisView.md) · [15 导出与集成 →](15-导出与集成.md)
@@ -11,6 +11,7 @@ const url = await view.exportImage({ scale: 2 }) // 位图:
11
11
  const plain = await view.exportImage({ background: 'none' }) // 位图:透明底
12
12
  const svg = await view.exportImage({ type: 'svg' }) // 矢量:放大不糊
13
13
  const svgPlain = await view.exportImage({ type: 'svg', background: 'none' }) // 矢量 + 透明底
14
+ const full = await view.exportImage({ scope: 'all', padding: 32 }) // 全部内容:临时适应数据范围后出图
14
15
  // 也可以直接触发下载:download: true(用默认文件名)或 download: '线路图.png'
15
16
  await view.exportImage({ download: '线路图.png' })
16
17
  await view.exportImage({ type: 'svg', background: 'none', download: '线路图.svg' })
@@ -21,6 +22,8 @@ await view.exportImage({ type: 'svg', background: 'none', download: '线路图.s
21
22
  | `type` | `'image/png' \| 'image/jpeg' \| 'image/webp' \| 'svg'` | `'image/png'` | 导出格式:位图(`jpeg` / `webp` 可以用 `quality`)或矢量 SVG(`'svg'` 与 `'image/svg+xml'` 都认) |
22
23
  | `quality` | `number` | 无 | 0 ~ 1,只对 `jpeg` / `webp` 有效 |
23
24
  | `scale` | `number` | 设备像素比(SVG 为 `1`) | 导出倍率(相对画布 CSS 像素);SVG 只写进 `width` / `height` 两个属性,`viewBox` 始终是 CSS 像素 |
25
+ | `scope` | `'viewport' \| 'all'` | `'viewport'` | `'viewport'` 只导当前可视区域;`'all'` 临时把相机适应到全部当前可见内容,导完恢复原相机。写其它值直接抛异常 |
26
+ | `padding` | `number` | `24` | 只对 `scope: 'all'` 生效:适应全部内容时四周留白(CSS 像素,负数按 0 处理);`scope: 'viewport'` 忽略 |
24
27
  | `background` | `string \| false` | 带背景 | 见下表:不带背景 = 透明底,颜色字符串 = 换一层底色 |
25
28
  | `includeLabels` | `boolean` | `true` | 是否把标签一起画进去 |
26
29
  | `download` | `boolean \| string` | `false` | 是否触发浏览器下载,字符串同时作为文件名 |
@@ -33,6 +36,8 @@ await view.exportImage({ type: 'svg', background: 'none', download: '线路图.s
33
36
  | `false` / `'none'` / `'transparent'` | **不带背景**:上面那些都不画,输出**透明底**(位图是带 alpha 通道的 png / webp,矢量则干脆不写这些元素) |
34
37
  | `'#ffffff'` 这类颜色字符串 | **带背景,但换底色**:只把最底下那一层清屏色换成它,底图、GeoJSON 区域填充与网格照旧(固定白底 / 深色底就写它;`jpeg` 不支持透明,建议给一个色) |
35
38
 
39
+ `scope: 'all'` 不是按原始比例拼一张超大的图,而是**临时改相机把全部可见内容框进当前视口**,因此输出尺寸仍由画布 CSS 尺寸 × `scale` 决定;筛选 / `visible: false` 隐藏的要素仍不会进入导出。位图与 SVG 两条路都支持,`finally` 中会恢复相机并重排标签、扩展图形与鹰眼图,使用方看到的视角不会停在导出那一版。
40
+
36
41
  导出用的是当前这一帧的实际画面:位图内部先同步重画一帧再取像素 —— 透明底与换底色这两种写法出的都是「只给导出用」的那一帧,读完像素立刻补画一帧正常画面的把画布还原,屏幕上不会停在导出那一版。整个过程不需要开 `preserveDrawingBuffer`,所以也没有常驻的额外显存开销。
37
42
 
38
43
  `type: 'svg'` 走的是矢量:线(含虚线图案与相位)、点(内置圆 / 方与 SVG 符号)、线路装饰、拐点、GeoJSON 区域、扩展图形与标签都是真正的 SVG 元素,颜色 / 线宽 / 线型取的就是当前那一帧解析出来的值(悬停、选中样式同样在内),放大到任意倍都不糊。PNG / JPEG / WebP 这一支先画引擎画布,再按「扩展图形 → 标签」叠上去,所以 `graphics` 里的 SVG 路径、图片与 Canvas 都在合成结果里。两点差异:热力密度场是烘焙出来的贴图、不进 SVG;位图符号(`symbol.type: 'image'`)、自定义图元(`registerSymbol` 的画笔)与图片 / Canvas 图形都以 `<image href>` 出现(前者引用原地址,后两者是原图或 data URL),把导出的 svg 当图片内嵌到别的页面时,跨域图片与外部字体会被浏览器挡掉(直接打开 svg 文件没问题)。需要那一份矢量数据自己拼别的格式时,用 `view.snapshotScene({ background, labels })`(它已经带上 `graphics`),或用导出的 `buildSceneSvg(scene, { scale })`;`svgDataUrl(markup)` 是把一段 SVG 标记转成 `data:` URL 的小工具(组件内部导 svg 图片时走它,自己拿到标记要拼进 `<img src>` 或 CSS 时也可以直接用)。
@@ -35,11 +35,11 @@
35
35
  | `zoomIn(step?)` / `zoomOut(step?)` / `zoomBy(factor, origin?)` | 相对缩放,`origin` 为画布 CSS 像素锚点(缺省视口中心) |
36
36
  | `zoomTo(scale, origin?)` | 直接设成指定比例(像素 / 世界单位) |
37
37
  | `zoomToBounds(bounds, padding?)` | 把一段世界坐标矩形适应到视图内,退化成一点时只居中不改比例 |
38
- | `focusOn(target, options?)` | 把视角移到目标上:单条(搜索命中 / 要素 id / `{ type, index }` / 坐标 / 矩形 / 通道写法 / `{ lngLat }`)或**一批**(数组取整体包围盒 —— 搜到三个节点就是这三个点的中心当屏幕中心);`options` 认 `scale`、`center: true`(只居中、比例一点不动)、`padding`、`maxZoom`、`select`(顺带选中这批,`mode` 决定怎么并进当前选中集) |
38
+ | `focusOn(target, options?)` | 把视角移到目标上:单条(搜索命中 / 要素 id / `{ type: 'point' \| 'line' \| 'channel', id \| index }` / `{ channelId }` 整条通道 / `{ type: 'group', channelId?, groupId }` / 坐标 / 矩形 / `{ lngLat }`)或**一批**(数组取整体包围盒 —— 搜到三个节点就是这三个点的中心当屏幕中心);字符串先节点后线路,裸字符串不表示通道,同 id 用 `type` 消歧,分组必须显式写 `type: 'group'`(全局唯一 `groupId` 可省略通道);节点 / 线路 / 通道 / 分组共用同一套范围适配,不额外放大。`options` 认 `scale`、`center: true`(只居中、比例一点不动)、`padding`、`maxZoom`、`select`(顺带选中这批,`mode` 决定怎么并进当前选中集) |
39
39
  | `search(keyword \| options)` | 按关键字搜索节点 / 线路 / 通道并返回命中(**不动视角**:要不要聚焦由调用方自己调 `focusOn`),见「搜索与定位」 |
40
40
  | `searchAsync(keyword \| options)` | 同一份口径的异步版:数据量大时把扫描放进 Worker(10 万点 / 10 万线下同步要占主线程 40 ms 上下的那一段),返回 `Promise<GisViewSearchResult>`;结果带 `worker` 与 `stale` 两个字段,见「异步搜索」 |
41
41
  | `setCamera(camera)` / `getCamera()` | 读写 `{ center, scale }` |
42
- | `getZoomBaseScale()` / `setZoomBaseScale(scale?)` | 读 / 重定**缩放联动的基准比例**(缺省是首次适应视图时的比例):不传参数就取当下相机的比例 —— 把现在这一屏当成「原样大小」,之后放大缩小都从它算倍数;与自适应比例开关 `zoomScaleAuto` 配套,见「自适应比例(zoomScaleAuto)」 |
42
+ | `getZoomBaseScale()` / `setZoomBaseScale(scale?)` | 读 / 重定**缩放联动的基准比例**(缺省是首次适应视图时的比例):不传参数就取当下相机的比例 —— 把现在这一屏当成「原样大小」,之后放大缩小都从它算倍数;配合 `pointZoomScale` / `lineZoomScale` 使用 |
43
43
  | `getBounds()` | 数据范围(没有点时回退到背景 GeoJSON 的 `rawBounds`,也就是 `aspectScale` 之前的真实经纬度范围) |
44
44
  | `getViewRect()` | 当前可视范围的世界坐标矩形;自建缩略图 / 坐标读数用它换算视口框,内置鹰眼图见「鹰眼图」 |
45
45
  | `getMinimapZoom()` / `setMinimapZoom(zoom)` / `zoomMinimapBy(factor, origin?)` | 读 / 写鹰眼图自身的倍数:`set` 夹在 `minimap.minZoom` ~ `maxZoom` 之间,`zoomMinimapBy` 以画布 CSS 像素 `origin` 为锚点缩放(缺省小图中心),没配 `minimap` 时是空操作 |
@@ -47,6 +47,8 @@
47
47
  | `getLine(ref)` | 取一条线路:拾取载荷 + `path` / `bounds` / `color` / `width`(与 `listLines()` 的一项同一份口径),通道回线还带 `channelId` / `groupId` / `channelIndex`;找不到返回 `null` |
48
48
  | `getChannel(ref)` | 取一条通道:数据那一份(`index` / `item` / `id`)+ 成员表(`lineIndexes` / `skeleton` / `offsets` / `paths`)+ 分组上下文 `context`(几组 / 当前哪组 / 每组拐点数)、`from` / `to` / `anchored` / `spacing`;找不到返回 `null` |
49
49
  | `listPoints(options?)` | 按条件列节点快照 `{ index, item, x, y, radius, layerRank, screen? }`:选项见 `GisViewListOptions`(`indexes` / `inView` / `limit` / `withScreen` / `visibleOnly`),可见判定与 `isPointVisible` 是同一份 |
50
+ | `getClusters()` | 当前点聚合的展示组 `GisViewCluster[]`:按数据顺序返回,含 `count: 1` 的单成员与 `count > 1` 的聚合圆;每项带 `id` / `pointIndexes` / `pointIds` / 世界坐标 `coordinate` / 当前屏幕坐标 `screen` / 半径 `radius` / 包围盒 `bounds`。原始 `points` 与 `getData()` 不变,聚合关掉时返回空表 |
51
+ | `getPointCluster()` | 当前点聚合配置的归一化读数 `GisViewPointClusterReadout \| null`:`{ enabled, radius, minPoints, maxZoom, active }`,`active` 说明当前缩放档是否仍在聚合(超过 `maxZoom` 后为 `false`);引擎未就绪时为 `null` |
50
52
  | `listLines(options?)` | 按条件列线路快照 `{ index, item, path, bounds, color, width, channelId?, groupId?, channelIndex?, screen? }`:选项与 `listPoints` 同一套;通道回线给的是骨架推导出来的那条折线(跟图上画的一致) |
51
53
  | `getLineChannel(ref)` | 这条线路属于哪条通道:`getLineChannel('line-a')`(id)/ `getLineChannel(12)`(`lines` 下标)给 `{ lineIndex, channelId, channelIndex, channel, groupId, group }`,不在通道里返回 `null`。点了一条回线想知道它归哪条通道 / 哪一组时用它,不必自己扫通道定义 |
52
54
  | `getElementBounds(keys)` | 一批要素 `GisViewFeatureKey[]`(`{ type: 'point' \| 'line', index }`,`getSelections()` 的结果可以直接传)的世界包围盒 `{ minX, minY, maxX, maxY }`;一个都没对上返回 `null`(数据已经换过) |
@@ -68,11 +70,13 @@
68
70
  | `hoverAt(clientX, clientY)` / `clearHover()` / `getHover()` | 程序化悬停:`hoverAt` 让指针「停」到某个客户端坐标上(悬停高亮、光标与 `onHover` 一起更新,返回与 `onHover` 载荷同一份读数,空白处 `null`;不走悬停阈值,也不用先订阅事件)、`clearHover()` 撤掉并补发一条 `onHover(null)`、`getHover()` 随时读当前这一份 —— 外部列表 / 表格 hover 到某一行时联动高亮图上的要素用它 |
69
71
  | `getFeaturesInRect(rect)` | 画布 CSS 像素矩形里有什么(与框选 / `selectInBox` 同一套坐标与判定):返回 `{ rect, worldRect, points, lines }` —— `points` 是框内的节点(与 `onSelect` 的 `items` 同一口径)、`lines` 是包围盒沾到框的线路(与 `listLines` 同一份)、`worldRect` 是这块像素对应的世界坐标范围;只读,不改选中也不画框 |
70
72
  | `getChannelsInView()` | 当前视口里有哪几条通道(同一通道的几回线只算一条):每条给 `{ channelId, channelIndex, channel, lineIndexes, groupIds }`,侧边栏「画面里的通道」、按视口统计用它 |
71
- | `selectFeatures(refs, options?)` | 接口选中(换一份选中集;`selectFeatures(null)` / 空数组就是清空):`refs` 认 id、id 数组、`{ type, index }`、通道写法(`{ type: 'channel', channelId }` / `{ channelId, groupId }` / `{ channelId, lineIds }`)以及拾取载荷与搜索命中;`options.mode` 给 `'replace'`(缺省,换一份)/ `'add'` / `'toggle'`(在就取消、不在就选上)/ `'remove'`,见「接口选中(selectFeatures)与读回选中」 |
73
+ | `selectFeatures(refs, options?)` | 接口选中(换一份选中集;`selectFeatures(null)` / 空数组就是清空):`refs` 认节点 / 线路 id、id 数组、`{ type, index }`、通道写法(`{ type: 'channel', channelId }` / `{ channelId, groupId }` / `{ channelId, lineIds }`)、分组写法(`{ type: 'group', channelId?, groupId }`,全局唯一 `groupId` 可省略通道)以及拾取载荷与搜索命中;`options.mode` 给 `'replace'`(缺省,换一份)/ `'add'` / `'toggle'`(在就取消、不在就选上)/ `'remove'`,见「接口选中(selectFeatures)与读回选中」 |
74
+ | `selectWaypoint(lineRef, pathIndex?, options?)` | 代码选中一个拐点:`selectWaypoint('line-1', 2)` / `{ lineIndex, pathIndex }` / `{ lineId, pathIndex }`,并同时选中所在线路;有效下标从 `getWaypoints()` 取。传 `null` 只取消拐点选中、不改要素选中集,见「代码选中拐点(selectWaypoint)」 |
72
75
  | `deselect(refs?)` | 取消选中:写法与 `selectFeatures` 完全一样,把这些从当前选中集里去掉、其余选中原样留着;不传 / `null` 等于取消全部(也就是 `selectFeatures(null)`),空数组是「没有目标」什么都不做,见「取消选中(deselect / selectFeatures(null))」 |
73
76
  | `getSelections()` | 读回选中结果 `[{ type, index, item, … }]`(按选中顺序):要 id 列表 / 个数 / 包围盒看 `getSelectionData()` |
74
77
  | `isSelected(ref)` | 这个要素在不在当前选中集里:`isSelected('node-0')`(id)/ `isSelected({ type: 'line', index: 12 })`(也可以只写下标,按 id 认不出来时当下标),返回 `true` / `false`。面板上高亮「当前这一行」时不必自己比对 `getSelections()`;选中集变化时缓存一份键集合(见「接口选中」),十万级选中下这一问也是常数级 |
75
78
  | `getSelectionData()` | 选中集一次取全:读数(`ids` / `count` / `pointCount` / `lineCount` / `channelCount`)加要素本身(`points` / `lines` / `channels`(按通道去重,与 `getData()` 同一批对象)),另有 `items`(与 `getSelections()` 同一份)、`pointIndexes` / `lineIndexes` 与 `bounds`(世界包围盒,空选中时 `null`),见「接口选中(selectFeatures)与读回选中」 |
79
+ | `getCascadeHighlight()` | 当前点击级联高亮的读数:`{ origin, scope, points, lines, pointIndexes, lineIndexes }`;没有高亮或 `cascadeHighlight` 关着时返回 `null`。它只读、不改选中集,见「点击级联高亮」 |
76
80
  | `selectInBox(box, options?)` | 按画布 CSS 像素矩形框选,等价于 Shift + 拖动(跟随 `shortcuts.boxSelect`);`options.selectionKinds` 可只覆盖这一次的节点 / 线路 / 通道类型过滤 |
77
81
  | `beginCopyPoint(options?)` | 开始复制一个节点:给 `id` / `index` 直接出预览(`commit: true` 出预览即落地,`follow: true` 跟随鼠标,`autoCommit: false` 停在预览态),不给来源就是「先点按钮再选节点」模式,见「节点复制」 |
78
82
  | `commitPointCopy()` / `cancelPointCopy()` | 落地 / 取消预览中的复制(返回 `end` / `cancel` 载荷) |
@@ -83,6 +87,7 @@
83
87
  | `copyPointToClipboard(sources?, options?)` / `copyPointsToClipboard(sources?, options?)` / `pastePoint(options?)` / `pastePoints(options?)` / `getPointClipboard()` | 剪贴板:`copyPointToClipboard` 默认收当前选中的全部节点(只有没选中才回落到光标下的那个),`copyPointsToClipboard` 只认给定 / 选中的一批;Ctrl+V 时单节点且没有内部线路 / 通道仍走原预览,多节点或带线路 / 通道走整组落地,`pastePoints` 强制整组(单节点也走批量载荷)。`getPointClipboard()` 读 `count` / `sources` / `items` / `lines` / `channels` / `anchor` 快照,见「节点复制」 |
84
88
  | `mergePoints(source, target, options?)` | 把来源节点并进目标节点(接口调用,不做确认):来源 / 目标收 id、下标或 `{ id }` / `{ index }`,节点数据按 `merge.createItem` 合并(缺省目标优先、尺寸取双方最大、图标优先目标),改线路绑定、删自环 / 零长线 / 自环通道、把撞成同一条的通道合成一条,返回 `{ ok, item, linesChanged, removedLines, removedChannels, mergedChannels }`;并不了返回 `ok: false` 并另发一条失败事件,见「节点合并」 |
85
89
  | `mergeSelectedPoints(options?)` | 框选 / 多选之后一步把选中的节点并到一个目标上(接口调用):`target` 不指定就自动挑**绑定线路最多**的那一个(一样多取画得大的、再取下标小的),来源默认是除目标之外的选中节点、也可以用 `sources`(id / 下标 / `{ id }` / `{ index }`,单个或数组)直接给 —— 这时不用先选中;`mergeable: false` 默认跳过(`force: true` 越过)、`label` 改这一步历史的标签。**多个来源合成一步历史**(数据变更收成一条 `batch`),返回 `{ phase: 'mergeSelected', ok, reason, target, merged, skipped, sourcesMerged, linesChanged, removedLines, removedChannels, mergedChannels, channelsChanged }`,见「节点合并」 |
90
+ | `splitPoint(source, parts, options?)` | `mergePoints` 的逆操作:把一个节点拆成两份或更多(第一份接在原下标上、缺省沿用原 id,其余追加到末尾),按每份的 `item` / 顶层字段生成新节点,按 `lines` 分配线路、按 `channels` / `channelFrom` / `channelTo` 分配受影响通道的两端。没显式分配的线路跟第一份;没显式分配的通道端点跟它第一条被分配过的成员回线。线路的 `from` / `to` 与和来源坐标重合的 `start` / `end` / `path` 顶点改到对应新节点;拆完后的自环 / 零长度线路与两端落到同一份的通道删掉,成员全没了的通道也删掉。整次调用只记一条历史、只发一条 `splitPoint` 数据变更,返回的 `changes` 可直接作为后端事务输入;至少两份、坐标必须有效、线路不能重复分配,写错时 `ok: false` 并在 `reason` 说明 |
86
91
  | `getPointMerge()` / `resolvePointMerge(confirmed?, options?)` / `commitPointMerge()` / `revertPointMerge(options?)` | 读 / 裁决 / 收尾当前待确认的合并(把一个节点拖到另一个节点上之后):`resolvePointMerge(true, { keep, item })` 推进到 `confirm` 阶段 —— `keep` 选保留碰撞目标(缺省)还是拖动的来源,`item` 改写最终节点的信息与样式,位置 / id 始终取保留端;`resolvePointMerge(false)` 让拖动的节点回原位。`pending` / `confirm` / `getPointMerge()` 的载荷带 `impact`(受影响线路条数、具体 lineIds 与通道读数;`lineCount` 按线路去重,`linesChanged` / `removedLines` 与落地结果同口径)。自己接管数据落地时用 `commitPointMerge()` 收状态、`revertPointMerge()` 还原(`silent: true` 不补发 cancel 载荷) |
87
92
  | `removePoints(refs, options?)` | 移除节点(接口调用):`'a'` / `{ index: 2 }` / 数组都收,绑在它上面的线路一并删掉、声明了它当端点的整条通道(含成员回线)跟着删,返回 `{ ok, indexes, items, removedLines, removedChannels }` |
88
93
  | `requestRemove(options?)` | 发起一次删除(Delete 键同一条路):把选中的拐点 / 线路 / 节点与它们的连带影响摊成一份载荷交给 `onRemove` / `'remove'`,停在 `pending` 等裁决(确认方式见 `remove.confirm`),没有可删的东西时返回 `{ phase: 'empty', ok: false, reason }`,见「删除(remove)」 |
@@ -91,7 +96,7 @@
91
96
  | `cancelRemove()` | 取消这次删除(与 Esc 同一条路):数据一个字节都不动,返回取消掉的那份载荷(`invalidated: true` 表示请求因为数据变过而作废) |
92
97
  | `removeSelected(options?)` | 移除**当前选中的那一批**(工具栏「删除选中」同一条路):选中的节点连同绑在它们身上的线路一起删、剩下的选中回线也删掉,删完清空选中;返回 `{ ok, reason, pointIndexes, lineIndexes, pointIds, lineIds, points, lines, removedChannels }`(`lineIds` 里也含「因为节点被删而连带走掉」的线路),没有选中东西时 `ok: false` 并带回原因。一次调用只落一条历史、只发一条 `removeSelected` 数据变更 |
93
98
 
94
- | `exportImage(options?)` | 导出当前画面(含标签)为图片,返回 data URL,见「导出图片」 |
99
+ | `exportImage(options?)` | 导出当前画面(含标签)为图片,返回 data URL;`scope: 'viewport'`(缺省)导当前视口、`'all'` 临时适应全部可见内容,另有 `padding` / `type` / `quality` / `scale` / `background` / `includeLabels` / `download`,见「导出图片」 |
95
100
  | `exportSelection(options?)` | 把选中的节点(可选带上两端都在集合里的线路)单独导成图片 / 数据:`points` 不给就用选中集,`withLines` / `format` / `padding` 见「导出选中的节点」;返回 `{ ok, reason, points, lines, counts, box, pointIndexes, lineIndexes, image }`,出图时临时藏起别的要素与相机、出完还原 |
96
101
  | `exportData(options?)` | **统一导出入口**:`scope` 选范围(`'all'` 缺省 / `'viewport'` / `'selection'`,也认 `visible` / `view` / `selected`),`parts` 选分项,`format` 选口径。`format: 'view'`(缺省)给 `{ points, lines, channels, parallelGroups }`、`'new'` 给 new.json 口径、`'test'` 给 test.json 口径、`'input'` 给与 `createGisView` 入参同构的回灌口径、`'full'` 给标准文档;`format` 也可以是注册过的自定义名字。`points` / `lines` / `channels` 可以只要一部分,解不出的引用进 `warnings`,`format` 写错(或没注册)抛异常,见「导出数据」与「自定义导出格式」 |
97
102
  | `snapshotScene(options?)` | 当前画面的矢量快照(屏幕坐标的线 / 点 / 标签 / 背景 / 网格):`{ width, height, background, backgroundImage, grid, regions, lines, points, backgroundPoints, decorations, waypoints, raised, labels }`,`options.background: false` 丢背景、`options.backgroundColor` 只换底色、`options.labels` 原样带进去,见「导出图片」 |
@@ -118,7 +123,7 @@
118
123
  | `addChannelGroupWaypoint(channelId, groupId?, coordinate?)` | 给某个分组加拐点、把这一组单独拉走:不给坐标就按默认位置放一个(第 n 组沿法线往外让 n 档),其余分组一根拐点都不动,见「分组拐点」 |
119
124
  | `removeChannelGroupWaypoint(channelId, groupId?, pathIndex?)` | 删掉某个分组的拐点(缺省删最后一个) |
120
125
  | `runChannelAction(actionId, options?)` | 执行右键交互框里的动作:`select-group` / `deselect-group` / `add-waypoint` / `remove-waypoint` / `split` / `merge`(`options` 带 `channelId` / `groupId` / `coordinate` / `pathIndex` / `size`),返回执行之后的上下文 |
121
- | `beginAddChannel(options?)` | 进入「新增通道」:点第一个节点当起点,一条线跟随光标,点第二个节点出方案;`options` 支持 `kind`(`'channel'` 新建通道 / `'line'` 新建独立线路,缺省 `'channel'`)/ `circuits`(一次加几条,缺省 1)/ `autoCommit` / `continuous` / `chain` / `name` / `lineId` / `lineProps` / `lineItems`(第二段,连同线路一起新建)/ `routing`(`'straight'` / `'orthogonal'`)/ `path`(中间拐点)/ `channelProps` / `channelDraftStyle`(本次草稿的预览样式)/ `handoff`(交接模式:ready 发完收起草稿,自己调 `addChannel` 落地)/ `select`(点中的节点顺手选中),见「新增通道」 |
126
+ | `beginAddChannel(options?)` | 进入「新增通道」:缺省点两个节点选起点 / 终点,或由 `options.from` 预指定起点、只需再点终点;`options` 还支持 `kind`(`'channel'` 新建通道 / `'line'` 新建独立线路,缺省 `'channel'`)/ `circuits`(一次加几条,缺省 1)/ `autoCommit` / `continuous` / `chain` / `name` / `lineId` / `id` / `lineProps` / `linePropsByMember` / `lineItems`(第二段,连同线路一起新建)/ `routing`(`'straight'` / `'orthogonal'`)/ `path`(中间拐点)/ `channelProps` / `channelDraftStyle`(本次草稿的预览样式)/ `handoff`(交接模式:ready 发完收起草稿,自己调 `addChannel` 落地)/ `select`(点中的节点顺手选中)/ `originalEvent`,见「新增通道」 |
122
127
  | `getAddChannelDraft()` | 当前新增通道草稿(两个节点的信息与可选方案),没有草稿时为 `null` |
123
128
  | `updateAddChannelStyle(patch?)` | 字段级合并当前草稿的预览样式(`color` / `width` / `dot` / `dotColor` / `arrow` / `arrowSize` / `arrowPositions` / `arrowReverse` / `arrowColor` / `circuitSpacing` / `opacity` / `customDraw`);传 `null` 回到全局 `channelDraftStyle`,没有草稿时什么也不做 |
124
129
  | `continueAddChannel(options?)` | 开下一段草稿:连续模式落地时内部自动调(发 `phase: 'restart'`),也可以手动调;草稿已经退出时按 `options` 重新进来 |
@@ -139,6 +144,7 @@
139
144
  | `removeChannel(channelId)` | 移除整条通道:它名下的回线一条条摘掉,返回 `{ ok, channelId, lineIds, lines, removedChannel }` |
140
145
  | `updatePoint(id, patch)` / `updateLine(id, patch)` / `updateChannel(id, patch)` | 定向更新一个节点 / 线路 / 通道并重渲染:单项字段(尺寸、图标、颜色、线宽、通道宽度 / 名称等)直接写补丁;只重排受影响的那一小片(节点 → 绑定线与锚定通道(尺寸 / 图标换过时,锚定通道的宽度一起按新尺寸重排,见「通道与多回线」),线路 → 这一条,通道 → 它的分组与成员),其余要素与缓存一个字节都不动;节点的 `visible: false` / 改回来会走一次整份重排(这一条要连绑定线、锚定通道与拾取索引一起退场 / 排回去,定向更新那条路落不下;同一个 `batch()` 里改一批只重排一次);`updateLine` 的补丁里还能写 `groupId` / `groupIndex`(可配 `channelId`)顺带把这条回线挪到目标分组,`updateChannel` 写 `groups` / `lineIds` 会顺带改分组并补一条 `channelGroups` 变更(见「通道分组」);`updateChannel` 还能给第三段 `linePatches`(一条或一批 `{ id \| index, ...字段 }`)把「通道声明 + 成员回线各自的字段 / 分组」一次下发 —— 声明先落、再逐条走 `updateLine`,整次调用只发一条 `batch` 数据变更、历史里只算一步(见「通道分组」与「逐方法示例」);第三段里认不出的成员 id 带得起定义(`from` / `to` 两端可解析、可用的 `path`、或通道自己声明了两端)会当场建一条回线并挂进通道,给不出定义才跳过点名;`updateLine` 的**拐点**写在 `path` 上(整条折线,含首尾端点 —— 只写中间拐点用 `setLineWaypoints(id, coordinates)`),通道里的回线几何由通道骨架推导(`path` 写了也会被覆盖),改它走 `setLineWaypoints` / `setChannelPath` |
141
146
 
147
+ | `setFeatureVisible(kind, refs, visible)` / `hideFeatures(kind, refs)` / `showFeatures(kind, refs)` | 统一隐藏 / 显示节点、线路或通道:只改数据里的 `visible`,不删要素,分组 / 拐点 / 编辑状态都保留。`kind` 收 `point` / `line` / `channel`,`refs` 收单个或一批 id / 下标 / `{ id }` / `{ index }`,也认 `'selection'`(通道按选中回线的归属取整条);`hideFeatures` / `showFeatures` 是 `false` / `true` 的快捷写法。整次调用收成一条历史与一条 `batch` 数据变更,返回 `{ ok, reason, kind, visible, indexes, ids, items, skipped }`;认不出的引用逐条进 `skipped`,不影响其余条目 |
142
148
  | `movePoint(id, coordinate)` / `movePoint(id, x, y)` | 挪一个节点:`{ x, y }` / `[x, y]` / 两个数字都认;绑在它身上的线路端点与以它为锚点的通道骨架跟着重排(与拖动落地同一条路) |
143
149
  | `setLineWaypoints(id, coordinates)` | 整份换一条线路的拐点:**只写中间拐点**(不含首尾两端;传单个坐标 = 只留一个拐点,空数组 = 拉直)。两端按节点 / `start` / `end` 补齐(绑了 `from` / `to` 时两端始终按节点坐标走),存回 `lines[].path` 的是「首尾端点 + 中间顶点」那一份;只插一个用 `addWaypoint(id, coordinate)`,删一个用 `removeWaypoint(id, pathIndex)`;通道里的回线改的是它所在分组的骨架(与 `setChannelPath` 同一份声明) |
144
150
  | `getLineDecorations(id)` / `setLineDecorations(id, list)` / `addLineDecoration(id, decoration)` / `updateLineDecoration(id, index, patch)` / `removeLineDecoration(id, index)` | 线路上标记(图片 / SVG 装饰)的读写:`get` 给副本(要素没找到给空数组),`set` 整份换(空数组 = 全摘掉),`add` 追加在末尾并返回加进去的那一条,`update` / `remove` 按 `decorations` 的顺序数(返回改动的那一条,要素或下标找不到时给 `null` 并记一条 warning);四个写方法各只发一条数据变更、只落一步历史 |
@@ -151,6 +157,10 @@
151
157
  | `getVisibleData()` | 过滤之后的数据副本(`points` / `lines`) |
152
158
  | `getMovedPoints()` | 位置对账:相对上次对账(建视图 / 整份换数据 / 上一次 `clearMovedPoints`)位置变过的节点,按 id 去重、一趟扫出 `[{ index, id, x, y, item }]` —— 拖动 / `updatePoint` / `patchPoints` / 撤销都算,`removePoints` 删掉的记录顺手清掉,见「节点位置对账」 |
153
159
  | `clearMovedPoints(refs?)` | 位置对账:把当前坐标记成新基准(发完接口告诉组件「这些已经同步过了」):不传 / `null` 全部对账,传一个或一批(写法与 `removePoints` 一致)只清这些,认不出来的跳过并记一条提醒;返回实例本身 |
160
+ | `getChangedChannels(options?)` | 通道变化对账:相对上次对账(建视图 / 整份换通道声明 / 上一次 `clearChangedChannels`)成员、分组、拐点、端点、可见性或其余声明字段变过的通道。每项带当前 `groups` 快照、六个 `*Changed` 标记、`fields` 与 `reasons`;新加通道只补账不报变化,删掉的通道(含 `removeLines` 连带删除)从账本清掉。`options.reasons` 只取指定原因(空数组直接返回空表),`options.ignoreGroups` 忽略分组新增 / 删除 / 重排、仍报已有分组的拐点变化。节点拖动只改坐标、不改 `from` / `to`,因此不算端点变化;拖 / 加 / 删分组拐点会进 `waypoints`,见「通道变化对账」 |
161
+ | `clearChangedChannels(refs?)` | 通道变化对账:把当前声明记成新基准(发完接口告诉组件「这些已经同步过了」):不传 / `null` 全部对账,传一个或一批(写法与 `removePoints` 一致)只清这些,认不出来的跳过并记一条提醒;返回实例本身 |
162
+ | `getChangedLines(options?)` | 线路变化对账:相对上次对账(建视图 / 整份换线路 / 上一次 `clearChangedLines`)`path` / `start` / `end`、`from` / `to`、可见性或其余声明字段变过的线路。每项带 `{ index, id, item, path, start, end, from, to, visible, pathChanged, endpointsChanged, visibilityChanged, fieldsChanged, fields, reasons }`;`options.reasons` 可写 `path` / `endpoints` / `visibility` / `fields`。新加线路只补账不报,删除线路(含 `removeChannel` 连带删除)同步清账;通道骨架变化不算线路变化,见「线路变化对账」 |
163
+ | `clearChangedLines(refs?)` | 线路变化对账:不传 / `null` 全部对账,传一个或一批(写法与 `removePoints` 一致)只清这些,空数组无操作;认不出来的跳过并记一条提醒,返回实例本身 |
154
164
  | `isPointVisible(index)` | 某个下标的节点当前在不在图上:被筛选 / `visible: false` 隐藏掉的不算,初始化发散渲染还没排到的那部分也不算 |
155
165
  | `resize()` / `refresh(options?)` | 手动同步尺寸 / 重新应用配置并重绘(`refresh({ reflow: false })` 只重画一帧,不重解析数据与配置);容器尺寸或祖先的 `transform: scale` 变过之后要调一次 `resize()` 同步(CSS `zoom` 会自己感知) |
156
166
  | `destroy()` | 销毁引擎并移除根节点 |
@@ -182,7 +192,7 @@
182
192
 
183
193
  返回 `this` 的方法都可以链式调用;返回其它值的方法(`get*` / `list*` / `export*` / `search` / `pick*` / `add*` / `remove*` / `copy*` / `merge*` / `locate*` 这些)在下面的「参数速查」里都标了返回类型。
184
194
 
185
- **怎么确认「组件到底有哪些属性和方法」**:上面那张表按功能分组介绍,下面的「参数速查」把实例的**每个成员逐个列全**(当前 241 个:238 个方法 + `el` / `canvas` / `gl` 三个属性),`options` 表列全 90 个键,数据字段表列全 238 个字段。这三份清单不靠手抄 —— 它们直接对着源码与类型对账:
195
+ **怎么确认「组件到底有哪些属性和方法」**:上面那张表按功能分组介绍,下面的「参数速查」把实例的**每个成员逐个列全**(当前 253 个:250 个方法 + `el` / `canvas` / `gl` 三个属性),`options` 表列全 95 个键,数据字段表列全 249 个字段。这三份清单不靠手抄 —— 它们直接对着源码与类型对账:
186
196
 
187
197
  - `node docs/steps/gen-api-index.mjs`:从 `create.js` 的实例定义、本文档的两张表与 `types.d.ts` 生成 `docs/demos/api-index.js`(`API_SUMMARY` / `API_SECTIONS` / `API_OPTIONS`),demo 的「实例 API」面板读的就是它;带 `--check` 时只校验有没有过期。
188
198
  - `node docs/steps/check-doc-coverage.mjs`:10 项双向对账 —— 实例成员 × 文档 / × 参数速查 / × 类型、参数速查与实例方法主表各自反查过期条目、类型反查过期声明、数据字段 × 文档、options 键 × 文档 / options 表反查过期字段、demo 清单是否最新。加一个实例成员、改一个 options 键而没同步文档时,这个脚本会直接报出来。
@@ -190,4 +200,6 @@
190
200
 
191
201
  ---
192
202
 
203
+ **相关章节**:[08 选中与拐点](08-选中与拐点.md) · [17 参数速查(逐个方法)](17-参数速查.md) · [18 逐方法示例(复制就能跑)](18-逐方法示例.md)
204
+
193
205
  [← 15 导出与集成](15-导出与集成.md) · [手册目录](../GisView.md) · [17 参数速查(逐个方法) →](17-参数速查.md)