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.
- package/README.md +40 -8
- package/dist/gis-view.css +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1036 -249
- package/dist/index.js +1 -1
- package/docs/AI/345/257/271/350/257/235/346/261/207/346/200/273.md +194 -0
- package/docs/GisView.md +9 -8
- package/docs/GisView/345/256/236/347/216/260/345/216/237/347/220/206.md +2 -2
- 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
- package/docs/api/index.html +545 -59
- package/docs/demos/BackendSyncDemo.vue +10 -8
- package/docs/demos/BasicDemo.vue +101 -101
- package/docs/demos/BatchAddDemo.vue +5 -5
- package/docs/demos/ChannelDemo.vue +617 -10
- package/docs/demos/ConnectDemo.vue +392 -0
- package/docs/demos/CopyMergeDemo.vue +980 -929
- package/docs/demos/DecorationDraftDemo.vue +1300 -0
- package/docs/demos/DemoShell.vue +569 -240
- package/docs/demos/DragDropDemo.vue +11 -11
- package/docs/demos/EffectsDemo.vue +6 -6
- package/docs/demos/EventDemo.vue +327 -327
- package/docs/demos/ExtensionDemo.vue +458 -0
- package/docs/demos/GisViewDemo.vue +61 -48
- package/docs/demos/GraphicDemo.vue +338 -0
- package/docs/demos/LabelDemo.vue +553 -497
- package/docs/demos/MinimapDemo.vue +358 -0
- package/docs/demos/PerfDemo.vue +29 -0
- package/docs/demos/SearchFilterDemo.vue +412 -0
- package/docs/demos/StyleDemo.vue +121 -121
- package/docs/demos/api-index.js +98 -77
- package/docs/demos/demos.js +159 -135
- package/docs/demos/shared/demoData.js +107 -107
- package/docs/demos/shared/demoStyle.css +249 -249
- package/docs/demos/shared/fakeBackend.js +93 -6
- 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
- package/docs/format/04-/351/200/232/351/201/223/344/270/216/346/240/207/347/255/276.md +5 -4
- 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
- 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
- package/docs/guide/sample-document.html +4 -4
- package/docs/index.html +180 -9
- 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
- package/docs/internals/13-/351/271/260/347/234/274/345/233/276.md +3 -2
- 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
- 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
- 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
- package/docs/manual/02-/345/235/220/346/240/207/344/270/216/346/225/260/346/215/256.md +18 -4
- package/docs/manual/03-/351/205/215/347/275/256/351/241/271.md +110 -25
- package/docs/manual/04-/344/272/213/344/273/266/344/270/216/345/233/236/350/260/203.md +13 -8
- 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
- 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
- package/docs/manual/08-/351/200/211/344/270/255/344/270/216/346/213/220/347/202/271.md +17 -15
- 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
- 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
- package/docs/manual/11-/346/240/267/345/274/217/344/270/216/350/247/204/345/210/231.md +3 -2
- package/docs/manual/12-/346/240/207/347/255/276/344/270/216/350/243/205/351/245/260.md +260 -3
- package/docs/manual/13-/345/212/250/346/225/210/344/270/216/345/233/276/345/205/203.md +6 -5
- package/docs/manual/14-/350/247/206/345/233/276/344/270/216/347/255/233/351/200/211.md +27 -14
- package/docs/manual/15-/345/257/274/345/207/272/344/270/216/351/233/206/346/210/220.md +142 -49
- 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
- package/docs/manual/17-/345/217/202/346/225/260/351/200/237/346/237/245.md +55 -39
- package/docs/manual/18-/351/200/220/346/226/271/346/263/225/347/244/272/344/276/213.md +189 -64
- 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
- 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
- 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
- package/docs/playground/index.html +5 -5
- package/docs/steps/README.md +14 -10
- package/docs/steps/check-doc-truth.mjs +1 -1
- package/docs/steps/selftest-actions.html +10 -10
- package/docs/steps/selftest-data.html +96 -14
- package/docs/steps/selftest-decoration.html +577 -0
- package/docs/steps/selftest-demo.html +6 -5
- package/docs/steps/selftest-demos.html +2201 -1612
- package/docs/steps/selftest-docs.html +89 -16
- package/docs/steps/selftest-effects.html +5 -5
- package/docs/steps/selftest-features.html +757 -86
- package/docs/steps/selftest-full.html +583 -583
- package/docs/steps/selftest-labels.html +1034 -652
- package/docs/steps/selftest-perf.html +1899 -1844
- package/docs/steps/selftest-scale.html +117 -7
- package/docs/steps/selftest-select.html +24 -17
- package/docs/steps/step-10-full.js +1 -1
- package/docs//346/216/245/345/217/243/345/215/217/350/256/256.md +167 -17
- package/docs//346/225/260/346/215/256/346/240/274/345/274/217.md +1 -1
- package/package.json +43 -43
- package/docs/demos/HistoryDemo.vue +0 -230
- package/docs/demos/InterfaceDemo.vue +0 -309
- package/docs/demos/SampleDemo.vue +0 -283
|
@@ -23,6 +23,11 @@ const view = createGisView('#map', {
|
|
|
23
23
|
view.updateOptions({ showGrid: false, zoomStep: 1.6 })
|
|
24
24
|
view.updateOptions({ points: nextPoints, lines: nextLines }, true)
|
|
25
25
|
|
|
26
|
+
// denseMode:大数据展示模式('auto' 缺省 / 'on' / 'off',或对象写法换三档阈值)
|
|
27
|
+
view.updateOptions({ denseMode: 'on' }) // 临时强制进入密集档(标签避让 / 截断,动效暂停)
|
|
28
|
+
view.updateOptions({ denseMode: { mode: 'auto', visibleThreshold: 3000, densityThreshold: 2500, pointThreshold: 60000 } })
|
|
29
|
+
const dense = view.getStats().dense // { mode, active, reason, visiblePoints, visibleThreshold, densityThreshold, pointThreshold }
|
|
30
|
+
|
|
26
31
|
// updatePointStyle(patch):按字段合并全局点样式;写 null 清掉整份
|
|
27
32
|
view.updatePointStyle({ color: '#f60', size: 9 })
|
|
28
33
|
view.updatePointStyle(null)
|
|
@@ -39,13 +44,11 @@ view.updateData(nextPoints, nextLines) // 只换点与线
|
|
|
39
44
|
view.updateData(nextPoints, nextLines, nextChannels) // 切场景一次换完,只发一条 dataChange
|
|
40
45
|
view.updateData({ lines: nextLines, channels: nextChannels }) // 单对象写法:只换给出的那几列
|
|
41
46
|
|
|
42
|
-
//
|
|
43
|
-
view.
|
|
44
|
-
view.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
view.updatePoints([{ id: 'a', x: 0, y: 0 }, { id: 'b', x: 400, y: 200 }])
|
|
48
|
-
view.updateLines([{ id: 'l1', from: 'a', to: 'b' }])
|
|
47
|
+
// 只换一列也走 updateData 的单对象写法(没给的那几列不动)
|
|
48
|
+
view.updateData({ channels: [{ id: 'c1', name: '二号通道', lineIds: ['l1', 'l2'] }] })
|
|
49
|
+
view.updateData({ channels: [] }) // 清空通道声明
|
|
50
|
+
view.updateData({ points: [{ id: 'a', x: 0, y: 0 }, { id: 'b', x: 400, y: 200 }] })
|
|
51
|
+
view.updateData({ lines: [{ id: 'l1', from: 'a', to: 'b' }] })
|
|
49
52
|
|
|
50
53
|
// importData(input, options?):标准文档 / 与创建入参同构的对象 / 节点数组都能灌
|
|
51
54
|
view.importData({ format: 'gis-view', points: nextPoints, lines: nextLines, channels: nextChannels })
|
|
@@ -86,8 +89,14 @@ view.updateChannel('c1', { name: '东干线' }, [
|
|
|
86
89
|
], ['l1', 'l2']) // 表里没有了的旧成员直接从数据里删掉(要只移出用 removeChannelLines)
|
|
87
90
|
view.updateChannel('c1', {}, [{ id: 'l1', width: 4 }]) // 只传线路数组:只改成员回线的字段,数量不动
|
|
88
91
|
|
|
89
|
-
//
|
|
90
|
-
view.
|
|
92
|
+
// setFeatureVisible(kind, refs, visible) / hideFeatures(kind, refs) / showFeatures(kind, refs):只改 visible,不删数据
|
|
93
|
+
const hiddenChannel = view.hideFeatures('channel', 'c1') // 'point' / 'line' / 'channel',refs 单个 / 数组 / 'selection'
|
|
94
|
+
view.showFeatures('channel', 'c1') // 成员回线、分组、拐点原样回来
|
|
95
|
+
view.setFeatureVisible('point', ['a', 'b'], false) // 一批节点一次收起(一条历史、一条 batch)
|
|
96
|
+
console.log(hiddenChannel.ok, hiddenChannel.indexes, hiddenChannel.skipped)
|
|
97
|
+
|
|
98
|
+
// 整份换通道声明(与 updateChannel 的分工:这条全换、点不出某一条)
|
|
99
|
+
view.updateData({ channels: [{ id: 'c1', lineIds: ['l1', 'l2'] }, { id: 'c2', from: 'a', to: 'b' }] })
|
|
91
100
|
|
|
92
101
|
// patchPoints(entries, patch?):批量打补丁 —— 每条自带补丁,或一批定位 + 一份公共补丁
|
|
93
102
|
view.patchPoints([{ id: 'a', name: 'A' }, { index: 1, x: 10 }])
|
|
@@ -204,9 +213,9 @@ view.setQuality('lean')
|
|
|
204
213
|
view.setQuality({ mode: 'balanced', budgetMs: 8 })
|
|
205
214
|
const quality = view.getQuality()
|
|
206
215
|
|
|
207
|
-
// getEffects() /
|
|
208
|
-
view.
|
|
209
|
-
view.
|
|
216
|
+
// getEffects() / updateOptions({ effects }):全局动画配置(effects: false = 一处关掉),写法与 options.effects 一致
|
|
217
|
+
view.updateOptions({ effects: false })
|
|
218
|
+
view.updateOptions({ effects: { line: { size: 10, speed: 60 }, point: { radius: 30 } } })
|
|
210
219
|
const effects = view.getEffects()
|
|
211
220
|
|
|
212
221
|
// getLineEffects() / getPointEffects():这一帧动点 / 涟漪的读数(没配时是空表)
|
|
@@ -275,10 +284,10 @@ const lightContext = view.getContextAt(320, 200, { selection: false, lngLat: fal
|
|
|
275
284
|
const nearPoints = view.nearestPointsAt(320, 200, { limit: 3, withScreen: true })
|
|
276
285
|
const nearLine = view.nearestLineAt(320, 200, { maxDistance: 60 }) // 落点比例在 nearLine.position
|
|
277
286
|
|
|
278
|
-
// setDropPreview(preview) / getDropPreview()
|
|
287
|
+
// setDropPreview(preview) / getDropPreview():拖拽落点预览(setDropPreview(null) 清掉)
|
|
279
288
|
view.setDropPreview({ kind: 'point', screen: [320, 200], item: { name: '新站' } })
|
|
280
289
|
const preview = view.getDropPreview()
|
|
281
|
-
view.
|
|
290
|
+
view.setDropPreview(null)
|
|
282
291
|
|
|
283
292
|
// getPoint(ref) / getLine(ref) / getChannel(ref):一个要素的几何读数
|
|
284
293
|
const pointInfo = view.getPoint('a')
|
|
@@ -313,14 +322,14 @@ const state = view.getViewState()
|
|
|
313
322
|
#### 选中、复制与合并
|
|
314
323
|
|
|
315
324
|
```js
|
|
316
|
-
//
|
|
317
|
-
view.
|
|
318
|
-
view.
|
|
319
|
-
view.
|
|
325
|
+
// selectFeatures(refs, options?):接口选中(与点击选中走同一份 state)
|
|
326
|
+
view.selectFeatures('a') // 选中一个
|
|
327
|
+
view.selectFeatures(['a', 'b']) // 一批
|
|
328
|
+
view.selectFeatures('a', { mode: 'add' }) // mode:replace(缺省)/ add / toggle / remove
|
|
320
329
|
view.selectFeatures([{ type: 'line', index: 0 }], { mode: 'toggle' })
|
|
321
|
-
view.
|
|
330
|
+
view.selectFeatures(null) // 清空(空数组同义)
|
|
322
331
|
|
|
323
|
-
// deselect(refs?):取消选中(写法与
|
|
332
|
+
// deselect(refs?):取消选中(写法与 selectFeatures 一致;不传 / null 全清,空数组什么都不做)
|
|
324
333
|
view.deselect('a')
|
|
325
334
|
view.deselect(['a', 'b'])
|
|
326
335
|
view.deselect()
|
|
@@ -369,14 +378,21 @@ const mergedCustom = view.mergePoints('a', 'b', { item: { name: '合并结果',
|
|
|
369
378
|
const mergedAll = view.mergeSelectedPoints()
|
|
370
379
|
view.mergeSelectedPoints({ target: 'b', sources: ['a', 'c'], force: true })
|
|
371
380
|
|
|
372
|
-
//
|
|
381
|
+
// splitPoint(source, parts, options?):把一个节点拆成多份(mergePoints 的逆操作)
|
|
382
|
+
const splitResult = view.splitPoint('a', [
|
|
383
|
+
{ x: 0, y: 0, name: '甲站(主)' }, // 第一份沿用原下标 / id
|
|
384
|
+
{ x: 80, y: 40, name: '甲站(东)', lines: ['l1'] } // 显式分给第二份;未分配的线路跟第一份
|
|
385
|
+
], { label: '拆分甲站' })
|
|
386
|
+
console.log(splitResult.pointIds, splitResult.linesChanged, splitResult.changes)
|
|
387
|
+
|
|
388
|
+
// getPointMerge() / resolvePointMerge(confirmed?, options?) / commitPointMerge() / revertPointMerge()
|
|
373
389
|
const pendingMerge = view.getPointMerge() // 待确认的那一份载荷(没有就是 null)
|
|
374
390
|
console.log(pendingMerge?.impact?.lineCount) // 这次合并会影响几条线路(改写 + 删除,按线路去重)
|
|
375
391
|
console.log(pendingMerge?.impact?.lineIds) // 具体是哪几条(没有 id 时是 lines 下标)
|
|
376
392
|
view.resolvePointMerge(true) // 确认(false = 取消)
|
|
377
393
|
view.resolvePointMerge(true, { keep: 'source', item: { name: '合并结果', size: 18 } })
|
|
378
|
-
view.
|
|
379
|
-
view.
|
|
394
|
+
view.resolvePointMerge(true, { keep: 'target', item: { color: '#2f6fd0', strokeColor: '#ffffff' } })
|
|
395
|
+
view.resolvePointMerge(false) // 取消(拖动的节点回原位,数据不动)
|
|
380
396
|
view.commitPointMerge() // 自己接管数据时收尾
|
|
381
397
|
view.revertPointMerge() // 反悔(silent: true 时不补发 cancel 载荷)
|
|
382
398
|
|
|
@@ -427,6 +443,21 @@ const groups = view.getChannelGroups('c1')
|
|
|
427
443
|
const allGroups = view.getChannelGroups()
|
|
428
444
|
const groupWaypoints = view.getChannelGroupWaypoints('c1', 'c1::2')
|
|
429
445
|
|
|
446
|
+
// getChangedChannels(options?) / clearChangedChannels(refs?):通道变化对账
|
|
447
|
+
view.updateChannel('c1', { name: '东干线' })
|
|
448
|
+
view.addChannelGroupWaypoint('c1', 'c1', [200, 120]) // 拐点变化入账(单组通道的组 id 就是通道 id)
|
|
449
|
+
const changedChannels = view.getChangedChannels() // [{ index, id, item, groups, reasons, … }]
|
|
450
|
+
console.log(changedChannels[0]?.reasons) // ['waypoints'] / ['members'] / ['visibility']…
|
|
451
|
+
const waypointOnly = view.getChangedChannels({ reasons: ['waypoints'], ignoreGroups: true })
|
|
452
|
+
view.clearChangedChannels('c1') // 发完接口对账这一条;不传 = 全部对账
|
|
453
|
+
|
|
454
|
+
// getChangedLines(options?) / clearChangedLines(refs?):只有节点和线路时也能独立对账
|
|
455
|
+
view.updateLine('l1', { name: '东联络线' })
|
|
456
|
+
const changedLines = view.getChangedLines() // [{ index, id, item, path, from, to, reasons, … }]
|
|
457
|
+
console.log(changedLines[0]?.reasons) // ['fields'] / ['path'] / ['endpoints'] / ['visibility']
|
|
458
|
+
const pathOnly = view.getChangedLines({ reasons: ['path'] })
|
|
459
|
+
view.clearChangedLines('l1') // 发完接口对账这一条;不传 = 全部对账
|
|
460
|
+
|
|
430
461
|
// getChannelContext(channelId?, groupId?):右键交互框那一份读数(含 actions 与各自能不能点)
|
|
431
462
|
const channelContext = view.getChannelContext('c1')
|
|
432
463
|
const groupContext = view.getChannelContext('c1', 'c1::2')
|
|
@@ -468,9 +499,18 @@ view.runChannelAction('split', { lineId: 'l2' })
|
|
|
468
499
|
view.addChannelGroupWaypoint('c1', 'c1::2', [200, 120])
|
|
469
500
|
view.removeChannelGroupWaypoint('c1', 'c1::2', 1)
|
|
470
501
|
|
|
471
|
-
// beginAddChannel(options?) / getAddChannelDraft() / continueAddChannel(options?) / commitAddChannel(resolution?) / cancelAddChannel() / endAddChannel()
|
|
472
|
-
view.beginAddChannel({
|
|
473
|
-
|
|
502
|
+
// beginAddChannel(options?) / getAddChannelDraft() / updateAddChannelStyle(patch?) / continueAddChannel(options?) / commitAddChannel(resolution?) / cancelAddChannel() / endAddChannel()
|
|
503
|
+
view.beginAddChannel({
|
|
504
|
+
kind: 'channel',
|
|
505
|
+
circuits: 3,
|
|
506
|
+
channelDraftStyle: {
|
|
507
|
+
color: '#2f6fd0', width: 2.5, arrow: true, arrowSize: 14, arrowPositions: [0, 0.5, 1],
|
|
508
|
+
circuitSpacing: 12, opacity: 0.9
|
|
509
|
+
}
|
|
510
|
+
}) // 点两个节点:第一个起点、第二个终点;通道 3 回时预览也画 3 条平行回线
|
|
511
|
+
const draft = view.getAddChannelDraft() // 草稿读数(跟随光标的预览、两个节点、方案表与实际 style)
|
|
512
|
+
view.updateAddChannelStyle({ arrowPositions: [0.25, 0.75], arrowReverse: true }) // 字段级合并,只改箭头
|
|
513
|
+
view.updateAddChannelStyle(null) // 回到全局 channelDraftStyle
|
|
474
514
|
view.commitAddChannel() // 两个节点选完后落地(resolution 缺省用推荐方案)
|
|
475
515
|
view.commitAddChannel({ type: 'joinGroup', groupId: 'c1::2' }) // 指定方案
|
|
476
516
|
view.continueAddChannel({ kind: 'line' }) // 连续模式下开下一段(也能换这一段加什么)
|
|
@@ -528,24 +568,24 @@ view.removeLines('l1') // 单条也认
|
|
|
528
568
|
view.removeChannel('c1')
|
|
529
569
|
```
|
|
530
570
|
|
|
531
|
-
####
|
|
571
|
+
#### 定向改一个要素(一次只动一条)
|
|
532
572
|
|
|
533
573
|
```js
|
|
534
|
-
//
|
|
535
|
-
view.
|
|
536
|
-
view.
|
|
537
|
-
view.
|
|
538
|
-
view.
|
|
539
|
-
view.
|
|
574
|
+
// updatePoint(id, patch):只写要改的字段(size / symbol / color…)
|
|
575
|
+
view.updatePoint('a', { size: 12 })
|
|
576
|
+
view.updatePoint('a', { symbol: { kind: 'square' } })
|
|
577
|
+
view.updatePoint('a', { symbol: 'https://example.com/pin.png' })
|
|
578
|
+
view.updatePoint('a', { symbol: null }) // 回到内置圆
|
|
579
|
+
view.updatePoint('a', { color: '#ff6b35' })
|
|
540
580
|
|
|
541
581
|
// movePoint(id, coordinate) / movePoint(id, x, y)
|
|
542
582
|
view.movePoint('a', { x: 120, y: 80 })
|
|
543
583
|
view.movePoint('a', [120, 80])
|
|
544
584
|
view.movePoint('a', 120, 80)
|
|
545
585
|
|
|
546
|
-
//
|
|
547
|
-
view.
|
|
548
|
-
view.
|
|
586
|
+
// updateLine(id, patch) / setLineWaypoints(id, coordinates)
|
|
587
|
+
view.updateLine('l1', { width: 3 })
|
|
588
|
+
view.updateLine('l1', { color: '#f00' })
|
|
549
589
|
view.setLineWaypoints('l1', [[100, 40], [200, 120]]) // 中间拐点(不含首尾两端)
|
|
550
590
|
view.setLineWaypoints('l1', [120, 80]) // 只留一个拐点
|
|
551
591
|
view.setLineWaypoints('l1', []) // 拉直
|
|
@@ -559,16 +599,43 @@ view.updateLineDecoration('l1', 0, { size: 20, color: '#f60' })
|
|
|
559
599
|
view.removeLineDecoration('l1', 0)
|
|
560
600
|
view.setLineDecorations('l1', []) // 全摘掉
|
|
561
601
|
|
|
562
|
-
//
|
|
563
|
-
|
|
564
|
-
view.
|
|
565
|
-
view.
|
|
566
|
-
view.
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
602
|
+
// getPointDecorations(id) / setPointDecorations(id, list) / addPointDecoration(id, decoration)
|
|
603
|
+
// updatePointDecoration(id, index, patch) / removePointDecoration(id, index)
|
|
604
|
+
const pointDecorations = view.getPointDecorations('node-0')
|
|
605
|
+
view.setPointDecorations('node-0', [{ symbol: 'square', size: 14, offset: [0, -24] }])
|
|
606
|
+
view.addPointDecoration('node-0', {
|
|
607
|
+
symbol: 'star',
|
|
608
|
+
size: 20,
|
|
609
|
+
direction: 45,
|
|
610
|
+
distance: 32,
|
|
611
|
+
connector: { color: '#7fa9dc', width: 1.2, dashed: true }
|
|
612
|
+
})
|
|
613
|
+
view.updatePointDecoration('node-0', 0, { size: 24, color: '#ff6644' })
|
|
614
|
+
view.removePointDecoration('node-0', 0)
|
|
615
|
+
view.setPointDecorations('node-0', []) // 全摘掉
|
|
616
|
+
|
|
617
|
+
// beginDecorationDraft(options?) / getDecorationDraft() / updateDecorationDraft(patch?)
|
|
618
|
+
// commitDecorationDraft(overrides?) / cancelDecorationDraft(options?)
|
|
619
|
+
view.beginDecorationDraft({ target: 'line', symbol: 'square', size: 20, parallel: true, scope: 'channel' })
|
|
620
|
+
view.updateDecorationDraft({ symbol: 'circle', size: 24, parallel: false, scope: 'group' })
|
|
621
|
+
view.updateDecorationDraft({ target: 'point', placement: 'offset', anchor: 'fixed', direction: 90, distance: 36, connector: true })
|
|
622
|
+
view.updateDecorationDraft({ target: 'both', allowedPoints: ['node-0', 'node-1'], maxPerPoint: 2 })
|
|
623
|
+
view.updateDecorationDraft({ allowDuplicate: false, duplicateStrategy: 'offset', duplicateDistance: 8 })
|
|
624
|
+
const currentDraft = view.getDecorationDraft() // phase / hitKind / targets / pointTargets / position...
|
|
625
|
+
const committedDraft = view.commitDecorationDraft({ position: 0.5 })
|
|
626
|
+
if (!committedDraft.ok) console.warn(committedDraft.reason)
|
|
627
|
+
view.cancelDecorationDraft() // 没有草稿时是空操作,数据不动
|
|
628
|
+
|
|
629
|
+
// updateChannel(id, patch):宽度 / 取宽系数 / 内收 / 切点内收
|
|
630
|
+
view.updateChannel('c1', { width: 24 })
|
|
631
|
+
view.updateChannel('c1', { nodeWidth: 0 }) // 关掉「按两端节点尺寸取宽」,改用 width
|
|
632
|
+
view.updateChannel('c1', { nodeWidth: 1 })
|
|
633
|
+
view.updateChannel('c1', { inset: 2 })
|
|
634
|
+
view.updateChannel('c1', { tangentInset: 0 }) // 0 = 切点落在节点边上;null 回默认
|
|
635
|
+
|
|
636
|
+
// updateChannel(id, { name }) / setChannelLineWidth(id, width) / setChannelColors(id, colors)
|
|
637
|
+
view.updateChannel('c1', { name: '东干线' })
|
|
638
|
+
view.updateChannel('c1', { name: null }) // 摘掉名称
|
|
572
639
|
view.setChannelLineWidth('c1', 3)
|
|
573
640
|
view.setChannelColors('c1', ['#f00', '#0f0']) // 不够时按顺序循环取
|
|
574
641
|
view.setChannelColors('c1', '#2f6fd0') // 只给一个 = 整条通道一个色
|
|
@@ -637,34 +704,92 @@ view.resetLabels() // 两类都复位
|
|
|
637
704
|
view.resetLabels('line')
|
|
638
705
|
```
|
|
639
706
|
|
|
707
|
+
#### 扩展图形
|
|
708
|
+
|
|
709
|
+
```js
|
|
710
|
+
// addGraphic(graphic) / addGraphics(list) / updateGraphic(id, patch) / removeGraphic(id) / clearGraphics() / getGraphic(id) / getGraphics()
|
|
711
|
+
// SVG 路径:viewBox 是路径坐标系,width / height 是屏幕尺寸
|
|
712
|
+
view.addGraphic({
|
|
713
|
+
id: 'zone-frame', type: 'path', x: 120, y: 80,
|
|
714
|
+
d: 'M0 0H200V120H0Z', viewBox: [0, 0, 200, 120],
|
|
715
|
+
width: 200, height: 120, anchor: 'center',
|
|
716
|
+
fill: 'rgba(47,111,208,0.08)', stroke: '#2f6fd0', strokeWidth: 2
|
|
717
|
+
})
|
|
718
|
+
|
|
719
|
+
// 一批图形:图片与外部 Canvas
|
|
720
|
+
view.addGraphics([
|
|
721
|
+
{ id: 'logo', type: 'image', src: '/logo.png', x: 260, y: 80, width: 48, height: 48, anchor: 'bottom-left' },
|
|
722
|
+
{ id: 'badge', type: 'canvas', canvas, x: 260, y: 140, width: 60, height: 20, scaleWithView: true }
|
|
723
|
+
])
|
|
724
|
+
|
|
725
|
+
// 改位置与尺寸(style.width / style.height 也认);随视图缩放
|
|
726
|
+
view.updateGraphic('zone-frame', { x: 160, y: 100, style: { width: 260, height: 150 }, scaleWithView: true })
|
|
727
|
+
|
|
728
|
+
view.getGraphic('logo') // 一条图形的副本
|
|
729
|
+
view.getGraphics() // 全部图形,按 zIndex / 加入顺序
|
|
730
|
+
view.removeGraphic('badge') // 删一条,返回删掉的读数
|
|
731
|
+
view.clearGraphics() // 清空整层,返回条数
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
#### WebGL 自定义层
|
|
735
|
+
|
|
736
|
+
```js
|
|
737
|
+
// requestRender() / addWebGLLayer(layer) / updateWebGLLayer / removeWebGLLayer / clearWebGLLayers / getWebGLLayers
|
|
738
|
+
const offGlow = view.addWebGLLayer({
|
|
739
|
+
id: 'glow', order: 10,
|
|
740
|
+
draw({ gl, canvas, viewport, project, requestRender }) {
|
|
741
|
+
const [x, y] = project(120, 40) // 世界坐标 -> 画布 CSS 像素
|
|
742
|
+
gl.enable(gl.BLEND)
|
|
743
|
+
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
|
|
744
|
+
// 这里直接用 gl 画自己的缓冲区 / shader;canvas.width / height 是设备像素
|
|
745
|
+
requestRender() // 需要连续动画时在层内继续排下一帧
|
|
746
|
+
}
|
|
747
|
+
})
|
|
748
|
+
view.updateWebGLLayer('glow', { enabled: false }) // 暂停这一层
|
|
749
|
+
view.updateWebGLLayer('glow', { enabled: true, order: 20 })
|
|
750
|
+
view.getWebGLLayers() // [{ id, order, enabled, draw }, ...]
|
|
751
|
+
view.requestRender() // 外部数据变了,主动重画一帧
|
|
752
|
+
offGlow() // 卸载 addWebGLLayer 返回的函数
|
|
753
|
+
view.clearWebGLLayers() // 清空全部自定义层
|
|
754
|
+
```
|
|
755
|
+
|
|
640
756
|
#### 导出
|
|
641
757
|
|
|
642
758
|
```js
|
|
643
|
-
// exportData(options?)
|
|
644
|
-
const viewFormat = view.exportData()
|
|
759
|
+
// exportData(options?):统一入口 —— scope 选范围(all / viewport / selection),parts 选分项,format 选口径
|
|
760
|
+
const viewFormat = view.exportData() // 全量 + view 口径(缺省)
|
|
761
|
+
const inViewport = view.exportData({ scope: 'viewport', margin: 24 }) // 视口内,可外扩 24 屏幕像素
|
|
762
|
+
const picked = view.exportData({ scope: 'selection', parts: ['station', 'channel'], withLines: true })
|
|
645
763
|
const fullDoc = view.exportData({ format: 'full' }) // 标准数据文档:整份喂回 createGisView / importData 就回到这一屏
|
|
646
764
|
const testFormat = view.exportData({ format: 'test', parts: ['line', 'waypoint'] })
|
|
647
765
|
const onlyPoints = view.exportData({ points: ['a', 'b'], labels: false, extra: { exported_at: Date.now() } })
|
|
648
766
|
|
|
649
|
-
//
|
|
650
|
-
const
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
const
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
767
|
+
// 自定义格式:挂载时注册并拿到清理函数;adapter 拿到的是归一后的标准文档
|
|
768
|
+
const disposeBackendDto = defineExportFormat('backend-dto', ({ document, counts }) => ({
|
|
769
|
+
schema: 'backend.v1',
|
|
770
|
+
counts,
|
|
771
|
+
stations: document.points.map(point => ({ id: point.id, x: point.x, y: point.y }))
|
|
772
|
+
}))
|
|
773
|
+
const dto = view.exportData({ format: 'backend-dto' })
|
|
774
|
+
disposeBackendDto() // 卸载时只清自己的 adapter(registerExportFormat / unregisterExportFormat 也仍可用)
|
|
775
|
+
|
|
776
|
+
// 原来的分项入口全部由统一入口代替
|
|
777
|
+
const inViewportExpanded = view.exportData({ scope: 'viewport', margin: 40 })
|
|
778
|
+
view.exportData({ scope: 'viewport', rect: view.getViewRect(), parts: ['station', 'channel'] })
|
|
779
|
+
const stationData = view.exportData({ format: 'new', parts: ['station'] })
|
|
780
|
+
const pointData = view.exportData({ format: 'test', parts: ['station'] })
|
|
781
|
+
const lineData = view.exportData({ format: 'test', parts: ['line'] })
|
|
782
|
+
const channelData = view.exportData({ format: 'new', parts: ['channel'] })
|
|
783
|
+
const parallelData = view.exportData({ format: 'test', parts: ['waypoint'], all: true })
|
|
659
784
|
|
|
660
785
|
// exportImage(options?):导出图片(Promise<dataURL>;type: 'svg' 走矢量导出)
|
|
661
786
|
const imageUrl = await view.exportImage({ scale: 2, background: '#ffffff' })
|
|
662
787
|
await view.exportImage({ type: 'image/jpeg', quality: 0.9, download: '场景.jpg' }) // 也可以直接下载
|
|
663
788
|
|
|
664
|
-
// exportSelection(options?) /
|
|
789
|
+
// exportSelection(options?) / exportData({ scope: 'selection' }):导出选中的那几个
|
|
665
790
|
const exportedSelection = await view.exportSelection({ withLines: true, scale: 2 })
|
|
666
791
|
view.exportSelection({ points: ['a', 1], format: 'data', withLines: false })
|
|
667
|
-
const selectionData = view.
|
|
792
|
+
const selectionData = view.exportData({ scope: 'selection', parts: ['station', 'channel'] })
|
|
668
793
|
```
|
|
669
794
|
|
|
670
795
|
#### 筛选
|
|
@@ -675,8 +800,8 @@ view.setFilter({ vn_kv: { gte: 525 } })
|
|
|
675
800
|
view.setFilter({ points: { station_type: 'coal' }, lines: { vn_kv: { gte: 525 } }, cascade: 'binding' })
|
|
676
801
|
view.setFilter(null)
|
|
677
802
|
|
|
678
|
-
//
|
|
679
|
-
view.
|
|
803
|
+
// getFilter() / getFilterResult():当前条件(副本)/ 最近一次结果
|
|
804
|
+
view.setFilter(null)
|
|
680
805
|
const filter = view.getFilter()
|
|
681
806
|
const filterResult = view.getFilterResult() // { hiddenPoints, hiddenLines, visiblePoints, visibleLines, worker, durationMs }
|
|
682
807
|
```
|
|
@@ -692,7 +817,7 @@ view.off('dataChange') // 摘掉这个事件的全部回调
|
|
|
692
817
|
view.off('dataChange', sync) // 只摘掉这一个(返回摘掉几条)
|
|
693
818
|
|
|
694
819
|
// use(plugin):运行时挂插件(立即执行一次,返回卸载函数)
|
|
695
|
-
const dispose = view.use(view => ({ destroy() { /*
|
|
820
|
+
const dispose = view.use((view, context) => ({ destroy() { /* 插件自己的清理;context.apiVersion 当前为 1 */ } }))
|
|
696
821
|
dispose()
|
|
697
822
|
|
|
698
823
|
// getFeatures() / setFeatures(next):功能开关的读 / 写(预设名 / 开关表 / 布尔值)
|
|
@@ -733,7 +858,7 @@ const off = view.registerSymbol('三角', (context, info) => {
|
|
|
733
858
|
|
|
734
859
|
// 注册过之后:新增的点、改图标都能直接用这个名字
|
|
735
860
|
view.addPoint({ id: 'c', x: 120, y: 0, size: 22, symbol: '三角' })
|
|
736
|
-
view.
|
|
861
|
+
view.updatePoint('a', { symbol: '三角' }) // 换一个已经在画面上的点的图标:当场重解析
|
|
737
862
|
view.listSymbols() // ['三角']
|
|
738
863
|
view.unregisterSymbol('三角') // 直接注销(返回 true / false)
|
|
739
864
|
off() // 注册时返回的注销函数:幂等,重复调用只清一次
|
|
@@ -77,7 +77,7 @@ canvas.addEventListener('dragover', (event) => {
|
|
|
77
77
|
canvas.addEventListener('drop', (event) => {
|
|
78
78
|
const [x, y] = view.clientToScreen(event.clientX, event.clientY)
|
|
79
79
|
const line = view.nearestLineAt(x, y, { maxDistance: 60 })
|
|
80
|
-
view.
|
|
80
|
+
view.setDropPreview(null)
|
|
81
81
|
if (line) {
|
|
82
82
|
// line.position 是落点投到线上的比例:加拐点 / 挂装饰直接用它
|
|
83
83
|
view.addLineDecoration(line.index, { position: line.position, symbol: 'circle', size: 16, color: '#e0592e' })
|
|
@@ -86,12 +86,12 @@ canvas.addEventListener('drop', (event) => {
|
|
|
86
86
|
```
|
|
87
87
|
|
|
88
88
|
- `kind` 决定预览走哪条链:`point` / `waypoint` / `decoration` 走节点样式链,`line` / `channel` 走线路样式链,`label` 只画落点光环(标签没有地图图标)。
|
|
89
|
-
- 读数里 `size` 是这一刻画出来的宽度、`radius` 是这一刻的屏幕半径,`styleSize` 才是样式链定下来的那一份 —— 与落地后 `getPointStyle().size` 是同一个数(不悬停 / 不选中时 `size` 与它相同);`setDropPreview(null)`
|
|
89
|
+
- 读数里 `size` 是这一刻画出来的宽度、`radius` 是这一刻的屏幕半径,`styleSize` 才是样式链定下来的那一份 —— 与落地后 `getPointStyle().size` 是同一个数(不悬停 / 不选中时 `size` 与它相同);`setDropPreview(null)` 收掉预览,`getDropPreview()` 随时读当前那一份。
|
|
90
90
|
- `nearestPointsAt` / `nearestLineAt` 内部走点网格 / 线段网格(与拾取同一份索引),十万级数据下每帧调也只碰光标附近的单元格;两处的 `maxDistance` 都是**屏幕像素**。
|
|
91
91
|
- 完整可跑的示例见 `docs/demos/DragDropDemo.vue`(演示页的「拖拽落点」页签),自测见 `docs/steps/selftest-demos.html`。
|
|
92
92
|
- 拖拽期间不要浏览器默认的那一个拖影:把 `dataTransfer.setDragImage()` 指到一个 1×1 的透明节点上,光标下就只剩组件画出来的落点图标(`DragDropDemo.vue` 里的 `ensureDragGhost()` 在挂载时建好、卸载时摘掉)。
|
|
93
|
-
- 「拖到落地」中间可以停一步:`drop` 时不写数据,落点的字段(名字 / 电压等级 / 图标…)交给自己的面板去改,改一次就按同一份字段再 `setDropPreview()` 一次 —— 预览与落地走的是同一个解析结果,尺寸 / 颜色 / 符号完全一致;确认了再 `addPoint()` / `addLines()`,不想要就 `
|
|
94
|
-
- 松手之后那条 `dragend` 不要拿来收预览:浏览器在 `drop` 之后还会给拖动的源元素补一条 `dragend`,顺手 `
|
|
93
|
+
- 「拖到落地」中间可以停一步:`drop` 时不写数据,落点的字段(名字 / 电压等级 / 图标…)交给自己的面板去改,改一次就按同一份字段再 `setDropPreview()` 一次 —— 预览与落地走的是同一个解析结果,尺寸 / 颜色 / 符号完全一致;确认了再 `addPoint()` / `addLines()`,不想要就 `setDropPreview(null)` 收掉,`getDropPreview()` 随时能读到当前那一份。`docs/demos/DragDropDemo.vue` 的「按属性新增 / 取消新增」就是这条路的完整示例。
|
|
94
|
+
- 松手之后那条 `dragend` 不要拿来收预览:浏览器在 `drop` 之后还会给拖动的源元素补一条 `dragend`,顺手 `setDropPreview(null)` 的话,「松手先显示在地图上」只剩一眨眼。写法是「有草稿就把预览按原字段摆回落点,没有才收掉」——`DragDropDemo.vue` 里就是 `settleTransientPreview()`:拖出画布(`dragleave`)与 `dragend` 都走它,中途拖了别的项进来又没落上时,点会回到草稿自己的位置上;真正收掉预览的只有「按属性新增」与「取消新增」这两条路。
|
|
95
95
|
|
|
96
96
|
---
|
|
97
97
|
|
package/docs/manual/20-/346/200/247/350/203/275/344/270/216/345/244/247/346/225/260/346/215/256.md
CHANGED
|
@@ -2,13 +2,43 @@
|
|
|
2
2
|
|
|
3
3
|
> 十万级数据的开关与实测、WebGPU 取舍、渲染后端开关。
|
|
4
4
|
|
|
5
|
+
## 大数据展示模式(denseMode)
|
|
6
|
+
|
|
7
|
+
`denseMode` 是数据侧的自动降级开关,解决的是「数据本身已经铺满屏,再按完整档画标签与动效只会把主线程拖垮」这一类场景。它不改数据、不改相机,也不替你隐藏要素,只切换展示策略:
|
|
8
|
+
|
|
9
|
+
- **进入密集档后**:节点 / 线路标签自动走避让与截断(等价 `labelOptimize: true`,扫描上限仍按 `label.scanLimit`),线路动点与节点涟漪暂停;用户写在 `effects` 里的配置仍保留在 `getOptions()` 中,退出密集档后按原配置恢复。
|
|
10
|
+
- **自动判定有三路信号,任意一项越线就进入**:视口内可见节点数达到 `visibleThreshold`(缺省 4000);可见密度达到 `densityThreshold`(缺省每百万 CSS 像素 3000 个);数据总量达到 `pointThreshold`(缺省 50000,不要求同屏)。
|
|
11
|
+
- **回滞与延迟**:判定延后 160 ms 执行;退出要回落到进入阈值的 78%(留 22% 缓冲),避免在阈值边界往返缩放时标签层一帧一变。总量触发时同样按 78% 的阈值退出;发散渲染还没铺完时自动等下一轮再数。
|
|
12
|
+
- **强制档**:`'on'` / `'off'`(或 `true` / `false`)绕过自动判定;运行时 `updateOptions({ denseMode })` 立即重判。阈值只在对象写法的 `{ mode, visibleThreshold, densityThreshold, pointThreshold }` 里改,非正数按缺省处理。
|
|
13
|
+
|
|
14
|
+
读数是 `getStats().dense`,`onStats` / `onRender` 的载荷里也带同一项:
|
|
15
|
+
|
|
16
|
+
```js
|
|
17
|
+
const view = createGisView(container, {
|
|
18
|
+
points,
|
|
19
|
+
lines,
|
|
20
|
+
denseMode: { mode: 'auto', visibleThreshold: 3000, densityThreshold: 2500, pointThreshold: 60000 }
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const dense = view.getStats().dense
|
|
24
|
+
// { mode: 'auto', active: true, reason: 'density', visiblePoints: 3000,
|
|
25
|
+
// visibleThreshold: 3000, densityThreshold: 2500, pointThreshold: 60000 }
|
|
26
|
+
|
|
27
|
+
view.updateOptions({ denseMode: 'on' }) // 手动进入
|
|
28
|
+
view.updateOptions({ denseMode: 'off' }) // 手动退出
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`reason` 的取值:`manual-on` / `manual-off`(强制档)、`total`(总量越线)、`visible`(可见节点数越线)、`density`(屏幕密度越线)、`below-threshold`(auto 档已退出)、`checking`(引擎还没建好或正在等首轮布局)。`visiblePoints` 只数到触发阈值就停,因此可能正好等于阈值;按总量触发时是 `null`。PerfDemo 的面板可以直接切 auto / on / off,并在读数里看到这一份状态。
|
|
32
|
+
|
|
5
33
|
## 大数据量
|
|
6
34
|
|
|
7
35
|
10 万点 + 10 万线(含通道)在 `docs/demos/GisViewDemo.vue` 里可以直接切到,交互路径上没有按总量线性增长的循环:
|
|
8
36
|
|
|
37
|
+
- **高密度纯文本标签自动画布化**:强制显示档(`labelOptimize` 未开、`denseMode` 未激活)下,节点层或线路层里能画布的纯文本标签达到 **1024** 条时,默认 `labelRender: 'auto'` 会让该层改用一张 `.gis-view__label-canvas`,不再为每条标签建 `div`。HTML、函数返回元素、可拖拽 / 可编辑及超出 Canvas 样式能力的标签仍留在同层 DOM,因此画布与少量 DOM 可以混排;`labelRender: 'dom'` 可强制回退逐条 DOM。优化档 / 密集档仍走 DOM(避让、截断与命中判定要真实量尺寸),这一步不是它们的替代品。`selftest-labels.html` 用 1100 条纯文本验证了点标签层只剩一张画布、混入一条 `html: true` 时只保留那一条 DOM,并覆盖切回 `dom`、再切回 `auto` 的节点回收与 SVG 导出。
|
|
9
38
|
- 拾取与框选共用网格索引:点走按数据范围重建的均匀网格,线走随线段重建的 `createSegmentGrid`,每次交互只遍历光标附近的候选,耗时与总量基本脱钩,`getStats().pickMs` 是随手可读的实测值(10 万规模下热态约 0.2 ~ 0.3 ms / 次)。线段按格线步进登记进它真正穿过的格子(早期按包围盒填格,一条斜跨全图的线会占掉上万格、只好整条丢进「超长线」单,而十万条线里绝大多数都是这种,于是每次拾取都要重算十万条的距离);剩下的长线只记一条包围盒,查询时按盒子先筛一遍。10 万点 / 10 万线的合成数据里(随机配对、长线极密),500 次 `pick` 从 3530 ms 降到 164 ms(0.3 ms / 次)。
|
|
10
39
|
- 拖动时「哪些线跟着这个点走」按需建索引:`collectDragLinks` 从前每次都把全部线路的端点与组内成员逐个比一遍,10 万点 / 12 万线下这一步占总 CPU 的 27%,是拖动路径上唯一还按总量线性付费的循环。现在线路超过 `LINE_BIND_TOLERANCE_SCAN_LIMIT`(2048)条时改用一张线路归属表(按 `from` / `to` 的 id 与端点坐标建,整份换数据 / 增删线 / 改绑定才重建,只改坐标就地把那一条换掉),只对命中的候选逐字段判定 —— 十万级拖动降到 CPU 的 1% 以下,单条 `updatePoint`(改名这类不动几何的改动)55 → 3.3 ms(稳态 0.02 ms)。线路不多时保留原来的浮点容差扫描(行为与从前逐字不差);索引那条路只认 id 绑定与坐标精确相等 —— 这个量级坐标都是同一份来源算出来的,容差是给手写数据留的余量。
|
|
11
40
|
- 视口里的线路改走线段网格:线路标签排版这类「只看屏幕里这一片」的路径(`forEachLineInView`)原先要把每条线的包围盒过一遍,10 万级 + 标签的 CPU profile 里它占 47 ~ 58%。现在线路超过 `LINE_VIEW_GRID_MIN_LINES`(2048)条、且估计出来的候选线段只占总数的 1/32 以内(`LINE_VIEW_GRID_COST_SHARE`)时改查拾取用的那张线段网格,再逐条按包围盒确认;候选排回要素下标顺序,脏线与没有线段的退化线单独补一遍,访问顺序与判定口径与整份扫描逐行相同。网格脏着(正在拖线 / 改线)或线路不多时照旧整份扫描。
|
|
41
|
+
- 视口里的节点遍历也先查点网格:`forEachPointInView` 在节点超过 `POINT_VIEW_GRID_MIN_POINTS`(2048)、点网格不脏且估计候选不足总数的 1/32(`POINT_VIEW_GRID_COST_SHARE`)时,只查视口覆盖的格子、把候选排回数据顺序再逐个精确判定;网格脏着(刚改过数据 / 拖拽中)或视口几乎铺满时仍走整份扫描,避免为了省一趟遍历先付出建网格和排序的成本。`createPointGrid.estimate(..., stopAfter)` 与查询共用同一套格子口径,传 `stopAfter` 时一旦超过阈值立即返回,只判断候选是否过密时不必扫完视口里的全部格子;框选线路则复用已有的 `forEachLineInView`,不再为每条候选线复制路径 / 包围盒 / 颜色快照。
|
|
12
42
|
- 悬停、选中、拖拽只做增量更新:选中集合用点 / 线两张下标 `Set` 维护,样式变化按要素下标重新解析,顶点与实例数据就地改写后按「实例槽位」上传,不重建整批缓冲。
|
|
13
43
|
- 状态样式就地更新:悬停 / 选中样式只改颜色、大小、描边时按实例槽位覆盖,不重建整批;符号换成 / 换出贴图、阴影条数变化这类批次结构变化才回退整批重排。哪些要素当前真的按状态样式画过另有一份记账(`stateStyledSignatures`),整体重建后立刻重算,取消选中时按它精确还原本体样式,不会留下残留。
|
|
14
44
|
- 拐点只处理当前显示的那些:`waypointVisibility: 'selected'` 下没有选中线路时,上万条线的拐点解析与上传都省掉了。
|
|
@@ -41,7 +71,7 @@
|
|
|
41
71
|
- 批量改动走 `view.batch()`:一次改一万个节点只发一条 `onDataChange`(`changes` 按顺序列出每一步)与一步历史,回调与记录不随改动次数线性增长,见「批量更新」。
|
|
42
72
|
- 「把地图网格拆开、分区域显示」值不值:**渲染侧不值** —— 一帧的绘制是按实例化一次提交的(2 万点 / 10 万点都是 2 ~ 4 个 `drawCalls`),视口外的实例由 GPU 裁掉,`frameMs` 在「整份在视口里」与「放大到只剩 1 个点」之间都是 0.1 ~ 0.8 ms,拆块省不到东西(线路批次这一轮另按视口逐段裁剪,那是裁「铺进批次的实例」、省顶点变换,见 15.47);**按区域取数值**,但值在「每次交互要重算的那部分」上:拾取早就有均匀网格与线段网格,标签这一侧这一轮补上 `forEachPointInView(rect, visit)` —— 只遍历落在给定世界矩形里的可见节点(数据顺序、零分配、回调返回 `false` 提前结束,与 `listPoints()` 同一份可见判定),节点标签的两趟扫描都改走它,放大看一小片时不再先拿整份快照再筛,候选配额也只数视口里的点。
|
|
43
73
|
- 定向更新的重排在标签层:`updatePoint` / `patchPoints` 之后整层标签要重排一次,而重排里「量尺寸」与「摘节点」原来是交错的 —— `dropLabel` 的 `remove()` 会让布局作废,于是这一批里每条标签都要各自触发一次重排。现在这一批的盒子一次量完、再统一回收要撤下的节点(`placeLabelBatch` 的 `flush`),实测 2 万点全在视口里、标签上限 200 时一次 `updatePoint` 从 ~55 ms 降到 ~17 ms;`docs/steps/selftest-perf.html` 里有这两条读数,以及「定向更新之后上一轮画出来的标签一条不少」与「整层重排只在新签名上量布局」这一类断言(后者对着旧写法是 832 次布局读 / 轮、31 ms)。
|
|
44
|
-
- 定向更新不再扫数组、标签重排不再重写没变的标签:`updatePoint` / `
|
|
74
|
+
- 定向更新不再扫数组、标签重排不再重写没变的标签:`updatePoint` / `movePoint` / `removePoints` / `patchPoints` 这些「按 id 找一个要素」的方法以前每次都 `points.findIndex(...)` 线性扫一遍(10 万点下单次约 0.4 ms、批量改 500 条 73 ms),现在走一张按数据版本维护的定位表 —— 表在就 O(1) 查、查不到直接判「这个 id 不在数据里」,表不在先扫描并在同一数据版本里记账、扫够 8 次才建;只改坐标 / 样式这种不动 id 与下标的动作不推版本,增删数据与撤销重做才推,命中时还要对一眼 `list[index].id` 才算数。真时间实测:最坏情况(id 在数组末尾)单次定位 0.435 → 0.047 ms、数字当索引 0.479 → 0.005 ms、批量改 500 条 73.0 → 2.2 ms、批量删 1000 条 432 → 32 ms;标签重排这一侧加了「写之前先比签名、`transform` 没变不赋值、量过的盒子按条件复用、候选节点按槽位停着、留下的才进 DOM」,可见标签集合与改前逐字节一致,带标签的单条 `updatePoint` 5.12 → 0.75 ms。语义一条没变:重复 id 取第一个,数字型 ref 在表里查不到时仍按索引回落。
|
|
45
75
|
- 追加数据只补这几条、标签没变就不排、搜索搬进 Worker:三件都还是同一条思路 —— **把「整份」换成「这一份」、把主线程上的活挪走**。① **追加式补点**:引擎认得出「数据末尾就多了这几条」(`appendPoints`),只解析这几条、只往现成的实例批次里补这几条 —— 实例缓冲按容量记账(够就 `bufferSubData` 只传新段)、形状符号插在圆点那一段末尾并把后面的背景点 / 装饰 / 抬层点整体后移、贴图符号追加到现成那一组、包围盒只扩不缩、背景与背景文字跟着新范围重算;`addPoint` / `addPoints` 因此从「每次整份重建」变成增量(10 万点下逐个 `addPoint` 16.4 → 1.0 ms,第一次 4.6 ms 是整份实例缓冲重传一次),场景结构做不到等价时(新点不在最上面那一层、贴图没有现成的组、通道或线路按 id 认领了它)自己回落整份重建,画出来与整份重建**逐像素相同**(分层 / 贴图 / 阴影三种场景都验过)。② **标签层**多了一道「输入指纹」判据:改动的字段与标签画出来的东西无关时整层重排整个跳过(`overlay.labelInputKey` 把一条要素里影响它那份标签的字段拼成指纹,`updatePoint` / `updateLine` / 批量补丁三处都接),10 万点 + 标签改一条 0.75 → 0.099 ms,改 `name` 这种真会换标签的仍是 0.956 ms。③ **搜索**搬进了 Worker(`search.worker.js` + `search-runner.js` + `view.searchAsync`):数据交给 Worker 一次(改过的按条补、结构变了重新交一份),之后每次搜索只发关键字,命中只回下标与字段、要素与包围盒由主线程按下标补回来,结果按「请求序号 + 数据版本」作废(回来晚了的那一份带着 `stale: true` 交回去,视角不会跟着它动),环境不支持 Worker / 数据量不到 `searchWorker.threshold`(默认 20000)/ Worker 报错时同步回落。10 万点 / 10 万线连着搜 6 次占主线程 43.3 → 0.1 ms(第一次含把数据交给 Worker 的 42.7 ms)。④ 另外 `refresh({ reflow: false })` 只重画一帧、跳过整份重解析(10 万点 / 1 万线 41.0 → 0.1 ms),给「数据没变、就是想重画一遍」的场合一个明确的口子。
|
|
46
76
|
- 标签尺寸按「内容签名」缓存、避让判定不再读布局:整层重排从前要为每条候选 `getBoundingClientRect()` 一次(每读一次就是一次强制重排),根因是 `parseLabelTransform` 的正则从来没匹配上(`0px?` 的写法)—— 量好的盒子每次都被 `writeLabelTransform` 作废。现在:正则修对、解析结果缓存在 `state.placement` 上,尺寸按「画上去的那一份签名」(kind / class / 文字 / 内联样式 / 字体代号)缓存(LRU 8192,与节点身份解耦)、位置从节点自己写下的 `transform` 里解析,避让判定只剩算术,只有签名第一次出现才真量一次;一批里没量过的节点先全部挂进 DOM 再一趟读完(写读分离)。实测 10 万点 / 10 万线、54 条标签下单次定向更新 73.8 → 3.5 ms、建场景 453 → 257 ms;重标签场景(1000 条标签 / 8000 候选)500 ms 上下 → 25 ~ 45 ms。
|
|
47
77
|
- MSAA 与交互期细线:决定十万级交互帧率的是**光栅化的总覆盖量 × MSAA 倍数**,不是条数 —— 微基准(898×518 画布、十二万段、同一份实例数据、真 GPU)里每条线展开成四边形是 192 ms/帧、换成 `gl.LINES` 是 112 ms/帧,关掉 MSAA 之后分别是 38 / 23 ms,而 JS + 命令提交只要 0.01 ms(空页面 7.2 ms、只 clear 7.0 ms)。于是 `antialias` 进选项(软件光栅自动关),交互期新增细线快路径 `interactionThinLines`(线宽 ≤ 3px 且无虚线时改走 `gl.LINES`,停手回到四边形展开;两种画法共用同一张实例缓冲,所以位置不跳),`getStats()` 多出 `antialias` / `software` / `thinLines` 三个读数。实测:同一页交替开关 6 轮(两次独立运行)交互期 p50 194 ~ 208 → 132 ~ 139 ms、同一 2.2 s 窗口帧数 13 → 19 ~ 24;同样 10 万点换成网格邻居线(19.9 万条、线短不叠)拖动 p50 只要 7 ms。再往后一轮把这条账的另一半收掉:交互期与动画期干脆不带 MSAA 画 —— `interactionNoMsaa`(缺省开)让拖动 / 缩放与动画在跑的那些帧先画进一张不带 MSAA 的后备贴图、帧末整幅贴回,停手 / 停帧后那一帧照旧走带 MSAA 的画布(静止画面逐像素不变);真机实测交互期单帧 133 → 36.6 ms、拖动 p50 130 → 16 ~ 22 ms、缩放 p50 239.5 → 16.7 ms、平移 p95 129 → 17 ms、动画帧 212 → 53 ms/帧(只开涟漪 187 → 48、只开线路动点 199 → 47),读数看 `getStats().noMsaa`。
|
|
@@ -50,7 +80,7 @@
|
|
|
50
80
|
- 整份重排(`layoutScene`)也少扫几趟十万条:点 glyph 从对象池里复用(逐字段就地改写,不再每轮造十万个对象)、圆点实例用 `writeCircleGlyph` 直写进 `prepareInstances` 的批次数组(不再逐条造 `circleFromGlyph` 的中间对象);没有线路装饰 / 没有跑动动画 / 没有节点阴影 / 没有逐要素状态样式时,各自的十万级空扫直接跳过(`hasLineDecorations` / `hasLineEffects` / `hasPointShadows`,节点侧另有 `hasPointStateStyles()`);细线快路径的账本只在「线宽或虚线开关真的变了」时才标脏;`zIndex` 分层只有一层(绝大多数场景)时不再重铺点顺序。同一份 10 万级的 CPU profile 里 `syncRaiseSets` 6.2 → 0 ms、`buildLineEffectGlyphs` 3.69 → 0 ms、`buildLineDecorationGlyphs` 1.66 → 0.62 ms、`thinLineBatchEligible` 2.2 → 0 ms。
|
|
51
81
|
- 定向更新的线路标签候选复用:`updatePoint` / `patchPoints` 只改节点侧(补丁没动坐标 / id、筛选也没开)时,线路标签层接着用上一轮收好的候选 —— 视口矩形、画布尺寸与「上一轮显示过的下标集合」逐项核对(定义对象是缓存键),十万条线的视口扫描整个省掉;改线路内容 / 挪节点 / 换 id / 筛选联动时判据返回 `undefined`,照旧整层重扫。10 万点 / 10 万条长线 + 两层标签(DOM 75 条)实测单次 `updatePoint` p50 **4.7 → 1.7 ms**,CPU profile 采样 531 → 226、`forEachLineInView` 从热点清单消失。
|
|
52
82
|
- 超长线段换更粗的一层网格:线段网格原来给「一层里铺不下的长线」只记一条包围盒、查询按盒子先筛,长线密布时几乎等于全量重算;现在每层格子边长 ×4,粗层同样只登记线段真正穿过的格子(跨层 stamp 去重),候选从「包围盒相交」变成「几何上真的经过光标附近」。10 万条随机长线实测一次查询的候选 11101 → **2210** 条、`pick` p50 / p95 / 平均 **0.9 / 6.2 / 1.90 → 0.3 / 1.7 / 0.53 ms**;代价是极限数据集建网格 16.3 → 41.6 ms、堆内存多约 26 MB(短线为主的数据集不变)。
|
|
53
|
-
- **补点 / 删点不再因为「场景没铺完」回落整份重建**:发散渲染(见《实现原理》15.46)欠着账时,`addPoint` / `addPoints` / `addLines` 原先一律回落整份重解析(几十毫秒),「删一条 → 立刻补一条」这种最常见的串操作因此每一轮都付整份的钱。现在补点这一趟自己把没铺完的那一轮停掉、只补末尾这几条,再按新的解析结果重开一轮;删除那一趟同理(15.89)。5 万点 / 3 万线实测:稳态 `addPoint` 0.4 ~ 1.3 ms,「删一条 → 立刻补一条」29.7 → **1.3 ~ 6.4 ms**。纯删点这一趟还整段跳过通道解析(通道声明逐条没变、没删到回线与锚点时,骨架与全部回线折线照用上一轮的),选中通道时抬层集合也改成按内容比对(内容没变就不再重铺十万条线的批次)。导出这条链顺手切了三刀(空标签表先判空、节点坐标不造中间对象、折线端点共用一个取值回调):50k 点 + 30k 线全量 `exportData()` 中位 22.1 → 21.6 ms、`
|
|
83
|
+
- **补点 / 删点不再因为「场景没铺完」回落整份重建**:发散渲染(见《实现原理》15.46)欠着账时,`addPoint` / `addPoints` / `addLines` 原先一律回落整份重解析(几十毫秒),「删一条 → 立刻补一条」这种最常见的串操作因此每一轮都付整份的钱。现在补点这一趟自己把没铺完的那一轮停掉、只补末尾这几条,再按新的解析结果重开一轮;删除那一趟同理(15.89)。5 万点 / 3 万线实测:稳态 `addPoint` 0.4 ~ 1.3 ms,「删一条 → 立刻补一条」29.7 → **1.3 ~ 6.4 ms**。纯删点这一趟还整段跳过通道解析(通道声明逐条没变、没删到回线与锚点时,骨架与全部回线折线照用上一轮的),选中通道时抬层集合也改成按内容比对(内容没变就不再重铺十万条线的批次)。导出这条链顺手切了三刀(空标签表先判空、节点坐标不造中间对象、折线端点共用一个取值回调):50k 点 + 30k 线全量 `exportData()` 中位 22.1 → 21.6 ms、`exportData({ scope: 'viewport' })` 36.7 → 32.5 ms(首跑那一次 36 ~ 38 ms 是 id 表与 JIT 冷着,跑第二遍就落回 20 ms 上下)。
|
|
54
84
|
|
|
55
85
|
## 性能实测与 WebGPU 评估
|
|
56
86
|
|
|
@@ -87,6 +117,16 @@
|
|
|
87
117
|
|
|
88
118
|
规模缩到 5000 点(视口里 134 vs 5000 条标签)两档分别是平移 17.16 / 24.46 ms、缩放 17.5 / 43.2 ms —— 差得没 2 万点那么夸张,但趋势一样。开关只动标签层:切它走 `updateOptions` 的快速通道(`pickEngineOptions` 挑不出引擎字段,只重排覆盖层),引擎一帧都不重排;另一处顺带的省法是强制显示档不铺底图区域文字那张「软」网格(那一步要逐条量区域文字盒子)。
|
|
89
119
|
|
|
120
|
+
再往后一轮继续收紧**优化档的节点标签候选**:数据前部经常是一大簇同屏节点,旧顺序会先收满 `scanLimit` 个候选,后面散开的节点一个标签都轮不到。现在优化档先按 32px 屏幕小格去重(同格只留一条主候选),再按 160px 粗格限流(每格最多 3 条主候选),其余节点进补位队列;主队列按屏幕行列排,密集簇被压成少量代表之后,扫描名额能落到整个视口。补位候选只在主候选被避让丢掉时接上,所以不会改变「先铺开、再补位」的优先级。缺省强制显示档完全不走这两层筛选与补位:`maxCount` / `scanLimit` / `avoidOverlap` 仍不生效,同格的每条标签都画。专项自测用「前 900 个同格点 + 后 12 个散点、maxCount 8 / scanLimit 24」验证散点仍有标签、总数不超上限且互不重叠,另有 24 个同格点在强制显示档全部画出的回归。
|
|
121
|
+
|
|
122
|
+
**标签内容的写入也按“哪一项真的变了”拆开**:`paintLabel` 原来把 class、dataset、内容、内联样式合成一份签名,改字号或只换锚点都会把同一段 HTML 重新 `innerHTML` 一遍。现在分四类比较:class、内联样式、内容(`html` 标志 + 最终字符串)、尺寸签名;只有内容变化才写 DOM,样式变化只改 `style`,位置变化只改 `transform`。尺寸缓存键去掉下标与标签 id,只留 `kind` / `className` / `inline style` / `content`,同一屏上 120 条同文案 HTML 标签首次排版共用一次量测,批量循环里也会当场命中刚写入的缓存。自测页直接统计标签节点的 `getBoundingClientRect`:120 条同内容标签首轮真实布局读取 ≤ 4 次(旧写法逐条读约 120 次);只改字号时 HTML 子节点身份保持不变,`innerHTML` 写入为 0。内容本身确实变了(例如每条文案都不同)时仍要逐条解析 HTML,这部分是浏览器必须付的成本,能靠 `style` / `className` 做外观时不要改内容。
|
|
123
|
+
|
|
124
|
+
覆盖层的**区域文字**(GeoJSON 的 `label`)也按同样的思路收紧:`overlay.js` 现在按数组下标复用 DOM 节点,文字 / 字号 / 字体 / 颜色等样式签名没变时,只调整 `transform`;同一相机重复刷新连 `display` 与 `transform` 都不再写。签名用 `JSON.stringify([...])` 而不是分隔符拼串,避免文字本身含 `|` / `~` 时两份不同内容被判成同一份;单条样式变化只改那一条并作废它的尺寸缓存,不再整层删除重建。相机事件在同一帧连发时统一并到下一帧,12 次 `cameraChange` 只触发 1 次重投影;同一份底图重新下发时另有一份锚点签名兜底 —— 样式没变但 `x` / `y` / 偏移变了照样重投影,完全没变才跳过这一帧。离屏节点上一次的 `display` / `transform` 状态会保留,样式更新后回到视野仍能正常恢复显示。鹰眼图的视口框同样缓存上一次写出的 `transform` / 宽 / 高,位置与尺寸都没变时直接返回,不碰 `style`。这两处的读取口径也统一了:引擎尚未创建时,覆盖层、鹰眼图、`getViewport()` 与 `getViewState().viewport` 共用同一个 `fallbackViewport()`,`pixelRatio` 不再一处是 1、另一处是 `devicePixelRatio`。
|
|
125
|
+
|
|
126
|
+
鹰眼图这一轮补了两个开销边界:`maxPixelRatio` 把备份倍率封在 1 ~ 4(默认 4),实际倍率取 `devicePixelRatio × 宿主显示比例` 与该上限的较小值,高分屏 / 宿主放大时不再无限增加填充像素;小图容器加 `contain: layout paint` 与 `isolation: isolate`,布局与重绘留在自身。扩展内容改由 `layers` 按 `order` 分层执行,每层独立 `save` / `restore`,旧的单层 `draw` 仍兼容并最后执行 —— 多个业务标记可以各自维护,不会互相串改画布状态。
|
|
127
|
+
|
|
128
|
+
同一轮加的 `graphics` 扩展图形层是**独立 DOM 覆盖层**:只读相机做投影,增删改不触发引擎重排;锚点随相机平移 / 缩放,尺寸默认保持屏幕像素,`scaleWithView: true` 才按相机比例缩放。图形位于画布之上、标签之下,不参与拾取;如果只是少量线框 / 图片 / Canvas,开销与这几十个 DOM 节点相当,不会把十万级要素的渲染批次重新排一遍。
|
|
129
|
+
|
|
90
130
|
## 渲染后端(WebGPU 开关)
|
|
91
131
|
|
|
92
132
|
`renderer` 把「换绘制后端」收成一个开关:`'auto'`(缺省)/ `'webgl2'` / `'webgpu'`,布尔值 `true` / `false` 等价于 `'webgpu'` / `'webgl2'`。
|