gis-view-webgl2 0.1.13 → 0.1.15

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 (62) hide show
  1. package/dist/index.cjs +1 -1
  2. package/dist/index.d.ts +140 -60
  3. package/dist/index.js +1 -1
  4. package/docs/AI/345/257/271/350/257/235/346/261/207/346/200/273.md +73 -5
  5. package/docs/GisView.md +49 -49
  6. 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 +87 -16
  7. package/docs/demos/ApiCoverageDemo.vue +1 -5
  8. package/docs/demos/BackendSyncDemo.vue +1 -2
  9. package/docs/demos/BackgroundDemo.vue +68 -1
  10. package/docs/demos/BasicDemo.vue +76 -76
  11. package/docs/demos/BatchAddDemo.vue +2 -1
  12. package/docs/demos/ChannelDemo.vue +1 -3
  13. package/docs/demos/ClusterDemo.vue +48 -1
  14. package/docs/demos/CopyMergeDemo.vue +899 -904
  15. package/docs/demos/DemoShell.vue +336 -336
  16. package/docs/demos/EventDemo.vue +300 -300
  17. package/docs/demos/ExtensionDemo.vue +200 -200
  18. package/docs/demos/GisViewDemo.vue +64 -64
  19. package/docs/demos/LabelDemo.vue +489 -489
  20. package/docs/demos/StyleDemo.vue +67 -67
  21. package/docs/demos/api-index.js +24 -29
  22. package/docs/demos/demos.js +92 -92
  23. package/docs/demos/gis-view-demo/useGisViewDemo.js +19 -14
  24. package/docs/demos/shared/demoData.js +107 -107
  25. package/docs/demos/shared/demoStyle.css +249 -249
  26. package/docs/format/02-/350/212/202/347/202/271/344/270/216/347/272/277/350/267/257.md +3 -3
  27. 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 +3 -0
  28. package/docs/from0/01-/345/205/210/346/212/212/345/234/260/345/233/276/346/203/263/346/270/205/346/245/232.md +8 -7
  29. package/docs/index.html +3 -3
  30. package/docs/internals/04-/347/233/270/346/234/272/345/235/220/346/240/207/344/270/216/345/203/217/347/264/240.md +6 -4
  31. package/docs/internals/14-/350/203/214/346/231/257/345/261/202.md +2 -2
  32. package/docs/manual/02-/345/235/220/346/240/207/344/270/216/346/225/260/346/215/256.md +36 -16
  33. package/docs/manual/03-/351/205/215/347/275/256/351/241/271.md +38 -84
  34. package/docs/manual/04-/344/272/213/344/273/266/344/270/216/345/233/236/350/260/203.md +36 -19
  35. 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 +1 -1
  36. 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 +7 -7
  37. package/docs/manual/08-/351/200/211/344/270/255/344/270/216/346/213/220/347/202/271.md +15 -15
  38. 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 +1 -1
  39. package/docs/manual/12-/346/240/207/347/255/276/344/270/216/350/243/205/351/245/260.md +6 -0
  40. package/docs/manual/14-/350/247/206/345/233/276/344/270/216/347/255/233/351/200/211.md +1 -1
  41. package/docs/manual/15-/345/257/274/345/207/272/344/270/216/351/233/206/346/210/220.md +2 -0
  42. package/docs/manual/16-/345/256/236/344/276/213/346/226/271/346/263/225/346/200/273/350/241/250.md +10 -8
  43. package/docs/manual/17-/345/217/202/346/225/260/351/200/237/346/237/245.md +11 -7
  44. package/docs/manual/18-/351/200/220/346/226/271/346/263/225/347/244/272/344/276/213.md +27 -8
  45. package/docs/manual/20-/346/200/247/350/203/275/344/270/216/345/244/247/346/225/260/346/215/256.md +27 -4
  46. package/docs/manual/21-/350/257/264/346/230/216/344/270/216/345/270/270/350/247/201/351/227/256/351/242/230.md +3 -3
  47. package/docs/playground/index.html +1 -1
  48. package/docs/sample-document/345/255/227/346/256/265/350/257/246/350/247/243.md +2 -2
  49. package/docs/steps/check-doc-truth.mjs +1 -1
  50. package/docs/steps/selftest-actions.html +4 -4
  51. package/docs/steps/selftest-data.html +5 -5
  52. package/docs/steps/selftest-demos.html +1683 -1619
  53. package/docs/steps/selftest-docs.html +4 -4
  54. package/docs/steps/selftest-effects.html +1 -1
  55. package/docs/steps/selftest-features.html +548 -47
  56. package/docs/steps/selftest-full.html +566 -566
  57. package/docs/steps/selftest-labels.html +647 -647
  58. package/docs/steps/selftest-perf.html +2282 -2237
  59. package/docs/steps/selftest-select.html +53 -14
  60. package/docs/steps/step-10-full.js +1 -4
  61. package/docs/steps/step-12-events.js +1 -4
  62. package/package.json +43 -43
@@ -4,7 +4,7 @@
4
4
  * 编辑本身完全由组件的 beginBackgroundEdit / updateBackgroundEdit / endBackgroundEdit 接管;
5
5
  * 这个页面只负责导入素材、调背景透明度与内置编辑器的配置。
6
6
  */
7
- import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
7
+ import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, shallowRef, watch } from 'vue'
8
8
  import { createGisView } from '../../src/index.js'
9
9
  import { buildDemoData } from './shared/demoData.js'
10
10
  import { buildDemoBasemap } from './demoBackground.js'
@@ -104,6 +104,9 @@ const heightDraft = ref(formatWorld(editHeight.value))
104
104
  const rotationDraft = ref('0')
105
105
  const resolvedReadout = ref('尚未添加背景')
106
106
  const notice = ref('先看网架;再添加背景。编辑背景时网架不可点击,滚轮仍可整体缩放。')
107
+ const savedLngLat = shallowRef(null)
108
+ const savedAnchor = shallowRef(null)
109
+ const roundtripReadout = ref('尚未保存经纬度。先切到 GeoJSON 背景,再保存整份网架。')
107
110
  let view = null
108
111
  let readoutTimer = 0
109
112
 
@@ -325,6 +328,59 @@ const rotateBy = (delta) => {
325
328
  view.updateBackgroundEdit({ angle: normalizeRotation(editRotation.value + delta) })
326
329
  syncEditorState(view.getBackgroundEdit())
327
330
  }
331
+ const formatLngLat = coordinate => coordinate && Number.isFinite(Number(coordinate[0])) && Number.isFinite(Number(coordinate[1]))
332
+ ? `[${Number(coordinate[0]).toFixed(6)}, ${Number(coordinate[1]).toFixed(6)}]`
333
+ : '—'
334
+ const saveNetworkLngLat = () => {
335
+ if (!view || backgroundMode.value !== 'geojson') {
336
+ roundtripReadout.value = '保存经纬度需要 GeoJSON 背景:先用上方按钮添加底图。'
337
+ return
338
+ }
339
+ const data = view.getData()
340
+ const firstPoint = data.points[0] ?? null
341
+ const saved = view.mapDataToLngLat()
342
+ if (!saved) {
343
+ roundtripReadout.value = '当前没有可用的 GeoJSON 投影,未能保存经纬度。'
344
+ return
345
+ }
346
+ savedLngLat.value = saved
347
+ savedAnchor.value = firstPoint ? { id: firstPoint.id, x: firstPoint.x, y: firstPoint.y } : null
348
+ const savedFirstPoint = firstPoint ? saved.points?.find(point => point.id === firstPoint.id) ?? saved.points?.[0] : null
349
+ roundtripReadout.value = firstPoint && savedFirstPoint
350
+ ? `已保存 ${saved.points?.length ?? 0} 节点 / ${saved.lines?.length ?? 0} 线路 / ${saved.channels?.length ?? 0} 通道。首节点 ${firstPoint.id}:世界 [${formatWorld(firstPoint.x)}, ${formatWorld(firstPoint.y)}] → 经纬度 ${formatLngLat([savedFirstPoint.x, savedFirstPoint.y])}。`
351
+ : `已保存 ${saved.points?.length ?? 0} 节点 / ${saved.lines?.length ?? 0} 线路 / ${saved.channels?.length ?? 0} 通道;网架里还没有节点。`
352
+ emit('notice', '经纬度存档已生成:现在可以清空网架,再用 mapLngLatToData() 原样回灌。')
353
+ }
354
+ const clearNetwork = () => {
355
+ if (!view) {
356
+ return
357
+ }
358
+ view.updateData({ points: [], lines: [], channels: [] })
359
+ roundtripReadout.value = savedLngLat.value
360
+ ? '网架已清空;经纬度存档仍在,点「3. 原样回灌」恢复。'
361
+ : '网架已清空;当前没有经纬度存档。'
362
+ emit('notice', '网架已清空,背景与相机位置保持不变。')
363
+ }
364
+ const restoreNetworkLngLat = () => {
365
+ if (!view || backgroundMode.value !== 'geojson' || !savedLngLat.value) {
366
+ roundtripReadout.value = '回灌需要 GeoJSON 背景和一份已保存的经纬度存档。'
367
+ return
368
+ }
369
+ const restored = view.mapLngLatToData(savedLngLat.value)
370
+ if (!restored) {
371
+ roundtripReadout.value = '当前没有可用的 GeoJSON 投影,未能回灌。'
372
+ return
373
+ }
374
+ view.updateData(restored)
375
+ const anchor = savedAnchor.value
376
+ const restoredAnchor = anchor ? view.getData().points.find(point => point.id === anchor.id) ?? null : null
377
+ const delta = anchor && restoredAnchor ? Math.hypot(restoredAnchor.x - anchor.x, restoredAnchor.y - anchor.y) : null
378
+ const savedFirstPoint = anchor ? savedLngLat.value.points?.find(point => point.id === anchor.id) ?? savedLngLat.value.points?.[0] : null
379
+ roundtripReadout.value = anchor && restoredAnchor && delta !== null && delta < 1e-7
380
+ ? `回灌完成:${restored.points?.length ?? 0} 节点 / ${restored.lines?.length ?? 0} 线路 / ${restored.channels?.length ?? 0} 通道。首节点 ${anchor.id} 经纬度 ${formatLngLat(savedFirstPoint ? [savedFirstPoint.x, savedFirstPoint.y] : null)} → 世界 [${formatWorld(restoredAnchor.x)}, ${formatWorld(restoredAnchor.y)}],与保存前一致(误差 ${delta.toExponential(1)})。`
381
+ : `回灌完成,但首节点世界坐标与保存前不一致${delta === null ? '' : `(误差 ${delta.toExponential(1)})`};请确认底图 bounds / rotation 没有在保存后改动。`
382
+ emit('notice', '经纬度存档已按当前底图投影回灌为世界坐标。')
383
+ }
328
384
  const toggleNetwork = () => {
329
385
  networkVisible.value = !networkVisible.value
330
386
  }
@@ -533,6 +589,17 @@ onBeforeUnmount(() => {
533
589
  <p class="dv-readout" data-demo="background-resolved">{{ resolvedReadout }}</p>
534
590
  </div>
535
591
 
592
+ <div class="dv-group">
593
+ <h3>经纬度存档回灌<span class="dv-chip">mapDataToLngLat → mapLngLatToData</span></h3>
594
+ <div class="dv-row">
595
+ <button type="button" class="dv-button" data-demo="background-save-lnglat" :disabled="backgroundMode !== 'geojson'" @click="saveNetworkLngLat">1. 保存经纬度</button>
596
+ <button type="button" class="dv-button" data-demo="background-clear-network" :disabled="!savedLngLat" @click="clearNetwork">2. 清空网架</button>
597
+ <button type="button" class="dv-button dv-button--primary" data-demo="background-restore-lnglat" :disabled="!savedLngLat || backgroundMode !== 'geojson'" @click="restoreNetworkLngLat">3. 原样回灌</button>
598
+ </div>
599
+ <p class="dv-readout" data-demo="background-lnglat-readout">{{ roundtripReadout }}</p>
600
+ <p class="dv-hint">保存时把节点、线路、通道分组一起写成经纬度;回灌先由 `mapLngLatToData()` 按当前最终底图投影写回世界坐标,再交给 `updateData()`。底图未改动时世界坐标与屏幕落点原样恢复。</p>
601
+ </div>
602
+
536
603
  <div class="dv-group">
537
604
  <h3>操作提示</h3>
538
605
  <p class="dv-hint" data-demo="background-notice">{{ notice }}</p>
@@ -1,5 +1,5 @@
1
- <script setup>
2
- /**
1
+ <script setup>
2
+ /**
3
3
  * 基础视图:一份数据 + 最少的配置,重点看相机、比例、空闲与拾取读数怎么拿。
4
4
  *
5
5
  * 首屏用顶层 center / scale 落位;面板能读写比例、观察 idleTimeout 的回调与事件,
@@ -7,16 +7,16 @@
7
7
  * onCameraChange / onClick / onSelect / onStats / onIdle 的值直接摊开 ——
8
8
  * 事件载荷长什么样、什么时候发,一眼能对上。
9
9
  */
10
- import { onBeforeUnmount, onMounted, ref } from 'vue'
11
- import { createGisView } from '../../src/index.js'
10
+ import { onBeforeUnmount, onMounted, ref } from 'vue'
11
+ import { createGisView } from '../../src/index.js'
12
12
  import { buildDemoData, DEMO_CENTER } from './shared/demoData.js'
13
- import './shared/demoStyle.css'
14
-
15
- const emit = defineEmits(['notice'])
16
- const container = ref(null)
17
- const pointCount = ref(28)
18
- const gridOn = ref(true)
19
- const labelOn = ref(true)
13
+ import './shared/demoStyle.css'
14
+
15
+ const emit = defineEmits(['notice'])
16
+ const container = ref(null)
17
+ const pointCount = ref(28)
18
+ const gridOn = ref(true)
19
+ const labelOn = ref(true)
20
20
  const channelOn = ref(true)
21
21
  const scaleDraft = ref('72')
22
22
  const currentScale = ref(72)
@@ -90,10 +90,10 @@ const buildOptions = () => ({
90
90
  event: `onIdle:停手 ${Math.round(idleTime)} ms(阈值 ${timeout} ms)`
91
91
  }
92
92
  },
93
- onReady: () => {
94
- emit('notice', `已加载 ${dataset.points.length} 个节点 / ${dataset.lines.length} 条线路`)
95
- readouts.value = { ...readouts.value, event: 'ready:WebGL2 上下文就绪' }
96
- },
93
+ onReady: () => {
94
+ emit('notice', `已加载 ${dataset.points.length} 个节点 / ${dataset.lines.length} 条线路`)
95
+ readouts.value = { ...readouts.value, event: 'ready:WebGL2 上下文就绪' }
96
+ },
97
97
  onClick: (payload) => {
98
98
  readouts.value = { ...readouts.value, pick: describeTarget(payload), event: `click:${describeTarget(payload)}` }
99
99
  },
@@ -107,10 +107,10 @@ const buildOptions = () => ({
107
107
  event: count ? `select:${mode} 选中 ${count} 个` : 'select:清空选中'
108
108
  }
109
109
  },
110
- onCameraChange: (camera) => {
111
- // 相机每帧都在变:只把读数写进面板,不再触发别的重活
112
- readouts.value = { ...readouts.value, camera: `scale ${camera.scale.toPrecision(4)} px/单位 · 中心 ${camera.center[0].toFixed(3)}, ${camera.center[1].toFixed(3)}` }
113
- },
110
+ onCameraChange: (camera) => {
111
+ // 相机每帧都在变:只把读数写进面板,不再触发别的重活
112
+ readouts.value = { ...readouts.value, camera: `scale ${camera.scale.toPrecision(4)} px/单位 · 中心 ${camera.center[0].toFixed(3)}, ${camera.center[1].toFixed(3)}` }
113
+ },
114
114
  onStats: (next) => {
115
115
  // fps 每帧都变,节流后再写响应式数据(十万级数据下每帧重渲染面板本身也是开销)
116
116
  const now = performance.now()
@@ -122,24 +122,24 @@ const buildOptions = () => ({
122
122
  const speed = next.idle ? '空闲' : `${Math.round(next.fps)} fps`
123
123
  readouts.value = { ...readouts.value, stats: `${speed} · 最近一帧 ${next.frameMs.toFixed(2)} ms · ${next.pointCount.toLocaleString()} 点 / ${next.lineCount.toLocaleString()} 线` }
124
124
  },
125
- onWarning: (text) => {
126
- readouts.value = { ...readouts.value, event: `warning:${text}` }
127
- }
128
- })
129
-
130
- /** 换数据量:数据与通道一起换掉,相机重新适应(不改选项里的样式,所以点位外观不跳) */
131
- const applyData = () => {
132
- if (!view) {
133
- return
134
- }
135
- dataset = buildDemoData({ count: pointCount.value })
136
- view.updateData(dataset.points, channelOn.value ? dataset.lines : dataset.linesWithoutChannel)
125
+ onWarning: (text) => {
126
+ readouts.value = { ...readouts.value, event: `warning:${text}` }
127
+ }
128
+ })
129
+
130
+ /** 换数据量:数据与通道一起换掉,相机重新适应(不改选项里的样式,所以点位外观不跳) */
131
+ const applyData = () => {
132
+ if (!view) {
133
+ return
134
+ }
135
+ dataset = buildDemoData({ count: pointCount.value })
136
+ view.updateData(dataset.points, channelOn.value ? dataset.lines : dataset.linesWithoutChannel)
137
137
  view.updateOptions({ channels: channelOn.value ? dataset.channels : [] })
138
138
  view.fitView(40)
139
139
  syncScaleFromView()
140
140
  emit('notice', `已换成 ${dataset.points.length} 个节点 / ${dataset.lines.length} 条线路`)
141
- }
142
-
141
+ }
142
+
143
143
  /**
144
144
  * 开关键:只改与自己相关的那几项 —— 一次「把整份 options 再发一遍」会连数据一起重发,
145
145
  * 拖过的节点与标签偏移都会被冲回原样,这里只改网格与标签两项。
@@ -155,7 +155,7 @@ const applyChannel = () => {
155
155
  channels: channelOn.value ? dataset.channels : []
156
156
  })
157
157
  }
158
-
158
+
159
159
  const resetCamera = () => {
160
160
  view?.fitView(40)
161
161
  syncScaleFromView()
@@ -209,7 +209,7 @@ const toggleIdle = () => {
209
209
  event: idleEnabled.value ? `idleTimeout 已开启(${IDLE_TIMEOUT_MS} ms)` : 'idleTimeout 已关闭(0)'
210
210
  }
211
211
  }
212
-
212
+
213
213
  onMounted(() => {
214
214
  view = createGisView(container.value, buildOptions())
215
215
  syncScaleFromView()
@@ -238,18 +238,18 @@ onBeforeUnmount(() => {
238
238
  window.clearInterval(idleTicker)
239
239
  idleTicker = null
240
240
  view?.destroy()
241
- view = null
242
- })
243
- </script><template>
244
- <section class="dv-demo">
245
- <div class="dv-stage">
246
- <div ref="container" class="dv-canvas"></div>
247
- <div class="dv-toolbar">
248
- <button type="button" @click="resetCamera">适应全部</button>
249
- <button type="button" @click="zoomBy(1.4)">放大</button>
250
- <button type="button" @click="zoomBy(1 / 1.4)">缩小</button>
251
- </div>
252
- </div>
241
+ view = null
242
+ })
243
+ </script><template>
244
+ <section class="dv-demo">
245
+ <div class="dv-stage">
246
+ <div ref="container" class="dv-canvas"></div>
247
+ <div class="dv-toolbar">
248
+ <button type="button" @click="resetCamera">适应全部</button>
249
+ <button type="button" @click="zoomBy(1.4)">放大</button>
250
+ <button type="button" @click="zoomBy(1 / 1.4)">缩小</button>
251
+ </div>
252
+ </div>
253
253
  <aside class="dv-panel">
254
254
  <div class="dv-group">
255
255
  <h3>比例读写<span class="dv-chip">center / scale 首屏</span></h3>
@@ -275,36 +275,36 @@ onBeforeUnmount(() => {
275
275
  </div>
276
276
  <div class="dv-group">
277
277
  <h3>数据</h3>
278
- <div class="dv-row">
279
- <label>节点数
280
- <select v-model.number="pointCount" @change="applyData">
281
- <option :value="12">12</option>
282
- <option :value="28">28</option>
283
- <option :value="60">60</option>
284
- </select>
285
- </label>
286
- <button type="button" class="dv-button" @click="applyData">重建数据</button>
287
- </div>
288
- <div class="dv-row">
289
- <label><input v-model="gridOn" type="checkbox" @change="applyOptions" /> 网格</label>
290
- <label><input v-model="labelOn" type="checkbox" @change="applyOptions" /> 节点标签</label>
278
+ <div class="dv-row">
279
+ <label>节点数
280
+ <select v-model.number="pointCount" @change="applyData">
281
+ <option :value="12">12</option>
282
+ <option :value="28">28</option>
283
+ <option :value="60">60</option>
284
+ </select>
285
+ </label>
286
+ <button type="button" class="dv-button" @click="applyData">重建数据</button>
287
+ </div>
288
+ <div class="dv-row">
289
+ <label><input v-model="gridOn" type="checkbox" @change="applyOptions" /> 网格</label>
290
+ <label><input v-model="labelOn" type="checkbox" @change="applyOptions" /> 节点标签</label>
291
291
  <label><input v-model="channelOn" type="checkbox" @change="applyChannel" /> 通道</label>
292
- </div>
293
- <p class="dv-hint">打开标签后可以拖着标签走一段:松手后偏移记在状态里,再移动节点标签跟着走。</p>
294
- </div>
295
- <div class="dv-group">
296
- <h3>读数</h3>
297
- <p class="dv-readout">相机:{{ readouts.camera }}</p>
298
- <p class="dv-readout">拾取:{{ readouts.pick }}</p>
299
- <p class="dv-readout">性能:{{ readouts.stats }}</p>
300
- <p class="dv-readout">事件:{{ readouts.event }}</p>
301
- </div>
302
- <div class="dv-group">
303
- <h3>试一下</h3>
304
- <p class="dv-hint">单击节点 / 线路 / 通道看「拾取」;空白处拖框看「选中」;滚轮缩放、拖动平移时看相机的 scale 与中心。</p>
305
- </div>
306
- </aside>
307
- </section>
292
+ </div>
293
+ <p class="dv-hint">打开标签后可以拖着标签走一段:松手后偏移记在状态里,再移动节点标签跟着走。</p>
294
+ </div>
295
+ <div class="dv-group">
296
+ <h3>读数</h3>
297
+ <p class="dv-readout">相机:{{ readouts.camera }}</p>
298
+ <p class="dv-readout">拾取:{{ readouts.pick }}</p>
299
+ <p class="dv-readout">性能:{{ readouts.stats }}</p>
300
+ <p class="dv-readout">事件:{{ readouts.event }}</p>
301
+ </div>
302
+ <div class="dv-group">
303
+ <h3>试一下</h3>
304
+ <p class="dv-hint">单击节点 / 线路 / 通道看「拾取」;空白处拖框看「选中」;滚轮缩放、拖动平移时看相机的 scale 与中心。</p>
305
+ </div>
306
+ </aside>
307
+ </section>
308
308
  </template>
309
309
 
310
310
  <style scoped>
@@ -352,7 +352,8 @@ onMounted(() => {
352
352
  lineStyle: { width: 1.4, color: 'rgba(120, 168, 226, 0.6)' },
353
353
  showGrid: true,
354
354
  fitPadding: 48,
355
- waypointEditable: true,
355
+ // 批量拐点接口要打开拐点总闸;页面本身不需要画布拖拽
356
+ features: { waypoint: true },
356
357
  history: { limit: 50 },
357
358
  onReady: () => {
358
359
  refreshState()
@@ -316,7 +316,7 @@ const buildOptions = () => ({
316
316
  fitPadding: 60,
317
317
  // 开关缺省全关(见 features.js):这个演示要的是「拖节点改通道 + 右键通道回线出交互框 + Delete 删除」,逐项显式打开
318
318
  // 通道路由与普通右键共用 contextMenu,不再另开一个功能开关
319
- features: { pan: true, zoom: true, hover: true, select: true, contextMenu: true, keyboard: true, drag: true, remove: true },
319
+ features: { pan: true, zoom: true, hover: true, select: true, contextMenu: true, keyboard: true, drag: true, remove: true, waypoint: true },
320
320
  // 通道选中模式:点一条回线选它自己 / 连所在分组 / 连整条通道
321
321
  channelSelectMode: channelSelectMode.value,
322
322
  // 分组拐点拖动:共用骨架 / 只移同组同序号拐点
@@ -326,8 +326,6 @@ const buildOptions = () => ({
326
326
  channelHiddenLineLayout: channelHiddenLineLayout.value,
327
327
  // 删除配置:confirm 缺省(null)表示组件不弹界面、停在待确认 —— 这一页订阅 'remove' 事件自己画确认框
328
328
  remove: { scope: 'lines' },
329
- // 交互框里的「加拐点 / 删拐点」动的是分组骨架,要有这一项才不是灰的(Ctrl + 点击加点、双击删点也一起开了)
330
- waypointEditable: true,
331
329
  // 这两条是假接口的入口:拐点只接 add / remove / end,分组只接整份声明;事件载荷本身已经够组请求
332
330
  onWaypointChange: handleWaypointBackend,
333
331
  onChannelGroupChange: handleChannelGroupBackend,
@@ -22,6 +22,11 @@ const clusterOn = ref(true)
22
22
  const radius = ref(60)
23
23
  const minPoints = ref(2)
24
24
  const maxZoom = ref(0)
25
+ const clusterColor = ref('#2970db')
26
+ const clusterTextColor = ref('#ffffff')
27
+ const clusterIcon = ref('circle')
28
+ const clusterShowCount = ref(true)
29
+ const clusterSizeScale = ref(1)
25
30
  const statsText = ref('—')
26
31
  const clusterText = ref('—')
27
32
  const eventText = ref('等待点击聚合圆')
@@ -62,8 +67,25 @@ const buildDataset = (count) => {
62
67
  return { points }
63
68
  }
64
69
 
70
+ const CLUSTER_ICONS = {
71
+ star: { type: 'svg', path: 'M0 -11 L3.2 -3.4 L11.5 -3.4 L4.9 1.8 L7.3 9.6 L0 5 L-7.3 9.6 L-4.9 1.8 L-11.5 -3.4 L-3.2 -3.4 Z', viewBox: [-12, -12, 24, 24], fill: 'currentColor' },
72
+ pin: { type: 'svg', path: 'M0 -11 C-6 -11 -10 -7 -10 -1 C-10 6 0 12 0 12 C0 12 10 6 10 -1 C10 -7 6 -11 0 -11 Z', viewBox: [-12, -12, 24, 24], fill: 'currentColor' }
73
+ }
74
+
75
+ const clusterSymbol = () => CLUSTER_ICONS[clusterIcon.value] ?? clusterIcon.value
76
+
65
77
  const clusterOptions = () => clusterOn.value
66
- ? { enabled: true, radius: radius.value, minPoints: minPoints.value, maxZoom: maxZoom.value }
78
+ ? {
79
+ enabled: true,
80
+ radius: radius.value,
81
+ minPoints: minPoints.value,
82
+ maxZoom: maxZoom.value,
83
+ color: clusterColor.value,
84
+ textColor: clusterTextColor.value,
85
+ symbol: clusterSymbol(),
86
+ showCount: clusterShowCount.value,
87
+ sizeScale: clusterSizeScale.value
88
+ }
67
89
  : false
68
90
 
69
91
  const buildOptions = () => ({
@@ -209,6 +231,31 @@ onBeforeUnmount(() => {
209
231
  </select>
210
232
  </label>
211
233
  </div>
234
+ <div class='dv-row'>
235
+ <label>填充色
236
+ <input v-model='clusterColor' data-demo='cluster-color' type='color' @change='applyCluster' />
237
+ </label>
238
+ <label>文字色
239
+ <input v-model='clusterTextColor' data-demo='cluster-text-color' type='color' @change='applyCluster' />
240
+ </label>
241
+ </div>
242
+ <div class='dv-row'>
243
+ <label>图标
244
+ <select v-model='clusterIcon' data-demo='cluster-icon' @change='applyCluster'>
245
+ <option value='circle'>圆形</option>
246
+ <option value='square'>方形</option>
247
+ <option value='star'>星标</option>
248
+ <option value='pin'>定位钉</option>
249
+ </select>
250
+ </label>
251
+ <label>尺寸
252
+ <input v-model.number='clusterSizeScale' data-demo='cluster-size-scale' type='range' min='0.5' max='2' step='0.1' @change='applyCluster' />
253
+ <span class='cl-value'>{{ clusterSizeScale }}×</span>
254
+ </label>
255
+ </div>
256
+ <div class='dv-row'>
257
+ <label><input v-model='clusterShowCount' data-demo='cluster-show-count' type='checkbox' @change='applyCluster' /> 显示数量</label>
258
+ </div>
212
259
  <p class='dv-hint'>半径是屏幕像素;maxZoom 是相机比例相对基准比例的倍数,超过后展开原始点。聚合期间发散初始化暂停,缩放 / 数据 / 选中变化才重算。</p>
213
260
  </div>
214
261
  <div class='dv-group'>