gis-view-webgl2 0.1.2 → 0.1.4

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 +21 -14
  2. package/dist/gis-view.css +1 -1
  3. package/dist/index.cjs +1 -1
  4. package/dist/index.d.ts +421 -76
  5. package/dist/index.js +1 -1
  6. package/docs/AI/345/257/271/350/257/235/346/261/207/346/200/273.md +151 -0
  7. package/docs/GisView.md +4 -4
  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 +137 -0
  9. package/docs/api/index.html +23 -9
  10. package/docs/demos/ApiCoverageDemo.vue +644 -0
  11. package/docs/demos/ChannelDemo.vue +14 -12
  12. package/docs/demos/ClusterDemo.vue +251 -0
  13. package/docs/demos/CopyMergeDemo.vue +3 -4
  14. package/docs/demos/EventDemo.vue +3 -3
  15. package/docs/demos/ExtensionDemo.vue +69 -12
  16. package/docs/demos/GisViewDemo.vue +64 -8809
  17. package/docs/demos/SearchFilterDemo.vue +13 -11
  18. package/docs/demos/api-index.js +58 -47
  19. package/docs/demos/demos.js +34 -17
  20. package/docs/demos/gis-view-demo/GisViewDemoCardHeader.vue +29 -0
  21. package/docs/demos/gis-view-demo/GisViewDemoDialogs.vue +82 -0
  22. package/docs/demos/gis-view-demo/GisViewDemoFooter.vue +16 -0
  23. package/docs/demos/gis-view-demo/GisViewDemoHeading.vue +16 -0
  24. package/docs/demos/gis-view-demo/GisViewDemoPanelStack.vue +71 -0
  25. package/docs/demos/gis-view-demo/GisViewDemoStage.vue +219 -0
  26. package/docs/demos/gis-view-demo/demo.css +1230 -0
  27. package/docs/demos/gis-view-demo/demoContext.js +12 -0
  28. package/docs/demos/gis-view-demo/panels/DataPanels.vue +303 -0
  29. package/docs/demos/gis-view-demo/panels/OpsPanels.vue +174 -0
  30. package/docs/demos/gis-view-demo/panels/StylePanels.vue +561 -0
  31. package/docs/demos/gis-view-demo/panels/ViewPanels.vue +396 -0
  32. package/docs/demos/gis-view-demo/useGisViewDemo.js +6188 -0
  33. 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
  34. package/docs/index.html +41 -4
  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 +3 -2
  37. package/docs/manual/03-/351/205/215/347/275/256/351/241/271.md +104 -16
  38. package/docs/manual/04-/344/272/213/344/273/266/344/270/216/345/233/236/350/260/203.md +15 -14
  39. 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 +40 -3
  40. package/docs/manual/08-/351/200/211/344/270/255/344/270/216/346/213/220/347/202/271.md +50 -3
  41. package/docs/manual/09-/351/200/232/351/201/223/344/270/216/345/244/232/345/233/236/347/272/277.md +6 -2
  42. 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 +39 -7
  43. package/docs/manual/12-/346/240/207/347/255/276/344/270/216/350/243/205/351/245/260.md +13 -4
  44. package/docs/manual/13-/345/212/250/346/225/210/344/270/216/345/233/276/345/205/203.md +3 -0
  45. package/docs/manual/14-/350/247/206/345/233/276/344/270/216/347/255/233/351/200/211.md +121 -49
  46. package/docs/manual/15-/345/257/274/345/207/272/344/270/216/351/233/206/346/210/220.md +37 -3
  47. package/docs/manual/16-/345/256/236/344/276/213/346/226/271/346/263/225/346/200/273/350/241/250.md +17 -8
  48. package/docs/manual/17-/345/217/202/346/225/260/351/200/237/346/237/245.md +45 -25
  49. package/docs/manual/18-/351/200/220/346/226/271/346/263/225/347/244/272/344/276/213.md +91 -20
  50. 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 +3 -3
  51. package/docs/manual/20-/346/200/247/350/203/275/344/270/216/345/244/247/346/225/260/346/215/256.md +25 -1
  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 +11 -8
  53. package/docs/steps/README.md +17 -11
  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 +409 -32
  57. package/docs/steps/selftest-docs.html +33 -4
  58. package/docs/steps/selftest-effects.html +200 -0
  59. package/docs/steps/selftest-features.html +603 -21
  60. package/docs/steps/selftest-labels.html +74 -0
  61. package/docs/steps/selftest-perf.html +293 -20
  62. package/docs/steps/selftest-scale.html +150 -29
  63. package/docs/steps/selftest-select.html +90 -0
  64. package/docs/steps/step-10-full.js +1 -1
  65. package/docs/steps/step-12-events.js +1 -1
  66. package/package.json +1 -1
@@ -7,7 +7,7 @@
7
7
  * `mergeChannelGroups(id, [要并的组], 目标组)` 再并回去。
8
8
  * `addChannelGroup(id, [回线…], { id?, path? })` 一次把几条拆成单独一组(分组那一块的勾选就是它)。
9
9
  * - 外观:通道宽度 / 内收比例 / 偏移方式(tangent 沿切向,inset 按比例内收)都是 updateOptions。
10
- * - 交互:右键一条通道回线弹出交互框(onChannelContextMenu)—— 点分组切换当前分组并选中它的回线,
10
+ * - 交互:右键一条通道回线弹出交互框(统一的 onContextMenu + payload.channelContext)—— 点分组切换当前分组并选中它的回线,
11
11
  * 每条回线可以拆成新分组 / 并进别的分组(moveChannelLine),下面一排按钮走 runChannelAction。
12
12
  * - 选中模式 / 删除:channelSelectMode 决定点一条回线是只选它自己、连所在分组还是连整条通道;
13
13
  * Delete(或 requestRemove())先出删除载荷 —— 删什么、连带什么、能按「只删线路 / 整个分组 / 整条通道」哪个范围删,
@@ -303,7 +303,8 @@ const buildOptions = () => ({
303
303
  showGrid: true,
304
304
  fitPadding: 60,
305
305
  // 开关缺省全关(见 features.js):这个演示要的是「拖节点改通道 + 右键通道回线出交互框 + Delete 删除」,逐项显式打开
306
- features: { pan: true, zoom: true, hover: true, select: true, contextMenu: true, keyboard: true, drag: true, channelMenu: true, remove: true },
306
+ // 通道路由与普通右键共用 contextMenu,不再另开一个功能开关
307
+ features: { pan: true, zoom: true, hover: true, select: true, contextMenu: true, keyboard: true, drag: true, remove: true },
307
308
  // 通道选中模式:点一条回线选它自己 / 连所在分组 / 连整条通道
308
309
  channelSelectMode: channelSelectMode.value,
309
310
  // 分组拐点拖动:共用骨架 / 只移同组同序号拐点
@@ -315,7 +316,7 @@ const buildOptions = () => ({
315
316
  // 这两条是假接口的入口:拐点只接 add / remove / end,分组只接整份声明;事件载荷本身已经够组请求
316
317
  onWaypointChange: handleWaypointBackend,
317
318
  onChannelGroupChange: handleChannelGroupBackend,
318
- onChannelContextMenu: handleChannelContextMenu,
319
+ onContextMenu: handleContextMenu,
319
320
  onReady: () => {
320
321
  emit('notice', `${dataset.channels.length} 条通道 / ${dataset.channelLines.length} 回线`)
321
322
  pushLog('就绪:拖通道端点节点、改宽度或拆一组试试')
@@ -626,7 +627,7 @@ const confirmGroupDialog = () => {
626
627
  }
627
628
 
628
629
  /**
629
- * 右键交互框:右键一条通道回线时 onChannelContextMenu(payload) 把「这条通道几组 / 当前是哪一组 /
630
+ * 右键交互框:右键一条通道回线时 onContextMenu(payload) 的 payload.channelContext 把「这条通道几组 / 当前是哪一组 /
630
631
  * 这一组能做哪些交互」一次交出来,界面照着 groups 与 actions 渲染就行 —— 核心是四件事:
631
632
  * 点分组 = setCurrentChannelGroup(顺带选中这一组的回线)、每条回线的下拉 + 分配 = moveChannelLine、
632
633
  * 按钮 = runChannelAction、点空白处(回调收到 null)/ Esc / × 收框。
@@ -684,11 +685,12 @@ const syncMenu = () => {
684
685
  syncMenuLineTargets()
685
686
  }
686
687
 
687
- /** 右键回调:payload 就是整份上下文,null 表示点在空白处(把框收掉) */
688
- const handleChannelContextMenu = (payload) => {
689
- channelMenu.value = payload
690
- if (payload) {
691
- pushLog('右键 ' + payload.channelId + ':共 ' + payload.groupCount + ' 组,命中「' + payload.groupId + '」的 ' + payload.group.lineCount + ' 回线')
688
+ /** 右键回调:所有命中都走这一条,通道上下文挂在 payload.channelContext 上;点空白 / 节点时把框收掉 */
689
+ const handleContextMenu = (payload) => {
690
+ const channelContext = payload?.channelContext
691
+ channelMenu.value = channelContext ? { ...payload, ...channelContext } : null
692
+ if (channelContext) {
693
+ pushLog('右键 ' + channelContext.channelId + ':共 ' + channelContext.groupCount + ' 组,命中「' + channelContext.groupId + '」的 ' + channelContext.group.lineCount + ' 回线')
692
694
  }
693
695
  syncMenu()
694
696
  }
@@ -1032,7 +1034,7 @@ onBeforeUnmount(() => {
1032
1034
  </button>
1033
1035
  </div>
1034
1036
  <p class="dv-channel-menu__hint">
1035
- 命中回线 {{ channelMenu.lineId }} · 这些都在 onChannelContextMenu(payload) 里;点空白处 / Esc / × 收框
1037
+ 命中回线 {{ channelMenu.lineId }} · 这些都在 onContextMenu(payload).channelContext 里;点空白处 / Esc / × 收框
1036
1038
  </p>
1037
1039
  </div>
1038
1040
  <div v-if="groupDialog" class="dv-group-dialog" data-demo="channel-group-dialog" @click.self="closeGroupDialog">
@@ -1257,8 +1259,8 @@ onBeforeUnmount(() => {
1257
1259
  <h3>右键交互框<span class="dv-chip">{{ channelMenu ? '已打开' : '未打开' }}</span></h3>
1258
1260
  <p class="dv-readout">{{ menuText }}</p>
1259
1261
  <p class="dv-hint">
1260
- 右键一条通道回线:onChannelContextMenu(payload) 把「几组 / 当前哪一组 / 这一组能做什么」一次交出来,
1261
- 框里照着 groups 与 actions 渲染;点空白处回调收到 null,按 Esc 或点 × 也会收框。
1262
+ 右键一条通道回线:onContextMenu(payload) 在 payload.channelContext 里把「几组 / 当前哪一组 /
1263
+ 这一组能做什么」一次交出来,框里照着 groups 与 actions 渲染;点空白处 / 节点时没有 channelContext,框自动收起。
1262
1264
  </p>
1263
1265
  </div>
1264
1266
 
@@ -0,0 +1,251 @@
1
+ <script setup>
2
+ /**
3
+ * 点聚合演示:同一份合成数据在多簇中心附近铺开,面板上调开关 / 半径 / 最少成员数 / 展开缩放。
4
+ *
5
+ * 聚合只改「展示什么、能点到什么」:原始 points、getData()、listPoints() 与业务导出都不变;
6
+ * 聚合圆替代密集成员参与绘制、拾取、快照与 SVG / PNG 导出。点中聚合圆只发 clusterClick,
7
+ * 不会同时发普通 click,也不会改动选中集。
8
+ */
9
+ import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
10
+ import { createGisView } from '../../src/index.js'
11
+ import { DEMO_CENTER, DEMO_RADIUS, createRandom } from './shared/demoData.js'
12
+ import './shared/demoStyle.css'
13
+
14
+ const emit = defineEmits(['notice'])
15
+ const container = ref(null)
16
+ let view = null
17
+ let statsDrawnAt = Number.NEGATIVE_INFINITY
18
+
19
+ const DATA_COUNTS = [2000, 12000, 60000, 100000]
20
+ const pointCountChoice = ref(12000)
21
+ const clusterOn = ref(true)
22
+ const radius = ref(60)
23
+ const minPoints = ref(2)
24
+ const maxZoom = ref(0)
25
+ const statsText = ref('—')
26
+ const clusterText = ref('—')
27
+ const eventText = ref('等待点击聚合圆')
28
+ const normalClicks = ref(0)
29
+ const clusterClicks = ref(0)
30
+ const lastCluster = ref(null)
31
+ const firstClusterClient = ref(null)
32
+ let dataset = { points: [] }
33
+
34
+ /** 多簇数据:每簇一个中心,正态近似(四个随机数求和)让簇内密度自然衰减。 */
35
+ const buildDataset = (count) => {
36
+ const random = createRandom(20261021)
37
+ const points = []
38
+ const centers = [
39
+ [-0.56, 0.34, 0.16],
40
+ [0.08, 0.56, 0.13],
41
+ [0.62, 0.22, 0.18],
42
+ [-0.32, -0.42, 0.15],
43
+ [0.48, -0.5, 0.14],
44
+ [0.02, -0.08, 0.1]
45
+ ]
46
+ for (let index = 0; index < count; index += 1) {
47
+ const center = centers[index % centers.length]
48
+ const common = index < count * 0.08
49
+ const spread = common ? 1.6 : 0.68
50
+ const nx = (random() + random() + random() + random() - 2) * spread
51
+ const ny = (random() + random() + random() + random() - 2) * spread
52
+ points.push({
53
+ id: `cluster-point-${index}`,
54
+ x: Number((DEMO_CENTER[0] + (center[0] + nx * center[2]) * DEMO_RADIUS).toFixed(6)),
55
+ y: Number((DEMO_CENTER[1] + (center[1] + ny * center[2]) * DEMO_RADIUS).toFixed(6)),
56
+ name: `站点 ${index + 1}`,
57
+ zone_name: `分区 ${(index % centers.length) + 1}`,
58
+ vn_kv: [1050, 525, 230, 15.7][index % 4],
59
+ max_loading_rate: Number((0.35 + random() * 1.1).toFixed(3))
60
+ })
61
+ }
62
+ return { points }
63
+ }
64
+
65
+ const clusterOptions = () => clusterOn.value
66
+ ? { enabled: true, radius: radius.value, minPoints: minPoints.value, maxZoom: maxZoom.value }
67
+ : false
68
+
69
+ const buildOptions = () => ({
70
+ points: dataset.points,
71
+ pointStyle: { size: 4, color: '#ffffff', strokeColor: '#4c8dff', strokeWidth: 1 },
72
+ showGrid: true,
73
+ gridColor: 'rgba(126, 168, 214, 0.18)',
74
+ fitPadding: 42,
75
+ denseMode: 'off',
76
+ features: 'view',
77
+ pointCluster: clusterOptions(),
78
+ onReady: () => {
79
+ emit('notice', `点聚合演示已加载 ${dataset.points.length.toLocaleString()} 个点`)
80
+ queueMicrotask(syncClusters)
81
+ },
82
+ onClick: () => {
83
+ normalClicks.value += 1
84
+ eventText.value = '普通 click:聚合圆的点击不会再走到这里'
85
+ },
86
+ onClusterClick: payload => {
87
+ clusterClicks.value += 1
88
+ lastCluster.value = {
89
+ id: payload.id,
90
+ count: payload.count,
91
+ pointIds: [...payload.pointIds],
92
+ bounds: { ...payload.bounds },
93
+ coordinate: [...payload.coordinate]
94
+ }
95
+ const ids = payload.pointIds.slice(0, 6).join('、') + (payload.pointIds.length > 6 ? '…' : '')
96
+ eventText.value = `clusterClick:${payload.count} 点 · ${ids} · 包围盒 ${payload.bounds.minX.toFixed(3)}, ${payload.bounds.minY.toFixed(3)} ~ ${payload.bounds.maxX.toFixed(3)}, ${payload.bounds.maxY.toFixed(3)}`
97
+ },
98
+ onStats: next => {
99
+ const now = performance.now()
100
+ if (now - statsDrawnAt < 250) {
101
+ return
102
+ }
103
+ statsDrawnAt = now
104
+ statsText.value = `${next.pointCount.toLocaleString()} 原始点 · ${next.clusterCount.toLocaleString()} 个聚合圆(${next.clusterPointCount.toLocaleString()} 点)· 展示 ${(next.pointCount - next.clusterPointCount + next.clusterCount).toLocaleString()} 项 · 本帧 ${next.frameMs.toFixed(2)} ms`
105
+ clusterText.value = `聚合耗时 ${next.clusterMs.toFixed(2)} ms · ${view?.getPointCluster()?.active ? '当前缩放仍在聚合' : '当前缩放已展开原始点'}`
106
+ }
107
+ })
108
+
109
+ /** getClusters() 会按当前数据顺序展开单成员;这里只取第一个多成员组,给自测定位点击位置。 */
110
+ const syncClusters = async (options = {}) => {
111
+ if (!view) {
112
+ return
113
+ }
114
+ if (options.render !== false) {
115
+ await view.whenRendered()
116
+ }
117
+ const clusters = view.getClusters().filter(item => item.count > 1)
118
+ const first = clusters[0]
119
+ firstClusterClient.value = first ? view.screenToClient(first.screen[0], first.screen[1]) : null
120
+ }
121
+
122
+ const applyCluster = async () => {
123
+ if (!view) {
124
+ return
125
+ }
126
+ view.updateOptions({ pointCluster: clusterOptions() })
127
+ await syncClusters()
128
+ }
129
+
130
+ const applyData = async () => {
131
+ if (!view) {
132
+ return
133
+ }
134
+ dataset = buildDataset(pointCountChoice.value)
135
+ view.updateData(dataset.points, [])
136
+ view.fitView(42)
137
+ lastCluster.value = null
138
+ await syncClusters()
139
+ emit('notice', `已换成 ${dataset.points.length.toLocaleString()} 个点`)
140
+ }
141
+
142
+ const fitAll = () => {
143
+ view?.fitView(42)
144
+ }
145
+
146
+ const zoomLastCluster = () => {
147
+ if (lastCluster.value?.bounds) {
148
+ view?.zoomToBounds(lastCluster.value.bounds, 54)
149
+ }
150
+ }
151
+
152
+ const countText = computed(() => `${normalClicks.value} 次普通点击 / ${clusterClicks.value} 次聚合点击`)
153
+
154
+ onMounted(() => {
155
+ dataset = buildDataset(pointCountChoice.value)
156
+ view = createGisView(container.value, buildOptions())
157
+ })
158
+
159
+ onBeforeUnmount(() => {
160
+ view?.destroy()
161
+ view = null
162
+ })
163
+ </script>
164
+
165
+ <template>
166
+ <section class='dv-demo'>
167
+ <div class='dv-stage' data-demo='cluster-stage'>
168
+ <div ref='container' class='dv-canvas' data-demo='cluster-canvas' />
169
+ <div class='dv-toolbar'>
170
+ <button type='button' data-demo='cluster-fit' @click='fitAll'>适应全部</button>
171
+ <button type='button' data-demo='cluster-zoom' :disabled='!lastCluster' @click='zoomLastCluster'>放大到最近聚合</button>
172
+ </div>
173
+ <span
174
+ class='cl-anchor'
175
+ data-demo='cluster-first-client'
176
+ :data-client-x='firstClusterClient ? firstClusterClient[0] : undefined'
177
+ :data-client-y='firstClusterClient ? firstClusterClient[1] : undefined'
178
+ />
179
+ </div>
180
+ <aside class='dv-panel'>
181
+ <div class='dv-group'>
182
+ <h3>聚合配置</h3>
183
+ <div class='dv-row'>
184
+ <label><input v-model='clusterOn' data-demo='cluster-toggle' type='checkbox' @change='applyCluster' /> 开启聚合</label>
185
+ </div>
186
+ <div class='dv-row'>
187
+ <label>半径
188
+ <input v-model.number='radius' data-demo='cluster-radius' type='range' min='16' max='180' step='4' @change='applyCluster' />
189
+ <span class='cl-value'>{{ radius }}px</span>
190
+ </label>
191
+ </div>
192
+ <div class='dv-row'>
193
+ <label>最少点数
194
+ <select v-model.number='minPoints' data-demo='cluster-min-points' @change='applyCluster'>
195
+ <option :value='2'>2</option>
196
+ <option :value='3'>3</option>
197
+ <option :value='5'>5</option>
198
+ <option :value='8'>8</option>
199
+ <option :value='12'>12</option>
200
+ </select>
201
+ </label>
202
+ <label>展开缩放
203
+ <select v-model.number='maxZoom' data-demo='cluster-max-zoom' @change='applyCluster'>
204
+ <option :value='0'>不限</option>
205
+ <option :value='1.5'>1.5×</option>
206
+ <option :value='2'>2×</option>
207
+ <option :value='4'>4×</option>
208
+ <option :value='8'>8×</option>
209
+ </select>
210
+ </label>
211
+ </div>
212
+ <p class='dv-hint'>半径是屏幕像素;maxZoom 是相机比例相对基准比例的倍数,超过后展开原始点。聚合期间发散初始化暂停,缩放 / 数据 / 选中变化才重算。</p>
213
+ </div>
214
+ <div class='dv-group'>
215
+ <h3>数据规模</h3>
216
+ <div class='dv-row'>
217
+ <label>点数
218
+ <select v-model.number='pointCountChoice' data-demo='cluster-count'>
219
+ <option v-for='count in DATA_COUNTS' :key='count' :value='count'>{{ count.toLocaleString() }}</option>
220
+ </select>
221
+ </label>
222
+ <button type='button' class='dv-button' data-demo='cluster-rebuild' @click='applyData'>重建</button>
223
+ </div>
224
+ <p class='dv-hint'>十万点仍按整份网格 O(n) 建组;平移不重算,拖动 / 选中中的点强制保持单成员。</p>
225
+ </div>
226
+ <div class='dv-group'>
227
+ <h3>读数</h3>
228
+ <p class='dv-readout' data-demo='cluster-stats'>{{ statsText }}</p>
229
+ <p class='dv-readout' data-demo='cluster-summary'>{{ clusterText }}</p>
230
+ <p class='dv-readout' data-demo='cluster-click-count'>{{ countText }}</p>
231
+ <p class='dv-readout' data-demo='cluster-event'>{{ eventText }}</p>
232
+ <p class='dv-hint'>点击聚合圆应只增加「聚合点击」计数;普通 click 不会被聚合圆触发,选中集也保持不变。</p>
233
+ </div>
234
+ </aside>
235
+ </section>
236
+ </template>
237
+
238
+ <style scoped>
239
+ .cl-anchor {
240
+ position: absolute;
241
+ width: 1px;
242
+ height: 1px;
243
+ overflow: hidden;
244
+ pointer-events: none;
245
+ }
246
+ .cl-value {
247
+ min-width: 38px;
248
+ color: #526b8d;
249
+ font-size: 11px;
250
+ }
251
+ </style>
@@ -984,10 +984,9 @@ onMounted(() => {
984
984
  merge: true,
985
985
  keyboard: true,
986
986
  waypoint: false,
987
- remove: false,
988
- channelDraft: false,
989
- channelMenu: false,
990
- contextMenu: false
987
+ remove: false,
988
+ channelDraft: false,
989
+ contextMenu: false
991
990
  },
992
991
  copy: {
993
992
  enabled: true,
@@ -89,14 +89,14 @@ const dataLabel = (value) => {
89
89
  }
90
90
 
91
91
  /**
92
- * 要 DOM 事件的四个事件(pointerDown / pointerUp / contextMenu / channelContextMenu)在总线上
92
+ * 要 DOM 事件的三个事件(pointerDown / pointerUp / contextMenu)在总线上
93
93
  * 载荷是 `{ payload, event }`(与回调的两个参数一一对应),其余事件载荷就是回调里那一份 —— 这里统一拆开。
94
94
  */
95
- const EVENT_PAYLOAD_EVENTS = new Set(['pointerDown', 'pointerUp', 'contextMenu', 'channelContextMenu'])
95
+ const EVENT_PAYLOAD_EVENTS = new Set(['pointerDown', 'pointerUp', 'contextMenu'])
96
96
  const unwrapPayload = (raw, name) => (EVENT_PAYLOAD_EVENTS.has(name) && raw && typeof raw === 'object' && 'payload' in raw ? raw.payload : raw)
97
97
 
98
98
  /** 会带 action 的事件名:* 订阅里从载荷上认出「这一次是什么操作」 */
99
- const ACTION_EVENTS = new Set(['click', 'dblClick', 'pointerDown', 'pointerUp', 'contextMenu', 'channelContextMenu', 'labelClick', 'labelDrag', 'labelContextMenu', 'labelDblClick', 'selectChange', 'waypointChange'])
99
+ const ACTION_EVENTS = new Set(['click', 'dblClick', 'pointerDown', 'pointerUp', 'contextMenu', 'labelClick', 'labelDrag', 'labelContextMenu', 'labelDblClick', 'selectChange', 'waypointChange'])
100
100
 
101
101
  /** 最近一次操作的判读:面板上的「动作判读」直接读它 */
102
102
  const lastAction = ref('—')
@@ -1,8 +1,8 @@
1
1
  <script setup>
2
2
  /**
3
- * 扩展能力:用同一个视图把五个扩展点串起来 ——
3
+ * 扩展能力:用同一个视图把六个扩展点串起来 ——
4
4
  * 运行时插件(view.use)、自定义图元(registerSymbol)、自定义条件运算符(registerOperator)
5
- * 自定义导出格式(defineExportFormat)与 WebGL 自定义层(addWebGLLayer)。
5
+ * 自定义导出格式(defineExportFormat)、WebGL 自定义层(addWebGLLayer)与实例命令(registerCommand)。
6
6
  * 每一块都可单独开关,读数里能直接看到注册表与可见数据。
7
7
  */
8
8
  import { onBeforeUnmount, onMounted, ref } from 'vue'
@@ -20,6 +20,8 @@ const webglMounted = ref(false)
20
20
  const webglEnabled = ref(false)
21
21
  const webglOrder = ref(20)
22
22
  const webglDrawCount = ref(0)
23
+ const commandRegistered = ref(false)
24
+ const commandReadout = ref('实例命令尚未注册。注册后可以执行 focusHighestLoad。')
23
25
  const readout = ref('正在创建视图…')
24
26
  const exportText = ref('尚未导出。点上面的按钮,会走 defineExportFormat 注册的同步适配器。')
25
27
 
@@ -33,6 +35,7 @@ let disposeSymbol = null
33
35
  let disposePlugin = null
34
36
  let disposeExport = null
35
37
  let disposeWebgl = null
38
+ let disposeCommand = null
36
39
 
37
40
  /** WebGL 自定义层:按 gl 缓存程序 / 缓冲,draw 里只更新 uniform 与画一笔 */
38
41
  const webglResources = new WeakMap()
@@ -172,18 +175,21 @@ const paintSparkDiamond = (context, info) => {
172
175
  context.fill()
173
176
  }
174
177
 
175
- const visiblePointCount = () => view?.getVisibleData()?.points?.length ?? points.length
176
- const refreshReadout = () => {
178
+ const visiblePointCount = () => view?.getVisibleData()?.points?.length ?? points.length
179
+ const COMMAND_NAME = 'focusHighestLoad'
180
+ const refreshReadout = () => {
177
181
  if (!view) {
178
182
  return
179
183
  }
180
184
  const operators = view.listOperators()
181
185
  const symbols = view.listSymbols()
186
+ const commands = view.listCommands()
182
187
  readout.value = [
183
188
  `自定义运算符 ${operators.includes('evenIndex') ? 'evenIndex 已注册' : '未注册'}`,
184
189
  `筛选 ${operatorOn.value ? '开' : '关'}(可见 ${visiblePointCount()} / ${points.length} 个)`,
185
190
  `自定义图元 ${symbols.includes('spark') ? `spark 已注册(${symbolVariant.value === 'diamond' ? '菱形' : '十字'})` : '未注册'}${symbolOn.value ? ' / 使用中' : ''}`,
186
191
  `运行时插件 ${pluginOn.value ? '已挂载' : '未挂载'}`,
192
+ `实例命令 ${commands.includes(COMMAND_NAME) ? `${COMMAND_NAME} 已注册(${commands.length} 个)` : '未注册'}`,
187
193
  `WebGL 上下文 ${view.gl ? '可直接取' : '不可用'}`,
188
194
  `WebGL 层 ${webglMounted.value ? `已挂载 · ${webglEnabled.value ? '绘制中' : '已隐藏'} · order ${webglOrder.value} · draw ${webglDrawCount.value} 次` : '未挂载'}`
189
195
  ].join(' · ')
@@ -199,7 +205,7 @@ const buildOptions = () => ({
199
205
  fitPadding: 40,
200
206
  features: 'view',
201
207
  label: { enabled: true, content: '{name}', position: 'top', gap: 7, maxCount: 20, style: { color: '#27405f', fontSize: '11px' } },
202
- onReady: () => emit('notice', '扩展能力:已挂载 5 个扩展点')
208
+ onReady: () => emit('notice', '扩展能力:已挂载 6 个扩展点')
203
209
  })
204
210
 
205
211
  const toggleOperator = () => {
@@ -249,10 +255,47 @@ const togglePlugin = () => {
249
255
  disposePlugin?.()
250
256
  disposePlugin = null
251
257
  }
252
- refreshReadout()
258
+ refreshReadout()
253
259
  emit('notice', pluginOn.value ? '插件已挂载:左下角出现徽标' : '插件已卸载')
254
260
  }
255
261
 
262
+ /** 实例命令:业务侧把「选中最重载节点并定位」注册进视图;handler 里通过 context.view 继续调实例方法 */
263
+ const toggleCommand = () => {
264
+ if (!view) {
265
+ return
266
+ }
267
+ if (commandRegistered.value) {
268
+ disposeCommand?.()
269
+ disposeCommand = null
270
+ commandRegistered.value = false
271
+ commandReadout.value = `已卸载 ${COMMAND_NAME};listCommands() 里不再有它。`
272
+ refreshReadout()
273
+ emit('notice', `实例命令 ${COMMAND_NAME} 已卸载`)
274
+ return
275
+ }
276
+ disposeCommand = view.registerCommand(COMMAND_NAME, (payload, context) => {
277
+ const threshold = Number(payload?.threshold ?? 1)
278
+ const target = points.find(point => Number(point.max_loading_rate) >= threshold) ?? points[0]
279
+ context.view.focusOn(target.id, { select: true, zoom: true, maxZoom: 8 })
280
+ return { id: target.id, name: target.name, threshold, selected: context.view.getSelectionData().count }
281
+ })
282
+ commandRegistered.value = view.listCommands().includes(COMMAND_NAME)
283
+ commandReadout.value = `${COMMAND_NAME} 已注册,可执行;同名再注册会覆盖。`
284
+ refreshReadout()
285
+ emit('notice', `实例命令 ${COMMAND_NAME} 已注册`)
286
+ }
287
+ const runCommand = () => {
288
+ if (!view) {
289
+ return
290
+ }
291
+ const result = view.executeCommand(COMMAND_NAME, { threshold: 1 })
292
+ commandReadout.value = result.ok
293
+ ? `executeCommand -> ok: true · value: ${JSON.stringify(result.value)}`
294
+ : `executeCommand -> ok: false · ${result.reason ?? '未知原因'}`
295
+ refreshReadout()
296
+ emit('notice', result.ok ? `实例命令已执行:${result.value?.name ?? COMMAND_NAME}` : `实例命令执行失败:${result.reason ?? '未知原因'}`)
297
+ }
298
+
256
299
  /** 挂载 / 移除 WebGL 自定义层:层本身不改数据,只在每帧最后叠加一圈光环 */
257
300
  const toggleWebgl = () => {
258
301
  if (!view) {
@@ -350,6 +393,8 @@ onBeforeUnmount(() => {
350
393
  disposeExport = null
351
394
  disposeWebgl?.()
352
395
  disposeWebgl = null
396
+ disposeCommand?.()
397
+ disposeCommand = null
353
398
  view?.destroy()
354
399
  view = null
355
400
  })
@@ -365,7 +410,7 @@ onBeforeUnmount(() => {
365
410
  </div>
366
411
  <aside class="dv-panel">
367
412
  <div class="dv-group">
368
- <h3>五个扩展点</h3>
413
+ <h3>六个扩展点</h3>
369
414
  <div class="dv-row">
370
415
  <button
371
416
  type="button"
@@ -400,6 +445,17 @@ onBeforeUnmount(() => {
400
445
  <div class="dv-row">
401
446
  <button type="button" class="dv-button dv-button--primary" data-demo="extension-export" @click="exportSummary">自定义格式导出</button>
402
447
  </div>
448
+ <div class="dv-row">
449
+ <button
450
+ type="button"
451
+ class="dv-button"
452
+ :class="{ 'dv-button--primary': commandRegistered }"
453
+ :aria-pressed="commandRegistered"
454
+ data-demo="extension-command-register"
455
+ @click="toggleCommand"
456
+ >{{ commandRegistered ? '卸载实例命令' : '注册实例命令' }}</button>
457
+ <button type="button" class="dv-button" :disabled="!commandRegistered" data-demo="extension-command-run" @click="runCommand">执行命令</button>
458
+ </div>
403
459
  <div class="dv-row">
404
460
  <button
405
461
  type="button"
@@ -415,12 +471,13 @@ onBeforeUnmount(() => {
415
471
  <button type="button" class="dv-button" :disabled="!webglMounted" data-demo="extension-webgl-order" @click="cycleWebglOrder">order {{ webglOrder }}(切换)</button>
416
472
  <button type="button" class="dv-button" :disabled="!webglMounted" data-demo="extension-webgl-request" @click="requestWebglFrame">requestRender()</button>
417
473
  </div>
418
- <p class="dv-hint">运算符注册进条件语言,图元注册进符号表,插件挂在实例上,导出格式挂在模块注册表,WebGL 层直接拿 <code>gl</code> 在内置内容之后画自己的东西;五者都不需要改组件源码。图元切换走一次 <code>patchPoints</code> 批量补丁,WebGL 层可用 <code>enabled</code> / <code>order</code> 原地维护。</p>
419
- </div>
420
- <div class="dv-group">
421
- <h3>注册表读数</h3>
422
- <p class="dv-readout" data-demo="extension-readout" aria-live="polite">{{ readout }}</p>
474
+ <p class="dv-hint">运算符注册进条件语言,图元注册进符号表,插件挂在实例上,导出格式挂在模块注册表,WebGL 层直接拿 <code>gl</code> 在内置内容之后画自己的东西,实例命令把业务动作注册进当前视图;六者都不需要改组件源码。图元切换走一次 <code>patchPoints</code> 批量补丁,WebGL 层可用 <code>enabled</code> / <code>order</code> 原地维护,命令通过 <code>executeCommand</code> 同步执行并返回 handler 的结果。</p>
423
475
  </div>
476
+ <div class="dv-group">
477
+ <h3>注册表读数</h3>
478
+ <p class="dv-readout" data-demo="extension-readout" aria-live="polite">{{ readout }}</p>
479
+ <p class="dv-readout" data-demo="extension-command-readout" aria-live="polite">{{ commandReadout }}</p>
480
+ </div>
424
481
  <div class="dv-group">
425
482
  <h3>导出结果</h3>
426
483
  <pre class="dv-readout demo-extension-export" data-demo="extension-export-text">{{ exportText }}</pre>