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