@mesh3d/cesium-vectortile-gl 0.4.4 → 0.4.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.
Files changed (51) hide show
  1. package/.gitattributes +11 -0
  2. package/.gitconfig +3 -0
  3. package/.husky/pre-commit +1 -0
  4. package/.prettierignore +5 -0
  5. package/.vscode/settings.json +25 -0
  6. package/LICENSE.md +203 -203
  7. package/README.md +202 -167
  8. package/Source/Cesium.d.ts +2692 -2691
  9. package/Source/VectorTileLOD.js +720 -532
  10. package/Source/VectorTileRenderList.js +70 -70
  11. package/Source/VectorTileset.js +473 -447
  12. package/Source/layers/BackgroundRenderLayer.js +91 -89
  13. package/Source/layers/FillRenderLayer.js +18 -18
  14. package/Source/layers/IRenderLayer.js +160 -152
  15. package/Source/layers/LineRenderLayer.js +104 -94
  16. package/Source/layers/SymbolRenderLayer.js +30 -31
  17. package/Source/layers/index.js +23 -16
  18. package/Source/layers/registerRenderLayer.js +24 -24
  19. package/Source/layers/visualizers/FillLayerVisualizer.js +542 -426
  20. package/Source/layers/visualizers/ILayerVisualizer.js +90 -94
  21. package/Source/layers/visualizers/LineLayerVisualizer.js +702 -571
  22. package/Source/layers/visualizers/SymbolLayerVisualizer.js +514 -244
  23. package/Source/sources/GeoJSONSource.js +53 -46
  24. package/Source/sources/ISource.js +39 -39
  25. package/Source/sources/VectorSource.js +94 -52
  26. package/Source/sources/granularitySettings.js +23 -20
  27. package/Source/sources/index.js +6 -11
  28. package/Source/sources/registerSource.js +17 -19
  29. package/Source/style/StyleLayer.js +43 -43
  30. package/Source/style/StyleLayerProperties.js +44 -43
  31. package/Source/style/index.js +2 -2
  32. package/Source/symbol/SymbolPlacements.js +117 -88
  33. package/Source/workers/VectorTileWorker.js +41 -0
  34. package/Source/workers/ellipsoid.js +47 -0
  35. package/Source/workers/processTileTask.js +329 -0
  36. package/Source/workers/styleEvaluator.js +168 -0
  37. package/benchmark.html +148 -0
  38. package/dist/cvt-gl-worker.js +9274 -0
  39. package/dist/cvt-gl-worker.js.map +1 -0
  40. package/dist/cvt-gl.js +2570 -2001
  41. package/dist/cvt-gl.js.map +1 -1
  42. package/dist/cvt-gl.min.js +3 -3
  43. package/dist/cvt-gl.min.js.map +1 -1
  44. package/eslint.config.mjs +58 -0
  45. package/index.js +9 -6
  46. package/mlt.html +26 -25
  47. package/package.json +64 -41
  48. package/prettier.config.mjs +30 -0
  49. package/vite.config.mjs +43 -0
  50. package/vite.worker.config.mjs +31 -0
  51. package/worker.html +26 -0
@@ -1,88 +1,117 @@
1
- import { GridIndex } from 'maplibre-gl/src/symbol/grid_index'
2
- import { IRenderLayer } from '../layers/IRenderLayer';
3
-
4
- //参考 maplibre-gl/src/symbol/CollisionIndex.ts
5
-
6
- // When a symbol crosses the edge that causes it to be included in
7
- // collision detection, it will cause changes in the symbols around
8
- // it. This constant specifies how many pixels to pad the edge of
9
- // the viewport for collision detection so that the bulk of the changes
10
- // occur offscreen. Making this constant greater increases label
11
- // stability, but it's expensive.
12
- const viewportPadding = 100;
13
- let scratchScreenSpacePosition = null
14
- let scratchScreenSpaceBoundingBox = null
15
-
16
- /**
17
- * 基于 maplibre-gl GridIndex 实现符号碰撞检测(自动避让)。文字碰撞检测结果存放在 label 对象的扩展属性 vtPlaceable,该值为true表示文字可以显示到屏幕
18
- */
19
- export class SymbolPlacements {
20
- constructor() {
21
- scratchScreenSpacePosition = new Cesium.Cartesian2()
22
- scratchScreenSpaceBoundingBox = new Cesium.BoundingRectangle()
23
- }
24
- /**
25
- * @param {Cesium.FrameState} frameState
26
- * @param {IRenderLayer[]} orderedRenderLayers
27
- * @param {number} zoom
28
- */
29
- update(frameState, orderedRenderLayers, zoom) {
30
- const width = frameState.context.drawingBufferWidth / frameState.pixelRatio
31
- const height = frameState.context.drawingBufferHeight / frameState.pixelRatio
32
- const scene = frameState.camera._scene
33
-
34
- const grid = new GridIndex(width + 2 * viewportPadding, height + 2 * viewportPadding, 25)
35
-
36
- for (const layer of orderedRenderLayers) {
37
- const style = layer.style
38
- /**@type {Cesium.Label[]} */
39
- const labels = layer.labels
40
- if (layer.type !== 'symbol' || layer.visibility === 'none'
41
- || zoom < style.minzoom || zoom >= style.maxzoom
42
- || !Cesium.defined(labels) || !labels.length
43
- ) {
44
- continue
45
- }
46
-
47
- //layout属性取值时 getDataConstValue zoom 参数取瓦片层级,而不是全局缩放层级
48
- const textAllowOverlap = style.layout.getDataConstValue('text-allow-overlap', layer.tile.z)
49
- const textOverlap = style.layout.getDataConstValue('text-overlap', layer.tile.z)
50
- const textOverlapMode = getOverlapMode(textOverlap, textAllowOverlap)
51
-
52
- for (const label of labels) {
53
- const position = label.computeScreenSpacePosition(scene, scratchScreenSpacePosition)
54
- if (!position) {//可能未准备就绪,先标记为可以摆放,确保Cesium能生成文字
55
- label.vtPlaceable = true
56
- continue
57
- }
58
- /**@type {Cesium.BoundingRectangle} */
59
- const box = Cesium.Label.getScreenSpaceBoundingBox(label, position, scratchScreenSpaceBoundingBox)
60
-
61
- const tlX = box.x, tlY = box.y,
62
- brX = tlX + box.width, brY = tlY + box.height
63
- if (!grid.hitTest(tlX, tlY, brX, brY, textOverlapMode, null)) {//二维包围盒碰撞检测
64
- const textKey = { overlapMode: textOverlapMode, }
65
- grid.insert(textKey, tlX, tlY, brX, brY)
66
- label.vtPlaceable = true
67
- }
68
- else {
69
- label.vtPlaceable = false
70
- }
71
- }
72
- }
73
- }
74
- }
75
-
76
- function getOverlapMode(overlap, allowOverlap) {
77
- let result = 'never';
78
-
79
- if (overlap) {
80
- // if -overlap is set, use it
81
- result = overlap;
82
- } else if (allowOverlap) {
83
- // fall back to -allow-overlap, with false='never', true='always'
84
- result = 'always';
85
- }
86
-
87
- return result;
88
- }
1
+ import { GridIndex } from 'maplibre-gl/src/symbol/grid_index'
2
+ import { IRenderLayer } from '../layers/IRenderLayer'
3
+
4
+ //参考 maplibre-gl/src/symbol/CollisionIndex.ts
5
+
6
+ // When a symbol crosses the edge that causes it to be included in
7
+ // collision detection, it will cause changes in the symbols around
8
+ // it. This constant specifies how many pixels to pad the edge of
9
+ // the viewport for collision detection so that the bulk of the changes
10
+ // occur offscreen. Making this constant greater increases label
11
+ // stability, but it's expensive.
12
+ const viewportPadding = 100
13
+ let scratchScreenSpacePosition = null
14
+ let scratchScreenSpaceBoundingBox = null
15
+
16
+ /**
17
+ * 基于 maplibre-gl GridIndex 实现符号碰撞检测(自动避让)。文字碰撞检测结果存放在 label 对象的扩展属性 vtPlaceable,该值为true表示文字可以显示到屏幕
18
+ */
19
+ export class SymbolPlacements {
20
+ constructor() {
21
+ scratchScreenSpacePosition = new Cesium.Cartesian2()
22
+ scratchScreenSpaceBoundingBox = new Cesium.BoundingRectangle()
23
+ }
24
+ /**
25
+ * @param {Cesium.FrameState} frameState
26
+ * @param {IRenderLayer[]} orderedRenderLayers
27
+ * @param {number} zoom
28
+ */
29
+ update(frameState, orderedRenderLayers, zoom) {
30
+ const width = frameState.context.drawingBufferWidth / frameState.pixelRatio
31
+ const height =
32
+ frameState.context.drawingBufferHeight / frameState.pixelRatio
33
+ const scene = frameState.camera._scene
34
+
35
+ const grid = new GridIndex(
36
+ width + 2 * viewportPadding,
37
+ height + 2 * viewportPadding,
38
+ 25
39
+ )
40
+
41
+ for (const layer of orderedRenderLayers) {
42
+ const style = layer.style
43
+ /**@type {Cesium.Label[]} */
44
+ const labels = layer.labels
45
+ if (
46
+ layer.type !== 'symbol' ||
47
+ layer.visibility === 'none' ||
48
+ zoom < style.minzoom ||
49
+ zoom >= style.maxzoom ||
50
+ !Cesium.defined(labels) ||
51
+ !labels.length
52
+ ) {
53
+ continue
54
+ }
55
+
56
+ //layout属性取值时 getDataConstValue 的 zoom 参数取瓦片层级,而不是全局缩放层级
57
+ const textAllowOverlap = style.layout.getDataConstValue(
58
+ 'text-allow-overlap',
59
+ layer.tile.z
60
+ )
61
+ const textOverlap = style.layout.getDataConstValue(
62
+ 'text-overlap',
63
+ layer.tile.z
64
+ )
65
+ const textOverlapMode = getOverlapMode(textOverlap, textAllowOverlap)
66
+ const textPadding = style.layout.getDataConstValue(
67
+ 'text-padding',
68
+ layer.tile.z
69
+ )
70
+
71
+ for (const label of labels) {
72
+ const position = label.computeScreenSpacePosition(
73
+ scene,
74
+ scratchScreenSpacePosition
75
+ )
76
+ if (!position) {
77
+ //可能未准备就绪,先标记为可以摆放,确保Cesium能生成文字
78
+ label.vtPlaceable = true
79
+ continue
80
+ }
81
+ /**@type {Cesium.BoundingRectangle} */
82
+ const box = Cesium.Label.getScreenSpaceBoundingBox(
83
+ label,
84
+ position,
85
+ scratchScreenSpaceBoundingBox
86
+ )
87
+
88
+ const tlX = box.x - textPadding,
89
+ tlY = box.y - textPadding,
90
+ brX = tlX + box.width + textPadding,
91
+ brY = tlY + box.height + textPadding
92
+ if (!grid.hitTest(tlX, tlY, brX, brY, textOverlapMode, null)) {
93
+ //二维包围盒碰撞检测
94
+ const textKey = { overlapMode: textOverlapMode }
95
+ grid.insert(textKey, tlX, tlY, brX, brY)
96
+ label.vtPlaceable = true
97
+ } else {
98
+ label.vtPlaceable = false
99
+ }
100
+ }
101
+ }
102
+ }
103
+ }
104
+
105
+ function getOverlapMode(overlap, allowOverlap) {
106
+ let result = 'never'
107
+
108
+ if (overlap) {
109
+ // if -overlap is set, use it
110
+ result = overlap
111
+ } else if (allowOverlap) {
112
+ // fall back to -allow-overlap, with false='never', true='always'
113
+ result = 'always'
114
+ }
115
+
116
+ return result
117
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Vector tile Web Worker: 解析、过滤、坐标转换、几何数据构建。
3
+ * 遵循 Cesium TaskProcessor 协议:收到 { id, parameters },回复 { id, result }, transferList。
4
+ */
5
+ import { processTileTask } from './processTileTask.js'
6
+
7
+ self.onmessage = function (e) {
8
+ const { id, parameters } = e.data
9
+ const transferList = []
10
+
11
+ try {
12
+ const result = processTileTask(parameters)
13
+ collectTransferables(result, transferList)
14
+ self.postMessage({ id, result }, transferList)
15
+ } catch (err) {
16
+ self.postMessage({ id, result: { error: String(err.message || err) } }, [])
17
+ }
18
+ }
19
+
20
+ /**
21
+ * 从 result 中收集可 transfer 的 ArrayBuffer,用于 postMessage 的 transferList
22
+ * @param {object} result - { fill: [{ batches: [...] }], line: [{ batches: [...] }], symbol: [...] }
23
+ * @param {ArrayBuffer[]} transferList
24
+ */
25
+ function collectTransferables(result, transferList) {
26
+ if (!result) return
27
+ for (const key of ['fill', 'line']) {
28
+ const arr = result[key]
29
+ if (!Array.isArray(arr)) continue
30
+ for (const layer of arr) {
31
+ const batches = layer.batches
32
+ if (!Array.isArray(batches)) continue
33
+ for (const batch of batches) {
34
+ if (batch.positions?.buffer) transferList.push(batch.positions.buffer)
35
+ if (batch.normals?.buffer) transferList.push(batch.normals.buffer)
36
+ if (batch.st?.buffer) transferList.push(batch.st.buffer)
37
+ if (batch.indices?.buffer) transferList.push(batch.indices.buffer)
38
+ }
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * WGS84 椭球经纬度 → 世界坐标(不依赖 Cesium),供 Worker 内几何构建使用。
3
+ */
4
+ const A = 6378137
5
+ const E2 = 6.694379990141316e-2 // 2*f - f*f, f = 1/298.257223563
6
+
7
+ /**
8
+ * 经纬度(度) + 高度 → 笛卡尔坐标 [x, y, z]
9
+ * @param {number} lonDeg - 经度(度)
10
+ * @param {number} latDeg - 纬度(度)
11
+ * @param {number} height - 高度(米)
12
+ * @param {Float64Array} [out] - 可选,长度至少 3
13
+ * @returns {Float64Array}
14
+ */
15
+ export function fromDegrees(lonDeg, latDeg, height, out) {
16
+ const lon = (lonDeg * Math.PI) / 180
17
+ const lat = (latDeg * Math.PI) / 180
18
+ const sinLat = Math.sin(lat)
19
+ const cosLat = Math.cos(lat)
20
+ const n = A / Math.sqrt(1 - E2 * sinLat * sinLat)
21
+ const x = (n + height) * cosLat * Math.cos(lon)
22
+ const y = (n + height) * cosLat * Math.sin(lon)
23
+ const z = (n * (1 - E2) + height) * sinLat
24
+ const result = out || new Float64Array(3)
25
+ result[0] = x
26
+ result[1] = y
27
+ result[2] = z
28
+ return result
29
+ }
30
+
31
+ /**
32
+ * 向量归一化,写回原数组
33
+ * @param {Float64Array} v - 长度至少 3
34
+ * @returns {Float64Array}
35
+ */
36
+ export function normalize(v) {
37
+ const x = v[0]
38
+ const y = v[1]
39
+ const z = v[2]
40
+ const len = Math.sqrt(x * x + y * y + z * z)
41
+ if (len > 0) {
42
+ v[0] = x / len
43
+ v[1] = y / len
44
+ v[2] = z / len
45
+ }
46
+ return v
47
+ }
@@ -0,0 +1,329 @@
1
+ /**
2
+ * Worker 内瓦片任务:解析 PBF/MLT、按样式过滤要素、构建几何数据。
3
+ * 不依赖 Cesium/WebGL,仅使用 @mapbox/vector-tile、maplibre 纯 JS。
4
+ */
5
+ import {
6
+ VectorTile,
7
+ VectorTileFeature,
8
+ classifyRings
9
+ } from '@mapbox/vector-tile'
10
+ import Pbf from 'pbf'
11
+ import { MLTVectorTile } from 'maplibre-gl/src/source/vector_tile_mlt'
12
+ import { featureFilter } from '@maplibre/maplibre-gl-style-spec'
13
+ import { EXTENT } from 'maplibre-gl/src/data/extent'
14
+ import { loadGeometry } from 'maplibre-gl/src/data/load_geometry'
15
+ import { subdividePolygon } from 'maplibre-gl/src/render/subdivision'
16
+ import { subdivideVertexLine } from 'maplibre-gl/src/render/subdivision'
17
+ import { granularitySettings } from '../sources/granularitySettings.js'
18
+ import { fromDegrees, normalize } from './ellipsoid.js'
19
+ import {
20
+ evaluateFillPaint,
21
+ evaluateLinePaint,
22
+ evaluateSymbolLayout,
23
+ evaluateSymbolPaint,
24
+ colorToBytes
25
+ } from './styleEvaluator.js'
26
+
27
+ /**
28
+ * 解析瓦片 buffer 为 VectorTile 或 MLTVectorTile
29
+ * @param {ArrayBuffer} buffer
30
+ * @param {string} encoding - 'mvt' | 'mlt'
31
+ * @returns {import('@mapbox/vector-tile').VectorTile|import('maplibre-gl/src/source/vector_tile_mlt').MLTVectorTile}
32
+ */
33
+ function parseTile(buffer, encoding) {
34
+ if (encoding === 'mlt') {
35
+ return new MLTVectorTile(buffer)
36
+ }
37
+ return new VectorTile(new Pbf(buffer))
38
+ }
39
+
40
+ /**
41
+ * 处理单瓦片任务:解析 + 过滤;后续步骤在此扩展几何构建。
42
+ * @param {object} parameters
43
+ * @param {Record<string,{buffer:ArrayBuffer,encoding:string}>} parameters.sources
44
+ * @param {number} parameters.x
45
+ * @param {number} parameters.y
46
+ * @param {number} parameters.z
47
+ * @param {number} [parameters.extent]
48
+ * @param {Array<{id:string,type:string,source:string,sourceLayer?:string,filter?:object,paint?:object,layout?:object}>} parameters.styleLayers
49
+ * @returns {{ fill: Array, line: Array, symbol: Array, parsedSources?: object }}
50
+ */
51
+ export function processTileTask(parameters) {
52
+ const {
53
+ sources = {},
54
+ x,
55
+ y,
56
+ z,
57
+ extent = EXTENT,
58
+ styleLayers = []
59
+ } = parameters
60
+
61
+ const parsedSources = {}
62
+ for (const sourceId in sources) {
63
+ const { buffer, encoding } = sources[sourceId]
64
+ if (buffer) {
65
+ parsedSources[sourceId] = parseTile(buffer, encoding || 'mvt')
66
+ }
67
+ }
68
+
69
+ const result = { fill: [], line: [], symbol: [] }
70
+
71
+ for (const layerSpec of styleLayers) {
72
+ if (layerSpec.type === 'background') continue
73
+ const vt = parsedSources[layerSpec.source]
74
+ if (!vt) continue
75
+ const layerName = layerSpec.sourceLayer ?? layerSpec['source-layer']
76
+ const vectorLayer = vt.layers[layerName]
77
+ if (!vectorLayer) continue
78
+
79
+ const filter = layerSpec.filter ? featureFilter(layerSpec.filter) : null
80
+ const features = []
81
+ const featureCount = vectorLayer.length
82
+ for (let i = 0; i < featureCount; i++) {
83
+ const feature = vectorLayer.feature(i)
84
+ if (filter && !filter.filter({ zoom: z }, feature)) continue
85
+ features.push({ index: i, feature, layerSpec })
86
+ }
87
+
88
+ const type = layerSpec.type
89
+ if (type === 'fill' || type === 'line' || type === 'symbol') {
90
+ result[type].push({
91
+ layerId: layerSpec.id,
92
+ source: layerSpec.source,
93
+ sourceLayer: layerName,
94
+ styleLayer: layerSpec,
95
+ extent,
96
+ x,
97
+ y,
98
+ z,
99
+ features
100
+ })
101
+ }
102
+ }
103
+
104
+ // 几何构建在 Worker 内完成,只把可序列化数据 + transferable buffers 传回主线程;
105
+ // 此处先做解析+过滤,几何构建在 buildGeometryInWorker 中扩展
106
+ return buildGeometryResult(result, extent, x, y, z)
107
+ }
108
+
109
+ /**
110
+ * 瓦片坐标 → 经纬度(度),与 VectorTileLOD.transformPoint 一致
111
+ */
112
+ function transformPoint(x, y, size, x0, y0, out) {
113
+ out[0] = ((x + x0) * 360) / size - 180
114
+ out[1] =
115
+ (360 / Math.PI) *
116
+ Math.atan(Math.exp((1 - ((y + y0) * 2) / size) * Math.PI)) -
117
+ 90
118
+ return out
119
+ }
120
+
121
+ /**
122
+ * 根据解析+过滤结果构建几何数据(positions/normals/indices 等),仅返回可 transfer 的数据
123
+ */
124
+ function buildGeometryResult(layerResult, extent, x, y, z) {
125
+ const out = { fill: [], line: [], symbol: [] }
126
+ const size = extent * Math.pow(2, z)
127
+ const x0 = extent * x
128
+ const y0 = extent * y
129
+ const canonical = { x, y, z }
130
+
131
+ for (const item of layerResult.fill || []) {
132
+ const batches = buildFillBatches(item, extent, size, x0, y0, canonical)
133
+ out.fill.push({
134
+ layerId: item.layerId,
135
+ source: item.source,
136
+ sourceLayer: item.sourceLayer,
137
+ styleLayer: item.styleLayer,
138
+ batches,
139
+ firstBatchId: 0,
140
+ lastBatchId: batches.length - 1
141
+ })
142
+ }
143
+
144
+ for (const item of layerResult.line || []) {
145
+ const batches = buildLineBatches(item, extent, size, x0, y0)
146
+ out.line.push({
147
+ layerId: item.layerId,
148
+ source: item.source,
149
+ sourceLayer: item.sourceLayer,
150
+ styleLayer: item.styleLayer,
151
+ batches,
152
+ firstBatchId: 0,
153
+ lastBatchId: batches.length - 1
154
+ })
155
+ }
156
+
157
+ for (const item of layerResult.symbol || []) {
158
+ const placements = buildSymbolPlacements(item, extent, size, x0, y0)
159
+ out.symbol.push({
160
+ layerId: item.layerId,
161
+ source: item.source,
162
+ sourceLayer: item.sourceLayer,
163
+ styleLayer: item.styleLayer,
164
+ placements,
165
+ firstBatchId: 0,
166
+ lastBatchId: 0
167
+ })
168
+ }
169
+
170
+ return out
171
+ }
172
+
173
+ /**
174
+ * Symbol 图层:输出每个符号的 placement(coord, text, style 等),主线程用其创建 Cesium.Label
175
+ */
176
+ function buildSymbolPlacements(item, extent, size, x0, y0) {
177
+ const { features, styleLayer, z } = item
178
+ const coordDeg = [0, 0]
179
+ const placements = []
180
+
181
+ for (const { feature } of features) {
182
+ const type = VectorTileFeature.types[feature.type]
183
+ if (type !== 'Point' && type !== 'Unknown') continue
184
+ const vtCoords = loadGeometry(feature)
185
+ if (!vtCoords.length || !vtCoords[0].length) continue
186
+ const layout = evaluateSymbolLayout(styleLayer, z, feature)
187
+ if (!layout.text) continue
188
+ const paint = evaluateSymbolPaint(styleLayer, z, feature)
189
+ const textColorBytes = colorToBytes(paint.textColor)
190
+ const outlineColorBytes = colorToBytes(paint.outlineColor)
191
+ const firstRing = vtCoords[0]
192
+ for (let pi = 0; pi < firstRing.length; pi++) {
193
+ const p = firstRing[pi]
194
+ transformPoint(p.x, p.y, size, x0, y0, coordDeg)
195
+ placements.push({
196
+ coord: [coordDeg[0], coordDeg[1]],
197
+ text: layout.text,
198
+ font: layout.font,
199
+ textSize: layout.textSize,
200
+ textColorBytes: Array.from(textColorBytes),
201
+ outlineWidth: paint.outlineWidth,
202
+ outlineColorBytes: Array.from(outlineColorBytes),
203
+ textOffset: layout.textOffset,
204
+ textAnchor: layout.textAnchor,
205
+ id: feature.id ?? feature.properties?.id ?? null,
206
+ properties: feature.properties || {}
207
+ })
208
+ }
209
+ }
210
+ return placements
211
+ }
212
+
213
+ /**
214
+ * Fill 图层几何:每个 polygon 一个 batch(positions, normals, st, indices, colorBytes)
215
+ */
216
+ function buildFillBatches(item, extent, size, x0, y0, canonical) {
217
+ const { features, styleLayer, z } = item
218
+ const granularity =
219
+ granularitySettings.globe.line.getGranularityForZoomLevel(z) / 2
220
+ const coordDeg = [0, 0]
221
+ const posScratch = new Float64Array(3)
222
+ const batches = []
223
+ let batchId = 0
224
+
225
+ for (const { feature } of features) {
226
+ if (VectorTileFeature.types[feature.type] !== 'Polygon') continue
227
+ const paint = evaluateFillPaint(styleLayer, z, feature)
228
+ const fillColor = paint.fillColor
229
+ const fillOpacity = paint.fillOpacity
230
+ const colorBytes = colorToBytes(fillColor, fillOpacity)
231
+
232
+ const vtCoords = loadGeometry(feature)
233
+ const polygons = classifyRings(vtCoords)
234
+ for (const coordinates of polygons) {
235
+ if (coordinates.some(ring => ring.length < 3)) continue
236
+ const subdivisionRes = subdividePolygon(
237
+ coordinates,
238
+ canonical,
239
+ granularity,
240
+ false
241
+ )
242
+ const verticesFlattened = subdivisionRes.verticesFlattened
243
+ const vertCount = verticesFlattened.length / 2
244
+ const positions = new Float64Array(vertCount * 3)
245
+ const normals = new Float32Array(vertCount * 3)
246
+ const st = new Float32Array(vertCount * 2)
247
+
248
+ for (let i = 0, j = 0; i < verticesFlattened.length; i += 2, j++) {
249
+ const vx = verticesFlattened[i]
250
+ const vy = verticesFlattened[i + 1]
251
+ transformPoint(vx, vy, size, x0, y0, coordDeg)
252
+ fromDegrees(coordDeg[0], coordDeg[1], 0, posScratch)
253
+ positions[j * 3] = posScratch[0]
254
+ positions[j * 3 + 1] = posScratch[1]
255
+ positions[j * 3 + 2] = posScratch[2]
256
+ normalize(posScratch)
257
+ normals[j * 3] = posScratch[0]
258
+ normals[j * 3 + 1] = posScratch[1]
259
+ normals[j * 3 + 2] = posScratch[2]
260
+ st[j * 2] = vx / extent
261
+ st[j * 2 + 1] = vy / extent
262
+ }
263
+
264
+ const indices = new (
265
+ vertCount > 65535
266
+ ? Uint32Array
267
+ : vertCount > 255
268
+ ? Uint16Array
269
+ : Uint8Array
270
+ )(subdivisionRes.indicesTriangles)
271
+
272
+ batches.push({
273
+ batchId: batchId++,
274
+ positions,
275
+ normals,
276
+ st,
277
+ indices,
278
+ colorBytes,
279
+ id: feature.id ?? feature.properties?.id ?? null,
280
+ properties: feature.properties || {}
281
+ })
282
+ }
283
+ }
284
+ return batches
285
+ }
286
+
287
+ /**
288
+ * Line 图层几何:每条线一个 batch(positions, colorBytes, lineWidth)
289
+ */
290
+ function buildLineBatches(item, extent, size, x0, y0) {
291
+ const { features, styleLayer, z } = item
292
+ const granularity =
293
+ granularitySettings.globe.line.getGranularityForZoomLevel(z)
294
+ const coordDeg = [0, 0]
295
+ const posScratch = new Float64Array(3)
296
+ const batches = []
297
+ let batchId = 0
298
+
299
+ for (const { feature } of features) {
300
+ const featureType = VectorTileFeature.types[feature.type]
301
+ if (featureType === 'Point' || featureType === 'Unknown') continue
302
+ const paint = evaluateLinePaint(styleLayer, z, feature)
303
+ const colorBytes = colorToBytes(paint.lineColor, paint.lineOpacity)
304
+
305
+ const vtCoords = loadGeometry(feature)
306
+ for (let ri = 0; ri < vtCoords.length; ri++) {
307
+ const ring = subdivideVertexLine(vtCoords[ri], granularity, false)
308
+ if (ring.length < 2) continue
309
+ const positions = new Float64Array(ring.length * 3)
310
+ for (let i = 0; i < ring.length; i++) {
311
+ const p = ring[i]
312
+ transformPoint(p.x, p.y, size, x0, y0, coordDeg)
313
+ fromDegrees(coordDeg[0], coordDeg[1], 0, posScratch)
314
+ positions[i * 3] = posScratch[0]
315
+ positions[i * 3 + 1] = posScratch[1]
316
+ positions[i * 3 + 2] = posScratch[2]
317
+ }
318
+ batches.push({
319
+ batchId: batchId++,
320
+ positions,
321
+ colorBytes,
322
+ lineWidth: paint.lineWidth,
323
+ id: feature.id ?? feature.properties?.id ?? null,
324
+ properties: feature.properties || {}
325
+ })
326
+ }
327
+ }
328
+ return batches
329
+ }