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
@@ -1,121 +1,121 @@
1
- <script setup>
2
- /**
3
- * 事件与方法演示:一条 `view.on('*')` 把全部事件收进日志,旁边一排按钮调常用方法(合并 / 复制有确认条),
4
- * 「谁发的、载荷长什么样、方法返回什么」都写在面板上。
5
- *
6
- * 高频事件(pointer / stats / render 这些)默认不写日志 —— 每帧都来的东西会把日志冲掉,
7
- * 想看就把「含高频事件」勾上,日志会按 name 累计次数。
8
- */
9
- import { onBeforeUnmount, onMounted, ref, shallowRef } from 'vue'
10
- import { createGisView } from '../../src/index.js'
11
- import { buildDemoData } from './shared/demoData.js'
12
- import './shared/demoStyle.css'
13
-
14
- const emit = defineEmits(['notice'])
15
- const container = ref(null)
16
- const dataset = buildDemoData({ count: 18 })
17
- let view = null
18
- let offAll = null
19
-
20
- const showHighFrequency = ref(false)
21
- const logLines = ref([])
22
- const counts = ref({})
23
- const actionText = ref('—')
24
- let logSeq = 0
25
-
1
+ <script setup>
2
+ /**
3
+ * 事件与方法演示:一条 `view.on('*')` 把全部事件收进日志,旁边一排按钮调常用方法(合并 / 复制有确认条),
4
+ * 「谁发的、载荷长什么样、方法返回什么」都写在面板上。
5
+ *
6
+ * 高频事件(pointer / stats / render 这些)默认不写日志 —— 每帧都来的东西会把日志冲掉,
7
+ * 想看就把「含高频事件」勾上,日志会按 name 累计次数。
8
+ */
9
+ import { onBeforeUnmount, onMounted, ref, shallowRef } from 'vue'
10
+ import { createGisView } from '../../src/index.js'
11
+ import { buildDemoData } from './shared/demoData.js'
12
+ import './shared/demoStyle.css'
13
+
14
+ const emit = defineEmits(['notice'])
15
+ const container = ref(null)
16
+ const dataset = buildDemoData({ count: 18 })
17
+ let view = null
18
+ let offAll = null
19
+
20
+ const showHighFrequency = ref(false)
21
+ const logLines = ref([])
22
+ const counts = ref({})
23
+ const actionText = ref('—')
24
+ let logSeq = 0
25
+
26
26
  /**
27
27
  * 合并 / 复制的确认条:合并会停在「还没落地」的中间态等使用方裁决,复制则是**结束之后**才弹 ——
28
28
  * 这里把载荷存下来,条上的按钮就是调载荷给的 confirm() / cancel() / revert(),
29
29
  * 与 DragDropDemo 的落点草稿是同一套写法。载荷里挂着引擎的节点对象,不必深层响应式,用 shallowRef 收着。
30
- */
31
- const mergeState = shallowRef(null)
32
- const copyState = shallowRef(null)
33
- /** 光标读数:直接读画布上的 cursor —— 「拖到一起鼠标为什么会变」在这里看得见 */
34
- const cursorText = ref('—')
35
- let cursorTimer = null
36
-
37
- /** 高频事件名单与库里的口径一致:这些默认只计数不写日志 */
38
- const HIGH_FREQUENCY = new Set(['pointer', 'stats', 'render', 'cameraChange', 'viewport', 'cursor', 'hover'])
39
-
40
- const pushLog = (name, text) => {
41
- logSeq += 1
42
- logLines.value = [{ id: logSeq, name, text }, ...logLines.value].slice(0, 14)
43
- }
44
-
45
- /** 载荷压成一行:不同事件的字段差别大,这里只挑最常看的几个 */
46
- const describePayload = (payload) => {
47
- if (payload === undefined || payload === null) {
48
- return '(无载荷)'
49
- }
50
- if (typeof payload !== 'object') {
51
- return String(payload)
52
- }
53
- const parts = []
54
- if (payload.type !== undefined) parts.push(`type=${payload.type}`)
55
- if (payload.kind !== undefined) parts.push(`kind=${payload.kind}`)
56
- if (payload.phase !== undefined) parts.push(`phase=${payload.phase}`)
57
- if (payload.id !== undefined) parts.push(`id=${payload.id}`)
58
- if (payload.index !== undefined) parts.push(`index=${payload.index}`)
59
- if (payload.labelId !== undefined) parts.push(`labelId=${payload.labelId}`)
60
- if (payload.count !== undefined) parts.push(`count=${payload.count}`)
30
+ */
31
+ const mergeState = shallowRef(null)
32
+ const copyState = shallowRef(null)
33
+ /** 光标读数:直接读画布上的 cursor —— 「拖到一起鼠标为什么会变」在这里看得见 */
34
+ const cursorText = ref('—')
35
+ let cursorTimer = null
36
+
37
+ /** 高频事件名单与库里的口径一致:这些默认只计数不写日志 */
38
+ const HIGH_FREQUENCY = new Set(['pointer', 'stats', 'render', 'cameraChange', 'viewport', 'cursor', 'hover'])
39
+
40
+ const pushLog = (name, text) => {
41
+ logSeq += 1
42
+ logLines.value = [{ id: logSeq, name, text }, ...logLines.value].slice(0, 14)
43
+ }
44
+
45
+ /** 载荷压成一行:不同事件的字段差别大,这里只挑最常看的几个 */
46
+ const describePayload = (payload) => {
47
+ if (payload === undefined || payload === null) {
48
+ return '(无载荷)'
49
+ }
50
+ if (typeof payload !== 'object') {
51
+ return String(payload)
52
+ }
53
+ const parts = []
54
+ if (payload.type !== undefined) parts.push(`type=${payload.type}`)
55
+ if (payload.kind !== undefined) parts.push(`kind=${payload.kind}`)
56
+ if (payload.phase !== undefined) parts.push(`phase=${payload.phase}`)
57
+ if (payload.id !== undefined) parts.push(`id=${payload.id}`)
58
+ if (payload.index !== undefined) parts.push(`index=${payload.index}`)
59
+ if (payload.labelId !== undefined) parts.push(`labelId=${payload.labelId}`)
60
+ if (payload.count !== undefined) parts.push(`count=${payload.count}`)
61
61
  if (payload.channelId !== undefined) parts.push(`channel=${payload.channelId}`)
62
62
  if (payload.camera !== undefined) parts.push('camera=…')
63
63
  if (payload.idleTime !== undefined) parts.push(`idleTime=${Math.round(payload.idleTime)}ms`)
64
64
  if (payload.timeout !== undefined) parts.push(`timeout=${payload.timeout}ms`)
65
- if (payload.screen !== undefined) parts.push(`screen=[${payload.screen.map(value => Number(value).toFixed(0)).join(', ')}]`)
66
- if (payload.action) {
67
- const action = payload.action
65
+ if (payload.screen !== undefined) parts.push(`screen=[${payload.screen.map(value => Number(value).toFixed(0)).join(', ')}]`)
66
+ if (payload.action) {
67
+ const action = payload.action
68
68
  parts.push(`action=${action.type}/${action.target}${action.moved ? '+moved' : ''}${action.double ? '+double' : ''}@${payload.originalEvent?.button ?? '-'}`)
69
69
  parts.push(`data=${dataLabel(hitDataOf(payload))}`)
70
- }
71
- if (!parts.length) parts.push(`keys=[${Object.keys(payload).slice(0, 6).join(', ')}]`)
72
- return parts.join(' · ')
73
- }
74
-
75
- /**
70
+ }
71
+ if (!parts.length) parts.push(`keys=[${Object.keys(payload).slice(0, 6).join(', ')}]`)
72
+ return parts.join(' · ')
73
+ }
74
+
75
+ /**
76
76
  * 命中的数据在载荷顶层:节点 / 线路看 item,通道看 channel,选中集报 items 条数,
77
77
  * 标签(labelClick / labelDrag / labelContextMenu / labelDblClick)看 label / parent。
78
78
  */
79
- const dataLabel = (value) => {
80
- if (Array.isArray(value)) return `${value.length} 项`
81
- if (!value || typeof value !== 'object') return String(value)
82
- if (value.label) {
83
- const parent = value.parent
84
- const where = parent?.id !== undefined && parent?.id !== null
85
- ? `${parent.type ?? '要素'} ${parent.id}`
86
- : parent?.index !== undefined ? `${parent.type ?? '要素'} #${parent.index}` : ''
87
- return `标签「${value.label.content || value.label.id || '—'}」${where ? ` @ ${where}` : ''}`
88
- }
89
- return value.id ?? value.channelId ?? value.name ?? `对象 keys=[${Object.keys(value).slice(0, 4).join(', ')}]`
90
- }
91
-
79
+ const dataLabel = (value) => {
80
+ if (Array.isArray(value)) return `${value.length} 项`
81
+ if (!value || typeof value !== 'object') return String(value)
82
+ if (value.label) {
83
+ const parent = value.parent
84
+ const where = parent?.id !== undefined && parent?.id !== null
85
+ ? `${parent.type ?? '要素'} ${parent.id}`
86
+ : parent?.index !== undefined ? `${parent.type ?? '要素'} #${parent.index}` : ''
87
+ return `标签「${value.label.content || value.label.id || '—'}」${where ? ` @ ${where}` : ''}`
88
+ }
89
+ return value.id ?? value.channelId ?? value.name ?? `对象 keys=[${Object.keys(value).slice(0, 4).join(', ')}]`
90
+ }
91
+
92
92
  /** 一次事件里可读的那份顶层数据:不把通道 / 分组上下文再打成一个 action.data */
93
93
  const hitDataOf = payload => payload.item ?? payload.channel ?? payload.label ?? payload.line
94
94
  ?? (payload.type === 'cluster' ? { id: payload.id, count: payload.count } : null)
95
95
  ?? (Array.isArray(payload.items) ? payload.items : null)
96
-
97
- /** 会带 action 的事件名:* 订阅里从载荷上认出「这一次是什么操作」 */
96
+
97
+ /** 会带 action 的事件名:* 订阅里从载荷上认出「这一次是什么操作」 */
98
98
  const ACTION_EVENTS = new Set(['click', 'dblClick', 'pointerDown', 'pointerUp', 'contextMenu', 'labelClick', 'labelDrag', 'labelContextMenu', 'labelDblClick', 'selectChange', 'waypointChange'])
99
-
100
- /** 最近一次操作的判读:面板上的「动作判读」直接读它 */
101
- const lastAction = ref('—')
102
-
103
- /** 画布中心的客户端坐标与「中间半屏」的像素矩形:给 pickAt / getFeaturesInRect 当参数 */
104
- const canvasProbe = () => {
105
- const canvas = container.value?.querySelector?.('.gis-view__canvas') ?? container.value
106
- const box = canvas?.getBoundingClientRect?.() ?? { left: 0, top: 0, width: 0, height: 0 }
107
- return {
108
- center: { clientX: box.left + box.width / 2, clientY: box.top + box.height / 2 },
109
- rect: { x: box.width * 0.25, y: box.height * 0.25, width: box.width * 0.5, height: box.height * 0.5 }
110
- }
111
- }
112
-
113
- const buildOptions = () => ({
114
- points: dataset.points,
115
- lines: dataset.lines,
116
- channels: dataset.channels,
117
- pointStyle: { size: 7, color: '#ffffff', strokeColor: '#4c8dff', strokeWidth: 1.5 },
118
- lineStyle: { width: 1.5, color: 'rgba(120, 168, 226, 0.55)' },
99
+
100
+ /** 最近一次操作的判读:面板上的「动作判读」直接读它 */
101
+ const lastAction = ref('—')
102
+
103
+ /** 画布中心的客户端坐标与「中间半屏」的像素矩形:给 pickAt / getFeaturesInRect 当参数 */
104
+ const canvasProbe = () => {
105
+ const canvas = container.value?.querySelector?.('.gis-view__canvas') ?? container.value
106
+ const box = canvas?.getBoundingClientRect?.() ?? { left: 0, top: 0, width: 0, height: 0 }
107
+ return {
108
+ center: { clientX: box.left + box.width / 2, clientY: box.top + box.height / 2 },
109
+ rect: { x: box.width * 0.25, y: box.height * 0.25, width: box.width * 0.5, height: box.height * 0.5 }
110
+ }
111
+ }
112
+
113
+ const buildOptions = () => ({
114
+ points: dataset.points,
115
+ lines: dataset.lines,
116
+ channels: dataset.channels,
117
+ pointStyle: { size: 7, color: '#ffffff', strokeColor: '#4c8dff', strokeWidth: 1.5 },
118
+ lineStyle: { width: 1.5, color: 'rgba(120, 168, 226, 0.55)' },
119
119
  showGrid: true,
120
120
  fitPadding: 60,
121
121
  // 停手 3 秒发一次 idle,事件日志与计数里都能看到;
@@ -129,82 +129,82 @@ const buildOptions = () => ({
129
129
  // 复制走缺省口径:Alt 点击出预览 → 跟着鼠标走 → **松开左键**落地(不是松开 Alt),
130
130
  // 落地之后确认条给一次撤销机会(copy.autoCommit: false 才会停在预览态等确认,演示不用那一档)
131
131
  onReady: () => {
132
- emit('notice', '事件日志已接上:点、拖、滚、选都会记一行')
133
- pushLog('ready', '视图就绪')
134
- }
135
- })
136
-
137
- /** 方法按钮:每个都调一个实例方法,把返回值写进读数 */
138
- const call = (name, run) => {
139
- const started = performance.now()
140
- let result
141
- try {
142
- result = run()
143
- }
144
- catch (error) {
145
- actionText.value = `${name} 抛错:${error instanceof Error ? error.message : String(error)}`
146
- return
147
- }
148
- const elapsed = (performance.now() - started).toFixed(1)
149
- const text = summarize(result)
150
- actionText.value = `${name} → ${text}(${elapsed} ms)`
151
- pushLog('method', `${name} → ${text}`)
152
- }
153
-
154
- const summarize = (value) => {
155
- if (value === undefined) return 'undefined'
156
- if (value === null) return 'null'
157
- if (Array.isArray(value)) return `数组 ${value.length} 项`
158
- if (typeof value === 'object') return `对象 keys=[${Object.keys(value).slice(0, 6).join(', ')}]`
159
- return String(value)
160
- }
161
-
162
- const methods = [
163
- { name: 'fitView()', run: () => view?.fitView(60) },
164
- { name: 'zoomBy(1.3)', run: () => view?.zoomBy(1.3) },
132
+ emit('notice', '事件日志已接上:点、拖、滚、选都会记一行')
133
+ pushLog('ready', '视图就绪')
134
+ }
135
+ })
136
+
137
+ /** 方法按钮:每个都调一个实例方法,把返回值写进读数 */
138
+ const call = (name, run) => {
139
+ const started = performance.now()
140
+ let result
141
+ try {
142
+ result = run()
143
+ }
144
+ catch (error) {
145
+ actionText.value = `${name} 抛错:${error instanceof Error ? error.message : String(error)}`
146
+ return
147
+ }
148
+ const elapsed = (performance.now() - started).toFixed(1)
149
+ const text = summarize(result)
150
+ actionText.value = `${name} → ${text}(${elapsed} ms)`
151
+ pushLog('method', `${name} → ${text}`)
152
+ }
153
+
154
+ const summarize = (value) => {
155
+ if (value === undefined) return 'undefined'
156
+ if (value === null) return 'null'
157
+ if (Array.isArray(value)) return `数组 ${value.length} 项`
158
+ if (typeof value === 'object') return `对象 keys=[${Object.keys(value).slice(0, 6).join(', ')}]`
159
+ return String(value)
160
+ }
161
+
162
+ const methods = [
163
+ { name: 'fitView()', run: () => view?.fitView(60) },
164
+ { name: 'zoomBy(1.3)', run: () => view?.zoomBy(1.3) },
165
165
  { name: 'selectFeatures(node-0)', run: () => view?.selectFeatures('node-0') },
166
166
  { name: 'selectFeatures(null)', run: () => view?.selectFeatures(null) },
167
- { name: 'getSelections()', run: () => view?.getSelections() },
168
- { name: 'getSelectionData()', run: () => view?.getSelectionData() },
169
- { name: 'isSelected(node-0)', run: () => view?.isSelected('node-0') },
170
- { name: 'getVisibleData()', run: () => view?.getVisibleData() },
171
- { name: 'listPoints({ visibleOnly: true })', run: () => view?.listPoints({ visibleOnly: true }) },
172
- // 聚焦一批:数组取整体包围盒(这几个点的中心就是屏幕中心),select: true 顺带把它们选上
173
- { name: 'focusOn(前三个节点, { select: true })', run: () => { const ids = (view?.getData().points ?? []).slice(0, 3).map(point => point.id ?? 0); return ids.length > 0 ? view?.focusOn(ids, { select: true }) : null } },
174
- { name: 'getStats()', run: () => view?.getStats() },
175
- { name: 'getQuality()', run: () => view?.getQuality() },
167
+ { name: 'getSelections()', run: () => view?.getSelections() },
168
+ { name: 'getSelectionData()', run: () => view?.getSelectionData() },
169
+ { name: 'isSelected(node-0)', run: () => view?.isSelected('node-0') },
170
+ { name: 'getVisibleData()', run: () => view?.getVisibleData() },
171
+ { name: 'listPoints({ visibleOnly: true })', run: () => view?.listPoints({ visibleOnly: true }) },
172
+ // 聚焦一批:数组取整体包围盒(这几个点的中心就是屏幕中心),select: true 顺带把它们选上
173
+ { name: 'focusOn(前三个节点, { select: true })', run: () => { const ids = (view?.getData().points ?? []).slice(0, 3).map(point => point.id ?? 0); return ids.length > 0 ? view?.focusOn(ids, { select: true }) : null } },
174
+ { name: 'getStats()', run: () => view?.getStats() },
175
+ { name: 'getQuality()', run: () => view?.getQuality() },
176
176
  { name: 'getCamera()', run: () => view?.getCamera() },
177
177
  { name: 'getScale()', run: () => view?.getScale() },
178
178
  { name: 'setScale(当前 × 1.25)', run: () => view?.setScale(view.getScale() * 1.25) },
179
179
  { name: 'getIdleTime()', run: () => view?.getIdleTime() },
180
- { name: 'getBounds()', run: () => view?.getBounds() },
181
- { name: 'getFilterResult()', run: () => view?.getFilterResult() },
182
- { name: 'getLabelStates()', run: () => view?.getLabelStates() },
183
- { name: 'getChannels()', run: () => view?.getChannels() },
184
- { name: 'getChannelsInView()', run: () => view?.getChannelsInView() },
185
- // 回线属于哪条通道:线上点一条回线之后,想知道它归哪条通道 / 哪一组就问它
186
- { name: 'getLineChannel(第一条回线)', run: () => { const line = view?.listLines().find(entry => view.getLineChannel(entry.index)); return line ? view.getLineChannel(line.index) : '没有通道回线' } },
187
- // 导出当前数据:format 'input' 就是 createGisView 入参那一份,可以直接回灌
188
- { name: 'exportData(input)', run: () => view?.exportData({ format: 'input' }) },
189
- { name: 'getViewState()', run: () => view?.getViewState() },
190
- { name: 'getPointCopy()', run: () => view?.getPointCopy() },
191
- { name: 'getPointMerge()', run: () => view?.getPointMerge() },
192
- { name: 'canUndo()', run: () => view?.canUndo() },
193
- { name: 'undo()', run: () => view?.undo() },
194
- { name: 'refresh()', run: () => view?.refresh() },
195
- { name: 'resize()', run: () => view?.resize() },
196
- { name: 'pickAt(画布中心)', run: () => { const probe = canvasProbe(); return view?.pickAt(probe.center.clientX, probe.center.clientY) } },
197
- { name: 'getFeaturesInRect(中心半屏)', run: () => view?.getFeaturesInRect(canvasProbe().rect) }
198
- ]
199
-
200
- /** 确认条上写「哪个节点」:优先业务名,其次是 id,都没有就报下标 */
201
- const pointLabel = (entry) => {
202
- if (!entry?.item) {
203
- return entry?.index === undefined ? '—' : '下标 ' + entry.index
204
- }
205
- return entry.item.name ?? entry.item.id ?? '下标 ' + (entry.index ?? '—')
206
- }
207
-
180
+ { name: 'getBounds()', run: () => view?.getBounds() },
181
+ { name: 'getFilterResult()', run: () => view?.getFilterResult() },
182
+ { name: 'getLabelStates()', run: () => view?.getLabelStates() },
183
+ { name: 'getChannels()', run: () => view?.getChannels() },
184
+ { name: 'getChannelsInView()', run: () => view?.getChannelsInView() },
185
+ // 回线属于哪条通道:线上点一条回线之后,想知道它归哪条通道 / 哪一组就问它
186
+ { name: 'getLineChannel(第一条回线)', run: () => { const line = view?.listLines().find(entry => view.getLineChannel(entry.index)); return line ? view.getLineChannel(line.index) : '没有通道回线' } },
187
+ // 导出当前数据:format 'input' 就是 createGisView 入参那一份,可以直接回灌
188
+ { name: 'exportData(input)', run: () => view?.exportData({ format: 'input' }) },
189
+ { name: 'getViewState()', run: () => view?.getViewState() },
190
+ { name: 'getPointCopy()', run: () => view?.getPointCopy() },
191
+ { name: 'getPointMerge()', run: () => view?.getPointMerge() },
192
+ { name: 'canUndo()', run: () => view?.canUndo() },
193
+ { name: 'undo()', run: () => view?.undo() },
194
+ { name: 'refresh()', run: () => view?.refresh() },
195
+ { name: 'resize()', run: () => view?.resize() },
196
+ { name: 'pickAt(画布中心)', run: () => { const probe = canvasProbe(); return view?.pickAt(probe.center.clientX, probe.center.clientY) } },
197
+ { name: 'getFeaturesInRect(中心半屏)', run: () => view?.getFeaturesInRect(canvasProbe().rect) }
198
+ ]
199
+
200
+ /** 确认条上写「哪个节点」:优先业务名,其次是 id,都没有就报下标 */
201
+ const pointLabel = (entry) => {
202
+ if (!entry?.item) {
203
+ return entry?.index === undefined ? '—' : '下标 ' + entry.index
204
+ }
205
+ return entry.item.name ?? entry.item.id ?? '下标 ' + (entry.index ?? '—')
206
+ }
207
+
208
208
  /**
209
209
  * 实时光标:读画布上的 cursor 就够 —— 拖动是 move、拖到一起进「待确认合并」是 copy、
210
210
  * 复制预览与等待选点也走 copy(都取 cursorStyle 里那一档,缺省是系统 copy 光标)。
@@ -222,13 +222,13 @@ const copyCoordinateLabel = () => {
222
222
  const coordinate = copyState.value?.coordinate
223
223
  return Array.isArray(coordinate) ? coordinate.map(value => Number(value).toFixed(2)).join(', ') : '—'
224
224
  }
225
-
226
- /** 合并的中间态:pending 停在条上等裁决,confirm / merge / cancel 到了就收掉(落地读数看日志) */
227
- const onMergeEvent = (payload) => {
228
- mergeState.value = payload?.phase === 'pending' ? payload : null
229
- syncCursor()
230
- }
231
-
225
+
226
+ /** 合并的中间态:pending 停在条上等裁决,confirm / merge / cancel 到了就收掉(落地读数看日志) */
227
+ const onMergeEvent = (payload) => {
228
+ mergeState.value = payload?.phase === 'pending' ? payload : null
229
+ syncCursor()
230
+ }
231
+
232
232
  /** 复制的收尾条:只在 end(复制结束)那一拍出现 —— start / move 是复制进行中,不弹框 */
233
233
  const onCopyEvent = (payload) => {
234
234
  copyState.value = payload?.phase === 'end' ? payload : null
@@ -239,17 +239,17 @@ const onCopyEvent = (payload) => {
239
239
  * 确认条上的按钮:直接调载荷给的入口。
240
240
  * 合并的 confirm 阶段数据还没动(还能改 payload.item 或再 cancel 掉);
241
241
  * 复制这一条是结束后才出现的,给的是 payload.revert() —— 数据已经写了,整份撤回去。
242
- */
243
- const confirmMerge = () => {
244
- mergeState.value?.confirm()
245
- mergeState.value = null
246
- syncCursor()
247
- }
248
- const cancelMerge = () => {
249
- mergeState.value?.cancel()
250
- mergeState.value = null
251
- syncCursor()
252
- }
242
+ */
243
+ const confirmMerge = () => {
244
+ mergeState.value?.confirm()
245
+ mergeState.value = null
246
+ syncCursor()
247
+ }
248
+ const cancelMerge = () => {
249
+ mergeState.value?.cancel()
250
+ mergeState.value = null
251
+ syncCursor()
252
+ }
253
253
  const revertCopy = () => {
254
254
  copyState.value?.revert?.()
255
255
  copyState.value = null
@@ -259,62 +259,62 @@ const dismissCopy = () => {
259
259
  copyState.value = null
260
260
  syncCursor()
261
261
  }
262
-
263
- onMounted(() => {
264
- view = createGisView(container.value, buildOptions())
262
+
263
+ onMounted(() => {
264
+ view = createGisView(container.value, buildOptions())
265
265
  // 一条 '*' 订阅全部事件:回调拿到的第二参就是事件名;载荷与 options 里的 onXxx 完全同一份
266
266
  offAll = view.on('*', (payload, name) => {
267
- counts.value = { ...counts.value, [name]: (counts.value[name] ?? 0) + 1 }
268
- // 合并 / 复制:同一条 * 订阅里收中间态,确认条与日志读的是同一份载荷
269
- if (name === 'pointMerge') {
270
- onMergeEvent(payload)
271
- }
272
- else if (name === 'pointCopy') {
273
- onCopyEvent(payload)
274
- }
275
- if (name === 'cursor' || name === 'pointerDown' || name === 'pointerUp' || name === 'pointMerge' || name === 'pointCopy') {
276
- syncCursor()
277
- }
278
- if (ACTION_EVENTS.has(name) && payload?.action) {
279
- // 一次操作的判读:什么操作 / 点到了什么 / 拿到了什么数据 —— 左键与右键都走这一套
280
- const action = payload.action
267
+ counts.value = { ...counts.value, [name]: (counts.value[name] ?? 0) + 1 }
268
+ // 合并 / 复制:同一条 * 订阅里收中间态,确认条与日志读的是同一份载荷
269
+ if (name === 'pointMerge') {
270
+ onMergeEvent(payload)
271
+ }
272
+ else if (name === 'pointCopy') {
273
+ onCopyEvent(payload)
274
+ }
275
+ if (name === 'cursor' || name === 'pointerDown' || name === 'pointerUp' || name === 'pointMerge' || name === 'pointCopy') {
276
+ syncCursor()
277
+ }
278
+ if (ACTION_EVENTS.has(name) && payload?.action) {
279
+ // 一次操作的判读:什么操作 / 点到了什么 / 拿到了什么数据 —— 左键与右键都走这一套
280
+ const action = payload.action
281
281
  const event = payload.originalEvent
282
282
  const held = [event?.ctrlKey && 'ctrl', event?.metaKey && 'meta', event?.shiftKey && 'shift', event?.altKey && 'alt'].filter(Boolean).join('+')
283
283
  const data = hitDataOf(payload)
284
284
  lastAction.value = `${name}:${action.type} → ${action.target}${data === null || data === undefined ? '' : ` · ${dataLabel(data)}`}${event ? ` · button ${event.button}` : ''}${held ? ` · ${held}` : ''}${action.moved ? ' · 拖动过' : ''}${action.double ? ' · 双击第二下' : ''}`
285
- }
286
- if (!showHighFrequency.value && HIGH_FREQUENCY.has(name)) {
287
- return
288
- }
289
- pushLog(name, describePayload(payload))
290
- })
291
- // 光标在很多条路径上都会换(拖动 / 修饰键 / 合并与复制进出),确认条上的按钮又不经过画布 —— 每 250ms 读一次最省心
292
- cursorTimer = window.setInterval(syncCursor, 250)
293
- })
294
- onBeforeUnmount(() => {
295
- window.clearInterval(cursorTimer)
296
- cursorTimer = null
297
- offAll?.()
298
- offAll = null
299
- view?.destroy()
300
- view = null
301
- })
302
- </script>
303
-
304
- <template>
305
- <section class="dv-demo">
306
- <div class="dv-stage">
307
- <div ref="container" class="dv-canvas"></div>
308
- <div class="dv-toolbar">
309
- <button type="button" class="dv-button" @click="view?.fitView(60)">适应全部</button>
310
- </div>
311
- <div v-if="mergeState" class="dv-confirm">
312
- <span class="dv-confirm__text">
313
- 合并待确认:{{ pointLabel(mergeState.source) }} → {{ pointLabel(mergeState.target) }} · 拖动的节点停在落点上,数据还没动
314
- </span>
315
- <button type="button" class="dv-button dv-button--primary" @click="confirmMerge">确认合并(payload.confirm())</button>
316
- <button type="button" class="dv-button" @click="cancelMerge">取消(Esc / 右键)</button>
317
- </div>
285
+ }
286
+ if (!showHighFrequency.value && HIGH_FREQUENCY.has(name)) {
287
+ return
288
+ }
289
+ pushLog(name, describePayload(payload))
290
+ })
291
+ // 光标在很多条路径上都会换(拖动 / 修饰键 / 合并与复制进出),确认条上的按钮又不经过画布 —— 每 250ms 读一次最省心
292
+ cursorTimer = window.setInterval(syncCursor, 250)
293
+ })
294
+ onBeforeUnmount(() => {
295
+ window.clearInterval(cursorTimer)
296
+ cursorTimer = null
297
+ offAll?.()
298
+ offAll = null
299
+ view?.destroy()
300
+ view = null
301
+ })
302
+ </script>
303
+
304
+ <template>
305
+ <section class="dv-demo">
306
+ <div class="dv-stage">
307
+ <div ref="container" class="dv-canvas"></div>
308
+ <div class="dv-toolbar">
309
+ <button type="button" class="dv-button" @click="view?.fitView(60)">适应全部</button>
310
+ </div>
311
+ <div v-if="mergeState" class="dv-confirm">
312
+ <span class="dv-confirm__text">
313
+ 合并待确认:{{ pointLabel(mergeState.source) }} → {{ pointLabel(mergeState.target) }} · 拖动的节点停在落点上,数据还没动
314
+ </span>
315
+ <button type="button" class="dv-button dv-button--primary" @click="confirmMerge">确认合并(payload.confirm())</button>
316
+ <button type="button" class="dv-button" @click="cancelMerge">取消(Esc / 右键)</button>
317
+ </div>
318
318
  <div v-else-if="copyState" class="dv-confirm">
319
319
  <span class="dv-confirm__text">
320
320
  复制结束:{{ pointLabel(copyState.source) }} → ({{ copyCoordinateLabel() }}) · 松开左键才算结束,数据已经写进去了
@@ -322,73 +322,73 @@ onBeforeUnmount(() => {
322
322
  <button type="button" class="dv-button dv-button--primary" @click="revertCopy">撤销(payload.revert())</button>
323
323
  <button type="button" class="dv-button" @click="dismissCopy">知道了</button>
324
324
  </div>
325
- </div>
326
- <aside class="dv-panel">
327
- <div class="dv-group">
328
- <h3>方法</h3>
329
- <div class="dv-row">
330
- <button v-for="item in methods" :key="item.name" type="button" class="dv-button" @click="call(item.name, item.run)">{{ item.name }}</button>
331
- </div>
332
- <p class="dv-readout">{{ actionText }}</p>
333
- <p class="dv-hint">每个按钮只调一个方法,返回值与耗时都打在读数里;返回值是数组的只报条数,详情看日志。</p>
334
- </div>
335
- <div class="dv-group">
325
+ </div>
326
+ <aside class="dv-panel">
327
+ <div class="dv-group">
328
+ <h3>方法</h3>
329
+ <div class="dv-row">
330
+ <button v-for="item in methods" :key="item.name" type="button" class="dv-button" @click="call(item.name, item.run)">{{ item.name }}</button>
331
+ </div>
332
+ <p class="dv-readout">{{ actionText }}</p>
333
+ <p class="dv-hint">每个按钮只调一个方法,返回值与耗时都打在读数里;返回值是数组的只报条数,详情看日志。</p>
334
+ </div>
335
+ <div class="dv-group">
336
336
  <h3>光标读数<span class="dv-chip">{{ mergeState ? '等裁决' : copyState ? '复制结束' : '常态' }}</span></h3>
337
- <p class="dv-readout">{{ cursorText }}</p>
337
+ <p class="dv-readout">{{ cursorText }}</p>
338
338
  <p class="dv-hint">
339
339
  拖一个节点压到另一个节点上松手,鼠标会从 move 变成 copy:引擎进入「待确认合并」后 updateCursor() 把光标切成
340
340
  cursorStyle.copy 这一档,复制预览与等待选点走的是同一档(按住 Ctrl / ⌘ 还能用 cursorStyle.ctrl.copy 再换一份)。
341
341
  合并没配 confirm,停在原地等下面那条确认条(调载荷的 confirm() / cancel());复制是**松开左键就结束**的:
342
342
  Alt 点击出预览 → 跟着鼠标走 → 抬左键落地,弹出来的条是「复制结束」的收尾条,可以 payload.revert() 整份撤销。
343
343
  </p>
344
- </div>
345
- <div class="dv-group">
346
- <h3>
347
- 事件日志
348
- <label class="dv-hint"><input v-model="showHighFrequency" type="checkbox" /> 含高频事件</label>
349
- </h3>
350
- <ul class="dv-log">
351
- <li v-for="line in logLines" :key="line.id"><strong>{{ line.name }}</strong> · {{ line.text }}</li>
352
- <li v-if="!logLines.length">还没有事件:在画布上点一下 / 拖一段 / 滚一下滚轮</li>
353
- </ul>
354
- </div>
355
- <div class="dv-group">
356
- <h3>动作判读(action)</h3>
357
- <p class="dv-readout">{{ lastAction }}</p>
344
+ </div>
345
+ <div class="dv-group">
346
+ <h3>
347
+ 事件日志
348
+ <label class="dv-hint"><input v-model="showHighFrequency" type="checkbox" /> 含高频事件</label>
349
+ </h3>
350
+ <ul class="dv-log">
351
+ <li v-for="line in logLines" :key="line.id"><strong>{{ line.name }}</strong> · {{ line.text }}</li>
352
+ <li v-if="!logLines.length">还没有事件:在画布上点一下 / 拖一段 / 滚一下滚轮</li>
353
+ </ul>
354
+ </div>
355
+ <div class="dv-group">
356
+ <h3>动作判读(action)</h3>
357
+ <p class="dv-readout">{{ lastAction }}</p>
358
358
  <p class="dv-hint">在画布上点一下 / 右键 / 拖一段 / 框选一下:action.type 说清「是什么操作」、action.target 说清「点到了什么」,命中的数据直接读载荷顶层的 item / channel / label 等字段;按键与修饰键读 originalEvent。</p>
359
- </div>
360
- <div class="dv-group">
361
- <h3>事件计数(* 订阅)</h3>
362
- <p class="dv-readout">
363
- <template v-for="(value, name) in counts" :key="name">{{ name }}: {{ value }} </template>
364
- </p>
365
- </div>
366
- </aside>
367
- </section>
368
- </template>
369
-
370
- <style scoped>
371
- /* 合并 / 复制的确认条:与 DragDropDemo 的「落点草稿」同一套外观,贴在画布下沿正中 */
372
- .dv-confirm {
373
- position: absolute;
374
- left: 50%;
375
- bottom: 12px;
376
- z-index: 5;
377
- display: flex;
378
- align-items: center;
379
- flex-wrap: wrap;
380
- gap: 8px;
381
- max-width: calc(100% - 24px);
382
- padding: 6px 8px;
383
- border-radius: 10px;
384
- background: rgba(255, 255, 255, 0.95);
385
- box-shadow: 0 10px 26px rgba(31, 62, 105, 0.2);
386
- transform: translateX(-50%);
387
- }
388
-
389
- .dv-confirm__text {
390
- color: #526b8d;
391
- font-size: 12px;
392
- line-height: 1.5;
393
- }
394
- </style>
359
+ </div>
360
+ <div class="dv-group">
361
+ <h3>事件计数(* 订阅)</h3>
362
+ <p class="dv-readout">
363
+ <template v-for="(value, name) in counts" :key="name">{{ name }}: {{ value }} </template>
364
+ </p>
365
+ </div>
366
+ </aside>
367
+ </section>
368
+ </template>
369
+
370
+ <style scoped>
371
+ /* 合并 / 复制的确认条:与 DragDropDemo 的「落点草稿」同一套外观,贴在画布下沿正中 */
372
+ .dv-confirm {
373
+ position: absolute;
374
+ left: 50%;
375
+ bottom: 12px;
376
+ z-index: 5;
377
+ display: flex;
378
+ align-items: center;
379
+ flex-wrap: wrap;
380
+ gap: 8px;
381
+ max-width: calc(100% - 24px);
382
+ padding: 6px 8px;
383
+ border-radius: 10px;
384
+ background: rgba(255, 255, 255, 0.95);
385
+ box-shadow: 0 10px 26px rgba(31, 62, 105, 0.2);
386
+ transform: translateX(-50%);
387
+ }
388
+
389
+ .dv-confirm__text {
390
+ color: #526b8d;
391
+ font-size: 12px;
392
+ line-height: 1.5;
393
+ }
394
+ </style>