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
@@ -0,0 +1,458 @@
1
+ <script setup>
2
+ /**
3
+ * 扩展能力:用同一个视图把五个扩展点串起来 ——
4
+ * 运行时插件(view.use)、自定义图元(registerSymbol)、自定义条件运算符(registerOperator)
5
+ * 自定义导出格式(defineExportFormat)与 WebGL 自定义层(addWebGLLayer)。
6
+ * 每一块都可单独开关,读数里能直接看到注册表与可见数据。
7
+ */
8
+ import { onBeforeUnmount, onMounted, ref } from 'vue'
9
+ import { createGisView, defineExportFormat } from '../../src/index.js'
10
+ import { buildDemoData } from './shared/demoData.js'
11
+ import './shared/demoStyle.css'
12
+
13
+ const emit = defineEmits(['notice'])
14
+ const container = ref(null)
15
+ const operatorOn = ref(false)
16
+ const symbolOn = ref(false)
17
+ const pluginOn = ref(false)
18
+ const symbolVariant = ref('cross')
19
+ const webglMounted = ref(false)
20
+ const webglEnabled = ref(false)
21
+ const webglOrder = ref(20)
22
+ const webglDrawCount = ref(0)
23
+ const readout = ref('正在创建视图…')
24
+ const exportText = ref('尚未导出。点上面的按钮,会走 defineExportFormat 注册的同步适配器。')
25
+
26
+ const dataset = buildDemoData({ count: 18, channels: 0 })
27
+ const points = dataset.points.map((point, index) => ({ ...point, extension_index: index }))
28
+ const lines = dataset.linesWithoutChannel
29
+
30
+ let view = null
31
+ let disposeOperator = null
32
+ let disposeSymbol = null
33
+ let disposePlugin = null
34
+ let disposeExport = null
35
+ let disposeWebgl = null
36
+
37
+ /** WebGL 自定义层:按 gl 缓存程序 / 缓冲,draw 里只更新 uniform 与画一笔 */
38
+ const webglResources = new WeakMap()
39
+ const ensureWebglResources = (gl) => {
40
+ const cached = webglResources.get(gl)
41
+ if (cached) {
42
+ return cached
43
+ }
44
+ const compile = (type, source) => {
45
+ const shader = gl.createShader(type)
46
+ gl.shaderSource(shader, source)
47
+ gl.compileShader(shader)
48
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
49
+ const reason = gl.getShaderInfoLog(shader)
50
+ gl.deleteShader(shader)
51
+ throw new Error(reason || 'shader 编译失败')
52
+ }
53
+ return shader
54
+ }
55
+ const vertex = compile(gl.VERTEX_SHADER, `
56
+ attribute vec2 a_position;
57
+ uniform vec2 u_center;
58
+ uniform vec2 u_radius;
59
+ varying vec2 v_uv;
60
+ void main() {
61
+ v_uv = a_position;
62
+ gl_Position = vec4(u_center + a_position * u_radius, 0.0, 1.0);
63
+ }
64
+ `)
65
+ const fragment = compile(gl.FRAGMENT_SHADER, `
66
+ precision mediump float;
67
+ varying vec2 v_uv;
68
+ uniform vec4 u_color;
69
+ void main() {
70
+ float d = length(v_uv);
71
+ float ring = smoothstep(0.62, 0.78, d) * (1.0 - smoothstep(0.92, 1.08, d));
72
+ float glow = (1.0 - smoothstep(0.0, 1.0, d)) * 0.18;
73
+ gl_FragColor = vec4(u_color.rgb, u_color.a * (ring + glow));
74
+ }
75
+ `)
76
+ const program = gl.createProgram()
77
+ gl.attachShader(program, vertex)
78
+ gl.attachShader(program, fragment)
79
+ gl.linkProgram(program)
80
+ gl.deleteShader(vertex)
81
+ gl.deleteShader(fragment)
82
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
83
+ const reason = gl.getProgramInfoLog(program)
84
+ gl.deleteProgram(program)
85
+ throw new Error(reason || 'shader program 链接失败')
86
+ }
87
+ const vao = gl.createVertexArray()
88
+ const buffer = gl.createBuffer()
89
+ gl.bindVertexArray(vao)
90
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
91
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]), gl.STATIC_DRAW)
92
+ const position = gl.getAttribLocation(program, 'a_position')
93
+ gl.enableVertexAttribArray(position)
94
+ gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0)
95
+ gl.bindVertexArray(null)
96
+ const resources = {
97
+ program,
98
+ vao,
99
+ center: gl.getUniformLocation(program, 'u_center'),
100
+ radius: gl.getUniformLocation(program, 'u_radius'),
101
+ color: gl.getUniformLocation(program, 'u_color')
102
+ }
103
+ webglResources.set(gl, resources)
104
+ return resources
105
+ }
106
+ /** 在内置画面之上画一圈跟着第一节点呼吸的自定义光环(完全用自己的 GL 程序) */
107
+ const drawWebglExtension = ({ gl, canvas, viewport, frame, project }) => {
108
+ try {
109
+ const resources = ensureWebglResources(gl)
110
+ const anchor = points[0] ?? { x: 0, y: 0 }
111
+ const [x, y] = project(anchor.x, anchor.y)
112
+ const width = Math.max(viewport.cssWidth, 1)
113
+ const height = Math.max(viewport.cssHeight, 1)
114
+ const radius = 18 + Math.sin(frame * 0.12) * 4
115
+ gl.useProgram(resources.program)
116
+ gl.bindVertexArray(resources.vao)
117
+ gl.uniform2f(resources.center, x / width * 2 - 1, 1 - y / height * 2)
118
+ gl.uniform2f(resources.radius, radius / width, radius / height)
119
+ gl.uniform4f(resources.color, 0.95, 0.45, 0.18, 0.9)
120
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4)
121
+ gl.bindVertexArray(null)
122
+ webglDrawCount.value += 1
123
+ }
124
+ catch (error) {
125
+ emit('notice', `WebGL 自定义层绘制失败:${error instanceof Error ? error.message : String(error)}`)
126
+ }
127
+ }
128
+
129
+ /** 自定义图元:外圈实心 + 十字,颜色 / 描边 / 尺寸都取组件递进来的读数 */
130
+ const paintSpark = (context, info) => {
131
+ const centerX = info.width / 2
132
+ const centerY = info.height / 2
133
+ const radius = Math.min(info.width, info.height) * 0.31
134
+ const stroke = Math.max(1, info.strokeWidth * info.scale)
135
+ context.clearRect(0, 0, info.width, info.height)
136
+ context.beginPath()
137
+ context.arc(centerX, centerY, radius, 0, Math.PI * 2)
138
+ context.fillStyle = info.color
139
+ context.fill()
140
+ context.lineWidth = stroke
141
+ context.strokeStyle = info.strokeColor
142
+ context.stroke()
143
+ context.beginPath()
144
+ context.moveTo(centerX - radius * 0.52, centerY)
145
+ context.lineTo(centerX + radius * 0.52, centerY)
146
+ context.moveTo(centerX, centerY - radius * 0.52)
147
+ context.lineTo(centerX, centerY + radius * 0.52)
148
+ context.lineWidth = Math.max(1, stroke * 0.72)
149
+ context.strokeStyle = 'rgba(255, 255, 255, 0.92)'
150
+ context.stroke()
151
+ }
152
+ /** 同名覆盖的第二支画笔:菱形,用来演示 registerSymbol 重新注册后贴图与标签锚点一起更新 */
153
+ const paintSparkDiamond = (context, info) => {
154
+ const centerX = info.width / 2
155
+ const centerY = info.height / 2
156
+ const radius = Math.min(info.width, info.height) * 0.42
157
+ context.clearRect(0, 0, info.width, info.height)
158
+ context.beginPath()
159
+ context.moveTo(centerX, centerY - radius)
160
+ context.lineTo(centerX + radius, centerY)
161
+ context.lineTo(centerX, centerY + radius)
162
+ context.lineTo(centerX - radius, centerY)
163
+ context.closePath()
164
+ context.fillStyle = info.color
165
+ context.fill()
166
+ context.lineWidth = Math.max(1, info.strokeWidth * info.scale)
167
+ context.strokeStyle = info.strokeColor
168
+ context.stroke()
169
+ context.beginPath()
170
+ context.arc(centerX, centerY, radius * 0.18, 0, Math.PI * 2)
171
+ context.fillStyle = 'rgba(255, 255, 255, 0.92)'
172
+ context.fill()
173
+ }
174
+
175
+ const visiblePointCount = () => view?.getVisibleData()?.points?.length ?? points.length
176
+ const refreshReadout = () => {
177
+ if (!view) {
178
+ return
179
+ }
180
+ const operators = view.listOperators()
181
+ const symbols = view.listSymbols()
182
+ readout.value = [
183
+ `自定义运算符 ${operators.includes('evenIndex') ? 'evenIndex 已注册' : '未注册'}`,
184
+ `筛选 ${operatorOn.value ? '开' : '关'}(可见 ${visiblePointCount()} / ${points.length} 个)`,
185
+ `自定义图元 ${symbols.includes('spark') ? `spark 已注册(${symbolVariant.value === 'diamond' ? '菱形' : '十字'})` : '未注册'}${symbolOn.value ? ' / 使用中' : ''}`,
186
+ `运行时插件 ${pluginOn.value ? '已挂载' : '未挂载'}`,
187
+ `WebGL 上下文 ${view.gl ? '可直接取' : '不可用'}`,
188
+ `WebGL 层 ${webglMounted.value ? `已挂载 · ${webglEnabled.value ? '绘制中' : '已隐藏'} · order ${webglOrder.value} · draw ${webglDrawCount.value} 次` : '未挂载'}`
189
+ ].join(' · ')
190
+ }
191
+
192
+ const buildOptions = () => ({
193
+ points,
194
+ lines,
195
+ pointStyle: { size: 8, color: '#ffffff', strokeColor: '#3678bf', strokeWidth: 2 },
196
+ lineStyle: { width: 1.5, color: 'rgba(120, 168, 226, 0.62)' },
197
+ showGrid: true,
198
+ gridColor: 'rgba(126, 168, 214, 0.2)',
199
+ fitPadding: 40,
200
+ features: 'view',
201
+ label: { enabled: true, content: '{name}', position: 'top', gap: 7, maxCount: 20, style: { color: '#27405f', fontSize: '11px' } },
202
+ onReady: () => emit('notice', '扩展能力:已挂载 5 个扩展点')
203
+ })
204
+
205
+ const toggleOperator = () => {
206
+ operatorOn.value = !operatorOn.value
207
+ view?.setFilter(operatorOn.value ? { extension_index: { evenIndex: true } } : null)
208
+ refreshReadout()
209
+ emit('notice', operatorOn.value ? '自定义运算符 evenIndex 正在筛选偶数节点' : '已清空自定义筛选')
210
+ }
211
+
212
+ const toggleSymbol = () => {
213
+ symbolOn.value = !symbolOn.value
214
+ // 一次批量补丁:只发一条 updatePoints、只记一步历史(与逐个 updatePoint({ symbol }) 的最终数据一致)
215
+ const result = view?.patchPoints(points.map(point => point.id), { symbol: symbolOn.value ? 'spark' : null })
216
+ refreshReadout()
217
+ emit('notice', symbolOn.value ? `已切到自定义图元 spark(${result?.count ?? 0} 个节点)` : '已回到内置圆形图元')
218
+ }
219
+ const cycleSymbolPainter = () => {
220
+ if (!view) {
221
+ return
222
+ }
223
+ const next = symbolVariant.value === 'diamond' ? 'cross' : 'diamond'
224
+ const previous = disposeSymbol
225
+ disposeSymbol = view.registerSymbol('spark', next === 'diamond' ? paintSparkDiamond : paintSpark)
226
+ previous?.()
227
+ symbolVariant.value = next
228
+ refreshReadout()
229
+ emit('notice', `同名重新注册 spark:画笔已换成${next === 'diamond' ? '菱形' : '十字'},画面当场重烘焙`)
230
+ }
231
+
232
+ const togglePlugin = () => {
233
+ pluginOn.value = !pluginOn.value
234
+ if (pluginOn.value) {
235
+ disposePlugin = view?.use(instance => {
236
+ const badge = document.createElement('div')
237
+ badge.className = 'demo-extension-badge'
238
+ badge.dataset.demo = 'extension-plugin-badge'
239
+ badge.textContent = 'plugin · view.use()'
240
+ instance.el.appendChild(badge)
241
+ return {
242
+ destroy() {
243
+ badge.remove()
244
+ }
245
+ }
246
+ }) ?? null
247
+ }
248
+ else {
249
+ disposePlugin?.()
250
+ disposePlugin = null
251
+ }
252
+ refreshReadout()
253
+ emit('notice', pluginOn.value ? '插件已挂载:左下角出现徽标' : '插件已卸载')
254
+ }
255
+
256
+ /** 挂载 / 移除 WebGL 自定义层:层本身不改数据,只在每帧最后叠加一圈光环 */
257
+ const toggleWebgl = () => {
258
+ if (!view) {
259
+ return
260
+ }
261
+ if (webglMounted.value) {
262
+ view.removeWebGLLayer('extension-webgl')
263
+ disposeWebgl = null
264
+ webglMounted.value = false
265
+ webglEnabled.value = false
266
+ webglDrawCount.value = 0
267
+ }
268
+ else {
269
+ disposeWebgl = view.addWebGLLayer({
270
+ id: 'extension-webgl',
271
+ order: webglOrder.value,
272
+ enabled: true,
273
+ draw: drawWebglExtension
274
+ })
275
+ webglMounted.value = true
276
+ webglEnabled.value = true
277
+ webglDrawCount.value = 0
278
+ }
279
+ refreshReadout()
280
+ emit('notice', webglMounted.value ? 'WebGL 自定义层已挂载:第一个节点外出现自定义光环' : 'WebGL 自定义层已移除')
281
+ }
282
+ /** 只切 enabled:层还留着,跳过 draw 但不丢程序与顺序 */
283
+ const toggleWebglEnabled = () => {
284
+ if (!view || !webglMounted.value) {
285
+ return
286
+ }
287
+ webglEnabled.value = !webglEnabled.value
288
+ view.updateWebGLLayer('extension-webgl', { enabled: webglEnabled.value })
289
+ refreshReadout()
290
+ emit('notice', webglEnabled.value ? 'WebGL 自定义层恢复绘制' : 'WebGL 自定义层已隐藏(enabled: false)')
291
+ }
292
+ /** 换 order:单层时看不出叠放差异,但读数与 getWebGLLayers 的排序会跟着变 */
293
+ const cycleWebglOrder = () => {
294
+ if (!view || !webglMounted.value) {
295
+ return
296
+ }
297
+ webglOrder.value = webglOrder.value === 20 ? 40 : 20
298
+ view.updateWebGLLayer('extension-webgl', { order: webglOrder.value })
299
+ refreshReadout()
300
+ emit('notice', `WebGL 自定义层 order 改为 ${webglOrder.value}`)
301
+ }
302
+ const requestWebglFrame = () => {
303
+ view?.requestRender()
304
+ refreshReadout()
305
+ emit('notice', '已调用 requestRender():下一帧会重新执行自定义层的 draw')
306
+ }
307
+
308
+ const exportSummary = () => {
309
+ if (!view) {
310
+ return
311
+ }
312
+ try {
313
+ const result = view.exportData({ format: 'extension-summary', labels: false })
314
+ exportText.value = JSON.stringify(result, null, 2)
315
+ emit('notice', `自定义导出完成:${result.highLoadCount} 个高负载节点`)
316
+ }
317
+ catch (error) {
318
+ exportText.value = `导出失败:${error instanceof Error ? error.message : String(error)}`
319
+ }
320
+ }
321
+
322
+ onMounted(() => {
323
+ view = createGisView(container.value, buildOptions())
324
+ disposeOperator = view.registerOperator('evenIndex', enabled => current => !enabled || Number(current) % 2 === 0)
325
+ disposeSymbol = view.registerSymbol('spark', paintSpark)
326
+ disposeExport = defineExportFormat('extension-summary', ({ document, counts }) => {
327
+ const highLoad = document.points.filter(point => Number(point.max_loading_rate) >= 1)
328
+ return {
329
+ schema: 'gis-view.extension-summary.v1',
330
+ counts,
331
+ highLoadCount: highLoad.length,
332
+ highLoadStations: highLoad.map(point => ({
333
+ id: point.id,
334
+ name: point.name,
335
+ load: point.max_loading_rate
336
+ }))
337
+ }
338
+ })
339
+ refreshReadout()
340
+ })
341
+
342
+ onBeforeUnmount(() => {
343
+ disposePlugin?.()
344
+ disposePlugin = null
345
+ disposeSymbol?.()
346
+ disposeSymbol = null
347
+ disposeOperator?.()
348
+ disposeOperator = null
349
+ disposeExport?.()
350
+ disposeExport = null
351
+ disposeWebgl?.()
352
+ disposeWebgl = null
353
+ view?.destroy()
354
+ view = null
355
+ })
356
+ </script>
357
+
358
+ <template>
359
+ <section class="dv-demo">
360
+ <div class="dv-stage">
361
+ <div ref="container" class="dv-canvas"></div>
362
+ <div class="dv-toolbar">
363
+ <button type="button" @click="view?.fitView(40)">适应全部</button>
364
+ </div>
365
+ </div>
366
+ <aside class="dv-panel">
367
+ <div class="dv-group">
368
+ <h3>五个扩展点</h3>
369
+ <div class="dv-row">
370
+ <button
371
+ type="button"
372
+ class="dv-button"
373
+ :class="{ 'dv-button--primary': operatorOn }"
374
+ :aria-pressed="operatorOn"
375
+ data-demo="extension-operator"
376
+ @click="toggleOperator"
377
+ >{{ operatorOn ? '关闭自定义筛选' : '自定义运算符筛选' }}</button>
378
+ </div>
379
+ <div class="dv-row">
380
+ <button
381
+ type="button"
382
+ class="dv-button"
383
+ :class="{ 'dv-button--primary': symbolOn }"
384
+ :aria-pressed="symbolOn"
385
+ data-demo="extension-symbol"
386
+ @click="toggleSymbol"
387
+ >{{ symbolOn ? '换回内置圆形' : '使用自定义图元' }}</button>
388
+ <button type="button" class="dv-button" :disabled="!symbolOn" data-demo="extension-symbol-variant" @click="cycleSymbolPainter">同名换画笔</button>
389
+ </div>
390
+ <div class="dv-row">
391
+ <button
392
+ type="button"
393
+ class="dv-button"
394
+ :class="{ 'dv-button--primary': pluginOn }"
395
+ :aria-pressed="pluginOn"
396
+ data-demo="extension-plugin"
397
+ @click="togglePlugin"
398
+ >{{ pluginOn ? '卸载运行时插件' : '挂载运行时插件' }}</button>
399
+ </div>
400
+ <div class="dv-row">
401
+ <button type="button" class="dv-button dv-button--primary" data-demo="extension-export" @click="exportSummary">自定义格式导出</button>
402
+ </div>
403
+ <div class="dv-row">
404
+ <button
405
+ type="button"
406
+ class="dv-button"
407
+ :class="{ 'dv-button--primary': webglMounted }"
408
+ :aria-pressed="webglMounted"
409
+ data-demo="extension-webgl"
410
+ @click="toggleWebgl"
411
+ >{{ webglMounted ? '移除 WebGL 自定义层' : '挂载 WebGL 自定义层' }}</button>
412
+ <button type="button" class="dv-button" :disabled="!webglMounted" data-demo="extension-webgl-enabled" @click="toggleWebglEnabled">{{ webglEnabled ? '隐藏层' : '显示层' }}</button>
413
+ </div>
414
+ <div class="dv-row">
415
+ <button type="button" class="dv-button" :disabled="!webglMounted" data-demo="extension-webgl-order" @click="cycleWebglOrder">order {{ webglOrder }}(切换)</button>
416
+ <button type="button" class="dv-button" :disabled="!webglMounted" data-demo="extension-webgl-request" @click="requestWebglFrame">requestRender()</button>
417
+ </div>
418
+ <p class="dv-hint">运算符注册进条件语言,图元注册进符号表,插件挂在实例上,导出格式挂在模块注册表,WebGL 层直接拿 <code>gl</code> 在内置内容之后画自己的东西;五者都不需要改组件源码。图元切换走一次 <code>patchPoints</code> 批量补丁,WebGL 层可用 <code>enabled</code> / <code>order</code> 原地维护。</p>
419
+ </div>
420
+ <div class="dv-group">
421
+ <h3>注册表读数</h3>
422
+ <p class="dv-readout" data-demo="extension-readout" aria-live="polite">{{ readout }}</p>
423
+ </div>
424
+ <div class="dv-group">
425
+ <h3>导出结果</h3>
426
+ <pre class="dv-readout demo-extension-export" data-demo="extension-export-text">{{ exportText }}</pre>
427
+ </div>
428
+ </aside>
429
+ </section>
430
+ </template>
431
+
432
+ <style scoped>
433
+ .demo-extension-export {
434
+ max-height: 190px;
435
+ margin: 0;
436
+ overflow: auto;
437
+ white-space: pre-wrap;
438
+ word-break: break-all;
439
+ font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
440
+ font-size: 11px;
441
+ }
442
+
443
+ :global(.demo-extension-badge) {
444
+ position: absolute;
445
+ left: 10px;
446
+ bottom: 10px;
447
+ z-index: 4;
448
+ padding: 5px 9px;
449
+ border: 1px solid rgba(54, 120, 191, 0.35);
450
+ border-radius: 999px;
451
+ background: rgba(255, 255, 255, 0.9);
452
+ color: #2f5e94;
453
+ font-size: 11px;
454
+ line-height: 1.4;
455
+ pointer-events: none;
456
+ box-shadow: 0 4px 14px rgba(31, 62, 105, 0.14);
457
+ }
458
+ </style>