gis-view-webgl2 0.1.0 → 0.1.2

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 (87) hide show
  1. package/README.md +40 -8
  2. package/dist/gis-view.css +1 -1
  3. package/dist/index.cjs +1 -1
  4. package/dist/index.d.ts +1036 -249
  5. package/dist/index.js +1 -1
  6. package/docs/AI/345/257/271/350/257/235/346/261/207/346/200/273.md +194 -0
  7. package/docs/GisView.md +9 -8
  8. package/docs/GisView/345/256/236/347/216/260/345/216/237/347/220/206.md +2 -2
  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 +172 -0
  10. package/docs/api/index.html +545 -59
  11. package/docs/demos/BackendSyncDemo.vue +10 -8
  12. package/docs/demos/BasicDemo.vue +101 -101
  13. package/docs/demos/BatchAddDemo.vue +5 -5
  14. package/docs/demos/ChannelDemo.vue +617 -10
  15. package/docs/demos/ConnectDemo.vue +392 -0
  16. package/docs/demos/CopyMergeDemo.vue +980 -929
  17. package/docs/demos/DecorationDraftDemo.vue +1300 -0
  18. package/docs/demos/DemoShell.vue +569 -240
  19. package/docs/demos/DragDropDemo.vue +11 -11
  20. package/docs/demos/EffectsDemo.vue +6 -6
  21. package/docs/demos/EventDemo.vue +327 -327
  22. package/docs/demos/ExtensionDemo.vue +458 -0
  23. package/docs/demos/GisViewDemo.vue +61 -48
  24. package/docs/demos/GraphicDemo.vue +338 -0
  25. package/docs/demos/LabelDemo.vue +553 -497
  26. package/docs/demos/MinimapDemo.vue +358 -0
  27. package/docs/demos/PerfDemo.vue +29 -0
  28. package/docs/demos/SearchFilterDemo.vue +412 -0
  29. package/docs/demos/StyleDemo.vue +121 -121
  30. package/docs/demos/api-index.js +98 -77
  31. package/docs/demos/demos.js +159 -135
  32. package/docs/demos/shared/demoData.js +107 -107
  33. package/docs/demos/shared/demoStyle.css +249 -249
  34. package/docs/demos/shared/fakeBackend.js +93 -6
  35. 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 +28 -8
  36. package/docs/format/04-/351/200/232/351/201/223/344/270/216/346/240/207/347/255/276.md +5 -4
  37. package/docs/format/06-/345/257/274/345/205/245/345/257/274/345/207/272/344/270/216/346/240/207/345/207/206/346/226/207/346/241/243.md +8 -6
  38. 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 +6 -6
  39. package/docs/guide/sample-document.html +4 -4
  40. package/docs/index.html +180 -9
  41. package/docs/internals/11-/350/246/206/347/233/226/345/261/202/346/240/207/347/255/276/344/270/216/346/217/220/347/244/272.md +1 -1
  42. package/docs/internals/13-/351/271/260/347/234/274/345/233/276.md +3 -2
  43. package/docs/internals/15-03-/351/200/220/350/275/256/350/256/260/345/275/225-/346/200/247/350/203/275/344/270/216/344/277/256/345/244/215-/344/270/213.md +16 -0
  44. package/docs/internals/16-03-/344/274/230/345/214/226/344/270/216/345/217/226/350/210/215-/344/270/213.md +11 -0
  45. package/docs/internals/17-/350/260/203/350/257/225/344/270/216/346/211/251/345/261/225/345/273/272/350/256/256.md +1 -1
  46. package/docs/manual/02-/345/235/220/346/240/207/344/270/216/346/225/260/346/215/256.md +18 -4
  47. package/docs/manual/03-/351/205/215/347/275/256/351/241/271.md +110 -25
  48. package/docs/manual/04-/344/272/213/344/273/266/344/270/216/345/233/236/350/260/203.md +13 -8
  49. 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 +111 -10
  50. 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 +47 -9
  51. package/docs/manual/08-/351/200/211/344/270/255/344/270/216/346/213/220/347/202/271.md +17 -15
  52. package/docs/manual/09-/351/200/232/351/201/223/344/270/216/345/244/232/345/233/236/347/272/277.md +61 -11
  53. 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 +74 -7
  54. package/docs/manual/11-/346/240/267/345/274/217/344/270/216/350/247/204/345/210/231.md +3 -2
  55. package/docs/manual/12-/346/240/207/347/255/276/344/270/216/350/243/205/351/245/260.md +260 -3
  56. package/docs/manual/13-/345/212/250/346/225/210/344/270/216/345/233/276/345/205/203.md +6 -5
  57. package/docs/manual/14-/350/247/206/345/233/276/344/270/216/347/255/233/351/200/211.md +27 -14
  58. package/docs/manual/15-/345/257/274/345/207/272/344/270/216/351/233/206/346/210/220.md +142 -49
  59. package/docs/manual/16-/345/256/236/344/276/213/346/226/271/346/263/225/346/200/273/350/241/250.md +31 -29
  60. package/docs/manual/17-/345/217/202/346/225/260/351/200/237/346/237/245.md +55 -39
  61. package/docs/manual/18-/351/200/220/346/226/271/346/263/225/347/244/272/344/276/213.md +189 -64
  62. 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 +4 -4
  63. package/docs/manual/20-/346/200/247/350/203/275/344/270/216/345/244/247/346/225/260/346/215/256.md +42 -2
  64. 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 +22 -6
  65. package/docs/playground/index.html +5 -5
  66. package/docs/steps/README.md +14 -10
  67. package/docs/steps/check-doc-truth.mjs +1 -1
  68. package/docs/steps/selftest-actions.html +10 -10
  69. package/docs/steps/selftest-data.html +96 -14
  70. package/docs/steps/selftest-decoration.html +577 -0
  71. package/docs/steps/selftest-demo.html +6 -5
  72. package/docs/steps/selftest-demos.html +2201 -1612
  73. package/docs/steps/selftest-docs.html +89 -16
  74. package/docs/steps/selftest-effects.html +5 -5
  75. package/docs/steps/selftest-features.html +757 -86
  76. package/docs/steps/selftest-full.html +583 -583
  77. package/docs/steps/selftest-labels.html +1034 -652
  78. package/docs/steps/selftest-perf.html +1899 -1844
  79. package/docs/steps/selftest-scale.html +117 -7
  80. package/docs/steps/selftest-select.html +24 -17
  81. package/docs/steps/step-10-full.js +1 -1
  82. package/docs//346/216/245/345/217/243/345/215/217/350/256/256.md +167 -17
  83. package/docs//346/225/260/346/215/256/346/240/274/345/274/217.md +1 -1
  84. package/package.json +43 -43
  85. package/docs/demos/HistoryDemo.vue +0 -230
  86. package/docs/demos/InterfaceDemo.vue +0 -309
  87. package/docs/demos/SampleDemo.vue +0 -283
@@ -9,6 +9,7 @@
9
9
  | `lines[].path` | `GisViewCoordinate[]` | 这条线路的整条折线:首尾是端点(绑了 `from` / `to` 时取节点坐标),中间的点就是拐点。只有两端点时没有拐点 |
10
10
  | `lines[].from` / `to` + `path` | `id` + `GisViewCoordinate[]` | 两端绑节点时 `path` 照样要写全(首尾两个顶点会被节点坐标替换):拖节点时端点跟着走,拐点一个都不动 |
11
11
  | `channels[].path` / `channels[].groups[].path` | `GisViewCoordinate[]` | 通道**骨架**的中间拐点(两端由通道的 `from` / `to` 补齐):同一分组里的所有回线共用这一条骨架 |
12
+ | `channels[].groups[].waypointOffsets` | `Record<string, GisViewCoordinate>` | 分组刚性拐点偏移:键是含首尾端点的骨架顶点序号(只认中间拐点),值是组内所有回线在该顶点的世界坐标增量 |
12
13
 
13
14
  **`path` 要写全**:线路的 `path` 是整条折线,首尾两个顶点给的是端点(绑了 `from` / `to` 时引擎用节点当前坐标替换它们),
14
15
  **拐点 = 第 2 ~ n-1 个顶点**。只写中间那几个点会被首尾替换掉、直接退化成两端直连 —— 想要两个拐点就写成四个顶点;
@@ -23,7 +24,7 @@
23
24
 
24
25
  两种写法在 `sample-waypoints.json` / `sample-channels.json` 里各有一份可以对着看。
25
26
 
26
- **通道 / 分组只有公共拐点这一份存储**:`channels[].path`(单组)与 `channels[].groups[].path`(多组)就是整组共用的骨架中间拐点;每条回线画出来的折线是它按自己的车道偏移推出来的(只读,不回写)。读取时 `getChannelGroups(channelId)` 的 `path` / `skeleton` 是公共那一份,`getChannelGroupWaypoints(channelId, groupId)` 返回里的 `scope: 'group'` 说明这一点,`lines[].derived: true` 的 `path` / `waypoints` 才是各条回线偏移后的读数。
27
+ **通道 / 分组的拐点存在公共骨架和分组偏移两层**:`channels[].path`(单组)与 `channels[].groups[].path`(多组)是整组共用的骨架中间拐点;`waypointOffsets` 只记 `channelWaypointDragMode: 'vertex'` 拖出来的按序号刚性增量。每条回线画出来的折线是“公共骨架 → 车道平行偏移 → 分组刚性偏移”之后的结果(只读,不回写)。读取时 `getChannelGroups(channelId)` 的 `path` / `skeleton` / `waypointOffsets` 是分组那一份,`getChannelGroupWaypoints(channelId, groupId)` 返回里的 `scope: 'group'` 说明这一点,`lines[].derived: true` 的 `path` / `waypoints` 才是各条回线偏移后的读数。偏移不写或为空对象就是普通平行骨架;切回 `'parallel'` 不会自动清掉已有偏移。
27
28
 
28
29
  读写接口(都在实例方法里,逐要素改不改整份数据):
29
30
 
@@ -36,13 +37,14 @@
36
37
  | `setChannelPath(channelId, coordinates)` | 整份换一条通道的骨架拐点(只写中间点,两端由节点坐标补齐) |
37
38
  | `addChannelGroupWaypoint(channelId, groupId, coordinate)` / `removeChannelGroupWaypoint(channelId, groupId, pathIndex)` | 按**分组**加 / 删拐点:只挪这一组、同一通道的其它分组不动 |
38
39
 
39
- 导出侧同样是这三处:`input` 口径折在 `lines[].path` 与 `channels[].groups[].path` 里(`counts.parallelGroups` 报通道分组数、`counts` 另有拐点总数的是分项 `exportParallelGroups`);
40
+ 导出侧同样是这三处:`input` 口径折在 `lines[].path` 与 `channels[].groups[].path` 里(`counts.parallelGroups` 报通道分组数;业务口径用 `exportData({ parts: ['waypoint'] })` 单列拐点分组);
40
41
  业务口径里拐点单列一份 `parallel_groups`(`test.json` 口径)或跟着通道走(`new.json` 的 `coords`)——
41
42
 
42
43
  ```js
43
- view.exportParallelGroups()
44
+ view.exportData({ format: 'test', parts: ['waypoint'] })
44
45
  // { parallel_groups: [{ group_id, member_line_ids, group_route: [[x, y], [拐点], …, [x, y]] }], counts, warnings }
45
- // group_route 含两端,中间就是拐点;通道的每个分组各成一组,通道外的折线线路自己成一组
46
+ // group_route 含两端,中间就是拐点;有 waypointOffsets 时已经叠成实际路由
47
+ // 通道的每个分组各成一组,通道外的折线线路自己成一组
46
48
  ```
47
49
 
48
50
  事件:`onWaypointChange` 的载荷给 `phase`(`add` / `remove` / `select` / `start` / `move` / `end`)、`lineIndex` / `pathIndex` / `coordinate`、
@@ -50,7 +52,9 @@ view.exportParallelGroups()
50
52
 
51
53
  ## 标记 / 装饰(decorations)
52
54
 
53
- 线路上任意位置的图片 / SVG(见 GisView.md 的「线路上的装饰」),写法是 `lines[].decorations` 数组:
55
+ 线路和节点都能挂图片 / SVG:线路写 `lines[].decorations`,节点写 `points[].decorations`。两边共用同一套符号、大小、颜色、描边、透明度与阴影字段;线路按沿线位置或世界坐标定位,节点按相对节点的屏幕像素偏移或世界坐标定位。
56
+
57
+ 线路装饰字段(见 GisView.md 的「线路上的装饰」):
54
58
 
55
59
  | 字段 | 类型 | 默认值 | 说明 |
56
60
  | --- | --- | --- | --- |
@@ -67,6 +71,21 @@ view.exportParallelGroups()
67
71
  `addLineDecoration(id, decoration)`(追加,返回加进去的那一条)/ `updateLineDecoration(id, index, patch)`(只合并第 `index` 条)/
68
72
  `removeLineDecoration(id, index)`(摘掉某一条)。
69
73
 
74
+ 节点装饰额外认这些定位字段:
75
+
76
+ | 字段 | 类型 | 默认值 | 说明 |
77
+ | --- | --- | --- | --- |
78
+ | `anchor` | `'offset' \| 'fixed'` | `'offset'` | 相对节点偏移,或写死世界坐标;`at` 也会自动按 `fixed` 处理 |
79
+ | `offset` | `[dx, dy] \| { x, y }` | 由方向 / 距离算出 | 相对节点的屏幕像素偏移,优先于 `distance` / `direction` |
80
+ | `distance` | `number` | `28` | 离节点的屏幕像素距离 |
81
+ | `direction` | `number` | `0` | 相对方向:0 度向上、顺时针 |
82
+ | `at` | `GisViewCoordinate` | 无 | 固定世界坐标;节点移动不带走,仍随地图平移 / 缩放 |
83
+ | `angle` | `number` | `0` | 图标自身旋转角(度) |
84
+ | `connector` | `boolean \| object` | 不画 | 从节点中心连到装饰中心;对象可写 `color` / `width` / `opacity` / `dashed` / `lineType` |
85
+
86
+ 节点装饰的运行时读写:`getPointDecorations(id)` / `setPointDecorations(id, list)` / `addPointDecoration(id, decoration)` /
87
+ `updatePointDecoration(id, index, patch)` / `removePointDecoration(id, index)`,返回值与 `decorations` 的下标口径和线路那一组一致。
88
+
70
89
  ## 偏移与颜色样式
71
90
 
72
91
  「偏移」在两处,含义不同:
@@ -74,7 +93,8 @@ view.exportParallelGroups()
74
93
  | 偏移 | 写在哪 | 单位与说明 |
75
94
  | --- | --- | --- |
76
95
  | 标签偏移 | `labels[].offset`(点 / 线都有)、`lineLabelOffset`(旧式写法) | `[dx, dy]`,**CSS 像素**,相对标签锚点;标签被拖到过哪里也写在这里,导出即回显 |
77
- | 标记偏移 | `decorations[].position` / `.at` | `position` 按**沿线**算(比例 / 百分比 / 世界单位距离),`at` 是**世界坐标**;没有像素偏移这一说 |
96
+ | 线路标记偏移 | `lines[].decorations[].position` / `.at` | `position` 按**沿线**算(比例 / 百分比 / 世界单位距离),`at` 是**世界坐标** |
97
+ | 节点标记偏移 | `points[].decorations[].offset` / `.distance` + `.direction` / `.at` | `offset` 与 `distance` 是**屏幕像素**(缩放时保持像素距离),`at` 是**世界坐标** |
78
98
 
79
99
  颜色与尺寸按「全局 → 规则 → 要素自身 → 状态(悬停 / 选中 / 框选)→ 热力图」逐层覆盖:
80
100
 
@@ -86,8 +106,8 @@ view.exportParallelGroups()
86
106
  | 悬停 / 选中 / 框选 | `pointHoverStyle` / `pointSelectStyle` / `pointMultiSelectStyle` / `pointBoxSelectStyle`、`lineHoverStyle` / `lineSelectStyle` / `lineMultiSelectStyle`,逐要素可用 `hoverStyle` / `selectStyle` / `multiSelectStyle` 覆盖 |
87
107
  | 标签 / 标记的文字与符号 | 标签 `style`(内联 CSS)+ `className`(自带类名);标记 `color` / `strokeColor` / `strokeWidth` / `opacity` / `shadow` |
88
108
 
89
- 单独改一个要素不必整份重灌:`setPointSize` / `setPointSymbol` / `setPointColor` / `movePoint` / `setLineWidth` / `setLineColor` /
90
- `setLineWaypoints` / `addLineDecoration` 与 `setChannelWidth` / `setChannelNodeWidth` / `setChannelInset` / `setChannelTangentInset` /
109
+ 单独改一个要素不必整份重灌:单项字段用 `updatePoint` / `updateLine` / `updateChannel` 的补丁写法;
110
+ 需要坐标归一化、分组或成员回线联动时用 `movePoint` / `setLineWaypoints` / `addLineDecoration` /
91
111
  `setChannelLineWidth` / `setChannelColors` / `setChannelPath`(清单见 GisView.md 的「实例方法」,一次调用只动这一条、只发一条数据变更)。
92
112
 
93
113
  ---
@@ -12,7 +12,7 @@
12
12
  | `circuits` | `number` | `1` | 回线数量(`createChannels` 用它展开回线) |
13
13
  | `lineIds` | `Array<string \| number>` | 无(`createChannels` 生成的那一份就是它) | 组成这条通道的线路 id,**顺序即回线顺序**(沿骨架左法线从负到正) |
14
14
  | `path` | `GisViewCoordinate[]` | 无(两端直连) | 骨架的**中间拐点**(不含两端,两端由节点坐标补齐):与线路的 `path` 相反,这里只写中间 |
15
- | `groups` | `GisViewChannelGroup[]` | 无(= 一个通道一组) | 分组声明:`[{ id, lineIds, path }]`,一组一条走廊;拆分 / 合并的结果写在这里,见「分组」 |
15
+ | `groups` | `GisViewChannelGroup[]` | 无(= 一个通道一组) | 分组声明:`[{ id, lineIds, path, waypointOffsets? }]`,一组一条走廊;拆分 / 合并的结果写在这里,见「分组」 |
16
16
  | `width` / `offsetMode` / `inset` / `tangentInset` | `number` / `'tangent' \| 'inset'` / `number` / `number \| null` | 分别回落到 `options.channelWidth`(`0.02`)/ `'tangent'` / `options.channelInset`(`0`)/ `options.channelTangentInset`(`null` = 按回线线宽收) | 通道外观:宽度(`tangent` 下是最外侧两条回线的中心距)、偏移方式、内收比例(负数为外收)、两端切点内收 |
17
17
  | `nodeWidth` | `number` | `options.channelNodeWidth`(`1`) | 「按两端节点尺寸取宽」的倍数:`1` = 回线正好铺在节点两边,`0` = 关掉自动取宽改用 `width` / `channelWidth` |
18
18
  | `colors` / `lineWidth` / `opacity` | 数组 / 数字 | `options.colors`(`CHANNEL_DEFAULT_COLORS`)/ 无 / `1` | 回线的逐回颜色(不够时按顺序循环取)、线宽、透明度 |
@@ -29,9 +29,10 @@
29
29
  - **成员池 = 顶层 `lineIds` + 只写在 `groups` 里的回线**(按分组顺序补在后面):车道顺序、「两端从第一条回线继承」与分组编辑(`splitChannelGroups` / `mergeChannelGroups` / `moveChannelLine` / `addChannelGroup`)都按这一份口径认成员。因此**只写 `groups`、完全不写顶层 `lineIds` 的通道也支持**:渲染照常,拆 / 并 / 挪分组、`removeLines` 收拾成员一样生效(`lineIds` 只列了一部分、其余只在 `groups` 里写的情况同理)。
30
30
  - **往通道里加一条回线**:数据侧先有这条线(`lines[].id`)再写进 `lineIds` / `groups`;运行时用 `addChannelLines(channelId, lineIds)` 挂现成的线,或让 `updateChannel` 的第三段带着定义当场建 —— 认不出的成员 id 给了 `from` / `to`(两端都能在 `points` 里找到)、给了能用的 `path`,或这条通道自己声明了两端时就建(表单整份覆盖成员表走的这条路);`updateChannel` 的第四段 `lineIds` 是**整表对齐**:与第三段的线路数组一起给时,表里有、数据里没有的当场建,表里有的逐条改,表里没有了的旧成员直接从数据里删(视图跟着少);只传线路数组(不传第四段)就只改 id 匹配的成员、数量不动。移出用 `removeChannelLines`(留线)/ `removeLines`(删线)。
31
31
  - **分组 id 在一条通道里要唯一**:id 是写回声明、按 id 指名分组的键(`moveChannelLine` / 选中 / 分组拐点读数都按它找组),两个组同名时只会认第一组。`addChannelGroup(..., { id })` 撞上已有分组时**返回 `ok: false`** 并说明(想并进去用 `moveChannelLine`);`splitChannelGroups` 这类不写 id 的入口按 `${通道 id}::N` 自动编号,不会撞名。
32
- - 同一分组内的回线共用一条骨架:拖任一回线的拐点,整组一起折;两端节点、宽度、颜色这些配置仍由**通道**统管。
33
- - **存的是公共一份**:`groups[].path` 只有一条(整组共用),逐条回线各自的拐点不落数据 —— 加 / 删 / 拖只写这一份、按各条车道偏移推出来的是只读读数(`getChannelGroupWaypoints` 的 `scope: 'group'` / `lines[].derived`)。
34
- - 运行时读数:`getChannels()` 给**分组级**的一项(`{ id, channelId, channelIndex, lineIndexes, skeleton, offsets, paths, from, to, anchored, spacing }`,
32
+ - 同一分组内的回线共用一条骨架:`parallel` 模式拖任一回线的拐点整组一起折,`vertex` 模式只移同组同一序号的拐点;两端节点、宽度、颜色这些配置仍由**通道**统管。
33
+ - **存的是公共骨架 + 分组偏移**:`groups[].path` 只有一条(整组共用),`groups[].waypointOffsets` 记 `vertex` 拖动按骨架序号产生的刚性增量;逐条回线各自的拐点不落数据 —— 加 / 删 / 拖只写这两层、按各条车道偏移推出来的是只读读数(`getChannelGroupWaypoints` 的 `scope: 'group'` / `lines[].derived`)。
34
+ - **`waypointOffsets` 的键按含首尾端点的骨架序号算**:只接受中间拐点,值可以是 `[dx, dy]` 或 `{ x, y }`;组内所有成员回线都叠这一份偏移。不写或写空对象是普通平行骨架,切回 `parallel` 不会自动清空;要清除就重写分组声明并带上当前 `path`、省略该项。
35
+ - 运行时读数:`getChannels()` 给**分组级**的一项(`{ id, channelId, channelIndex, lineIndexes, skeleton, waypointOffsets, offsets, paths, from, to, anchored, spacing }`,
35
36
  一条通道拆成几组就有几项,`id` 是分组 id、`channelId` 才是通道 id);
36
37
  `getChannelGroups(channelId)` 按通道 id 或通道定义 id 取该通道的全部分组,读数更细
37
38
  (另给 `lineIds` / `lineCount` / `waypointCount` / `edited` / `current` 与可直接写回 `declaredPath` 的 `path`)。
@@ -2,7 +2,7 @@
2
2
 
3
3
  > 导出数据与「接收 == 导出」的 full 标准文档。
4
4
 
5
- 五种口径,`exportData({ format })` 一次拿全,也可以按分项取(`exportStationData` / `exportPointData` / `exportLineData` / `exportChannelData` / `exportParallelGroups`):
5
+ 内置五种口径,`exportData({ format })` 一次拿全,也可以写 `parts: ['station' | 'line' | 'channel' | 'waypoint']` 按分项取:
6
6
 
7
7
  | `format` | 用途 | 节点 | 线路 | 通道 | 拐点 | 标签 / 标记 |
8
8
  | --- | --- | --- | --- | --- | --- | --- |
@@ -12,6 +12,8 @@
12
12
  | `'input'` | 回灌 / 存档 | `points`(数据对象本身) | `lines`(`path` 拐点与 `decorations` 标记都在要素里) | `channels`(`groups` 分组与拐点都在里面) | 折在 `path` 里 | `labels` 在要素里 + 单独一列 `labelStates` |
13
13
  | `'full'` | 标准文档(接收 == 导出) | `points` | `lines` | `channels` | 折在 `path` 里 | 要素里的 `labels` / `decorations` + 单独一列 `labelStates`;另有 `view` / `filter` 两列 |
14
14
 
15
+ 后端 DTO 的列名与这五套都不一样时,用模块级 `defineExportFormat(name, adapter)`(或底层的 `registerExportFormat`)注册一个同步导出格式:`exportData` 会把已经筛选、深拷贝、坐标归一、JSON 安全过滤的标准文档交给适配器,返回值就是这次导出的结果。内置五档是保留名,不会被覆盖;`defineExportFormat` 返回的清理函数可安全用于插件卸载;注册、上下文、注销与完整示例见 [15 导出与集成](../manual/15-导出与集成.md#自定义导出格式后端-dto)。
16
+
15
17
  ```js
16
18
  const exported = view.exportData({ format: 'input' })
17
19
  view.updateData(exported.points, exported.lines) // 直接喂回去就回显
@@ -24,13 +26,13 @@ const next = createGisView(el, doc)
24
26
  next.importData(doc)
25
27
  ```
26
28
 
27
- 演示:`docs/demos/BackendSyncDemo.vue`(页签「编辑与保存」,`?demo=backend`)把分组增删改 / 拐点增删改 / 场站改坐标 → 各类请求体、待提交队列与「整份保存」摆在一起,照着定接口传参。
29
+ 演示:`docs/demos/BackendSyncDemo.vue`(页签「编辑与保存」,`?demo=backend`)把分组增删改 / 拐点增删改 / 场站改坐标 → 各类请求体、待提交队列与「整份保存」摆在一起,照着定接口传参;`docs/demos/ChannelDemo.vue`(页签「通道与数据」,`?demo=channel`)的「后端 DTO(自定义格式)」按钮演示 `defineExportFormat` 的注册 → 导出 → 页面卸载时调用清理函数。
28
30
 
29
- 按视口取:`exportViewportData(options?)` 与 `exportData` 同一套口径与选项,只是范围换成当前可视范围(`getViewRect()`)——
31
+ 按视口取:`exportData({ scope: 'viewport', ... })` 与全量导出同一套口径与选项,只是范围换成当前可视范围(`getViewRect()`)——
30
32
  节点按中心、线路按包围盒、通道按「成员回线有一条在视口里」(分组一起跟着走);`margin` 是屏幕 CSS 像素外扩、
31
33
  `rect` 直接给一份世界矩形,返回多带 `viewRect` / `margin` 两条读数(见 `docs/GisView.md` 的「导出数据」)。
32
34
 
33
- 常用选项(`exportData` 与各分项共用):
35
+ 常用选项:
34
36
 
35
37
  | 选项 | 说明 |
36
38
  | --- | --- |
@@ -40,7 +42,7 @@ next.importData(doc)
40
42
  | `all`(拐点) | `true` 时把声明里就写了拐点、没被编辑过的分组也列出来 |
41
43
  | `extra` | 额外挂到结果上的字段(`tc_file_path` 这类业务元数据) |
42
44
 
43
- 注意:`format` 写错会**抛异常**(接口调用错误早报早好),其它坏引用(找不到的 id)只记进 `warnings` 并跳过,
45
+ 注意:`format` 写错(或用了没注册的自定义名)会**抛异常**(接口调用错误早报早好),其它坏引用(找不到的 id)只记进 `warnings` 并跳过,
44
46
  `ok` 仍是 `true`;`format: 'input'` 给的是内部对象的引用(想改先克隆),`format: 'full'` 给的是深拷贝的
45
47
  标准文档(没有 `ok` / `counts` / `warnings` 这几列,见下一节)。
46
48
 
@@ -65,7 +67,7 @@ next.importData(doc)
65
67
  - **坐标一律 `[x, y]`**:`{ x, y }` 这种写法读得进来,导出的都变成数组;缺了按 `0` 算(与引擎同一份口径)。
66
68
  - **`labels` / `decorations` 一律数组**:单个对象读得进来,出了一律包装成数组。
67
69
  - **通道一律 `groups`**:只写 `lineIds` / `path` 的短写法读得进来,出了会展开成一组(组 id 取通道 id),与引擎内部 `channelGroupsOf` 的兜底逐字一致。
68
- - **JSON 安全**:导出的是深拷贝;函数 / Symbol / DOM 元素 / 画布 / `Map` / `Set` / 循环引用这类存不进 JSON 的值按固定规则跳过(SVG 元素转成 outerHTML 字符串、图片元素转成 `{ type: 'image', src }`、`NaN` / `Infinity` 写 `null`),每一条都通过 `onWarning` 报一句,不会静默丢字段。
70
+ - **JSON 安全**:导出的是深拷贝;函数 / Symbol / DOM 元素 / 画布 / `DataView` / `Map` / `Set` / 循环引用这类存不进 JSON 的值按固定规则跳过(SVG 元素转成 outerHTML 字符串、图片元素转成 `{ type: 'image', src }`、类型化数组展开成普通数组、`BigInt` 元素写字符串、`NaN` / `Infinity` 写 `null`),每一条都通过 `onWarning` 报一句,不会静默丢字段。
69
71
  - **键序保留**:既有键不重排、只补缺的,所以「读文件 → 导出 → 再读回来」逐字节相同;`sample-document.json` 与演示页的「与文件对一遍」就是按这个口径对账的。
70
72
 
71
73
  ```js
@@ -75,11 +75,11 @@
75
75
  外加 `parallel` / `opacity` / `shadow`。
76
76
  - `sample-channels.json`(6 点 / 8 线 / 3 通道):通道与分组。4 回线拆成两组(`groups` 各带自己的骨架拐点)、
77
77
  按两端节点取宽的 2 回线(`nodeWidth` + `tangentInset`)、单回线各一条;逐回颜色与线宽写在回线上。
78
- 演示页「完整样本」(`?demo=samples`)把四份切着看:页面上的「与文件对一遍」用 `exportData({ format: 'input' })`
79
- 与文件逐字节对账,「按导出重建」演示存档 → 重建的完整回显。
78
+ 这四份文件可以直接用来对账:把样本交给 `createGisView`,再用 `exportData({ format: 'input' })`
79
+ 导出并逐字节比较;需要现场试验时,可在调试页加载样本或走 `importData` 回灌。
80
80
  - 仓库根目录的 `sample-document.json` 是上面 `sample-full.json` 的**标准文档**写法(`format: 'gis-view'` 八列齐全,
81
- 含 `view.camera` 与 `filter`):演示页「完整样本」(`?demo=samples`)里那一枚「标准文档(整份)」芯片就是它 ——
82
- 按 `exportData({ format: 'full' })` 与文件逐字节对账,再用导出的那一份重建实例、再导出一次,两次逐字节相同。
81
+ 含 `view.camera` 与 `filter`):它就是 `exportData({ format: 'full' })` 的完整产物 ——
82
+ 可先按整份导出与文件逐字节对账,再用导出的那一份重建实例、再导出一次,两次逐字节相同。
83
83
  `node scripts/build-sample-document.mjs` 由 `sample-full.json` 生成它(归一化 + 补 `view.camera`,不手写)。
84
84
  - `docs/demos/shared/demoData.js` 里的 `buildDemoData({ count })` / `connectDemoLines(points)` /
85
85
  `createChannels` 是各演示页共用的合成数据:球状铺点 + 环形连线 + 几条通道,字段与真实数据同构,
@@ -93,8 +93,8 @@
93
93
  引擎会整批解析;逐点写 `color` / `size` 会让解析按条走(十万级下差别很大)。
94
94
  - **增量更新优先**:改几条用 `updatePoint` / `updateLine` / `patchPoints`,一批改动用 `batch(() => { ... })`
95
95
  收成一次重建与一条历史;整份换数据才用 `updateData`。
96
- - **只改一样就用细粒度方法**:`setPointSize` / `setPointColor` / `movePoint` / `setLineWidth` / `setLineWaypoints` /
97
- `addLineDecoration` / `setChannelWidth` / `setChannelColors` 这一类一次只动一条(通道那两个是「成员回线 + 通道声明」一起,
96
+ - **只改一样就用定向补丁**:`updatePoint` / `updateLine` / `updateChannel` 的单项字段补丁;需要坐标归一化或成员联动时用
97
+ `movePoint` / `setLineWaypoints` / `addLineDecoration` / `setChannelColors` 这一类一次只动一条(通道那两个是「成员回线 + 通道声明」一起,
98
98
  包在一个 `batch` 里),走的还是定向更新那条路 —— 不整份重灌,也不会把别的编辑结果冲掉。
99
99
  - **条件里别写函数**:函数条件只能主线程算;纯字段条件在数据量超过 `filterWorker.threshold`(默认 20000)后走 Worker。
100
100
  - **标签**:`maxCount` + `scanLimit` 兜住 DOM 数量与每帧考察量;`visibility: 'auto'` 已经做了避让与视野收缩。(这三项只在 `labelOptimize: true` 时生效:缺省是「强制显示」档,不避让、不截断,视口里该出现的标签一条不少。)
@@ -442,11 +442,11 @@
442
442
  const [type, a, b] = String(key).split(':')
443
443
  if (type === 'point') {
444
444
  current = { key, type: 'point', index: Number(a) }
445
- view.select({ type: 'point', index: current.index })
445
+ view.selectFeatures({ type: 'point', index: current.index })
446
446
  }
447
447
  else if (type === 'line') {
448
448
  current = { key, type: 'line', index: Number(a) }
449
- view.select({ type: 'line', index: current.index })
449
+ view.selectFeatures({ type: 'line', index: current.index })
450
450
  }
451
451
  else if (type === 'channel') {
452
452
  current = { key, type: 'channel', channelId: a }
@@ -462,7 +462,7 @@
462
462
  const targetIndex = state.kind === 'point' ? pointIndexById.get(String(state.id)) : lineIndexById.get(String(state.id))
463
463
  current = { key, type: 'labelState', index, kind: state.kind, targetIndex }
464
464
  if (targetIndex !== undefined) {
465
- view.select({ type: state.kind, index: targetIndex })
465
+ view.selectFeatures({ type: state.kind, index: targetIndex })
466
466
  }
467
467
  }
468
468
  if (options.center !== false && centerOnClick && current) {
@@ -684,7 +684,7 @@
684
684
  document.querySelector('#btn-fit').addEventListener('click', () => view.fitView())
685
685
  document.querySelector('#btn-clear').addEventListener('click', () => {
686
686
  current = null
687
- view.select(null)
687
+ view.selectFeatures(null)
688
688
  renderPanel()
689
689
  draw()
690
690
  })
package/docs/index.html CHANGED
@@ -604,6 +604,98 @@
604
604
  background: var(--accent);
605
605
  color: #fff;
606
606
  }
607
+ /* README 首屏的推荐阅读路径:按任务给四条入口,不替代正文目录 */
608
+ .doc-paths {
609
+ margin: 0 0 22px;
610
+ padding: 16px;
611
+ border: 1px solid var(--accent-line);
612
+ border-radius: 13px;
613
+ background: linear-gradient(135deg, var(--accent-soft), var(--surface) 58%);
614
+ }
615
+ .doc-paths__head {
616
+ display: flex;
617
+ align-items: baseline;
618
+ flex-wrap: wrap;
619
+ gap: 4px 12px;
620
+ margin-bottom: 12px;
621
+ }
622
+ .doc-paths__head strong {
623
+ color: var(--text-strong);
624
+ font-size: 0.98em;
625
+ }
626
+ .doc-paths__head span {
627
+ color: var(--muted);
628
+ font-size: 12px;
629
+ }
630
+ .doc-paths__grid {
631
+ display: grid;
632
+ grid-template-columns: repeat(auto-fit, minmax(185px, 1fr));
633
+ gap: 9px;
634
+ }
635
+ article a.doc-path {
636
+ position: relative;
637
+ display: flex;
638
+ flex-direction: column;
639
+ gap: 3px;
640
+ min-width: 0;
641
+ min-height: 126px;
642
+ padding: 12px 13px;
643
+ border: 1px solid var(--line);
644
+ border-radius: 10px;
645
+ background: var(--surface);
646
+ color: var(--text);
647
+ text-decoration: none;
648
+ transition: transform 0.14s ease, border-color 0.14s ease, box-shadow 0.14s ease;
649
+ }
650
+ article a.doc-path::before {
651
+ content: '';
652
+ position: absolute;
653
+ inset: 0 auto 0 0;
654
+ width: 3px;
655
+ border-radius: 10px 0 0 10px;
656
+ background: var(--accent);
657
+ opacity: 0.72;
658
+ }
659
+ article a.doc-path:hover {
660
+ border-color: var(--accent-line);
661
+ background: var(--surface);
662
+ color: var(--text);
663
+ box-shadow: var(--shadow-2);
664
+ transform: translateY(-1px);
665
+ }
666
+ .doc-path__badge {
667
+ align-self: flex-start;
668
+ padding: 1px 7px;
669
+ border-radius: 999px;
670
+ background: var(--accent-soft);
671
+ color: var(--accent-strong);
672
+ font: 600 10px/1.7 Consolas, 'Courier New', monospace;
673
+ letter-spacing: 0.3px;
674
+ }
675
+ .doc-path b {
676
+ margin-top: 3px;
677
+ color: var(--text-strong);
678
+ font-size: 13.5px;
679
+ }
680
+ .doc-path__desc {
681
+ color: var(--muted);
682
+ font-size: 12px;
683
+ line-height: 1.65;
684
+ }
685
+ .doc-path small {
686
+ margin-top: auto;
687
+ color: var(--accent);
688
+ font-size: 11px;
689
+ line-height: 1.6;
690
+ }
691
+ @media (max-width: 620px) {
692
+ .doc-paths {
693
+ padding: 12px;
694
+ }
695
+ article a.doc-path {
696
+ min-height: 0;
697
+ }
698
+ }
607
699
  @keyframes fade-in {
608
700
  from {
609
701
  opacity: 0;
@@ -766,6 +858,39 @@
766
858
  article blockquote p {
767
859
  margin: 4px 0;
768
860
  }
861
+ /* 文档开头那一句摘要:几乎所有 md 都在 H1 后写一条引用,单独做成阅读导语卡片 */
862
+ article blockquote.doc-summary {
863
+ position: relative;
864
+ margin: 0 0 24px;
865
+ padding: 15px 18px 15px 21px;
866
+ border: 1px solid var(--accent-line);
867
+ border-left: 4px solid var(--accent);
868
+ border-radius: 6px 12px 12px 6px;
869
+ background: linear-gradient(135deg, var(--accent-soft), var(--surface) 72%);
870
+ color: var(--text);
871
+ box-shadow: var(--shadow-1);
872
+ }
873
+ article blockquote.doc-summary::before {
874
+ content: '摘要';
875
+ display: block;
876
+ margin-bottom: 4px;
877
+ color: var(--accent-strong);
878
+ font-size: 10.5px;
879
+ font-weight: 700;
880
+ letter-spacing: 0.14em;
881
+ }
882
+ article blockquote.doc-summary p {
883
+ margin: 0;
884
+ font-size: 0.98em;
885
+ line-height: 1.85;
886
+ }
887
+ /* 没有引用摘要的文档,把 H1 后的第一段稍微抬成导语,阅读层级保持一致 */
888
+ article p.doc-lead {
889
+ margin: 0 0 20px;
890
+ color: var(--text);
891
+ font-size: 1.02em;
892
+ line-height: 1.9;
893
+ }
769
894
  article ul,
770
895
  article ol {
771
896
  margin: 11px 0;
@@ -1038,6 +1163,16 @@
1038
1163
  border-color: var(--accent);
1039
1164
  background: var(--accent-soft);
1040
1165
  }
1166
+ /* 阅读进度数字:跟着顶部进度条一起更新,滚下去才显示 */
1167
+ #backtop .read-percent {
1168
+ min-width: 3.4em;
1169
+ padding: 1px 6px;
1170
+ border-radius: 999px;
1171
+ background: var(--accent-soft);
1172
+ color: var(--accent-strong);
1173
+ font-variant-numeric: tabular-nums;
1174
+ text-align: center;
1175
+ }
1041
1176
  /* 字号三档(标准 / 大 / 特大):只放大正文,标题 / 表格 / 代码块跟着 em 一起走 */
1042
1177
  html.font-large article {
1043
1178
  font-size: 17px;
@@ -1165,7 +1300,7 @@
1165
1300
  <nav></nav>
1166
1301
  </aside>
1167
1302
  </div>
1168
- <button type="button" id="backtop" title="回到顶部">↑ 回到顶部</button>
1303
+ <button type="button" id="backtop" title="回到顶部">↑ 回到顶部 <span id="backtop-percent" class="read-percent">0%</span></button>
1169
1304
  <script type="module">
1170
1305
  import { collectRunnableSamples, isRunnableSample } from './runnable-sample.js'
1171
1306
  /* 文档清单:file 是相对本页的路径,title 是目录里显示的名字 */
@@ -1188,7 +1323,7 @@
1188
1323
  { file: 'manual/14-视图与筛选.md', title: '手册 14 · 视图与筛选', blurb: '筛选 / 搜索 / 视图与背景', parent: 'GisView.md' },
1189
1324
  { file: 'manual/15-导出与集成.md', title: '手册 15 · 导出与集成', blurb: '导出 / 插件 / 自建界面', parent: 'GisView.md' },
1190
1325
  { file: 'manual/16-实例方法总表.md', title: '手册 16 · 实例方法总表', blurb: '实例方法主表', parent: 'GisView.md' },
1191
- { file: 'manual/17-参数速查.md', title: '手册 17 · 参数速查', blurb: '243 个成员逐个列全', parent: 'GisView.md' },
1326
+ { file: 'manual/17-参数速查.md', title: '手册 17 · 参数速查', blurb: '248 个成员逐个列全', parent: 'GisView.md' },
1192
1327
  { file: 'manual/18-逐方法示例.md', title: '手册 18 · 逐方法示例', blurb: '每个成员一段示例', parent: 'GisView.md' },
1193
1328
  { file: 'manual/19-读数与拖拽预览.md', title: '手册 19 · 读数与拖拽预览', blurb: '读数 / 定位 / 落点预览', parent: 'GisView.md' },
1194
1329
  { file: 'manual/20-性能与大数据.md', title: '手册 20 · 性能与大数据', blurb: '大数据开关与实测', parent: 'GisView.md' },
@@ -1220,11 +1355,11 @@
1220
1355
  { file: 'internals/14-背景层.md', title: '原理 14 · 背景层:图片与 GeoJSON', blurb: '底图的解析、绘制与导出时怎么带上。', parent: 'GisView实现原理.md' },
1221
1356
  { file: 'internals/15-01-逐轮记录-新增与修复.md', title: '原理 15-01 · 逐轮记录:新增与修复', blurb: '早期各轮的新增能力与 bug 修复(15.1 ~ 15.20)。', parent: 'GisView实现原理.md' },
1222
1357
  { file: 'internals/15-02-逐轮记录-性能与修复-上.md', title: '原理 15-02 · 逐轮记录:性能与修复(上)', blurb: '百万级、标签与性能相关的各轮(15.48 ~ 15.70)。', parent: 'GisView实现原理.md' },
1223
- { file: 'internals/15-03-逐轮记录-性能与修复-下.md', title: '原理 15-03 · 逐轮记录:性能与修复(下)', blurb: '增量删除、事件捕捉与文档相关的各轮(15.71 ~ 15.98)。', parent: 'GisView实现原理.md' },
1358
+ { file: 'internals/15-03-逐轮记录-性能与修复-下.md', title: '原理 15-03 · 逐轮记录:性能与修复(下)', blurb: '增量删除、事件捕捉与文档相关的各轮(15.71 ~ 15.99)。', parent: 'GisView实现原理.md' },
1224
1359
  { file: 'internals/15-04-逐轮记录-热力图与性能.md', title: '原理 15-04 · 逐轮记录:热力图与性能', blurb: '密度场、标签排版与线路批次裁剪等各轮(15.21 ~ 15.47)。', parent: 'GisView实现原理.md' },
1225
1360
  { file: 'internals/16-01-优化与取舍-上.md', title: '原理 16-01 · 优化与取舍(上)', blurb: '这一章的开篇数据表与 16.1 ~ 16.38 的取舍记录。', parent: 'GisView实现原理.md' },
1226
1361
  { file: 'internals/16-02-优化与取舍-中.md', title: '原理 16-02 · 优化与取舍(中)', blurb: '16.39 ~ 16.56:删除 / 增量 / 文档对账相关的取舍。', parent: 'GisView实现原理.md' },
1227
- { file: 'internals/16-03-优化与取舍-下.md', title: '原理 16-03 · 优化与取舍(下)', blurb: '16.57 ~ 16.73:坐标、通道、筛选与文档口径的取舍。', parent: 'GisView实现原理.md' },
1362
+ { file: 'internals/16-03-优化与取舍-下.md', title: '原理 16-03 · 优化与取舍(下)', blurb: '16.57 ~ 16.74:坐标、通道、筛选与文档口径的取舍。', parent: 'GisView实现原理.md' },
1228
1363
  { file: 'internals/17-调试与扩展建议.md', title: '原理 17 · 调试与扩展建议', blurb: '怎么排查 GL 错误、怎么接自己的覆盖层与插件。', parent: 'GisView实现原理.md' },
1229
1364
  { file: 'internals/18-性能实测与WebGPU评估.md', title: '原理 18 · 性能实测与 WebGPU 评估', blurb: '十五轮实测数据、瓶颈定位与 WebGPU 值不值得上。', parent: 'GisView实现原理.md' },
1230
1365
  { file: 'AI对话汇总.md', title: 'AI 对话汇总(索引)', blurb: '重要问答与 39 个会话索引(正文在 ai/)', groupLabel: '6 章' },
@@ -1264,7 +1399,7 @@
1264
1399
  { file: '../src/gis-view/README.md', title: 'src/gis-view/README.md', blurb: '源码导读与逐轮实现记录' }
1265
1400
  ]
1266
1401
  const PAGES = [
1267
- { href: 'api/index.html', title: 'API 速查(方法 + 示例)', blurb: '243 个实例成员 / 86 个 options,逐个带示例与复制' },
1402
+ { href: 'api/index.html', title: 'API 速查(方法 + 示例)', blurb: '258 个实例成员 / 88 个 options,逐个带示例与复制' },
1268
1403
  { href: 'playground/index.html', title: '调试演示页(粘代码就跑)', blurb: '左侧编辑、右侧真实实例 + GeoJSON 底图:内置示例与「这一份文档」的示例同在一个下拉,粘代码直接跑' },
1269
1404
  { href: 'guide/sample-document.html', title: 'sample-document.json 图解', blurb: '点数据看端点 / 拐点 / 分组骨架' },
1270
1405
  { href: 'steps/index.html', title: '从 0 手写分步 demo', blurb: '13 步,每步一个文件' },
@@ -1435,7 +1570,9 @@
1435
1570
  quote.push(lines[index].replace(/^\s*>\s?/, ''))
1436
1571
  index += 1
1437
1572
  }
1438
- html.push(`<blockquote><p>${inline(quote.join(' '))}</p></blockquote>`)
1573
+ // H1 后紧随的引用是这份文档的摘要:单独一张导语卡片;正文里的普通引用仍用基础样式
1574
+ const summary = html.length === 1
1575
+ html.push(`<blockquote${summary ? ' class="doc-summary"' : ''}><p>${inline(quote.join(' '))}</p></blockquote>`)
1439
1576
  continue
1440
1577
  }
1441
1578
  if (/^\s*\|/.test(line) && index + 1 < lines.length && /^\s*\|[\s:|-]+\|\s*$/.test(lines[index + 1])) {
@@ -1481,7 +1618,8 @@
1481
1618
  paragraph.push(lines[index])
1482
1619
  index += 1
1483
1620
  }
1484
- html.push(`<p>${inline(paragraph.join(' '))}</p>`)
1621
+ const lead = html.length === 1
1622
+ html.push(`<p${lead ? ' class="doc-lead"' : ''}>${inline(paragraph.join(' '))}</p>`)
1485
1623
  }
1486
1624
  return { html: html.join('\n'), toc }
1487
1625
  }
@@ -1776,6 +1914,22 @@
1776
1914
  * 打开一份文档。第二个参数带 slug 时(深链 `#文件~小节`)渲染完滚到那一节并闪一下;
1777
1915
  * 别的调用方(侧栏 / 正文里的 md 链接 / 自测页)照旧只传文件名。
1778
1916
  */
1917
+ /** README 首屏的推荐阅读路径:三条手册路线 + API 速查,卡片只在这一份顶层文档出现 */
1918
+ const READING_PATHS = [
1919
+ { badge: 'START', title: '第一次使用', desc: '引入、最小数据与首屏配置,先把图画出来。', meta: '01 快速开始 · 约 10 分钟', file: 'manual/01-快速开始.md' },
1920
+ { badge: 'BACKEND', title: '接后端与导出', desc: '编辑接口、导出回灌、插件与自建界面。', meta: '15 导出与集成', file: 'manual/15-导出与集成.md' },
1921
+ { badge: 'PERF', title: '十万级性能', desc: 'Worker 筛选 / 异步搜索、批量更新与质量档。', meta: '20 性能与大数据', file: 'manual/20-性能与大数据.md' },
1922
+ { badge: 'API', title: '查方法与参数', desc: '实例方法、options 与可复制示例集中检索。', meta: 'API 速查 · 248 个成员', href: 'api/index.html', external: true }
1923
+ ]
1924
+ const renderReadingPaths = () => `<section class='doc-paths' aria-label='推荐阅读路径'>` +
1925
+ `<div class='doc-paths__head'><strong>按任务选一条阅读路径</strong><span>第一次进来先看快速开始;带着明确目标时直接跳到对应章节。</span></div>` +
1926
+ `<div class='doc-paths__grid'>` +
1927
+ READING_PATHS.map(item => {
1928
+ const href = item.file ? `#${encodeURIComponent(item.file)}` : item.href
1929
+ const attrs = item.file ? ` data-doc='${escapeHtml(item.file)}'` : ` target='_blank' rel='noreferrer'`
1930
+ return `<a class='doc-path' href='${escapeHtml(href)}'${attrs}><span class='doc-path__badge'>${escapeHtml(item.badge)}</span><b>${escapeHtml(item.title)}</b><span class='doc-path__desc'>${escapeHtml(item.desc)}</span><small>${escapeHtml(item.meta)}</small></a>`
1931
+ }).join('') +
1932
+ `</div></section>`
1779
1933
  const load = async (file, options = {}) => {
1780
1934
  const doc = DOCS.find(item => item.file === file) ?? DOCS.find(item => item.file.endsWith(file))
1781
1935
  const target = doc ?? { file, title: file, blurb: '' }
@@ -1790,6 +1944,8 @@
1790
1944
  const source = await response.text()
1791
1945
  const { html, toc } = renderMarkdown(source)
1792
1946
  const fences = Math.floor((source.match(/^\s*```/gm) ?? []).length / 2)
1947
+ // 去空白字符后按 500 字 / 分钟粗估,代码与表格也算在内;只用来给一个阅读预期
1948
+ const readMinutes = Math.max(1, Math.round(source.replace(/\s+/g, '').length / 500))
1793
1949
  // 这一份里能在调试页跑的围栏数(「这一份的调试页 →」按钮上的读数)
1794
1950
  const runnable = collectRunnableSamples(source).length
1795
1951
  const playgroundButton = runnable
@@ -1797,6 +1953,7 @@
1797
1953
  : ''
1798
1954
  content.innerHTML = `<div class="meta"><span>${escapeHtml(target.file)}</span>` +
1799
1955
  `<span>${source.split('\n').length} 行 / ${(source.length / 1024).toFixed(1)} KB</span>` +
1956
+ `<span title="按正文去空白字符粗估">约 ${readMinutes} 分钟</span>` +
1800
1957
  `<span>${toc.length} 个小节</span><span>${fences} 段代码</span>` +
1801
1958
  playgroundButton +
1802
1959
  `<a href="${escapeHtml(target.file)}" target="_blank" rel="noreferrer">看原文</a></div>${html}`
@@ -1814,11 +1971,19 @@
1814
1971
  })
1815
1972
  })
1816
1973
  markCurrent(target.file)
1974
+ // README 首屏在正文前插一排阅读路径;切到别的文档时随正文一起换掉
1975
+ if (target.file === DOCS[0].file) {
1976
+ content.querySelector('.meta')?.insertAdjacentHTML('afterend', renderReadingPaths())
1977
+ }
1817
1978
  if (options.slug) {
1818
1979
  // 深链进来:等新内容排好版再定位(同一帧里量到的位置还是上一份文档的)
1819
1980
  requestAnimationFrame(() => jumpToSlug(options.slug, 'auto'))
1820
1981
  }
1821
- content.scrollIntoView({ block: 'start' })
1982
+ // 首屏不滚正文:窄屏时侧栏排在正文上方,滚到 content 会直接跳过目录。
1983
+ // 深链由上面的 jumpToSlug 定位;只有用户主动切换文档时才把正文带到视野顶部。
1984
+ if (!options.initial) {
1985
+ content.scrollIntoView({ block: 'start' })
1986
+ }
1822
1987
  bindScrollSpy()
1823
1988
  }
1824
1989
  catch (error) {
@@ -2218,11 +2383,13 @@
2218
2383
  /* 顶部进度条与「回到顶部」:滚动时同步一份(passive 监听,不拖慢滚动) */
2219
2384
  const progressEl = document.querySelector('#progress')
2220
2385
  const backtopEl = document.querySelector('#backtop')
2386
+ const backtopPercentEl = document.querySelector('#backtop-percent')
2221
2387
  const syncScrollReadouts = () => {
2222
2388
  const scrolled = window.scrollY ?? document.documentElement.scrollTop ?? 0
2223
2389
  const total = (document.documentElement.scrollHeight ?? 0) - window.innerHeight
2224
2390
  const ratio = total > 0 ? Math.min(1, Math.max(0, scrolled / total)) : 0
2225
2391
  progressEl.style.width = `${(ratio * 100).toFixed(2)}%`
2392
+ backtopPercentEl.textContent = `${Math.round(ratio * 100)}%`
2226
2393
  backtopEl.classList.toggle('on', scrolled > 600)
2227
2394
  }
2228
2395
  window.addEventListener('scroll', syncScrollReadouts, { passive: true })
@@ -2245,8 +2412,12 @@
2245
2412
  searchInput.select()
2246
2413
  })
2247
2414
  renderSidebar()
2415
+ // 深链 / 刷新时由页面自己定位;别让浏览器恢复上次的滚动位置,覆盖首屏或深链落点
2416
+ if ('scrollRestoration' in history) {
2417
+ history.scrollRestoration = 'manual'
2418
+ }
2248
2419
  const initialHash = parseHash(location.hash)
2249
- const initialLoad = load(initialHash.file || DOCS[0].file, { slug: initialHash.slug })
2420
+ const initialLoad = load(initialHash.file || DOCS[0].file, { slug: initialHash.slug, initial: true })
2250
2421
  // 想在控制台 / 自测页里摸这一页:ready 等首屏渲染完,render 是 markdown → HTML 的纯函数,docs / pages 是两份清单,
2251
2422
  // resolve 把一个 md 链接解析成清单里的文件名(解析不到给 null,外层就开新标签页);
2252
2423
  // parseHash 拆深链(`#文件~小节`),find / nextMatch / prevMatch / clearFind 是正文查找(命中数用 hits() 读),
@@ -11,7 +11,7 @@
11
11
  - **量测与避让**:标签元素按批创建 / 复用(池上限 `LABEL_POOL_LIMIT = 64` 之外的直接回收),先量尺寸再决定位置;避让用一张网格(`LABEL_GRID_CELL = 64`)记已经放下的标签矩形,命中就跳过该候选。
12
12
  - **撞上之后换落点**(`avoidOverlap` 的第二条路):节点标签按「对侧 → 左 / 上 / 下…」换锚点(`anchorFallback`,缺省 4),线路标签沿线左右各试 2 步(`slideFallback`,缺省 2,落点只取视口里的);判定全走「尺寸缓存 + `transform`」的纯算术,找到空位就写 class / `transform` 并把选择记进 meta,下一轮接着走。底图区域文字(`background.geoJsonStyle.label`)另占一张「软」网格:先躲、躲不开照画(数据标签优先)。
13
13
  - **贴边**:`anchor: 'edge'` 时从符号外沿量 `gap`(节点半径来自 `getVisiblePoints()` 的 `radius`),所以节点放大标签跟着往外让。
14
- - **交互期整体变换**:平移 / 缩放 / 拖点时标签层只写一次 `transform`,相机停手 `LABEL_SHIFT_IDLE_MS = 180ms` 后再真实排版(必要时按 `LABEL_SHIFT_REFRESH_MS = 220ms` 节流刷新)。这是十万级数据下标签不拖垮 fps 的关键。
14
+ - **交互期整体变换**:平移 / 缩放 / 拖点时标签层只写一次 `transform`,相机停手 `LABEL_SHIFT_IDLE_MS = 180ms` 后再真实排版(必要时按 `LABEL_SHIFT_REFRESH_MS = 220ms` 节流刷新)。这是十万级数据下标签不拖垮 fps 的关键。整层位移与每条标签 / 提示框 / 底图区域文字的落点都吸附到**设备像素网格**(`devicePixelRatio × 宿主显示比例`),1.25 / 1.5 倍屏上不会停在半个物理像素;标签层整体保留 `will-change` 与 `contain: layout style paint` / `isolation: isolate`,逐条标签自己的 `will-change: transform` 已移除,避免拖动时每个标签各成一个合成层、因相位差而轻晃或重复栅格化。
15
15
 
16
16
  **交互期只重算动过的线**:`lineLabel.followOnInteract`(默认 `true`)打开时,拖动节点 / 拐点期间 `moveLineLabels(indexes)` 只重算这几条线的标签锚点 —— `host.getLine(index)` 取最新路径,重新投影位置与切向角,并反解掉标签层欠下的整层位移,每帧只写几个 `transform`;关掉后交互期不动、停手统一归位。`hideOnInteract` 收起的层不参与。
17
17
  - **提示框**:单实例 DOM,跟随光标或贴在要素上(`follow`),模板里的取值统一转义后再拼(`escapeHtml` / `interpolate`),`pointer-events: none` 不挡交互。落在通道回线上时由 `tooltip.channelScope`(`line` / `group` / `channel`,默认 `channel`)决定 `{字段}` 先取哪一层,以及「要素 `tooltip` → `tooltip.group` → `tooltip.channel` → `tooltip.line` → `tooltip.content`」的回落链;`{circuits}` 取当前范围(本组 / 整条通道)的回线数,引擎的 `channelGroupReadout(state)` 给载荷补上分组读数。
@@ -5,11 +5,12 @@
5
5
  - **底图**:`minimap.js` 把节点与线条直接写进一块离屏像素缓冲(`ImageData`),按「层级 + 颜色 + 线宽 + 透明度」分桶后再画 —— 绘制次数随分桶数走,不随数据量走;底图只在数据或筛选结果变化时重画;节点像素结果按「数据引用 + 尺寸 + 范围 + 变换 + 样式」缓存,同一份数据前后两次重画直接复用(十万点重铺 20ms → 0.3ms),数据范围也按数据对象进 `WeakMap`。
6
6
  - **视口框**:单独一个 DOM 元素,相机变化只改 `transform`(不重画底图),所以拖动小图不掉帧。
7
7
  - **规则**:小图可以有自己的 `pointRules` / `lineRules`(颜色、半径 / 线宽、形状、层级),不配时沿用主图规则但**只取颜色与层级**(主图的尺寸是主画布上的像素,搬到小图上会糊成一片)。
8
- - **交互**:视口框上按下 = 拖框;空白处长按(`longPressDelay`)= 视角跟随;`zoomable` 打开后滚轮按 `zoomStep` 缩放小图自身(锚点落光标处),Ctrl / ⌘ + 滚轮留给主图;`dragAction` 决定空白处拖动是平移小图还是框选(按住 Ctrl / ⌘ 自动切到另一个动作)。拖框与视角跟随带来的相机变化是「程序化」的:标签层不进交互态(`hideOnInteract` 不会把主视图的标签收起来),拖动结束补一次真实排版。
8
+ - **交互**:视口框上按下 = 拖框;空白处长按(`longPressDelay`)= 视角跟随;`zoomable` 打开后滚轮按 `zoomStep` 缩放小图自身(锚点落光标处),Ctrl / ⌘ + 滚轮留给主图;`dragAction` 决定空白处拖动是平移小图还是框选(按住 Ctrl / ⌘ 自动切到另一个动作)。拖框与视角跟随带来的相机变化是「程序化」的:标签层不进交互态(`hideOnInteract` 不会把主视图的标签收起来),拖动结束补一次真实排版。每次真正有位移的手势结束时通过 `onInteraction` 发一条 `{ type, originalEvent, local, world, zoom, extent, camera, viewport }`(`type` 为 `box-drag` / `select` / `pan` / `zoom` / `wheel-main` / `long-press`);无位移的点击不发,`pointermove` 不逐帧回调,长按在触发跟随时发。
9
9
  - **范围**:`extent` 决定小图按哪段世界范围铺 —— `measure(data)` 量出可见数据的范围,`backgroundBoundsOf(background)` 量出底图范围(GeoJSON 用画出来的 `bounds`,图片用四角 quad 的包围盒),`auto`(默认)取两者的并集,`data` 只看数据(旧行为),`background` 只看底图。数据为空时 `measure()` 返回 `null`、不会当成「范围是正负一」;修掉的问题见 15.12。`auto` 的例外(本轮):相机正落在其中一份范围里、而那一份在并集里连 6 个像素都占不到时(数据还没映射到底图坐标系就是这样)只画正在看的那一份;`followFocusedExtent()` 在相机挪进 / 挪出这份小范围时补一次重画 —— 判断只用上一次重画缓存的两份范围,每帧几次比较。
10
10
  - **通道**:`channelCircuits: false` 时每个通道只画偏移最靠中间的那一根回线(底图分辨率本来就在一像素上下,多回线只会糊成一片)。
11
11
  - **背景**:默认 `background: 'inherit'`,直接拿引擎解析好的那份背景在 Canvas2D 上重画:底色铺满,图片按 quad 拆成两组三角形做仿射 + 裁剪(任意四边形都不会画歪),GeoJSON 面用奇偶规则填充(洞自动镂空)、纯线几何的区域再描一遍、点要素画成固定 1.2px 半径的小圆;底图的线宽(取配置值、最低 0.4px 保可见)与区域文字字号(固定档,夹在 5 ~ 12px)都是小图上的固定像素,不乘「小图比例 ÷ 主图比例」—— 缩放主视图时底图不会单独放大 / 缩小(本轮删掉了 `mapRatio`,见 15.17)。也可以给 `minimap.background` 单独配一份(结构与主视图的 `background` 完全一致,只是换 Canvas2D 画),`backgroundStyle` 是小图容器的 CSS 底色。区域文字先量「区域在小图上的屏幕矩形 ≥ 文字尺寸 × 0.85」再画,放不下就整条跳过(缩略图上那些小块本来也读不出来);能画的按区域从大到小排,和已经放下的标签重叠就跳过 —— 小图上几十个区域各画一行字只会叠成一团。
12
- - **清晰度**:小图画布用自己的一份倍率(`clamp(devicePixelRatio, 1, 3)`),主视图为全屏帧率按像素预算压低渲染倍率时,小图不受影响。
12
+ - **扩展层**:`layers` 接收一组 `{ id?, order?, placement?, enabled?, draw }`。`placement: 'below-data'` 在内置线路 / 节点之前执行,适合网格、分区底色;`'above-data'`(缺省)在数据之后执行,适合指北针、业务标记。两段内部各按 `order` 从小到大,相同或缺省 `order` 按数组顺序。`setOptions` 时把过滤、排序与分段结果缓存成两条数组,绘制帧只遍历当前段,不再每帧 `map` / `filter` / `sort`。每个回调独立 `save` / `restore`,一层改过的画布状态不会漏进下一层;`enabled: false` 可临时关掉一层,旧的单层 `draw` 保持兼容并固定最后执行,回调里的 `stage` 为 `'overlay'`。这样网格、指北针、业务标记可以各自维护,不必把所有内容堆进一个钩子。
13
+ - **清晰度与开销**:小图画布用自己的一份倍率(`clamp(devicePixelRatio × 宿主显示比例, 1, maxPixelRatio)`,`maxPixelRatio` 默认 4、夹在 1 ~ 4),主视图为全屏帧率按像素预算压低渲染倍率时,小图不受影响;小图尺寸大或高分屏填充压力高时可以把上限降到 1 ~ 2。容器另有 `contain: layout paint` 与 `isolation: isolate`,把布局与重绘影响收在自身。
13
14
 
14
15
  ---
15
16
 
@@ -500,4 +500,20 @@
500
500
 
501
501
  ---
502
502
 
503
+ ### 15.99 四条修改:接口文档落地、JSON 安全与导出性能、自定义导出格式(本轮已做,对着「修改.txt」四条)
504
+
505
+ **需求**:0 完善接口文档,尽量贴合演示显示、前端能处理数据;1 排查组件 bug;2 优化组件性能;3 提高组件可扩展性。
506
+
507
+ **① 接口协议补成「后端实现 + 前端落地」两份口径**:假后端原先只看 `opId` 回放,头与 body 不一致、同一个 `opId` 换请求体都能混过去。现在 `submit` 在幂等缓存之前依次校验 `Idempotency-Key === body.opId`、`If-Match === "v" + baseVersion`、`baseVersion` 是有限数字,失败返回 `400 BAD_REQUEST`;同一个 `opId` 的请求指纹(method + url + 规范化 body)不同则返回 `409 IDEMPOTENCY_CONFLICT`,版本不前进。`docs/接口协议.md` 新增「前端落地映射」:成功统一从 `response.body.data.result` 取资源结果,复制按 `idMap` 改写引用并用 `counts` 对账,合并用 `retainedPointIds` / `removed*Ids` 核对本地已合并结果,分组用响应 `groups` 整份覆盖(`path: []` 保留直连),拐点按 `target` 精确覆盖(分组只写中间拐点、独立线路写整条折线),409 用 `data.current` 局部恢复后以新版本和新 `opId` rebase;最小验收用例补了头 / body 不一致 400 与同 `opId` 换 body 409 两条。
508
+
509
+ **② 两处 JSON 安全 bug**:`normalizeDocument` 碰到无效 `Date` 时直接 `toISOString()`,会抛 `RangeError` 把整份导出打断;现在写 `null` 并记一条「无效日期」告警。带原型的类实例原先走普通对象分支,`Object.keys` 为空时被悄悄写成 `{}`,导出与回灌都丢了数据;现在按「JSON 存不下」跳过该键并点名构造函数(如 `Box`),不再静默。
510
+
511
+ **③ 导出性能**:`document.js` 的 `toJsonValue` 原来每进一层都拼一次 `${path}.${key}` 字符串,50k 点 / 30k 线的文档里绝大多数值都是普通字段,这部分分配成了主要成本。现在只维护一个可复用的路径段数组,正常路径零字符串分配,只有函数 / DOM / 循环引用这类真正要告警时才 `pathToText` 拼一次。同机同脚本、交替取中位:50k 点 / 30k 线 `exportData({ format: 'full' })` **45.74 → 37.73 ms(约 -17.5%)**。
512
+
513
+ **④ 自定义导出格式**:新增模块级 `export-formats.js`,内置 `view / new / test / input / full` 保留且大小写变体不可覆盖;`registerExportFormat(name, adapter)` 注册同步导出器,`unregisterExportFormat` / `hasExportFormat` / `listExportFormats` 管注册表。`exportData` 与 `exportViewportData` 都认自定义格式,适配器拿到 `{ view, document, counts, options, parts, warnings }`,其中 `document` 已筛选、深拷贝、坐标归一、JSON 安全;返回 Promise 会明确报错。`exportViewportData` 在自定义结果上补 `viewRect / margin`。`ChannelDemo` 的「后端 DTO(自定义格式)」按钮演示注册 → 导出 → 卸载时注销,`types.d.ts` 补了 `GisViewExportFormatAdapter` / `GisViewExportFormatContext` 与两组泛型重载。
514
+
515
+ **验证**:`selftest-data.html` 92 → **98**(无效日期 / 类实例 / 自定义格式注册 / DTO 映射 / 深拷贝 / 视口导出 / 注销后失效),`selftest-demos.html` 219 → **227**(假后端严格契约 6 条 + 通道页自定义格式按钮 2 条),十三页自测 **1790 → 1804** 条 0 FAIL;`npx tsc --noEmit` 0 错、`npm run check:docs` 全绿、`npm run build` 通过。
516
+
517
+ ---
518
+
503
519
  [← 15-02 逐轮记录:性能与修复(上)](15-02-%E9%80%90%E8%BD%AE%E8%AE%B0%E5%BD%95-%E6%80%A7%E8%83%BD%E4%B8%8E%E4%BF%AE%E5%A4%8D-%E4%B8%8A.md) · [实现原理目录](../GisView实现原理.md) · [15-04 逐轮记录:热力图与性能 →](15-04-%E9%80%90%E8%BD%AE%E8%AE%B0%E5%BD%95-%E7%83%AD%E5%8A%9B%E5%9B%BE%E4%B8%8E%E6%80%A7%E8%83%BD.md)