gis-view-webgl2 0.1.4 → 0.1.6
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 +10 -7
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +225 -20
- package/dist/index.js +1 -1
- package/docs/AI/345/257/271/350/257/235/346/261/207/346/200/273.md +109 -0
- package/docs/GisView.md +53 -53
- 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 +91 -0
- package/docs/demos/ConnectDemo.vue +114 -3
- package/docs/demos/MinimapDemo.vue +216 -17
- package/docs/demos/SearchFilterDemo.vue +113 -1
- package/docs/demos/api-index.js +28 -20
- package/docs/demos/demos.js +1 -1
- package/docs/demos/gis-view-demo/GisViewDemoStage.vue +25 -16
- package/docs/demos/gis-view-demo/useGisViewDemo.js +27 -16
- package/docs/index.html +3 -3
- package/docs/internals/13-/351/271/260/347/234/274/345/233/276.md +8 -5
- package/docs/internals/16-01-/344/274/230/345/214/226/344/270/216/345/217/226/350/210/215-/344/270/212.md +18 -5
- 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 -1
- package/docs/internals/18-/346/200/247/350/203/275/345/256/236/346/265/213/344/270/216WebGPU/350/257/204/344/274/260.md +73 -1
- package/docs/manual/03-/351/205/215/347/275/256/351/241/271.md +14 -5
- package/docs/manual/04-/344/272/213/344/273/266/344/270/216/345/233/236/350/260/203.md +2 -2
- package/docs/manual/05-/347/224/273/350/264/250/344/270/216/346/211/271/351/207/217/346/233/264/346/226/260.md +1 -1
- package/docs/manual/09-/351/200/232/351/201/223/344/270/216/345/244/232/345/233/236/347/272/277.md +1 -1
- 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 +423 -3
- package/docs/manual/14-/350/247/206/345/233/276/344/270/216/347/255/233/351/200/211.md +99 -30
- package/docs/manual/16-/345/256/236/344/276/213/346/226/271/346/263/225/346/200/273/350/241/250.md +12 -7
- package/docs/manual/17-/345/217/202/346/225/260/351/200/237/346/237/245.md +14 -6
- package/docs/manual/18-/351/200/220/346/226/271/346/263/225/347/244/272/344/276/213.md +137 -6
- 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 -0
- package/docs/manual/20-/346/200/247/350/203/275/344/270/216/345/244/247/346/225/260/346/215/256.md +8 -3
- 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 +6 -3
- package/docs/steps/README.md +6 -6
- package/docs/steps/selftest-demos.html +252 -9
- package/docs/steps/selftest-docs.html +3 -3
- package/docs/steps/selftest-full.html +357 -13
- package/docs/steps/selftest-perf.html +599 -382
- package/package.json +1 -1
|
@@ -28,78 +28,78 @@
|
|
|
28
28
|
// 拾取用的节点:线路只挂在前面 8000 个节点上,取后面的才不会被线抢走命中
|
|
29
29
|
const clearPoint = 15000
|
|
30
30
|
try {
|
|
31
|
-
const { createGisView: createGisViewRaw, QUALITY_LEVELS, QUALITY_MODES, createQualityGovernor, normalizeQualityOptions } = await import('../../src/gis-view/index.js')
|
|
32
|
-
const { normalizePointCluster, pointClusterZoomActive } = await import('../../src/gis-view/cluster.js')
|
|
33
|
-
const { createHeatmap } = await import('../../src/gis-view/heatmap.js')
|
|
34
|
-
const { normalizeDensity, renderDensityField } = await import('../../src/gis-view/density.js')
|
|
35
|
-
const clusterDefault = normalizePointCluster(true)
|
|
36
|
-
const clusterClamped = normalizePointCluster({ enabled: true, radius: 2, minPoints: 1, maxZoom: -3 })
|
|
37
|
-
check('点聚合:true 用缺省档(半径 60 / 最少 2 点 / 不限缩放)',
|
|
38
|
-
clusterDefault.enabled && clusterDefault.radius === 60 && clusterDefault.minPoints === 2 && clusterDefault.maxZoom === 0,
|
|
39
|
-
JSON.stringify(clusterDefault))
|
|
40
|
-
check('点聚合:半径 / 最少点数 / maxZoom 都夹到可用区间', clusterClamped.radius === 8 && clusterClamped.minPoints === 2 && clusterClamped.maxZoom === 0,
|
|
41
|
-
JSON.stringify(clusterClamped))
|
|
42
|
-
check('点聚合:maxZoom 按相机比例 / 基准比例判定,超过后展开',
|
|
43
|
-
pointClusterZoomActive({ enabled: true, maxZoom: 2 }, 1.5, 1) === true && pointClusterZoomActive({ enabled: true, maxZoom: 2 }, 2.1, 1) === false,
|
|
44
|
-
'1.5× 聚合 / 2.1× 展开')
|
|
45
|
-
// 性能断言是照「优化档」(限数 + 避让)测的老口径:统一带上 labelOptimize: true
|
|
46
|
-
const createGisView = (host, options = {}) => createGisViewRaw(host, { labelOptimize: true, ...options })
|
|
47
|
-
/* ---------------- 热力图语义键:只改密度画法不重采要素 ---------------- */
|
|
48
|
-
let semanticHeatmapOption = {
|
|
49
|
-
enabled: true,
|
|
50
|
-
field: 'value',
|
|
51
|
-
targets: ['point', 'line'],
|
|
52
|
-
domain: [0, 100],
|
|
53
|
-
colors: ['#0b3d91', '#ffd166', '#ff5e5e'],
|
|
54
|
-
density: { opacity: 1, layer: 'below', radius: 24 }
|
|
55
|
-
}
|
|
56
|
-
const semanticHeatmap = createHeatmap(() => semanticHeatmapOption, () => [])
|
|
57
|
-
const styleKeyBefore = semanticHeatmap.styleKey()
|
|
58
|
-
const weightKeyBefore = semanticHeatmap.weightKey()
|
|
59
|
-
semanticHeatmapOption = {
|
|
60
|
-
...semanticHeatmapOption,
|
|
61
|
-
density: { ...semanticHeatmapOption.density, opacity: 0.25, layer: 'top', radius: 40 }
|
|
62
|
-
}
|
|
63
|
-
check('热力图语义键:只改 density 的透明度 / 画层 / 核半径,样式键与权重键都不变',
|
|
64
|
-
semanticHeatmap.styleKey() === styleKeyBefore && semanticHeatmap.weightKey() === weightKeyBefore)
|
|
65
|
-
const densityOnlyStyleKey = semanticHeatmap.styleKey()
|
|
66
|
-
const densityOnlyWeightKey = semanticHeatmap.weightKey()
|
|
67
|
-
semanticHeatmapOption = { ...semanticHeatmapOption, colors: ['#123456', '#7ce0b0', '#ff8a3d'] }
|
|
68
|
-
check('热力图语义键:换色带只改样式键,权重键保持不动',
|
|
69
|
-
semanticHeatmap.styleKey() !== densityOnlyStyleKey && semanticHeatmap.weightKey() === densityOnlyWeightKey)
|
|
70
|
-
const colorWeightKey = semanticHeatmap.weightKey()
|
|
71
|
-
semanticHeatmapOption = { ...semanticHeatmapOption, field: 'score' }
|
|
72
|
-
check('热力图语义键:换取值字段时两个键都变(密度场要重新采样)',
|
|
73
|
-
semanticHeatmap.weightKey() !== colorWeightKey && semanticHeatmap.styleKey() !== densityOnlyStyleKey)
|
|
74
|
-
|
|
75
|
-
/* ---------------- 热力图密度场:透明边缘的 RGB 预乘 alpha ---------------- */
|
|
76
|
-
const premultOptions = normalizeDensity({ style: 'smooth', max: 1, radius: 6, resolution: 64, threshold: 0, core: 0 })
|
|
77
|
-
const premultPalette = {
|
|
78
|
-
bands: [[1, 0, 0, 1]],
|
|
79
|
-
smooth: [[1, 0, 0, 1], [1, 0, 0, 1]],
|
|
80
|
-
core: null
|
|
81
|
-
}
|
|
82
|
-
const premultField = renderDensityField([{ x: 0, y: 0, ratio: 1 }], premultOptions, premultPalette)
|
|
83
|
-
let premultPartial = 0
|
|
84
|
-
let premultBad = 0
|
|
85
|
-
if (premultField) {
|
|
86
|
-
for (let offset = 0; offset < premultField.pixels.length; offset += 4) {
|
|
87
|
-
const alpha = premultField.pixels[offset + 3]
|
|
88
|
-
if (alpha === 0 || alpha === 255) {
|
|
89
|
-
continue
|
|
90
|
-
}
|
|
91
|
-
premultPartial += 1
|
|
92
|
-
// 纯红色带:预乘后 R 应与 A 同值、G/B 都还是 0;旧实现固定写 R=255,透明边缘会发灰。
|
|
93
|
-
if (premultField.pixels[offset] !== alpha || premultField.pixels[offset + 1] !== 0 || premultField.pixels[offset + 2] !== 0) {
|
|
94
|
-
premultBad += 1
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
check('热力图密度场:透明边缘的 RGB 已预乘(R 跟 A 同降、G/B 为 0,不再留灰边)',
|
|
99
|
-
Boolean(premultField) && premultPartial > 0 && premultBad === 0,
|
|
100
|
-
`${premultField?.width ?? 0}×${premultField?.height ?? 0} / 半透明 ${premultPartial} 格 / 未预乘 ${premultBad} 格`)
|
|
101
|
-
|
|
102
|
-
/* ---------------- 档位表与归一(纯函数) ---------------- */
|
|
31
|
+
const { createGisView: createGisViewRaw, QUALITY_LEVELS, QUALITY_MODES, createQualityGovernor, normalizeQualityOptions } = await import('../../src/gis-view/index.js')
|
|
32
|
+
const { normalizePointCluster, pointClusterZoomActive } = await import('../../src/gis-view/cluster.js')
|
|
33
|
+
const { createHeatmap } = await import('../../src/gis-view/heatmap.js')
|
|
34
|
+
const { normalizeDensity, renderDensityField } = await import('../../src/gis-view/density.js')
|
|
35
|
+
const clusterDefault = normalizePointCluster(true)
|
|
36
|
+
const clusterClamped = normalizePointCluster({ enabled: true, radius: 2, minPoints: 1, maxZoom: -3 })
|
|
37
|
+
check('点聚合:true 用缺省档(半径 60 / 最少 2 点 / 不限缩放)',
|
|
38
|
+
clusterDefault.enabled && clusterDefault.radius === 60 && clusterDefault.minPoints === 2 && clusterDefault.maxZoom === 0,
|
|
39
|
+
JSON.stringify(clusterDefault))
|
|
40
|
+
check('点聚合:半径 / 最少点数 / maxZoom 都夹到可用区间', clusterClamped.radius === 8 && clusterClamped.minPoints === 2 && clusterClamped.maxZoom === 0,
|
|
41
|
+
JSON.stringify(clusterClamped))
|
|
42
|
+
check('点聚合:maxZoom 按相机比例 / 基准比例判定,超过后展开',
|
|
43
|
+
pointClusterZoomActive({ enabled: true, maxZoom: 2 }, 1.5, 1) === true && pointClusterZoomActive({ enabled: true, maxZoom: 2 }, 2.1, 1) === false,
|
|
44
|
+
'1.5× 聚合 / 2.1× 展开')
|
|
45
|
+
// 性能断言是照「优化档」(限数 + 避让)测的老口径:统一带上 labelOptimize: true
|
|
46
|
+
const createGisView = (host, options = {}) => createGisViewRaw(host, { labelOptimize: true, ...options })
|
|
47
|
+
/* ---------------- 热力图语义键:只改密度画法不重采要素 ---------------- */
|
|
48
|
+
let semanticHeatmapOption = {
|
|
49
|
+
enabled: true,
|
|
50
|
+
field: 'value',
|
|
51
|
+
targets: ['point', 'line'],
|
|
52
|
+
domain: [0, 100],
|
|
53
|
+
colors: ['#0b3d91', '#ffd166', '#ff5e5e'],
|
|
54
|
+
density: { opacity: 1, layer: 'below', radius: 24 }
|
|
55
|
+
}
|
|
56
|
+
const semanticHeatmap = createHeatmap(() => semanticHeatmapOption, () => [])
|
|
57
|
+
const styleKeyBefore = semanticHeatmap.styleKey()
|
|
58
|
+
const weightKeyBefore = semanticHeatmap.weightKey()
|
|
59
|
+
semanticHeatmapOption = {
|
|
60
|
+
...semanticHeatmapOption,
|
|
61
|
+
density: { ...semanticHeatmapOption.density, opacity: 0.25, layer: 'top', radius: 40 }
|
|
62
|
+
}
|
|
63
|
+
check('热力图语义键:只改 density 的透明度 / 画层 / 核半径,样式键与权重键都不变',
|
|
64
|
+
semanticHeatmap.styleKey() === styleKeyBefore && semanticHeatmap.weightKey() === weightKeyBefore)
|
|
65
|
+
const densityOnlyStyleKey = semanticHeatmap.styleKey()
|
|
66
|
+
const densityOnlyWeightKey = semanticHeatmap.weightKey()
|
|
67
|
+
semanticHeatmapOption = { ...semanticHeatmapOption, colors: ['#123456', '#7ce0b0', '#ff8a3d'] }
|
|
68
|
+
check('热力图语义键:换色带只改样式键,权重键保持不动',
|
|
69
|
+
semanticHeatmap.styleKey() !== densityOnlyStyleKey && semanticHeatmap.weightKey() === densityOnlyWeightKey)
|
|
70
|
+
const colorWeightKey = semanticHeatmap.weightKey()
|
|
71
|
+
semanticHeatmapOption = { ...semanticHeatmapOption, field: 'score' }
|
|
72
|
+
check('热力图语义键:换取值字段时两个键都变(密度场要重新采样)',
|
|
73
|
+
semanticHeatmap.weightKey() !== colorWeightKey && semanticHeatmap.styleKey() !== densityOnlyStyleKey)
|
|
74
|
+
|
|
75
|
+
/* ---------------- 热力图密度场:透明边缘的 RGB 预乘 alpha ---------------- */
|
|
76
|
+
const premultOptions = normalizeDensity({ style: 'smooth', max: 1, radius: 6, resolution: 64, threshold: 0, core: 0 })
|
|
77
|
+
const premultPalette = {
|
|
78
|
+
bands: [[1, 0, 0, 1]],
|
|
79
|
+
smooth: [[1, 0, 0, 1], [1, 0, 0, 1]],
|
|
80
|
+
core: null
|
|
81
|
+
}
|
|
82
|
+
const premultField = renderDensityField([{ x: 0, y: 0, ratio: 1 }], premultOptions, premultPalette)
|
|
83
|
+
let premultPartial = 0
|
|
84
|
+
let premultBad = 0
|
|
85
|
+
if (premultField) {
|
|
86
|
+
for (let offset = 0; offset < premultField.pixels.length; offset += 4) {
|
|
87
|
+
const alpha = premultField.pixels[offset + 3]
|
|
88
|
+
if (alpha === 0 || alpha === 255) {
|
|
89
|
+
continue
|
|
90
|
+
}
|
|
91
|
+
premultPartial += 1
|
|
92
|
+
// 纯红色带:预乘后 R 应与 A 同值、G/B 都还是 0;旧实现固定写 R=255,透明边缘会发灰。
|
|
93
|
+
if (premultField.pixels[offset] !== alpha || premultField.pixels[offset + 1] !== 0 || premultField.pixels[offset + 2] !== 0) {
|
|
94
|
+
premultBad += 1
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
check('热力图密度场:透明边缘的 RGB 已预乘(R 跟 A 同降、G/B 为 0,不再留灰边)',
|
|
99
|
+
Boolean(premultField) && premultPartial > 0 && premultBad === 0,
|
|
100
|
+
`${premultField?.width ?? 0}×${premultField?.height ?? 0} / 半透明 ${premultPartial} 格 / 未预乘 ${premultBad} 格`)
|
|
101
|
+
|
|
102
|
+
/* ---------------- 档位表与归一(纯函数) ---------------- */
|
|
103
103
|
check('QUALITY_MODES 是五档:四个固定档 + auto', QUALITY_MODES.join(',') === 'high,balanced,lean,fast,auto', QUALITY_MODES.join(','))
|
|
104
104
|
check('QUALITY_LEVELS 从高到低,越往下省得越多', QUALITY_LEVELS.length === 4 &&
|
|
105
105
|
QUALITY_LEVELS[0].glow && QUALITY_LEVELS[0].ornament && QUALITY_LEVELS[0].scale === 1 &&
|
|
@@ -144,72 +144,72 @@
|
|
|
144
144
|
smoothGovernor.sample(0.2, 16.7)
|
|
145
145
|
}
|
|
146
146
|
check('自动档:60Hz 的正常间隔(16.7ms)不会误判成超标', smoothGovernor.state().name === 'high', smoothGovernor.state().name)
|
|
147
|
-
const fixed = createQualityGovernor('balanced')
|
|
148
|
-
fixed.sample(50)
|
|
149
|
-
check('固定档不受帧耗时影响', fixed.state().name === 'balanced' && fixed.state().glow === false && fixed.state().scale === 1, `${fixed.state().name} glow=${fixed.state().glow}`)
|
|
150
|
-
|
|
151
|
-
/* ---------------- 视口可见性:离开视口停动画,回来再续 ---------------- */
|
|
152
|
-
const visibilityHost = document.createElement('div')
|
|
153
|
-
visibilityHost.style.cssText = 'position:fixed;left:-10000px;top:0;width:320px;height:200px'
|
|
154
|
-
document.body.append(visibilityHost)
|
|
155
|
-
const visibilityView = createGisView(visibilityHost, {
|
|
156
|
-
points: [{ id: 'visibility-point', x: 0, y: 0 }],
|
|
157
|
-
lines: [],
|
|
158
|
-
effects: { point: { radius: 16, period: 1.5, count: 1 } },
|
|
159
|
-
showGrid: false
|
|
160
|
-
})
|
|
161
|
-
visibilityView.fitView()
|
|
162
|
-
await sleep(260)
|
|
163
|
-
const hiddenAnimating = visibilityView.getStats().animating
|
|
164
|
-
visibilityHost.style.left = '0px'
|
|
165
|
-
await sleep(260)
|
|
166
|
-
const shownAnimating = visibilityView.getStats().animating
|
|
167
|
-
check('画布离开视口时连续动画停住,回到视口后再继续',
|
|
168
|
-
hiddenAnimating === false && shownAnimating === true,
|
|
169
|
-
`视口外 animating=${hiddenAnimating} / 视口内 animating=${shownAnimating}`)
|
|
170
|
-
visibilityView.destroy()
|
|
171
|
-
visibilityHost.remove()
|
|
172
|
-
|
|
173
|
-
/* ---------------- 大数据密集档:手动开关 / auto 阈值 / 效果恢复 ---------------- */
|
|
174
|
-
const denseHost = document.createElement('div')
|
|
175
|
-
denseHost.style.cssText = 'width:420px;height:280px'
|
|
176
|
-
document.querySelector('#map').append(denseHost)
|
|
177
|
-
const densePoints = Array.from({ length: 10 }, (unused, index) => ({ id: `dp-${index}`, x: index * 20, y: index % 3 * 20, name: `密集 ${index}` }))
|
|
178
|
-
const denseView = createGisView(denseHost, {
|
|
179
|
-
points: densePoints,
|
|
180
|
-
lines: [],
|
|
181
|
-
width: 420,
|
|
182
|
-
height: 280,
|
|
183
|
-
denseMode: 'on'
|
|
184
|
-
})
|
|
185
|
-
const denseOn = denseView.getStats().dense
|
|
186
|
-
check('密集档 on:读数 active / reason=manual-on,动画整体暂停', denseOn?.active === true && denseOn?.reason === 'manual-on' && denseView.getEffects().enabled === false, JSON.stringify(denseOn))
|
|
187
|
-
// 密集期间改动画配置:有效配置仍关着,但用户写的那一份要留在 options 里
|
|
188
|
-
denseView.updateOptions({ effects: { point: { radius: 12, period: 2, count: 1 } } })
|
|
189
|
-
check('密集期间下发 effects:有效配置仍暂停(用户配置留待退出后恢复)', denseView.getEffects().enabled === false && denseView.getOptions().effects?.point?.radius === 12, JSON.stringify(denseView.getEffects()))
|
|
190
|
-
denseView.updateOptions({ denseMode: 'off' })
|
|
191
|
-
await sleep(30)
|
|
192
|
-
check('密集档 off:动画按用户配置恢复', denseView.getStats().dense?.active === false && denseView.getStats().dense?.reason === 'manual-off' && denseView.getEffects().enabled === true, JSON.stringify({ dense: denseView.getStats().dense, effects: denseView.getEffects().enabled }))
|
|
193
|
-
denseView.updateOptions({ denseMode: { mode: 'auto', visibleThreshold: 6, densityThreshold: 1e9, pointThreshold: 1e9 } })
|
|
194
|
-
await sleep(240)
|
|
195
|
-
const denseAuto = denseView.getStats().dense
|
|
196
|
-
check('密集档 auto:可见节点越过 visibleThreshold 时自动进入(reason=visible)', denseAuto?.active === true && denseAuto?.reason === 'visible' && denseAuto?.visiblePoints >= 6, JSON.stringify(denseAuto))
|
|
197
|
-
denseView.updateOptions({ denseMode: { mode: 'auto', visibleThreshold: 100, densityThreshold: 20, pointThreshold: 1e9 } })
|
|
198
|
-
await sleep(240)
|
|
199
|
-
const denseDensity = denseView.getStats().dense
|
|
200
|
-
check('密集档 auto:屏幕密度越线也能进入(reason=density)', denseDensity?.active === true && denseDensity?.reason === 'density', JSON.stringify(denseDensity))
|
|
201
|
-
denseView.updateOptions({ denseMode: { mode: 'auto', visibleThreshold: 999, densityThreshold: 1e9, pointThreshold: 5 } })
|
|
202
|
-
await sleep(240)
|
|
203
|
-
const denseTotal = denseView.getStats().dense
|
|
204
|
-
check('密集档 auto:数据总量越线直接进入(reason=total)', denseTotal?.active === true && denseTotal?.reason === 'total' && denseTotal?.visiblePoints === null, JSON.stringify(denseTotal))
|
|
205
|
-
denseView.updateOptions({ denseMode: { mode: 'auto', visibleThreshold: 100, densityThreshold: 1e9, pointThreshold: 1e9 } })
|
|
206
|
-
await sleep(240)
|
|
207
|
-
const denseBelow = denseView.getStats().dense
|
|
208
|
-
check('密集档 auto:阈值以下主动退出(reason=below-threshold)', denseBelow?.active === false && denseBelow?.reason === 'below-threshold' && denseView.getEffects().enabled === true, JSON.stringify(denseBelow))
|
|
209
|
-
denseView.destroy()
|
|
210
|
-
denseHost.remove()
|
|
211
|
-
|
|
212
|
-
/* ---------------- 大数据量:建场景 / 渲染 / 拾取 / 换档 ---------------- */
|
|
147
|
+
const fixed = createQualityGovernor('balanced')
|
|
148
|
+
fixed.sample(50)
|
|
149
|
+
check('固定档不受帧耗时影响', fixed.state().name === 'balanced' && fixed.state().glow === false && fixed.state().scale === 1, `${fixed.state().name} glow=${fixed.state().glow}`)
|
|
150
|
+
|
|
151
|
+
/* ---------------- 视口可见性:离开视口停动画,回来再续 ---------------- */
|
|
152
|
+
const visibilityHost = document.createElement('div')
|
|
153
|
+
visibilityHost.style.cssText = 'position:fixed;left:-10000px;top:0;width:320px;height:200px'
|
|
154
|
+
document.body.append(visibilityHost)
|
|
155
|
+
const visibilityView = createGisView(visibilityHost, {
|
|
156
|
+
points: [{ id: 'visibility-point', x: 0, y: 0 }],
|
|
157
|
+
lines: [],
|
|
158
|
+
effects: { point: { radius: 16, period: 1.5, count: 1 } },
|
|
159
|
+
showGrid: false
|
|
160
|
+
})
|
|
161
|
+
visibilityView.fitView()
|
|
162
|
+
await sleep(260)
|
|
163
|
+
const hiddenAnimating = visibilityView.getStats().animating
|
|
164
|
+
visibilityHost.style.left = '0px'
|
|
165
|
+
await sleep(260)
|
|
166
|
+
const shownAnimating = visibilityView.getStats().animating
|
|
167
|
+
check('画布离开视口时连续动画停住,回到视口后再继续',
|
|
168
|
+
hiddenAnimating === false && shownAnimating === true,
|
|
169
|
+
`视口外 animating=${hiddenAnimating} / 视口内 animating=${shownAnimating}`)
|
|
170
|
+
visibilityView.destroy()
|
|
171
|
+
visibilityHost.remove()
|
|
172
|
+
|
|
173
|
+
/* ---------------- 大数据密集档:手动开关 / auto 阈值 / 效果恢复 ---------------- */
|
|
174
|
+
const denseHost = document.createElement('div')
|
|
175
|
+
denseHost.style.cssText = 'width:420px;height:280px'
|
|
176
|
+
document.querySelector('#map').append(denseHost)
|
|
177
|
+
const densePoints = Array.from({ length: 10 }, (unused, index) => ({ id: `dp-${index}`, x: index * 20, y: index % 3 * 20, name: `密集 ${index}` }))
|
|
178
|
+
const denseView = createGisView(denseHost, {
|
|
179
|
+
points: densePoints,
|
|
180
|
+
lines: [],
|
|
181
|
+
width: 420,
|
|
182
|
+
height: 280,
|
|
183
|
+
denseMode: 'on'
|
|
184
|
+
})
|
|
185
|
+
const denseOn = denseView.getStats().dense
|
|
186
|
+
check('密集档 on:读数 active / reason=manual-on,动画整体暂停', denseOn?.active === true && denseOn?.reason === 'manual-on' && denseView.getEffects().enabled === false, JSON.stringify(denseOn))
|
|
187
|
+
// 密集期间改动画配置:有效配置仍关着,但用户写的那一份要留在 options 里
|
|
188
|
+
denseView.updateOptions({ effects: { point: { radius: 12, period: 2, count: 1 } } })
|
|
189
|
+
check('密集期间下发 effects:有效配置仍暂停(用户配置留待退出后恢复)', denseView.getEffects().enabled === false && denseView.getOptions().effects?.point?.radius === 12, JSON.stringify(denseView.getEffects()))
|
|
190
|
+
denseView.updateOptions({ denseMode: 'off' })
|
|
191
|
+
await sleep(30)
|
|
192
|
+
check('密集档 off:动画按用户配置恢复', denseView.getStats().dense?.active === false && denseView.getStats().dense?.reason === 'manual-off' && denseView.getEffects().enabled === true, JSON.stringify({ dense: denseView.getStats().dense, effects: denseView.getEffects().enabled }))
|
|
193
|
+
denseView.updateOptions({ denseMode: { mode: 'auto', visibleThreshold: 6, densityThreshold: 1e9, pointThreshold: 1e9 } })
|
|
194
|
+
await sleep(240)
|
|
195
|
+
const denseAuto = denseView.getStats().dense
|
|
196
|
+
check('密集档 auto:可见节点越过 visibleThreshold 时自动进入(reason=visible)', denseAuto?.active === true && denseAuto?.reason === 'visible' && denseAuto?.visiblePoints >= 6, JSON.stringify(denseAuto))
|
|
197
|
+
denseView.updateOptions({ denseMode: { mode: 'auto', visibleThreshold: 100, densityThreshold: 20, pointThreshold: 1e9 } })
|
|
198
|
+
await sleep(240)
|
|
199
|
+
const denseDensity = denseView.getStats().dense
|
|
200
|
+
check('密集档 auto:屏幕密度越线也能进入(reason=density)', denseDensity?.active === true && denseDensity?.reason === 'density', JSON.stringify(denseDensity))
|
|
201
|
+
denseView.updateOptions({ denseMode: { mode: 'auto', visibleThreshold: 999, densityThreshold: 1e9, pointThreshold: 5 } })
|
|
202
|
+
await sleep(240)
|
|
203
|
+
const denseTotal = denseView.getStats().dense
|
|
204
|
+
check('密集档 auto:数据总量越线直接进入(reason=total)', denseTotal?.active === true && denseTotal?.reason === 'total' && denseTotal?.visiblePoints === null, JSON.stringify(denseTotal))
|
|
205
|
+
denseView.updateOptions({ denseMode: { mode: 'auto', visibleThreshold: 100, densityThreshold: 1e9, pointThreshold: 1e9 } })
|
|
206
|
+
await sleep(240)
|
|
207
|
+
const denseBelow = denseView.getStats().dense
|
|
208
|
+
check('密集档 auto:阈值以下主动退出(reason=below-threshold)', denseBelow?.active === false && denseBelow?.reason === 'below-threshold' && denseView.getEffects().enabled === true, JSON.stringify(denseBelow))
|
|
209
|
+
denseView.destroy()
|
|
210
|
+
denseHost.remove()
|
|
211
|
+
|
|
212
|
+
/* ---------------- 大数据量:建场景 / 渲染 / 拾取 / 换档 ---------------- */
|
|
213
213
|
const pointCount = 20000
|
|
214
214
|
const lineCount = 8000
|
|
215
215
|
const points = new Array(pointCount)
|
|
@@ -243,6 +243,35 @@
|
|
|
243
243
|
const refreshMs = now() - refreshStarted
|
|
244
244
|
diag('整份重排一帧', `${refreshMs.toFixed(1)} ms(引擎读数 frameMs=${big.getStats().frameMs.toFixed(2)},drawCalls=${big.getStats().drawCalls})`)
|
|
245
245
|
check('大数据量下一帧画得完(有 drawCalls,没有报错)', big.getStats().drawCalls > 0)
|
|
246
|
+
// getBounds() 缓存:整份解析后直接命中;定向改点 / 改线要立即失效,不能读到旧范围
|
|
247
|
+
const boundsHost = document.createElement('div')
|
|
248
|
+
document.querySelector('#map').append(boundsHost)
|
|
249
|
+
const boundsView = createGisView(boundsHost, {
|
|
250
|
+
points: [{ id: 'bp0', x: 10, y: 10 }, { id: 'bp1', x: 20, y: 20 }],
|
|
251
|
+
lines: [{ id: 'bl0', path: [[10, 10], [15, 15]] }],
|
|
252
|
+
width: 320,
|
|
253
|
+
height: 200
|
|
254
|
+
})
|
|
255
|
+
const boundsBefore = boundsView.getBounds()
|
|
256
|
+
boundsView.updatePoint('bp0', { x: -100, y: -200 })
|
|
257
|
+
const boundsAfterPoint = boundsView.getBounds()
|
|
258
|
+
boundsView.updateLine('bl0', { path: [[-300, -400], [0, 0]] })
|
|
259
|
+
const boundsAfterLine = boundsView.getBounds()
|
|
260
|
+
check('数据包围盒缓存:整份解析后命中缓存,定向改点 / 改线都会立即失效',
|
|
261
|
+
boundsBefore.minX === 10 && boundsAfterPoint.minX === -100 && boundsAfterPoint.minY === -200 && boundsAfterLine.minX === -300 && boundsAfterLine.minY === -400,
|
|
262
|
+
`${JSON.stringify(boundsBefore)} → ${JSON.stringify(boundsAfterPoint)} → ${JSON.stringify(boundsAfterLine)}`)
|
|
263
|
+
boundsAfterLine.minX = 99999
|
|
264
|
+
const boundsAfterMutation = boundsView.getBounds()
|
|
265
|
+
check('数据包围盒缓存:调用方修改返回值不会污染下一次读数',
|
|
266
|
+
boundsAfterMutation.minX === -300 && boundsAfterMutation.minY === -400,
|
|
267
|
+
JSON.stringify(boundsAfterMutation))
|
|
268
|
+
boundsView.addPoint({ id: 'bp2', x: -600, y: -700 })
|
|
269
|
+
const boundsAfterAppend = boundsView.getBounds()
|
|
270
|
+
check('数据包围盒缓存:追加到范围外的节点会立即扩大读数',
|
|
271
|
+
boundsAfterAppend.minX === -600 && boundsAfterAppend.minY === -700,
|
|
272
|
+
JSON.stringify(boundsAfterAppend))
|
|
273
|
+
boundsView.destroy()
|
|
274
|
+
boundsHost.remove()
|
|
246
275
|
// 两万个点会走发散渲染:等它铺完再拾取(没铺到的节点按设计不参与拾取)
|
|
247
276
|
const readyDeadline = now() + 8000
|
|
248
277
|
while (!big.isLayoutReady() && now() < readyDeadline) {
|
|
@@ -265,7 +294,7 @@
|
|
|
265
294
|
// 选中 N 个节点时每个节点的连带线路都要算一遍:从前是「N × 全部线路」的比对,
|
|
266
295
|
// 框选几千个节点、删除确认面板还没弹出来就先卡住了;现在按 id 建表、线路与通道各扫一遍。
|
|
267
296
|
const impactCount = 8000
|
|
268
|
-
big.selectFeatures(points.slice(0, impactCount).map(point => point.id))
|
|
297
|
+
big.selectFeatures(points.slice(0, impactCount).map(point => point.id))
|
|
269
298
|
const removeStarted = now()
|
|
270
299
|
const removePlan = big.requestRemove()
|
|
271
300
|
const removeMs = now() - removeStarted
|
|
@@ -274,7 +303,7 @@
|
|
|
274
303
|
JSON.stringify(removePlan.counts))
|
|
275
304
|
diag(`删除确认载荷(选中 ${impactCount} 点 / ${lineCount} 线)`, `${removeMs.toFixed(1)} ms`)
|
|
276
305
|
removePlan.cancel()
|
|
277
|
-
big.selectFeatures(null)
|
|
306
|
+
big.selectFeatures(null)
|
|
278
307
|
|
|
279
308
|
/* ---------------- 通道在场时的补点 / 删点:这一轮之前一律回落整份重建 ---------------- */
|
|
280
309
|
// 通道的成员表按线路下标记、骨架按锚点 id 定:声明逐条没变、没删到回线与锚点时走增量
|
|
@@ -382,16 +411,16 @@
|
|
|
382
411
|
await sleep(20)
|
|
383
412
|
}
|
|
384
413
|
await sleep(300)
|
|
385
|
-
spriteProbe.selectFeatures(['sprite-0'])
|
|
414
|
+
spriteProbe.selectFeatures(['sprite-0'])
|
|
386
415
|
await sleep(500)
|
|
387
|
-
spriteProbe.selectFeatures(null)
|
|
416
|
+
spriteProbe.selectFeatures(null)
|
|
388
417
|
await sleep(200)
|
|
389
418
|
spriteProbe.refresh()
|
|
390
419
|
await sleep(120)
|
|
391
420
|
spriteProbe.refresh()
|
|
392
421
|
await sleep(120)
|
|
393
422
|
const spriteCallsBefore = spriteProbe.getStats().drawCalls
|
|
394
|
-
spriteProbe.selectFeatures(['sprite-0'])
|
|
423
|
+
spriteProbe.selectFeatures(['sprite-0'])
|
|
395
424
|
await sleep(200)
|
|
396
425
|
const spriteCallsAfter = spriteProbe.getStats().drawCalls
|
|
397
426
|
check('贴图增量建组只多一次绘制调用(编号表与绘制序分开,新建的组不会被画两遍)', spriteCallsAfter - spriteCallsBefore === 1, `${spriteCallsBefore} → ${spriteCallsAfter}`)
|
|
@@ -581,23 +610,25 @@
|
|
|
581
610
|
}
|
|
582
611
|
const anchorView = createGisView(anchorHost, { points: anchorPoints, lines: anchorLines, width: 900, height: 560, label: { content: '{name}', maxCount: 200 }, lineLabel: { maxCount: 60 } })
|
|
583
612
|
await sleep(200)
|
|
584
|
-
for (let step = 0; step < 6; step += 1) {
|
|
585
|
-
anchorView.setCamera({ center: [1000 + step * 120, 700 + step * 60], scale: 1.4 })
|
|
586
|
-
// 用 rAF
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
613
|
+
for (let step = 0; step < 6; step += 1) {
|
|
614
|
+
anchorView.setCamera({ center: [1000 + step * 120, 700 + step * 60], scale: 1.4 })
|
|
615
|
+
// 用 rAF 等这一步:相机每动一下标签层就照着新视口收一遍候选
|
|
616
|
+
await new Promise(resolve => requestAnimationFrame(() => resolve()))
|
|
617
|
+
}
|
|
618
|
+
// 程序化相机运动还有 160ms 的轻量画法窗口,停稳后会补一张全质量帧并再收一次候选。
|
|
619
|
+
// 先让这一帧落地,再开始计量,后面测到的才是稳态重排,而不是收尾那一帧的噪声。
|
|
620
|
+
await sleep(800)
|
|
590
621
|
const anchorStats = { reads: 0, duplicates: 0 }
|
|
591
|
-
const anchorSeen = new Set()
|
|
592
|
-
const anchorReads = []
|
|
593
|
-
const anchorDuplicates = []
|
|
622
|
+
const anchorSeen = new Set()
|
|
623
|
+
const anchorReads = []
|
|
624
|
+
const anchorDuplicates = []
|
|
594
625
|
const nativeAnchorRect = Element.prototype.getBoundingClientRect
|
|
595
626
|
Element.prototype.getBoundingClientRect = function (...args) {
|
|
596
|
-
if (this.classList?.contains('gis-view__label')) {
|
|
597
|
-
anchorStats.reads += 1
|
|
598
|
-
// 按标签身份(类型 + 行号 + 标签名)去重:标签节点是循环复用的,按节点去重会把「同一格换了条标签」误判成重复量
|
|
599
|
-
const anchorKey = `${this.dataset.labelKind}#${this.dataset.labelIndex}#${this.dataset.labelLabel}`
|
|
600
|
-
if (anchorSeen.has(anchorKey)) {
|
|
627
|
+
if (this.classList?.contains('gis-view__label')) {
|
|
628
|
+
anchorStats.reads += 1
|
|
629
|
+
// 按标签身份(类型 + 行号 + 标签名)去重:标签节点是循环复用的,按节点去重会把「同一格换了条标签」误判成重复量
|
|
630
|
+
const anchorKey = `${this.dataset.labelKind}#${this.dataset.labelIndex}#${this.dataset.labelLabel}`
|
|
631
|
+
if (anchorSeen.has(anchorKey)) {
|
|
601
632
|
anchorStats.duplicates += 1
|
|
602
633
|
}
|
|
603
634
|
anchorSeen.add(anchorKey)
|
|
@@ -605,9 +636,9 @@
|
|
|
605
636
|
return nativeAnchorRect.apply(this, args)
|
|
606
637
|
}
|
|
607
638
|
const anchorRound = () => {
|
|
608
|
-
anchorStats.reads = 0
|
|
609
|
-
anchorStats.duplicates = 0
|
|
610
|
-
anchorSeen.clear()
|
|
639
|
+
anchorStats.reads = 0
|
|
640
|
+
anchorStats.duplicates = 0
|
|
641
|
+
anchorSeen.clear()
|
|
611
642
|
}
|
|
612
643
|
try {
|
|
613
644
|
// 跑 6 轮:换锚点(anchorFallback)/ 沿线滑动(slideFallback)会让某一轮补画几十条
|
|
@@ -615,21 +646,21 @@
|
|
|
615
646
|
// 最后三轮盯的就是「收敛之后一次布局都不读」
|
|
616
647
|
for (let round = 0; round < 6; round += 1) {
|
|
617
648
|
anchorRound()
|
|
618
|
-
anchorView.updateOptions({ label: { content: '{name}', maxCount: 200 }, lineLabel: { maxCount: 60 } })
|
|
619
|
-
anchorReads.push(anchorStats.reads)
|
|
620
|
-
anchorDuplicates.push(anchorStats.duplicates)
|
|
649
|
+
anchorView.updateOptions({ label: { content: '{name}', maxCount: 200 }, lineLabel: { maxCount: 60 } })
|
|
650
|
+
anchorReads.push(anchorStats.reads)
|
|
651
|
+
anchorDuplicates.push(anchorStats.duplicates)
|
|
621
652
|
}
|
|
622
653
|
// 改一条视口里的标签(下标 = 76 行 × 400 列 + 150 → 世界坐标 1500, 760):这一轮同样不该整层重量
|
|
623
654
|
anchorRound()
|
|
624
|
-
anchorView.updatePoint('a' + (76 * 400 + 150), { name: '改过的名字' })
|
|
625
|
-
anchorReads.push(anchorStats.reads)
|
|
626
|
-
anchorDuplicates.push(anchorStats.duplicates)
|
|
655
|
+
anchorView.updatePoint('a' + (76 * 400 + 150), { name: '改过的名字' })
|
|
656
|
+
anchorReads.push(anchorStats.reads)
|
|
657
|
+
anchorDuplicates.push(anchorStats.duplicates)
|
|
627
658
|
}
|
|
628
659
|
finally {
|
|
629
660
|
Element.prototype.getBoundingClientRect = nativeAnchorRect
|
|
630
661
|
}
|
|
631
|
-
diag('放大后连续整层重排 · 标签量测读数', `逐轮 ${anchorReads.join(' / ')} 次 · 同一轮里重复量 ${anchorDuplicates.join(' / ')} 条(按标签身份去重;旧写法第 1 轮 2662 次里有 1235 次是把同一条标签又量了一遍)`)
|
|
632
|
-
check('稳态重排(后 3 轮)一次布局都不读(避让盒全走算术)', anchorReads.slice(3, 6).every(value => value === 0), `第 4~6 轮 ${anchorReads.slice(3, 6).join(' / ')} 次`)
|
|
662
|
+
diag('放大后连续整层重排 · 标签量测读数', `逐轮 ${anchorReads.join(' / ')} 次 · 同一轮里重复量 ${anchorDuplicates.join(' / ')} 条(按标签身份去重;旧写法第 1 轮 2662 次里有 1235 次是把同一条标签又量了一遍)`)
|
|
663
|
+
check('稳态重排(后 3 轮)一次布局都不读(避让盒全走算术)', anchorReads.slice(3, 6).every(value => value === 0), `第 4~6 轮 ${anchorReads.slice(3, 6).join(' / ')} 次`)
|
|
633
664
|
// 换落点那条路补画的新标签都是「第一次出现」,各量一次尺寸就进缓存;整段合计要有限,
|
|
634
665
|
// 真退回「每轮把所有标签都真量一遍」的老写法时这里会立刻爆掉(旧写法一轮 1299 ~ 2662 次)
|
|
635
666
|
check('稳态重排里补画的新标签只量第一次出现的那一次(合计有限、之后回零)', anchorReads.slice(1, 6).reduce((sum, value) => sum + value, 0) <= 400, `第 2~6 轮合计 ${anchorReads.slice(1, 6).reduce((sum, value) => sum + value, 0)} 次(逐轮 ${anchorReads.slice(1, 6).join(' / ')})`)
|
|
@@ -932,7 +963,7 @@
|
|
|
932
963
|
removeView.setFilter({ points: { flag: 'show' }, worker: false })
|
|
933
964
|
await sleep(60)
|
|
934
965
|
const hiddenBefore = [...removeView.getFilterResult().hiddenPoints]
|
|
935
|
-
removeView.selectFeatures('d15000')
|
|
966
|
+
removeView.selectFeatures('d15000')
|
|
936
967
|
await sleep(30)
|
|
937
968
|
const removeRebuildStarted = now()
|
|
938
969
|
removeView.refresh()
|
|
@@ -967,7 +998,7 @@
|
|
|
967
998
|
}
|
|
968
999
|
removedFullView.setCamera(removeCamera)
|
|
969
1000
|
removedFullView.setFilter({ points: { flag: 'show' }, worker: false })
|
|
970
|
-
removedFullView.selectFeatures('d15000')
|
|
1001
|
+
removedFullView.selectFeatures('d15000')
|
|
971
1002
|
await sleep(80)
|
|
972
1003
|
const removedFullScene = removedFullView.snapshotScene()
|
|
973
1004
|
const removedText = JSON.stringify(removedScene)
|
|
@@ -1065,7 +1096,7 @@
|
|
|
1065
1096
|
view.setFilter({ points: { flag: 'show' }, lines: { flag: 'show' }, worker: false })
|
|
1066
1097
|
}
|
|
1067
1098
|
await sleep(60)
|
|
1068
|
-
lineOnlyView.selectFeatures('q8000')
|
|
1099
|
+
lineOnlyView.selectFeatures('q8000')
|
|
1069
1100
|
await sleep(40)
|
|
1070
1101
|
let lineOnlyFallbackMs = Infinity
|
|
1071
1102
|
for (const id of lineOnlyRemovedIds) {
|
|
@@ -1094,7 +1125,7 @@
|
|
|
1094
1125
|
}
|
|
1095
1126
|
lineOnlyFullView.setCamera(lineOnlyCamera)
|
|
1096
1127
|
lineOnlyFullView.setFilter({ points: { flag: 'show' }, lines: { flag: 'show' }, worker: false })
|
|
1097
|
-
lineOnlyFullView.selectFeatures('q8000')
|
|
1128
|
+
lineOnlyFullView.selectFeatures('q8000')
|
|
1098
1129
|
await sleep(80)
|
|
1099
1130
|
const lineOnlyScene = lineOnlyView.snapshotScene()
|
|
1100
1131
|
const lineOnlyFullScene = lineOnlyFullView.snapshotScene()
|
|
@@ -1241,6 +1272,36 @@
|
|
|
1241
1272
|
check('样式快路径:只动线路样式,画出来的线宽照着新配置走',
|
|
1242
1273
|
widthAfter.length > 0 && widthAfter.every((width, index) => width > 0 && Math.abs(width - widthBefore[index] * 3) < 0.01),
|
|
1243
1274
|
`${widthBefore[0]} -> ${widthAfter[0]}`)
|
|
1275
|
+
// 线路窄样式:只改全局线宽 / 颜色 / 透明度,逐条重算 paint 链并补槽位;dataVersion 与虚线 / 自身样式都不动
|
|
1276
|
+
const lineNarrowVersion = fastView.dataVersion
|
|
1277
|
+
const lineNarrowStarted = now()
|
|
1278
|
+
fastView.updateLineStyle({ width: 7.5, color: '#1aa0b0', opacity: 0.66 })
|
|
1279
|
+
const lineNarrowMs = now() - lineNarrowStarted
|
|
1280
|
+
const lineNarrowStyle = fastView.getLineStyle(2)
|
|
1281
|
+
check('线路窄样式:getLineStyle 读到新的线宽 / 颜色 / 透明度,dataVersion 不变',
|
|
1282
|
+
lineNarrowStyle !== null && Math.abs(lineNarrowStyle.width - 7.5) < 1e-6 && Math.abs(lineNarrowStyle.opacity - 0.66) < 1e-6 &&
|
|
1283
|
+
lineNarrowStyle.color[1] > 0.5 && lineNarrowStyle.color[2] > 0.5 && lineNarrowVersion === fastView.dataVersion,
|
|
1284
|
+
`${JSON.stringify(lineNarrowStyle)} · dataVersion ${lineNarrowVersion} -> ${fastView.dataVersion}`)
|
|
1285
|
+
diag('线路窄样式:2000 条只改全局线宽 / 颜色 / 透明度', `${lineNarrowMs.toFixed(2)} ms`)
|
|
1286
|
+
fastView.updateLine('fl0', { width: 11, color: '#ff0000' })
|
|
1287
|
+
fastView.updateLine('fl1230', { dash: 'dashed' })
|
|
1288
|
+
fastView.updateLineStyle({ width: 3, color: '#33aaff', opacity: 1 })
|
|
1289
|
+
const lineOwnStyle = fastView.getLineStyle(0)
|
|
1290
|
+
const lineDashScene = fastView.snapshotScene()
|
|
1291
|
+
check('线路窄样式:逐线自身线宽 / 颜色优先,全局换色不会覆盖它',
|
|
1292
|
+
lineOwnStyle !== null && Math.abs(lineOwnStyle.width - 11) < 1e-6 && lineOwnStyle.color[0] > 0.99 && lineOwnStyle.color[1] < 0.01,
|
|
1293
|
+
JSON.stringify(lineOwnStyle))
|
|
1294
|
+
check('线路窄样式:只换线宽 / 颜色 / 透明度时虚线图案保持不动', lineDashScene.lines.some(line => Array.isArray(line.dash) && line.dash.length > 0))
|
|
1295
|
+
// 有线路规则时窄路必须让开:规则改出来的宽度 / 颜色仍按规则走,规则外的那条跟着全局样式
|
|
1296
|
+
fastView.updateOptions({ lineRules: [{ when: { id: 'fl2' }, style: { width: 8, color: '#00aa55' } }] })
|
|
1297
|
+
fastView.updateLineStyle({ width: 2, color: '#888888' })
|
|
1298
|
+
const lineRuleStyle = fastView.getLineStyle(2)
|
|
1299
|
+
const lineGlobalStyle = fastView.getLineStyle(3)
|
|
1300
|
+
check('线路窄样式:存在线路规则时回退整份解析,规则优先于新的全局线样式',
|
|
1301
|
+
lineRuleStyle !== null && Math.abs(lineRuleStyle.width - 8) < 1e-6 && lineRuleStyle.color[1] > 0.6 && lineRuleStyle.color[0] < 0.01 &&
|
|
1302
|
+
lineGlobalStyle !== null && Math.abs(lineGlobalStyle.width - 2) < 1e-6,
|
|
1303
|
+
`规则 ${JSON.stringify(lineRuleStyle)} / 全局 ${JSON.stringify(lineGlobalStyle)}`)
|
|
1304
|
+
fastView.updateOptions({ lineRules: [] })
|
|
1244
1305
|
// 热力图:值域色带换一份,重上色之后与「一开始就用这份色带」逐字段一致
|
|
1245
1306
|
const heatCount = 1200
|
|
1246
1307
|
const heatPoints = new Array(heatCount)
|
|
@@ -1278,63 +1339,63 @@
|
|
|
1278
1339
|
const heatFreshView = createGisView(heatFreshHost, { points: heatPoints, lines: heatLines, heatmap: heatB, width: 640, height: 380, quality: 'high' })
|
|
1279
1340
|
heatFreshView.setCamera({ center: [320, 200], scale: 1 })
|
|
1280
1341
|
const heatFreshText = JSON.stringify(heatFreshView.snapshotScene())
|
|
1281
|
-
check('样式快路径:同一份热力图配置原样再发一次,值域按当前数据重算(与整份重建一致)',
|
|
1282
|
-
heatRefreshedText !== heatAfter && heatRefreshedText === heatFreshText,
|
|
1283
|
-
heatRefreshedText === heatFreshText ? `快照 ${heatRefreshedText.length} 字节,逐字段一致` : diffText(heatRefreshedText, heatFreshText))
|
|
1284
|
-
// 密度场:只改 opacity 时复用上一份采样与贴图;换色带 / 换要素值才重烤。
|
|
1285
|
-
// 判断重烤用 heatDensityStats 的对象身份:只改画法那条快路不会换它。
|
|
1286
|
-
const densityOption = {
|
|
1287
|
-
enabled: true,
|
|
1288
|
-
field: 'value',
|
|
1289
|
-
targets: ['point'],
|
|
1290
|
-
domain: [0, 100],
|
|
1291
|
-
colors: ['#0b3d91', '#ffd166', '#ff5e5e'],
|
|
1292
|
-
density: { opacity: 1, radius: 22, resolution: 192, levels: 8 }
|
|
1293
|
-
}
|
|
1294
|
-
const densityHost = document.createElement('div')
|
|
1295
|
-
document.querySelector('#map').append(densityHost)
|
|
1296
|
-
const densityView = createGisView(densityHost, {
|
|
1297
|
-
points: heatPoints,
|
|
1298
|
-
lines: [],
|
|
1299
|
-
heatmap: densityOption,
|
|
1300
|
-
width: 640,
|
|
1301
|
-
height: 380,
|
|
1302
|
-
quality: 'high',
|
|
1303
|
-
showGrid: false,
|
|
1304
|
-
background: null
|
|
1305
|
-
})
|
|
1306
|
-
densityView.fitView()
|
|
1307
|
-
await densityView.whenRendered()
|
|
1308
|
-
await densityView.whenRendered()
|
|
1309
|
-
const densityFirst = densityView.getStats().heatDensity
|
|
1310
|
-
check('热力图密度场:首帧烤出有效场(有采样数、贴图尺寸非零)',
|
|
1311
|
-
Boolean(densityFirst) && densityFirst.samples > 0 && densityFirst.width > 0 && densityFirst.height > 0,
|
|
1312
|
-
JSON.stringify(densityFirst))
|
|
1313
|
-
const opacityOption = { ...densityOption, density: { ...densityOption.density, opacity: 0.35 } }
|
|
1314
|
-
const opacityStarted = now()
|
|
1315
|
-
densityView.updateOptions({ heatmap: opacityOption })
|
|
1316
|
-
const opacityUpdateMs = now() - opacityStarted
|
|
1317
|
-
await densityView.whenRendered()
|
|
1318
|
-
const densityAfterOpacity = densityView.getStats().heatDensity
|
|
1319
|
-
check('热力图密度场:只改 density.opacity 不重采、不重烤(复用同一份烘焙读数)',
|
|
1320
|
-
densityAfterOpacity === densityFirst)
|
|
1321
|
-
const colorOption = { ...opacityOption, colors: ['#123456', '#7ce0b0', '#ff8a3d'] }
|
|
1322
|
-
const colorStarted = now()
|
|
1323
|
-
densityView.updateOptions({ heatmap: colorOption })
|
|
1324
|
-
const colorUpdateMs = now() - colorStarted
|
|
1325
|
-
await densityView.whenRendered()
|
|
1326
|
-
const densityAfterColor = densityView.getStats().heatDensity
|
|
1327
|
-
check('热力图密度场:换色带会重烤(生成新一份烘焙读数)',
|
|
1328
|
-
densityAfterColor !== densityAfterOpacity && densityAfterColor.samples === densityAfterOpacity.samples)
|
|
1329
|
-
densityView.updatePoint('h0', { value: 7 })
|
|
1330
|
-
await densityView.whenRendered()
|
|
1331
|
-
const densityAfterPoint = densityView.getStats().heatDensity
|
|
1332
|
-
check('热力图密度场:定向 updatePoint 改要素值后场跟着重烤(不再停在旧数据上)',
|
|
1333
|
-
densityAfterPoint !== densityAfterColor && densityAfterPoint.samples === densityAfterColor.samples)
|
|
1334
|
-
diag('热力图密度场更新', `纯透明度 ${opacityUpdateMs.toFixed(2)} ms / 换色带 ${colorUpdateMs.toFixed(2)} ms;烘焙 ${densityAfterColor.ms.toFixed(1)} ms`)
|
|
1335
|
-
densityView.destroy()
|
|
1336
|
-
densityHost.remove()
|
|
1337
|
-
heatFreshView.destroy()
|
|
1342
|
+
check('样式快路径:同一份热力图配置原样再发一次,值域按当前数据重算(与整份重建一致)',
|
|
1343
|
+
heatRefreshedText !== heatAfter && heatRefreshedText === heatFreshText,
|
|
1344
|
+
heatRefreshedText === heatFreshText ? `快照 ${heatRefreshedText.length} 字节,逐字段一致` : diffText(heatRefreshedText, heatFreshText))
|
|
1345
|
+
// 密度场:只改 opacity 时复用上一份采样与贴图;换色带 / 换要素值才重烤。
|
|
1346
|
+
// 判断重烤用 heatDensityStats 的对象身份:只改画法那条快路不会换它。
|
|
1347
|
+
const densityOption = {
|
|
1348
|
+
enabled: true,
|
|
1349
|
+
field: 'value',
|
|
1350
|
+
targets: ['point'],
|
|
1351
|
+
domain: [0, 100],
|
|
1352
|
+
colors: ['#0b3d91', '#ffd166', '#ff5e5e'],
|
|
1353
|
+
density: { opacity: 1, radius: 22, resolution: 192, levels: 8 }
|
|
1354
|
+
}
|
|
1355
|
+
const densityHost = document.createElement('div')
|
|
1356
|
+
document.querySelector('#map').append(densityHost)
|
|
1357
|
+
const densityView = createGisView(densityHost, {
|
|
1358
|
+
points: heatPoints,
|
|
1359
|
+
lines: [],
|
|
1360
|
+
heatmap: densityOption,
|
|
1361
|
+
width: 640,
|
|
1362
|
+
height: 380,
|
|
1363
|
+
quality: 'high',
|
|
1364
|
+
showGrid: false,
|
|
1365
|
+
background: null
|
|
1366
|
+
})
|
|
1367
|
+
densityView.fitView()
|
|
1368
|
+
await densityView.whenRendered()
|
|
1369
|
+
await densityView.whenRendered()
|
|
1370
|
+
const densityFirst = densityView.getStats().heatDensity
|
|
1371
|
+
check('热力图密度场:首帧烤出有效场(有采样数、贴图尺寸非零)',
|
|
1372
|
+
Boolean(densityFirst) && densityFirst.samples > 0 && densityFirst.width > 0 && densityFirst.height > 0,
|
|
1373
|
+
JSON.stringify(densityFirst))
|
|
1374
|
+
const opacityOption = { ...densityOption, density: { ...densityOption.density, opacity: 0.35 } }
|
|
1375
|
+
const opacityStarted = now()
|
|
1376
|
+
densityView.updateOptions({ heatmap: opacityOption })
|
|
1377
|
+
const opacityUpdateMs = now() - opacityStarted
|
|
1378
|
+
await densityView.whenRendered()
|
|
1379
|
+
const densityAfterOpacity = densityView.getStats().heatDensity
|
|
1380
|
+
check('热力图密度场:只改 density.opacity 不重采、不重烤(复用同一份烘焙读数)',
|
|
1381
|
+
densityAfterOpacity === densityFirst)
|
|
1382
|
+
const colorOption = { ...opacityOption, colors: ['#123456', '#7ce0b0', '#ff8a3d'] }
|
|
1383
|
+
const colorStarted = now()
|
|
1384
|
+
densityView.updateOptions({ heatmap: colorOption })
|
|
1385
|
+
const colorUpdateMs = now() - colorStarted
|
|
1386
|
+
await densityView.whenRendered()
|
|
1387
|
+
const densityAfterColor = densityView.getStats().heatDensity
|
|
1388
|
+
check('热力图密度场:换色带会重烤(生成新一份烘焙读数)',
|
|
1389
|
+
densityAfterColor !== densityAfterOpacity && densityAfterColor.samples === densityAfterOpacity.samples)
|
|
1390
|
+
densityView.updatePoint('h0', { value: 7 })
|
|
1391
|
+
await densityView.whenRendered()
|
|
1392
|
+
const densityAfterPoint = densityView.getStats().heatDensity
|
|
1393
|
+
check('热力图密度场:定向 updatePoint 改要素值后场跟着重烤(不再停在旧数据上)',
|
|
1394
|
+
densityAfterPoint !== densityAfterColor && densityAfterPoint.samples === densityAfterColor.samples)
|
|
1395
|
+
diag('热力图密度场更新', `纯透明度 ${opacityUpdateMs.toFixed(2)} ms / 换色带 ${colorUpdateMs.toFixed(2)} ms;烘焙 ${densityAfterColor.ms.toFixed(1)} ms`)
|
|
1396
|
+
densityView.destroy()
|
|
1397
|
+
densityHost.remove()
|
|
1398
|
+
heatFreshView.destroy()
|
|
1338
1399
|
heatFreshHost.remove()
|
|
1339
1400
|
heatView.destroy()
|
|
1340
1401
|
heatHost.remove()
|
|
@@ -1811,18 +1872,18 @@
|
|
|
1811
1872
|
document.body.appendChild(thinHost)
|
|
1812
1873
|
const thinPoints = Array.from({ length: 12 }, (unused, index) => ({ id: `tp-${index}`, x: index * 10, y: index % 3 * 6, name: `小点 ${index}` }))
|
|
1813
1874
|
const thinLines = thinPoints.slice(1).map((point, index) => ({ id: `tl-${index}`, from: thinPoints[index].id, to: point.id }))
|
|
1814
|
-
const thinView = createGisView(thinHost, {
|
|
1875
|
+
const thinView = createGisView(thinHost, {
|
|
1815
1876
|
points: thinPoints,
|
|
1816
1877
|
lines: thinLines,
|
|
1817
1878
|
pointStyle: { size: 6 },
|
|
1818
1879
|
lineStyle: { width: 1 },
|
|
1819
1880
|
fitPadding: 20,
|
|
1820
1881
|
// 交互期的那几条读数(细线快路径 / 后备缓冲)靠拖动触发:平移显式打开
|
|
1821
|
-
features: { pan: true }
|
|
1822
|
-
})
|
|
1823
|
-
// 页面底部的临时画布先滚进视口:动画与交互帧只在可见时连续推进
|
|
1824
|
-
thinHost.scrollIntoView({ block: 'center', inline: 'nearest' })
|
|
1825
|
-
await sleep(400)
|
|
1882
|
+
features: { pan: true }
|
|
1883
|
+
})
|
|
1884
|
+
// 页面底部的临时画布先滚进视口:动画与交互帧只在可见时连续推进
|
|
1885
|
+
thinHost.scrollIntoView({ block: 'center', inline: 'nearest' })
|
|
1886
|
+
await sleep(400)
|
|
1826
1887
|
const thinCanvas = thinHost.querySelector('canvas')
|
|
1827
1888
|
const thinPointerId = 91
|
|
1828
1889
|
const fireThin = (type, at) => thinCanvas.dispatchEvent(new PointerEvent(type, {
|
|
@@ -1882,11 +1943,74 @@
|
|
|
1882
1943
|
thinView.updateOptions({ interactionNoMsaa: false })
|
|
1883
1944
|
const noMsaaOff = await statsWhilePanning()
|
|
1884
1945
|
check('interactionNoMsaa: false 时交互期也直接画进画布', noMsaaOff.noMsaa === false)
|
|
1885
|
-
thinView.updateOptions({ interactionNoMsaa: true })
|
|
1886
|
-
const noMsaaBack = await statsWhilePanning()
|
|
1887
|
-
check('改回 true 又走后备缓冲(开关是每帧现读的)', noMsaaBack.noMsaa === msaaOn)
|
|
1888
|
-
|
|
1889
|
-
|
|
1946
|
+
thinView.updateOptions({ interactionNoMsaa: true })
|
|
1947
|
+
const noMsaaBack = await statsWhilePanning()
|
|
1948
|
+
check('改回 true 又走后备缓冲(开关是每帧现读的)', noMsaaBack.noMsaa === msaaOn)
|
|
1949
|
+
|
|
1950
|
+
/* ---------------- 程序化相机运动复用交互快路径 ---------------- */
|
|
1951
|
+
// zoomBy / setCamera / centerOn 不经过 beginInteraction,但同样要吃到非 MSAA 与细线那一档:
|
|
1952
|
+
// 运动中的帧按轻量画法走,停稳 160ms 后自动补一张全质量静态帧。
|
|
1953
|
+
await sleep(400)
|
|
1954
|
+
check('相机静止时仍直接画进 MSAA 画布', thinView.getStats().noMsaa === false && thinView.getStats().thinLines === false)
|
|
1955
|
+
// 运动帧只能换画法,不能降低渲染分辨率:拉长帧间隔触发旧的自适应降档条件,
|
|
1956
|
+
// 只要 viewport 仍是画布尺寸,贴回就是 1:1,不会出现整幅放大后的短暂模糊。
|
|
1957
|
+
const cameraGl = thinCanvas.getContext('webgl2')
|
|
1958
|
+
const viewportPrototype = Object.getPrototypeOf(cameraGl)
|
|
1959
|
+
const originalViewport = viewportPrototype.viewport
|
|
1960
|
+
const viewportSizes = []
|
|
1961
|
+
viewportPrototype.viewport = function (...args) {
|
|
1962
|
+
if (this === cameraGl) {
|
|
1963
|
+
viewportSizes.push([args[2], args[3]])
|
|
1964
|
+
}
|
|
1965
|
+
return originalViewport.apply(this, args)
|
|
1966
|
+
}
|
|
1967
|
+
try {
|
|
1968
|
+
const motionStart = thinView.getCamera()
|
|
1969
|
+
for (let step = 0; step < 5; step += 1) {
|
|
1970
|
+
thinView.setCamera({
|
|
1971
|
+
center: [motionStart.center[0] + step + 1, motionStart.center[1] + step],
|
|
1972
|
+
scale: motionStart.scale * (1 + step * 0.01)
|
|
1973
|
+
})
|
|
1974
|
+
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))
|
|
1975
|
+
await sleep(35)
|
|
1976
|
+
}
|
|
1977
|
+
const mismatched = viewportSizes.filter(([width, height]) => width !== thinCanvas.width || height !== thinCanvas.height)
|
|
1978
|
+
check('相机运动帧始终保持画布全分辨率(不会先模糊一下)',
|
|
1979
|
+
viewportSizes.length > 0 && mismatched.length === 0,
|
|
1980
|
+
`viewport ${viewportSizes.length} 次 · 非全尺寸 ${mismatched.length} 次 · canvas=${thinCanvas.width}x${thinCanvas.height}${mismatched.length ? ` · 最后 ${mismatched.at(-1).join('x')}` : ''}`)
|
|
1981
|
+
}
|
|
1982
|
+
finally {
|
|
1983
|
+
viewportPrototype.viewport = originalViewport
|
|
1984
|
+
}
|
|
1985
|
+
await sleep(400)
|
|
1986
|
+
const statsWhileCameraMoving = async (move) => {
|
|
1987
|
+
move()
|
|
1988
|
+
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))
|
|
1989
|
+
return thinView.getStats()
|
|
1990
|
+
}
|
|
1991
|
+
const cameraMoving = await statsWhileCameraMoving(() => thinView.zoomBy(1.02))
|
|
1992
|
+
check('程序化 zoomBy 的运动帧走细线快路径', cameraMoving.thinLines === true,
|
|
1993
|
+
`thinLines=${cameraMoving.thinLines}`)
|
|
1994
|
+
check('程序化 zoomBy 的运动帧走非 MSAA 后备缓冲(画布开了 MSAA 时)',
|
|
1995
|
+
cameraMoving.noMsaa === msaaOn,
|
|
1996
|
+
`antialias=${msaaOn} noMsaa=${cameraMoving.noMsaa}`)
|
|
1997
|
+
await sleep(400)
|
|
1998
|
+
const cameraSettled = thinView.getStats()
|
|
1999
|
+
check('相机停稳后回到全质量静态帧', cameraSettled.thinLines === false && cameraSettled.noMsaa === false,
|
|
2000
|
+
`thinLines=${cameraSettled.thinLines} noMsaa=${cameraSettled.noMsaa}`)
|
|
2001
|
+
const sameCamera = thinView.getCamera()
|
|
2002
|
+
thinView.setCamera({ center: sameCamera.center, scale: sameCamera.scale })
|
|
2003
|
+
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))
|
|
2004
|
+
const cameraNoop = thinView.getStats()
|
|
2005
|
+
check('同一位置重复 setCamera 不会再进轻量档', cameraNoop.thinLines === false && cameraNoop.noMsaa === false,
|
|
2006
|
+
`thinLines=${cameraNoop.thinLines} noMsaa=${cameraNoop.noMsaa}`)
|
|
2007
|
+
thinView.updateOptions({ interactionNoMsaa: false })
|
|
2008
|
+
const cameraNoMsaaOff = await statsWhileCameraMoving(() => thinView.setCamera({ center: [thinView.getCamera().center[0] + 1, thinView.getCamera().center[1]] }))
|
|
2009
|
+
check('interactionNoMsaa: false 时程序化相机运动也直接画进画布', cameraNoMsaaOff.noMsaa === false)
|
|
2010
|
+
thinView.updateOptions({ interactionNoMsaa: true })
|
|
2011
|
+
await sleep(400)
|
|
2012
|
+
|
|
2013
|
+
// 动画在跑的那些帧也走这一档:开一组涟漪、不碰指针,等一帧读读数(判定是 interacting || animationRunning())
|
|
1890
2014
|
thinView.updateOptions({ effects: { point: { radius: 14, period: 2, count: 1 } } })
|
|
1891
2015
|
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))
|
|
1892
2016
|
await sleep(160)
|
|
@@ -1927,12 +2051,11 @@
|
|
|
1927
2051
|
const targetFlag = thinView.getStats().noMsaa
|
|
1928
2052
|
thinView.updateOptions({ interactionNoMsaa: false })
|
|
1929
2053
|
thinView.refresh({ reflow: false })
|
|
1930
|
-
const direct = readAll()
|
|
1931
|
-
const directFlag = thinView.getStats().noMsaa
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
let
|
|
1935
|
-
let totalDelta = 0
|
|
2054
|
+
const direct = readAll()
|
|
2055
|
+
const directFlag = thinView.getStats().noMsaa
|
|
2056
|
+
thinView.updateOptions({ interactionNoMsaa: true })
|
|
2057
|
+
let differing = 0
|
|
2058
|
+
let totalDelta = 0
|
|
1936
2059
|
for (let index = 0; index < withTarget.length; index += 1) {
|
|
1937
2060
|
const delta = Math.abs(withTarget[index] - direct[index])
|
|
1938
2061
|
totalDelta += delta
|
|
@@ -1941,10 +2064,53 @@
|
|
|
1941
2064
|
}
|
|
1942
2065
|
}
|
|
1943
2066
|
const thinPixels = thinCanvas.width * thinCanvas.height
|
|
1944
|
-
check('后备缓冲那一帧与直接画的那一帧画面基本一致(差别只在抗锯齿边缘)',
|
|
1945
|
-
targetFlag === true && directFlag === false && differing / 4 < thinPixels * 0.2 && totalDelta / withTarget.length < 4,
|
|
1946
|
-
`${(differing / 4 / thinPixels * 100).toFixed(1)}% 像素有差 · 平均差 ${(totalDelta / withTarget.length).toFixed(2)}`);
|
|
1947
|
-
|
|
2067
|
+
check('后备缓冲那一帧与直接画的那一帧画面基本一致(差别只在抗锯齿边缘)',
|
|
2068
|
+
targetFlag === true && directFlag === false && differing / 4 < thinPixels * 0.2 && totalDelta / withTarget.length < 4,
|
|
2069
|
+
`${(differing / 4 / thinPixels * 100).toFixed(1)}% 像素有差 · 平均差 ${(totalDelta / withTarget.length).toFixed(2)}`);
|
|
2070
|
+
// 细线快路径现在走专用的轻量片元管线:同一交互状态再切回四边形,颜色与位置必须对得上,
|
|
2071
|
+
// 差别只能来自 1 设备像素线宽 / 端点圆头,不能把整批线画丢、画黑或画成别的颜色。
|
|
2072
|
+
thinView.updateOptions({ interactionThinLines: false })
|
|
2073
|
+
thinView.refresh({ reflow: false })
|
|
2074
|
+
const quad = readAll()
|
|
2075
|
+
const quadFlag = thinView.getStats().thinLines
|
|
2076
|
+
thinView.updateOptions({ interactionThinLines: true })
|
|
2077
|
+
let thinDiffering = 0
|
|
2078
|
+
let thinTotalDelta = 0
|
|
2079
|
+
let thinInk = 0
|
|
2080
|
+
let quadInk = 0
|
|
2081
|
+
const background = [quad[0], quad[1], quad[2]]
|
|
2082
|
+
for (let index = 0; index < quad.length; index += 4) {
|
|
2083
|
+
const thinDelta = Math.max(
|
|
2084
|
+
Math.abs(withTarget[index] - background[0]),
|
|
2085
|
+
Math.abs(withTarget[index + 1] - background[1]),
|
|
2086
|
+
Math.abs(withTarget[index + 2] - background[2])
|
|
2087
|
+
)
|
|
2088
|
+
const quadDelta = Math.max(
|
|
2089
|
+
Math.abs(quad[index] - background[0]),
|
|
2090
|
+
Math.abs(quad[index + 1] - background[1]),
|
|
2091
|
+
Math.abs(quad[index + 2] - background[2])
|
|
2092
|
+
)
|
|
2093
|
+
if (thinDelta > 12) {
|
|
2094
|
+
thinInk += 1
|
|
2095
|
+
}
|
|
2096
|
+
if (quadDelta > 12) {
|
|
2097
|
+
quadInk += 1
|
|
2098
|
+
}
|
|
2099
|
+
for (let channel = 0; channel < 4; channel += 1) {
|
|
2100
|
+
const delta = Math.abs(quad[index + channel] - withTarget[index + channel])
|
|
2101
|
+
thinTotalDelta += delta
|
|
2102
|
+
if (delta > 0) {
|
|
2103
|
+
thinDiffering += 1
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
check('细线快路径与四边形画法位置 / 颜色一致(差别只在 1px 线宽边缘)',
|
|
2108
|
+
targetFlag === true && quadFlag === false && thinInk > 100 && quadInk > 100 &&
|
|
2109
|
+
thinInk / quadInk > 0.5 && thinInk / quadInk < 2 &&
|
|
2110
|
+
thinDiffering / 4 < thinPixels * 0.2 && thinTotalDelta / quad.length < 4,
|
|
2111
|
+
`${(thinDiffering / 4 / thinPixels * 100).toFixed(1)}% 像素有差 · 平均差 ${(thinTotalDelta / quad.length).toFixed(2)} · 线像素 ${thinInk}/${quadInk}`)
|
|
2112
|
+
fireThin('pointerup', thinAt)
|
|
2113
|
+
}
|
|
1948
2114
|
/* ---------------- 拖通道拐点:整条通道每帧重算的账(3000 回线不再是每次指针移动 140 ms) ---------------- */
|
|
1949
2115
|
// 老写法:拐点批次每帧整份重建 —— 通道里任一回线被选中就按整条通道的成员解析一遍把手,
|
|
1950
2116
|
// 3000 ~ 5000 回线的通道每次 pointermove 要 140 ms 上下(拖动时每帧一次,画面直接卡住)。
|
|
@@ -1964,17 +2130,17 @@
|
|
|
1964
2130
|
channelLines.push({ id: `cm-${index}`, from: 'p1', to: 'p2' })
|
|
1965
2131
|
channelLineIds.push(`cm-${index}`)
|
|
1966
2132
|
}
|
|
1967
|
-
const channelView = createGisView(channelHost, {
|
|
2133
|
+
const channelView = createGisView(channelHost, {
|
|
1968
2134
|
points: channelPoints,
|
|
1969
2135
|
lines: channelLines,
|
|
1970
2136
|
channels: [{ id: 'ch-cost', from: 'p1', to: 'p2', lineIds: channelLineIds, path: [[100, 30]] }],
|
|
1971
2137
|
fitPadding: 20,
|
|
1972
|
-
waypointEditable: true
|
|
1973
|
-
})
|
|
1974
|
-
channelView.selectFeatures('cm-0')
|
|
1975
|
-
// 拐点拖动靠真实指针命中:先把这张临时画布滚进视口,别让命中坐标落在视口外
|
|
1976
|
-
channelHost.scrollIntoView({ block: 'center', inline: 'nearest' })
|
|
1977
|
-
await sleep(200)
|
|
2138
|
+
waypointEditable: true
|
|
2139
|
+
})
|
|
2140
|
+
channelView.selectFeatures('cm-0')
|
|
2141
|
+
// 拐点拖动靠真实指针命中:先把这张临时画布滚进视口,别让命中坐标落在视口外
|
|
2142
|
+
channelHost.scrollIntoView({ block: 'center', inline: 'nearest' })
|
|
2143
|
+
await sleep(200)
|
|
1978
2144
|
const channelCanvas = channelHost.querySelector('canvas')
|
|
1979
2145
|
const channelWaypoint = channelView.getWaypoints(0).find(item => item.pathIndex === 1)
|
|
1980
2146
|
const channelSkeletonBefore = channelView.getChannelGroups('ch-cost')[0].skeleton.map(coordinate => coordinate[0])
|
|
@@ -2049,136 +2215,187 @@
|
|
|
2049
2215
|
const goCostBest = Math.min(...goCostMerge)
|
|
2050
2216
|
diag('分组编辑(2 万回线、只写 groups 的通道,合并一次)', `最快 ${goCostBest.toFixed(1)} ms`)
|
|
2051
2217
|
check('2 万回线的通道合并分组:40 ms 以内(成员池一次建表 + 集合去重,老的逐条 includes 是平方级)', goCostBest < 40, `最快 ${goCostBest.toFixed(1)} ms`)
|
|
2052
|
-
check('只写 groups 的通道合并之后成员一条不少(2 万条只走声明,不靠顶层 lineIds)', goCostMergedLines === goCostCount, `${goCostMergedLines} / ${goCostCount}`)
|
|
2053
|
-
|
|
2054
|
-
/* ---------------- 点聚合:展示成员、拾取、点击语义与 maxZoom ---------------- */
|
|
2055
|
-
const clusterPointCount = 12000
|
|
2056
|
-
const clusterRandom = (() => {
|
|
2057
|
-
let seed = 20261021
|
|
2058
|
-
return () => {
|
|
2059
|
-
seed = (seed * 1664525 + 1013904223) % 4294967296
|
|
2060
|
-
return seed / 4294967296
|
|
2061
|
-
}
|
|
2062
|
-
})()
|
|
2063
|
-
const clusterCenters = [[0, 0], [120, 0], [60, 90]]
|
|
2064
|
-
// 留出几个远离簇中心的点,覆盖 getClusters() 的单成员分支;它们仍计入总点数。
|
|
2065
|
-
const clusterIsolated = [
|
|
2066
|
-
[-360, -170], [380, -170], [-360, 200], [380, 200],
|
|
2067
|
-
[-440, 20], [460, 20], [20, -220], [20, 240]
|
|
2068
|
-
]
|
|
2069
|
-
const clusterPoints = new Array(clusterPointCount)
|
|
2070
|
-
for (let index = 0; index < clusterPointCount; index += 1) {
|
|
2071
|
-
const isolatedIndex = index - (clusterPointCount - clusterIsolated.length)
|
|
2072
|
-
if (isolatedIndex >= 0) {
|
|
2073
|
-
clusterPoints[index] = {
|
|
2074
|
-
id: `cp${index}`,
|
|
2075
|
-
x: clusterIsolated[isolatedIndex][0],
|
|
2076
|
-
y: clusterIsolated[isolatedIndex][1],
|
|
2077
|
-
name: `独立点 ${index}`
|
|
2078
|
-
}
|
|
2079
|
-
continue
|
|
2080
|
-
}
|
|
2081
|
-
const center = clusterCenters[index % clusterCenters.length]
|
|
2082
|
-
clusterPoints[index] = {
|
|
2083
|
-
id: `cp${index}`,
|
|
2084
|
-
x: center[0] + (clusterRandom() - 0.5) * 24,
|
|
2085
|
-
y: center[1] + (clusterRandom() - 0.5) * 24,
|
|
2086
|
-
name: `聚合点 ${index}`
|
|
2087
|
-
}
|
|
2088
|
-
}
|
|
2089
|
-
const clusterHost = document.createElement('div')
|
|
2090
|
-
clusterHost.style.cssText = 'width:640px;height:380px'
|
|
2091
|
-
document.body.appendChild(clusterHost)
|
|
2092
|
-
let clusterEvents = 0
|
|
2093
|
-
let plainEvents = 0
|
|
2094
|
-
let lastClusterPayload = null
|
|
2095
|
-
const clusterView = createGisView(clusterHost, {
|
|
2096
|
-
points: clusterPoints,
|
|
2097
|
-
width: 640,
|
|
2098
|
-
height: 380,
|
|
2099
|
-
pointStyle: { size: 4 },
|
|
2100
|
-
features: 'view',
|
|
2101
|
-
fitPadding: 24,
|
|
2102
|
-
pointCluster: { enabled: true, radius: 60, minPoints: 2 }
|
|
2103
|
-
})
|
|
2104
|
-
clusterView.on('clusterClick', payload => {
|
|
2105
|
-
clusterEvents += 1
|
|
2106
|
-
lastClusterPayload = payload
|
|
2107
|
-
})
|
|
2108
|
-
clusterView.on('click', () => {
|
|
2109
|
-
plainEvents += 1
|
|
2110
|
-
})
|
|
2111
|
-
await clusterView.whenRendered()
|
|
2112
|
-
const clusterStats = clusterView.getStats()
|
|
2113
|
-
check('点聚合:一万二千点建出多成员圆,统计口径自洽',
|
|
2114
|
-
clusterStats.clusterCount > 0 && clusterStats.clusterPointCount >= clusterStats.clusterCount && clusterStats.clusterMs >= 0,
|
|
2115
|
-
JSON.stringify({ clusterCount: clusterStats.clusterCount, clusterPointCount: clusterStats.clusterPointCount, clusterMs: clusterStats.clusterMs }))
|
|
2116
|
-
check('点聚合:getPointCluster() 给出归一化配置与 active',
|
|
2117
|
-
clusterView.getPointCluster()?.active === true && clusterView.getPointCluster()?.radius === 60,
|
|
2118
|
-
JSON.stringify(clusterView.getPointCluster()))
|
|
2119
|
-
const clusterGroups = clusterView.getClusters()
|
|
2120
|
-
const multiClusters = clusterGroups.filter(item => item.count > 1)
|
|
2121
|
-
const singletonGroups = clusterGroups.filter(item => item.count === 1)
|
|
2122
|
-
check('点聚合:getClusters() 含单成员,单成员 count=1、多成员 id 以 cluster: 开头',
|
|
2123
|
-
singletonGroups.length > 0 && multiClusters.length > 0 && singletonGroups.every(item => item.count === 1 && item.pointIndexes.length === 1) && multiClusters.every(item => item.id.startsWith('cluster:')),
|
|
2124
|
-
`${clusterGroups.length} 组(${singletonGroups.length} 单成员 / ${multiClusters.length} 多成员)`)
|
|
2125
|
-
check('点聚合:原始数据与 getData() 一条不少',
|
|
2126
|
-
clusterView.getData().points.length === clusterPointCount && clusterView.listPoints({ visibleOnly: false }).length === clusterPointCount,
|
|
2127
|
-
`getData ${clusterView.getData().points.length} / listPoints ${clusterView.listPoints({ visibleOnly: false }).length}`)
|
|
2128
|
-
const visibleSingletons = clusterView.listPoints({ visibleOnly: true })
|
|
2129
|
-
let visitedSingletons = 0
|
|
2130
|
-
clusterView.forEachPointInView(clusterView.getViewRect(), () => {
|
|
2131
|
-
visitedSingletons += 1
|
|
2132
|
-
})
|
|
2133
|
-
check('点聚合:聚合内部成员不出现在 visibleOnly / forEachPointInView',
|
|
2134
|
-
visibleSingletons.length === clusterPointCount - clusterStats.clusterPointCount && visitedSingletons === visibleSingletons.length,
|
|
2135
|
-
`可见 ${visibleSingletons.length} / 遍历 ${visitedSingletons} / 隐藏 ${clusterStats.clusterPointCount}`)
|
|
2136
|
-
const clusterPick = clusterView.pick(multiClusters[0].screen[0], multiClusters[0].screen[1])
|
|
2137
|
-
check('点聚合:聚合圆拾取返回 type=cluster,带 count / pointIds / bounds',
|
|
2138
|
-
clusterPick?.type === 'cluster' && clusterPick.count >= 2 && clusterPick.pointIds.length === clusterPick.count && Boolean(clusterPick.bounds),
|
|
2139
|
-
JSON.stringify({ type: clusterPick?.type, count: clusterPick?.count, ids: clusterPick?.pointIds?.length }))
|
|
2140
|
-
const selectionProbe = visibleSingletons[0]
|
|
2141
|
-
clusterView.selectFeatures([selectionProbe.item.id])
|
|
2142
|
-
await clusterView.whenRendered()
|
|
2143
|
-
const selectedBefore = clusterView.getSelections().map(entry => entry.item?.id ?? entry.index).join(',')
|
|
2144
|
-
const clusterCanvas = clusterHost.querySelector('canvas')
|
|
2145
|
-
// 合成指针事件使用真实客户端坐标:先把画布滚进视口,避免 clientY 落在窗口外
|
|
2146
|
-
clusterCanvas.scrollIntoView({ block: 'center' })
|
|
2147
|
-
await sleep(50)
|
|
2148
|
-
const clickPick = clusterView.pick(multiClusters[0].screen[0], multiClusters[0].screen[1])
|
|
2149
|
-
const clickScreen = clickPick?.screen ?? multiClusters[0].screen
|
|
2150
|
-
const clickClient = clusterView.screenToClient(clickScreen[0], clickScreen[1])
|
|
2151
|
-
const fireCluster = type => clusterCanvas.dispatchEvent(new PointerEvent(type, {
|
|
2152
|
-
bubbles: true, cancelable: true, pointerId: 93, pointerType: 'mouse', button: 0,
|
|
2153
|
-
buttons: type === 'pointerup' ? 0 : 1, clientX: clickClient[0], clientY: clickClient[1]
|
|
2154
|
-
}))
|
|
2155
|
-
fireCluster('pointerdown')
|
|
2156
|
-
fireCluster('pointerup')
|
|
2157
|
-
await sleep(30)
|
|
2158
|
-
const selectedAfter = clusterView.getSelections().map(entry => entry.item?.id ?? entry.index).join(',')
|
|
2218
|
+
check('只写 groups 的通道合并之后成员一条不少(2 万条只走声明,不靠顶层 lineIds)', goCostMergedLines === goCostCount, `${goCostMergedLines} / ${goCostCount}`)
|
|
2219
|
+
|
|
2220
|
+
/* ---------------- 点聚合:展示成员、拾取、点击语义与 maxZoom ---------------- */
|
|
2221
|
+
const clusterPointCount = 12000
|
|
2222
|
+
const clusterRandom = (() => {
|
|
2223
|
+
let seed = 20261021
|
|
2224
|
+
return () => {
|
|
2225
|
+
seed = (seed * 1664525 + 1013904223) % 4294967296
|
|
2226
|
+
return seed / 4294967296
|
|
2227
|
+
}
|
|
2228
|
+
})()
|
|
2229
|
+
const clusterCenters = [[0, 0], [120, 0], [60, 90]]
|
|
2230
|
+
// 留出几个远离簇中心的点,覆盖 getClusters() 的单成员分支;它们仍计入总点数。
|
|
2231
|
+
const clusterIsolated = [
|
|
2232
|
+
[-360, -170], [380, -170], [-360, 200], [380, 200],
|
|
2233
|
+
[-440, 20], [460, 20], [20, -220], [20, 240]
|
|
2234
|
+
]
|
|
2235
|
+
const clusterPoints = new Array(clusterPointCount)
|
|
2236
|
+
for (let index = 0; index < clusterPointCount; index += 1) {
|
|
2237
|
+
const isolatedIndex = index - (clusterPointCount - clusterIsolated.length)
|
|
2238
|
+
if (isolatedIndex >= 0) {
|
|
2239
|
+
clusterPoints[index] = {
|
|
2240
|
+
id: `cp${index}`,
|
|
2241
|
+
x: clusterIsolated[isolatedIndex][0],
|
|
2242
|
+
y: clusterIsolated[isolatedIndex][1],
|
|
2243
|
+
name: `独立点 ${index}`
|
|
2244
|
+
}
|
|
2245
|
+
continue
|
|
2246
|
+
}
|
|
2247
|
+
const center = clusterCenters[index % clusterCenters.length]
|
|
2248
|
+
clusterPoints[index] = {
|
|
2249
|
+
id: `cp${index}`,
|
|
2250
|
+
x: center[0] + (clusterRandom() - 0.5) * 24,
|
|
2251
|
+
y: center[1] + (clusterRandom() - 0.5) * 24,
|
|
2252
|
+
name: `聚合点 ${index}`
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
const clusterHost = document.createElement('div')
|
|
2256
|
+
clusterHost.style.cssText = 'width:640px;height:380px'
|
|
2257
|
+
document.body.appendChild(clusterHost)
|
|
2258
|
+
let clusterEvents = 0
|
|
2259
|
+
let plainEvents = 0
|
|
2260
|
+
let lastClusterPayload = null
|
|
2261
|
+
const clusterView = createGisView(clusterHost, {
|
|
2262
|
+
points: clusterPoints,
|
|
2263
|
+
width: 640,
|
|
2264
|
+
height: 380,
|
|
2265
|
+
pointStyle: { size: 4 },
|
|
2266
|
+
features: 'view',
|
|
2267
|
+
fitPadding: 24,
|
|
2268
|
+
pointCluster: { enabled: true, radius: 60, minPoints: 2 }
|
|
2269
|
+
})
|
|
2270
|
+
clusterView.on('clusterClick', payload => {
|
|
2271
|
+
clusterEvents += 1
|
|
2272
|
+
lastClusterPayload = payload
|
|
2273
|
+
})
|
|
2274
|
+
clusterView.on('click', () => {
|
|
2275
|
+
plainEvents += 1
|
|
2276
|
+
})
|
|
2277
|
+
await clusterView.whenRendered()
|
|
2278
|
+
const clusterStats = clusterView.getStats()
|
|
2279
|
+
check('点聚合:一万二千点建出多成员圆,统计口径自洽',
|
|
2280
|
+
clusterStats.clusterCount > 0 && clusterStats.clusterPointCount >= clusterStats.clusterCount && clusterStats.clusterMs >= 0,
|
|
2281
|
+
JSON.stringify({ clusterCount: clusterStats.clusterCount, clusterPointCount: clusterStats.clusterPointCount, clusterMs: clusterStats.clusterMs }))
|
|
2282
|
+
check('点聚合:getPointCluster() 给出归一化配置与 active',
|
|
2283
|
+
clusterView.getPointCluster()?.active === true && clusterView.getPointCluster()?.radius === 60,
|
|
2284
|
+
JSON.stringify(clusterView.getPointCluster()))
|
|
2285
|
+
const clusterGroups = clusterView.getClusters()
|
|
2286
|
+
const multiClusters = clusterGroups.filter(item => item.count > 1)
|
|
2287
|
+
const singletonGroups = clusterGroups.filter(item => item.count === 1)
|
|
2288
|
+
check('点聚合:getClusters() 含单成员,单成员 count=1、多成员 id 以 cluster: 开头',
|
|
2289
|
+
singletonGroups.length > 0 && multiClusters.length > 0 && singletonGroups.every(item => item.count === 1 && item.pointIndexes.length === 1) && multiClusters.every(item => item.id.startsWith('cluster:')),
|
|
2290
|
+
`${clusterGroups.length} 组(${singletonGroups.length} 单成员 / ${multiClusters.length} 多成员)`)
|
|
2291
|
+
check('点聚合:原始数据与 getData() 一条不少',
|
|
2292
|
+
clusterView.getData().points.length === clusterPointCount && clusterView.listPoints({ visibleOnly: false }).length === clusterPointCount,
|
|
2293
|
+
`getData ${clusterView.getData().points.length} / listPoints ${clusterView.listPoints({ visibleOnly: false }).length}`)
|
|
2294
|
+
const visibleSingletons = clusterView.listPoints({ visibleOnly: true })
|
|
2295
|
+
let visitedSingletons = 0
|
|
2296
|
+
clusterView.forEachPointInView(clusterView.getViewRect(), () => {
|
|
2297
|
+
visitedSingletons += 1
|
|
2298
|
+
})
|
|
2299
|
+
check('点聚合:聚合内部成员不出现在 visibleOnly / forEachPointInView',
|
|
2300
|
+
visibleSingletons.length === clusterPointCount - clusterStats.clusterPointCount && visitedSingletons === visibleSingletons.length,
|
|
2301
|
+
`可见 ${visibleSingletons.length} / 遍历 ${visitedSingletons} / 隐藏 ${clusterStats.clusterPointCount}`)
|
|
2302
|
+
const clusterPick = clusterView.pick(multiClusters[0].screen[0], multiClusters[0].screen[1])
|
|
2303
|
+
check('点聚合:聚合圆拾取返回 type=cluster,带 count / pointIds / bounds',
|
|
2304
|
+
clusterPick?.type === 'cluster' && clusterPick.count >= 2 && clusterPick.pointIds.length === clusterPick.count && Boolean(clusterPick.bounds),
|
|
2305
|
+
JSON.stringify({ type: clusterPick?.type, count: clusterPick?.count, ids: clusterPick?.pointIds?.length }))
|
|
2306
|
+
const selectionProbe = visibleSingletons[0]
|
|
2307
|
+
clusterView.selectFeatures([selectionProbe.item.id])
|
|
2308
|
+
await clusterView.whenRendered()
|
|
2309
|
+
const selectedBefore = clusterView.getSelections().map(entry => entry.item?.id ?? entry.index).join(',')
|
|
2310
|
+
const clusterCanvas = clusterHost.querySelector('canvas')
|
|
2311
|
+
// 合成指针事件使用真实客户端坐标:先把画布滚进视口,避免 clientY 落在窗口外
|
|
2312
|
+
clusterCanvas.scrollIntoView({ block: 'center' })
|
|
2313
|
+
await sleep(50)
|
|
2314
|
+
const clickPick = clusterView.pick(multiClusters[0].screen[0], multiClusters[0].screen[1])
|
|
2315
|
+
const clickScreen = clickPick?.screen ?? multiClusters[0].screen
|
|
2316
|
+
const clickClient = clusterView.screenToClient(clickScreen[0], clickScreen[1])
|
|
2317
|
+
const fireCluster = type => clusterCanvas.dispatchEvent(new PointerEvent(type, {
|
|
2318
|
+
bubbles: true, cancelable: true, pointerId: 93, pointerType: 'mouse', button: 0,
|
|
2319
|
+
buttons: type === 'pointerup' ? 0 : 1, clientX: clickClient[0], clientY: clickClient[1]
|
|
2320
|
+
}))
|
|
2321
|
+
fireCluster('pointerdown')
|
|
2322
|
+
fireCluster('pointerup')
|
|
2323
|
+
await sleep(30)
|
|
2324
|
+
const selectedAfter = clusterView.getSelections().map(entry => entry.item?.id ?? entry.index).join(',')
|
|
2159
2325
|
check('点聚合:点击聚合只发 clusterClick,不发普通 click,选中集不变',
|
|
2160
2326
|
clusterEvents === 1 && plainEvents === 0 && selectedAfter === selectedBefore && lastClusterPayload?.action?.type === 'clusterClick' && lastClusterPayload?.action?.data?.type === 'cluster',
|
|
2161
2327
|
`clusterClick=${clusterEvents} click=${plainEvents} 选中 ${selectedBefore} → ${selectedAfter} action=${lastClusterPayload?.action?.type} clickPick=${clickPick?.type ?? 'null'} clickScreen=${clickScreen.map(value => value.toFixed(1)).join(',')} client=${clickClient.map(value => value.toFixed(1)).join(',')}`)
|
|
2162
2328
|
const fitScale = clusterView.getCamera().scale
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2329
|
+
// 聚合成员在相机运动期会推迟重算;这一档没有线、又显式关掉非 MSAA 后备缓冲,
|
|
2330
|
+
// 当前帧不会画出任何轻量几何 —— 停稳计时器仍必须补一帧,不能把聚合结果留在旧缩放档。
|
|
2331
|
+
const clustersBeforeCamera = clusterView.getStats().clusterCount
|
|
2332
|
+
clusterView.updateOptions({ interactionNoMsaa: false })
|
|
2333
|
+
clusterView.setCamera({ scale: fitScale * 0.55 })
|
|
2334
|
+
await sleep(420)
|
|
2335
|
+
const clustersAfterCamera = clusterView.getStats().clusterCount
|
|
2336
|
+
check('点聚合:程序化相机停稳后一定补算成员(没有轻量帧也不例外)',
|
|
2337
|
+
clustersAfterCamera < clustersBeforeCamera,
|
|
2338
|
+
`${clustersBeforeCamera} → ${clustersAfterCamera}`)
|
|
2170
2339
|
clusterView.setCamera({ scale: fitScale })
|
|
2171
|
-
await
|
|
2172
|
-
clusterView.updateOptions({
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2340
|
+
await sleep(420)
|
|
2341
|
+
clusterView.updateOptions({ interactionNoMsaa: true })
|
|
2342
|
+
clusterView.updateOptions({ pointCluster: { enabled: true, radius: 60, minPoints: 2, maxZoom: 2 } })
|
|
2343
|
+
clusterView.setCamera({ scale: fitScale * 3 })
|
|
2344
|
+
await clusterView.whenRendered()
|
|
2345
|
+
const zoomedStats = clusterView.getStats()
|
|
2346
|
+
check('点聚合:缩放超过 maxZoom 后展开原始点(active=false、聚合圆清空)',
|
|
2347
|
+
clusterView.getPointCluster()?.active === false && zoomedStats.clusterCount === 0 && clusterView.listPoints({ visibleOnly: true }).length === clusterPointCount,
|
|
2348
|
+
JSON.stringify({ active: clusterView.getPointCluster()?.active, clusterCount: zoomedStats.clusterCount, visible: clusterView.listPoints({ visibleOnly: true }).length }))
|
|
2349
|
+
clusterView.setCamera({ scale: fitScale })
|
|
2350
|
+
await clusterView.whenRendered()
|
|
2351
|
+
clusterView.updateOptions({ pointCluster: false })
|
|
2352
|
+
await clusterView.whenRendered()
|
|
2353
|
+
const disabledStats = clusterView.getStats()
|
|
2354
|
+
check('点聚合:关掉之后原始点全部恢复,getPointCluster().enabled=false',
|
|
2355
|
+
clusterView.getPointCluster()?.enabled === false && clusterView.getPointCluster()?.active === false && disabledStats.clusterCount === 0 && disabledStats.clusterPointCount === 0,
|
|
2356
|
+
JSON.stringify({ cluster: clusterView.getPointCluster(), stats: { count: disabledStats.clusterCount, points: disabledStats.clusterPointCount } }))
|
|
2357
|
+
clusterView.destroy()
|
|
2358
|
+
clusterHost.remove()
|
|
2359
|
+
|
|
2360
|
+
/* ---------------- 同一批点线数据原样回灌:两半都没换时别白解析 ---------------- */
|
|
2361
|
+
// 组件把同一批对象原样再交一次是常规写法(刷新配置 / 只换别的选项时顺带带上 data)。
|
|
2362
|
+
// 这里量 5 次取中位;每次回灌之后数据、线路实例数与相机都必须原样。
|
|
2363
|
+
big.fitView(0)
|
|
2364
|
+
big.refresh()
|
|
2365
|
+
big.refresh()
|
|
2366
|
+
const refeedPoints = big.getData().points
|
|
2367
|
+
const refeedLines = big.getData().lines
|
|
2368
|
+
const refeedInstances = big.getStats().lineInstances
|
|
2369
|
+
const refeedStarted = now()
|
|
2370
|
+
const refeedSamples = []
|
|
2371
|
+
for (let round = 0; round < 5; round += 1) {
|
|
2372
|
+
const started = now()
|
|
2373
|
+
big.updateOptions({ points: refeedPoints, lines: refeedLines })
|
|
2374
|
+
refeedSamples.push(now() - started)
|
|
2375
|
+
}
|
|
2376
|
+
refeedSamples.sort((left, right) => left - right)
|
|
2377
|
+
const refeedMs = refeedSamples[Math.floor(refeedSamples.length / 2)]
|
|
2378
|
+
const refeedData = big.getData()
|
|
2379
|
+
check('同一批点线原样回灌:条数 / 线路实例数都不变(解析结果可以整半沿用)',
|
|
2380
|
+
refeedData.points.length === pointCount && refeedData.lines.length === lineCount && big.getStats().lineInstances === refeedInstances,
|
|
2381
|
+
JSON.stringify({ points: refeedData.points.length, lines: refeedData.lines.length, instances: big.getStats().lineInstances, expected: refeedInstances }))
|
|
2382
|
+
diag(`同一批点线原样回灌(${pointCount} 点 / ${lineCount} 线)`, `中位 ${refeedMs.toFixed(1)} ms(5 次:${refeedSamples.map(value => value.toFixed(1)).join(' / ')};总观测 ${(now() - refeedStarted).toFixed(1)} ms)`)
|
|
2383
|
+
// 同一批数据回灌不能让通道几何停在上一轮:声明没换时沿用,换了骨架必须照常重推
|
|
2384
|
+
big.updateOptions({ channels: [{ id: 'reuse-guard', from: 'p0', to: 'p1', lineIds: ['l0', 'l1'], path: [[20, 20]] }] })
|
|
2385
|
+
const guardBefore = big.getChannelGroupWaypoints('reuse-guard')
|
|
2386
|
+
big.updateOptions({ points: refeedPoints, lines: refeedLines })
|
|
2387
|
+
const guardAfter = big.getChannelGroupWaypoints('reuse-guard')
|
|
2388
|
+
check('同一批点线回灌:通道声明没换时,回线派生折线原样保留',
|
|
2389
|
+
JSON.stringify(guardBefore?.lines?.map(line => line.path)) === JSON.stringify(guardAfter?.lines?.map(line => line.path)),
|
|
2390
|
+
JSON.stringify({ before: guardBefore?.lines?.map(line => line.waypoints), after: guardAfter?.lines?.map(line => line.waypoints) }))
|
|
2391
|
+
big.updateOptions({ channels: [{ id: 'reuse-guard', from: 'p0', to: 'p1', lineIds: ['l0', 'l1'], path: [[80, 80]] }] })
|
|
2392
|
+
const guardChanged = big.getChannelGroupWaypoints('reuse-guard')
|
|
2393
|
+
check('同一批点线回灌:换通道骨架时线路那一半不能沿用旧几何',
|
|
2394
|
+
JSON.stringify(guardChanged?.lines?.map(line => line.waypoints)) !== JSON.stringify(guardAfter?.lines?.map(line => line.waypoints)),
|
|
2395
|
+
JSON.stringify({ before: guardAfter?.lines?.map(line => line.waypoints), after: guardChanged?.lines?.map(line => line.waypoints) }))
|
|
2396
|
+
big.updateOptions({ channels: [] })
|
|
2397
|
+
|
|
2398
|
+
thinView.destroy()
|
|
2182
2399
|
thinHost.remove()
|
|
2183
2400
|
big.destroy()
|
|
2184
2401
|
bigHost.remove()
|