maplibre-copc-layer 0.0.1
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 +124 -0
- package/dist/index.d.mts +176 -0
- package/dist/index.mjs +645 -0
- package/package.json +52 -0
- package/src/cache-manager.ts +184 -0
- package/src/copclayer.ts +704 -0
- package/src/globe-control.ts +70 -0
- package/src/index.ts +13 -0
- package/src/shaders/edl.frag.glsl +49 -0
- package/src/shaders/edl.vert.glsl +6 -0
- package/src/shaders/points.frag.glsl +19 -0
- package/src/shaders/points.vert.glsl +21 -0
- package/src/shaders/shaders.d.ts +4 -0
- package/src/worker/index.ts +347 -0
- package/src/worker/sse.ts +41 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
import maplibregl from "maplibre-gl";
|
|
2
|
+
import * as THREE from "three";
|
|
3
|
+
//#region src/cache-manager.ts
|
|
4
|
+
var CacheManager = class CacheManager {
|
|
5
|
+
cache = /* @__PURE__ */ new Map();
|
|
6
|
+
memoryUsage = 0;
|
|
7
|
+
options;
|
|
8
|
+
constructor(options = {}) {
|
|
9
|
+
this.options = {
|
|
10
|
+
maxNodes: options.maxNodes ?? 100,
|
|
11
|
+
maxMemoryBytes: options.maxMemoryBytes ?? 100 * 1024 * 1024,
|
|
12
|
+
debug: options.debug ?? false
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
get(nodeId) {
|
|
16
|
+
const data = this.cache.get(nodeId);
|
|
17
|
+
if (data) {
|
|
18
|
+
this.cache.delete(nodeId);
|
|
19
|
+
this.cache.set(nodeId, data);
|
|
20
|
+
data.lastAccessed = Date.now();
|
|
21
|
+
return data;
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
set(nodeData, protectedNodes) {
|
|
26
|
+
const { nodeId } = nodeData;
|
|
27
|
+
const existing = this.cache.get(nodeId);
|
|
28
|
+
if (existing) {
|
|
29
|
+
this.disposeNodeResources(existing);
|
|
30
|
+
this.memoryUsage -= existing.sizeBytes;
|
|
31
|
+
this.cache.delete(nodeId);
|
|
32
|
+
}
|
|
33
|
+
this.ensureCacheLimits(nodeData.sizeBytes, protectedNodes);
|
|
34
|
+
nodeData.lastAccessed = Date.now();
|
|
35
|
+
this.cache.set(nodeId, nodeData);
|
|
36
|
+
this.memoryUsage += nodeData.sizeBytes;
|
|
37
|
+
this.log(`Cached node ${nodeId} (${this.formatBytes(nodeData.sizeBytes)})`);
|
|
38
|
+
}
|
|
39
|
+
has(nodeId) {
|
|
40
|
+
return this.cache.has(nodeId);
|
|
41
|
+
}
|
|
42
|
+
delete(nodeId) {
|
|
43
|
+
const data = this.cache.get(nodeId);
|
|
44
|
+
if (!data) return false;
|
|
45
|
+
this.disposeNodeResources(data);
|
|
46
|
+
this.cache.delete(nodeId);
|
|
47
|
+
this.memoryUsage -= data.sizeBytes;
|
|
48
|
+
this.log(`Removed node ${nodeId} from cache`);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
clear() {
|
|
52
|
+
for (const data of this.cache.values()) this.disposeNodeResources(data);
|
|
53
|
+
this.cache.clear();
|
|
54
|
+
this.memoryUsage = 0;
|
|
55
|
+
}
|
|
56
|
+
updateOptions(newOptions, protectedNodes) {
|
|
57
|
+
Object.assign(this.options, newOptions);
|
|
58
|
+
this.ensureCacheLimits(0, protectedNodes);
|
|
59
|
+
}
|
|
60
|
+
getCachedNodeIds() {
|
|
61
|
+
return Array.from(this.cache.keys());
|
|
62
|
+
}
|
|
63
|
+
size() {
|
|
64
|
+
return this.cache.size;
|
|
65
|
+
}
|
|
66
|
+
static estimateNodeSize(positions, colors) {
|
|
67
|
+
return positions.length * (positions instanceof Float64Array ? 8 : 4) + colors.length * 4 + 1024;
|
|
68
|
+
}
|
|
69
|
+
static createNodeData(nodeId, positions, colors, materialConfig) {
|
|
70
|
+
return {
|
|
71
|
+
nodeId,
|
|
72
|
+
positions: new Float64Array(positions),
|
|
73
|
+
colors: new Float32Array(colors),
|
|
74
|
+
pointCount: positions.length / 3,
|
|
75
|
+
materialConfig: { ...materialConfig },
|
|
76
|
+
lastAccessed: Date.now(),
|
|
77
|
+
sizeBytes: CacheManager.estimateNodeSize(positions, colors)
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
ensureCacheLimits(newItemSize, protectedNodes) {
|
|
81
|
+
while (this.cache.size >= this.options.maxNodes || this.memoryUsage + newItemSize > this.options.maxMemoryBytes) {
|
|
82
|
+
if (this.cache.size === 0) break;
|
|
83
|
+
let lruNodeId = null;
|
|
84
|
+
for (const nodeId of this.cache.keys()) if (!protectedNodes?.has(nodeId)) {
|
|
85
|
+
lruNodeId = nodeId;
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
if (!lruNodeId) {
|
|
89
|
+
this.log("Warning: Cannot evict any nodes - all are protected. Cache limit exceeded.");
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
this.delete(lruNodeId);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
disposeNodeResources(data) {
|
|
96
|
+
data.geometry?.dispose();
|
|
97
|
+
if (data.points?.material instanceof THREE.Material) data.points.material.dispose();
|
|
98
|
+
}
|
|
99
|
+
formatBytes(bytes) {
|
|
100
|
+
const sizes = [
|
|
101
|
+
"B",
|
|
102
|
+
"KB",
|
|
103
|
+
"MB",
|
|
104
|
+
"GB"
|
|
105
|
+
];
|
|
106
|
+
if (bytes === 0) return "0 B";
|
|
107
|
+
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
108
|
+
return `${Math.round(bytes / 1024 ** i * 100) / 100} ${sizes[i]}`;
|
|
109
|
+
}
|
|
110
|
+
log(message) {
|
|
111
|
+
if (this.options.debug) console.log("[CacheManager]", message);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
//#endregion
|
|
115
|
+
//#region src/shaders/points.vert.glsl
|
|
116
|
+
var points_vert_default = "uniform float size;\nuniform float scale;\n\n#ifdef USE_COLOR\n varying vec3 vColor;\n#endif\n\nvoid main() {\n #ifdef USE_COLOR\n vColor = color;\n #endif\n\n vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);\n gl_Position = projectionMatrix * mvPosition;\n\n gl_PointSize = size;\n\n #ifdef USE_SIZEATTENUATION\n gl_PointSize *= (scale / -mvPosition.z);\n #endif\n}\n";
|
|
117
|
+
//#endregion
|
|
118
|
+
//#region src/shaders/points.frag.glsl
|
|
119
|
+
var points_frag_default = "uniform vec3 pointColor;\n\n#ifdef USE_COLOR\n varying vec3 vColor;\n#endif\n\nvoid main() {\n vec2 cxy = 2.0 * gl_PointCoord - 1.0;\n float r = dot(cxy, cxy);\n if (r > 1.0) {\n discard;\n }\n\n #ifdef USE_COLOR\n gl_FragColor = vec4(vColor, 1.0);\n #else\n gl_FragColor = vec4(pointColor, 1.0);\n #endif\n}\n";
|
|
120
|
+
//#endregion
|
|
121
|
+
//#region src/shaders/edl.vert.glsl
|
|
122
|
+
var edl_vert_default = "varying vec2 vUv;\n\nvoid main() {\n vUv = uv;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n}\n";
|
|
123
|
+
//#endregion
|
|
124
|
+
//#region src/shaders/edl.frag.glsl
|
|
125
|
+
var edl_frag_default = "uniform sampler2D tDepth;\nuniform sampler2D tColor;\nuniform vec2 screenSize;\nuniform float edlStrength;\nuniform float radius;\nvarying vec2 vUv;\n\nfloat readDepth(sampler2D depthSampler, vec2 coord) {\n return texture2D(depthSampler, coord).x;\n}\n\nvoid main() {\n vec4 color = texture2D(tColor, vUv);\n\n if (color.a == 0.0) {\n discard;\n }\n\n float depth = readDepth(tDepth, vUv);\n\n float response = 0.0;\n vec2 texelSize = 1.0 / screenSize;\n\n for (int i = -2; i <= 2; i++) {\n for (int j = -2; j <= 2; j++) {\n if (i == 0 && j == 0) continue;\n\n vec2 offset = vec2(float(i), float(j)) * texelSize * radius;\n vec2 sampleCoord = vUv + offset;\n\n if (sampleCoord.x < 0.0 || sampleCoord.x > 1.0 ||\n sampleCoord.y < 0.0 || sampleCoord.y > 1.0) {\n continue;\n }\n\n float sampleDepth = readDepth(tDepth, sampleCoord);\n float depthDiff = depth - sampleDepth;\n\n if (depthDiff > 0.0) {\n response += min(1.0, depthDiff * 100.0);\n }\n }\n }\n\n response /= 24.0;\n float edl = exp(-response * edlStrength);\n\n gl_FragColor = vec4(color.rgb * edl, color.a);\n}\n";
|
|
126
|
+
//#endregion
|
|
127
|
+
//#region src/copclayer.ts
|
|
128
|
+
const EARTH_CIRCUMFERENCE = 2 * Math.PI * 6378137;
|
|
129
|
+
const DEG2RAD = Math.PI / 180;
|
|
130
|
+
const DEFAULT_OPTIONS = {
|
|
131
|
+
pointSize: 6,
|
|
132
|
+
colorMode: "rgb",
|
|
133
|
+
maxCacheSize: 100,
|
|
134
|
+
sseThreshold: 8,
|
|
135
|
+
depthTest: true,
|
|
136
|
+
maxCacheMemory: 100 * 1024 * 1024,
|
|
137
|
+
debug: false,
|
|
138
|
+
enableEDL: false,
|
|
139
|
+
edlStrength: .4,
|
|
140
|
+
edlRadius: 1.5,
|
|
141
|
+
wasmPath: void 0,
|
|
142
|
+
onInitialized: () => {}
|
|
143
|
+
};
|
|
144
|
+
var CopcLayer = class {
|
|
145
|
+
id;
|
|
146
|
+
type = "custom";
|
|
147
|
+
renderingMode = "3d";
|
|
148
|
+
url;
|
|
149
|
+
map;
|
|
150
|
+
camera;
|
|
151
|
+
scene;
|
|
152
|
+
renderer;
|
|
153
|
+
worker;
|
|
154
|
+
cacheManager;
|
|
155
|
+
options;
|
|
156
|
+
visibleNodes = [];
|
|
157
|
+
workerInitialized = false;
|
|
158
|
+
pendingRequests = /* @__PURE__ */ new Set();
|
|
159
|
+
requestQueue = [];
|
|
160
|
+
lastCameraPosition = null;
|
|
161
|
+
sceneCenter = null;
|
|
162
|
+
colorTarget;
|
|
163
|
+
depthTarget;
|
|
164
|
+
edlMaterial;
|
|
165
|
+
edlQuadScene;
|
|
166
|
+
edlQuadCamera;
|
|
167
|
+
_tempMatrix1 = new THREE.Matrix4();
|
|
168
|
+
_tempMatrix2 = new THREE.Matrix4();
|
|
169
|
+
_lastEdlWidth = 0;
|
|
170
|
+
_lastEdlHeight = 0;
|
|
171
|
+
_lastUpdatePointsTime = 0;
|
|
172
|
+
constructor(url, options = {}, layerId = "copc-layer") {
|
|
173
|
+
this.id = layerId;
|
|
174
|
+
this.url = url;
|
|
175
|
+
this.options = {
|
|
176
|
+
...DEFAULT_OPTIONS,
|
|
177
|
+
...options
|
|
178
|
+
};
|
|
179
|
+
this.cacheManager = new CacheManager({
|
|
180
|
+
maxNodes: this.options.maxCacheSize,
|
|
181
|
+
maxMemoryBytes: this.options.maxCacheMemory,
|
|
182
|
+
debug: this.options.debug
|
|
183
|
+
});
|
|
184
|
+
this.camera = new THREE.Camera();
|
|
185
|
+
this.scene = new THREE.Scene();
|
|
186
|
+
this.worker = new Worker(new URL("./worker/index.ts", import.meta.url), { type: "module" });
|
|
187
|
+
this.setupWorkerMessageHandlers();
|
|
188
|
+
}
|
|
189
|
+
setupWorkerMessageHandlers() {
|
|
190
|
+
this.worker.onmessage = (event) => {
|
|
191
|
+
const message = event.data;
|
|
192
|
+
switch (message.type) {
|
|
193
|
+
case "initialized":
|
|
194
|
+
this.workerInitialized = true;
|
|
195
|
+
this.requestNodeData("0-0-0-0");
|
|
196
|
+
this.options.onInitialized?.(message);
|
|
197
|
+
break;
|
|
198
|
+
case "nodeLoaded":
|
|
199
|
+
this.handleNodeLoaded(message.node, message.positions, message.colors);
|
|
200
|
+
break;
|
|
201
|
+
case "nodesToLoad":
|
|
202
|
+
this.cancelAllPendingRequests();
|
|
203
|
+
this.lastCameraPosition = message.cameraPosition;
|
|
204
|
+
this.visibleNodes = message.nodes;
|
|
205
|
+
this.updateVisibleNodes();
|
|
206
|
+
break;
|
|
207
|
+
case "error":
|
|
208
|
+
if (this.options.debug) console.error("[CopcLayer] Worker error:", message.message);
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
this.map?.triggerRepaint();
|
|
212
|
+
};
|
|
213
|
+
this.worker.onerror = (error) => {
|
|
214
|
+
if (this.options.debug) console.error("[CopcLayer] Worker error event:", error);
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
handleNodeLoaded(nodeId, positionsBuffer, colorsBuffer) {
|
|
218
|
+
this.pendingRequests.delete(nodeId);
|
|
219
|
+
this.removeFromRequestQueue(nodeId);
|
|
220
|
+
const positions = new Float64Array(positionsBuffer);
|
|
221
|
+
const colors = new Float32Array(colorsBuffer);
|
|
222
|
+
const nodeData = CacheManager.createNodeData(nodeId, positions, colors, {
|
|
223
|
+
colorMode: this.options.colorMode,
|
|
224
|
+
pointSize: this.options.pointSize,
|
|
225
|
+
depthTest: this.options.depthTest
|
|
226
|
+
});
|
|
227
|
+
const geometry = new THREE.BufferGeometry();
|
|
228
|
+
if (this.sceneCenter) {
|
|
229
|
+
const { x: cx, y: cy, z: cz } = this.sceneCenter;
|
|
230
|
+
const relativePositions = new Float32Array(positions.length);
|
|
231
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
232
|
+
relativePositions[i] = positions[i] - cx;
|
|
233
|
+
relativePositions[i + 1] = positions[i + 1] - cy;
|
|
234
|
+
relativePositions[i + 2] = positions[i + 2] - cz;
|
|
235
|
+
}
|
|
236
|
+
geometry.setAttribute("position", new THREE.BufferAttribute(relativePositions, 3));
|
|
237
|
+
} else geometry.setAttribute("position", new THREE.BufferAttribute(new Float32Array(positions), 3));
|
|
238
|
+
geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3));
|
|
239
|
+
const material = this.createPointMaterial();
|
|
240
|
+
const points = new THREE.Points(geometry, material);
|
|
241
|
+
nodeData.geometry = geometry;
|
|
242
|
+
nodeData.points = points;
|
|
243
|
+
const protectedNodes = new Set(this.visibleNodes);
|
|
244
|
+
this.cacheManager.set(nodeData, protectedNodes);
|
|
245
|
+
}
|
|
246
|
+
updateVisibleNodes() {
|
|
247
|
+
while (this.scene.children.length > 0) this.scene.remove(this.scene.children[0]);
|
|
248
|
+
const nodesToRequest = [];
|
|
249
|
+
for (const nodeId of this.visibleNodes) {
|
|
250
|
+
const cachedData = this.cacheManager.get(nodeId);
|
|
251
|
+
if (cachedData?.points) {
|
|
252
|
+
this.scene.add(cachedData.points);
|
|
253
|
+
if (this.needsMaterialUpdate(cachedData)) this.updateNodeMaterial(cachedData);
|
|
254
|
+
} else if (!this.pendingRequests.has(nodeId)) nodesToRequest.push(nodeId);
|
|
255
|
+
}
|
|
256
|
+
for (const nodeId of this.prioritizeNodeRequests(nodesToRequest)) this.requestNodeData(nodeId);
|
|
257
|
+
}
|
|
258
|
+
requestNodeData(nodeId) {
|
|
259
|
+
if (this.pendingRequests.has(nodeId)) return;
|
|
260
|
+
this.pendingRequests.add(nodeId);
|
|
261
|
+
this.requestQueue.push(nodeId);
|
|
262
|
+
this.worker.postMessage({
|
|
263
|
+
type: "loadNode",
|
|
264
|
+
node: nodeId
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
needsMaterialUpdate(nodeData) {
|
|
268
|
+
const { materialConfig: c } = nodeData;
|
|
269
|
+
return c.colorMode !== this.options.colorMode || c.pointSize !== this.options.pointSize || c.depthTest !== this.options.depthTest;
|
|
270
|
+
}
|
|
271
|
+
updateNodeMaterial(nodeData) {
|
|
272
|
+
if (!nodeData.points) return;
|
|
273
|
+
if (nodeData.points.material instanceof THREE.Material) nodeData.points.material.dispose();
|
|
274
|
+
nodeData.points.material = this.createPointMaterial();
|
|
275
|
+
nodeData.materialConfig = {
|
|
276
|
+
colorMode: this.options.colorMode,
|
|
277
|
+
pointSize: this.options.pointSize,
|
|
278
|
+
depthTest: this.options.depthTest
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
rebuildAllMaterials() {
|
|
282
|
+
for (const nodeId of this.cacheManager.getCachedNodeIds()) {
|
|
283
|
+
const nodeData = this.cacheManager.get(nodeId);
|
|
284
|
+
if (nodeData) this.updateNodeMaterial(nodeData);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
removeFromRequestQueue(nodeId) {
|
|
288
|
+
const index = this.requestQueue.indexOf(nodeId);
|
|
289
|
+
if (index > -1) this.requestQueue.splice(index, 1);
|
|
290
|
+
}
|
|
291
|
+
cancelAllPendingRequests() {
|
|
292
|
+
if (this.pendingRequests.size === 0) return;
|
|
293
|
+
this.worker.postMessage({
|
|
294
|
+
type: "cancelRequests",
|
|
295
|
+
nodes: Array.from(this.pendingRequests)
|
|
296
|
+
});
|
|
297
|
+
this.pendingRequests.clear();
|
|
298
|
+
this.requestQueue.length = 0;
|
|
299
|
+
}
|
|
300
|
+
prioritizeNodeRequests(nodeIds) {
|
|
301
|
+
if (!this.lastCameraPosition || nodeIds.length <= 1) return nodeIds;
|
|
302
|
+
const [camLon, camLat] = this.lastCameraPosition;
|
|
303
|
+
const nodesWithPriority = nodeIds.map((nodeId) => {
|
|
304
|
+
const [depth, x, y] = nodeId.split("-").map(Number);
|
|
305
|
+
const nodeSize = 1 / 2 ** depth;
|
|
306
|
+
const nodeCenterX = x * nodeSize + nodeSize / 2;
|
|
307
|
+
const nodeCenterY = y * nodeSize + nodeSize / 2;
|
|
308
|
+
const dx = nodeCenterX - camLon;
|
|
309
|
+
const dy = nodeCenterY - camLat;
|
|
310
|
+
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
311
|
+
return {
|
|
312
|
+
nodeId,
|
|
313
|
+
priority: depth * 10 + (distance > 0 ? 1e3 / distance : 1e3) * .1
|
|
314
|
+
};
|
|
315
|
+
});
|
|
316
|
+
nodesWithPriority.sort((a, b) => b.priority - a.priority);
|
|
317
|
+
return nodesWithPriority.map((n) => n.nodeId);
|
|
318
|
+
}
|
|
319
|
+
async onAdd(map, gl) {
|
|
320
|
+
this.map = map;
|
|
321
|
+
this.worker.postMessage({
|
|
322
|
+
type: "init",
|
|
323
|
+
url: this.url,
|
|
324
|
+
options: {
|
|
325
|
+
colorMode: this.options.colorMode,
|
|
326
|
+
maxCacheSize: this.options.maxCacheSize,
|
|
327
|
+
wasmPath: this.options.wasmPath
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
this.renderer = new THREE.WebGLRenderer({
|
|
331
|
+
canvas: map.getCanvas(),
|
|
332
|
+
context: gl
|
|
333
|
+
});
|
|
334
|
+
this.renderer.outputColorSpace = THREE.LinearSRGBColorSpace;
|
|
335
|
+
this.renderer.autoClear = false;
|
|
336
|
+
if (this.options.enableEDL) this.setupEDL();
|
|
337
|
+
}
|
|
338
|
+
updateCacheConfig(config) {
|
|
339
|
+
Object.assign(this.options, config);
|
|
340
|
+
const protectedNodes = new Set(this.visibleNodes);
|
|
341
|
+
this.cacheManager.updateOptions({
|
|
342
|
+
maxNodes: this.options.maxCacheSize,
|
|
343
|
+
maxMemoryBytes: this.options.maxCacheMemory,
|
|
344
|
+
debug: this.options.debug
|
|
345
|
+
}, protectedNodes);
|
|
346
|
+
this.updateVisibleNodes();
|
|
347
|
+
}
|
|
348
|
+
setPointSize(size) {
|
|
349
|
+
this.options.pointSize = size;
|
|
350
|
+
this.rebuildAllMaterials();
|
|
351
|
+
this.map?.triggerRepaint();
|
|
352
|
+
}
|
|
353
|
+
setSseThreshold(threshold) {
|
|
354
|
+
this.options.sseThreshold = threshold;
|
|
355
|
+
this.updatePoints();
|
|
356
|
+
this.map?.triggerRepaint();
|
|
357
|
+
}
|
|
358
|
+
setDepthTest(enabled) {
|
|
359
|
+
this.options.depthTest = enabled;
|
|
360
|
+
this.rebuildAllMaterials();
|
|
361
|
+
this.map?.triggerRepaint();
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Compute camera altitude in meters above sea level.
|
|
365
|
+
* MapLibre's getCameraAltitude() returns NaN in Globe mode because
|
|
366
|
+
* the Globe transform's internal _pixelPerMeter is never initialized.
|
|
367
|
+
* We replicate the formula using publicly accessible values.
|
|
368
|
+
*/
|
|
369
|
+
computeCameraAltitude(fov, height, zoom) {
|
|
370
|
+
if (!this.map) return 0;
|
|
371
|
+
const latRad = this.map.getCenter().lat * DEG2RAD;
|
|
372
|
+
const fovRad = fov * DEG2RAD;
|
|
373
|
+
const pitchRad = this.map.getPitch() * DEG2RAD;
|
|
374
|
+
const pixelPerMeter = 512 * 2 ** zoom / (EARTH_CIRCUMFERENCE * Math.cos(latRad));
|
|
375
|
+
const cameraToCenterDistance = height / (2 * Math.tan(fovRad / 2));
|
|
376
|
+
return Math.cos(pitchRad) * cameraToCenterDistance / pixelPerMeter;
|
|
377
|
+
}
|
|
378
|
+
updatePoints() {
|
|
379
|
+
if (!this.map || !this.workerInitialized) return;
|
|
380
|
+
const now = performance.now();
|
|
381
|
+
if (now - this._lastUpdatePointsTime < 100) return;
|
|
382
|
+
this._lastUpdatePointsTime = now;
|
|
383
|
+
const fov = this.map.transform.fov;
|
|
384
|
+
const height = this.map.transform.height;
|
|
385
|
+
const zoom = this.map.getZoom();
|
|
386
|
+
const cameraLngLat = this.map.transform.getCameraLngLat().toArray();
|
|
387
|
+
const cameraAltitude = this.computeCameraAltitude(fov, height, zoom);
|
|
388
|
+
this.worker.postMessage({
|
|
389
|
+
type: "updatePoints",
|
|
390
|
+
cameraPosition: [...cameraLngLat, cameraAltitude],
|
|
391
|
+
mapHeight: height,
|
|
392
|
+
fov,
|
|
393
|
+
sseThreshold: this.options.sseThreshold,
|
|
394
|
+
zoom
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
render(_gl, options) {
|
|
398
|
+
if (!this.map || !this.renderer) return;
|
|
399
|
+
if (!this.sceneCenter || this.shouldUpdateSceneCenter()) {
|
|
400
|
+
const center = this.map.getCenter();
|
|
401
|
+
this.sceneCenter = maplibregl.MercatorCoordinate.fromLngLat(center);
|
|
402
|
+
this.clearCache();
|
|
403
|
+
}
|
|
404
|
+
const centerLngLat = this.sceneCenter.toLngLat();
|
|
405
|
+
const modelMatrix = this.map.transform.getMatrixForModel([centerLngLat.lng, centerLngLat.lat], 0);
|
|
406
|
+
const invS = 1 / this.sceneCenter.meterInMercatorCoordinateUnits();
|
|
407
|
+
this._tempMatrix1.fromArray(options.defaultProjectionData.mainMatrix);
|
|
408
|
+
this._tempMatrix2.fromArray(modelMatrix);
|
|
409
|
+
this._tempMatrix1.multiply(this._tempMatrix2);
|
|
410
|
+
this._tempMatrix2.set(invS, 0, 0, 0, 0, 0, EARTH_CIRCUMFERENCE, 0, 0, invS, 0, 0, 0, 0, 0, 1);
|
|
411
|
+
this._tempMatrix1.multiply(this._tempMatrix2);
|
|
412
|
+
this.camera.projectionMatrix.copy(this._tempMatrix1);
|
|
413
|
+
this.updatePoints();
|
|
414
|
+
this.renderer.setSize(this.map.getCanvas().width, this.map.getCanvas().height);
|
|
415
|
+
if (this.options.enableEDL) this.updateEDLSize();
|
|
416
|
+
this.renderer.resetState();
|
|
417
|
+
if (this.options.enableEDL && this.colorTarget && this.depthTarget && this.edlQuadScene && this.edlQuadCamera) {
|
|
418
|
+
this.renderer.setRenderTarget(this.depthTarget);
|
|
419
|
+
this.renderer.clear();
|
|
420
|
+
this.renderer.render(this.scene, this.camera);
|
|
421
|
+
this.renderer.setRenderTarget(this.colorTarget);
|
|
422
|
+
this.renderer.clear();
|
|
423
|
+
this.renderer.render(this.scene, this.camera);
|
|
424
|
+
this.renderer.setRenderTarget(null);
|
|
425
|
+
this.renderer.render(this.edlQuadScene, this.edlQuadCamera);
|
|
426
|
+
} else this.renderer.render(this.scene, this.camera);
|
|
427
|
+
this.map.triggerRepaint();
|
|
428
|
+
}
|
|
429
|
+
shouldUpdateSceneCenter() {
|
|
430
|
+
if (!this.sceneCenter || !this.map) return true;
|
|
431
|
+
const currentMercator = maplibregl.MercatorCoordinate.fromLngLat(this.map.getCenter());
|
|
432
|
+
const dx = currentMercator.x - this.sceneCenter.x;
|
|
433
|
+
const dy = currentMercator.y - this.sceneCenter.y;
|
|
434
|
+
return Math.sqrt(dx * dx + dy * dy) > .001;
|
|
435
|
+
}
|
|
436
|
+
onRemove(_map, _gl) {
|
|
437
|
+
this.worker.terminate();
|
|
438
|
+
this.cacheManager.clear();
|
|
439
|
+
this.visibleNodes.length = 0;
|
|
440
|
+
this.pendingRequests.clear();
|
|
441
|
+
this.requestQueue.length = 0;
|
|
442
|
+
this.colorTarget?.dispose();
|
|
443
|
+
this.colorTarget = void 0;
|
|
444
|
+
this.depthTarget?.dispose();
|
|
445
|
+
this.depthTarget = void 0;
|
|
446
|
+
this.edlMaterial?.dispose();
|
|
447
|
+
this.edlMaterial = void 0;
|
|
448
|
+
this.edlQuadScene?.clear();
|
|
449
|
+
this.edlQuadScene = void 0;
|
|
450
|
+
this.edlQuadCamera = void 0;
|
|
451
|
+
}
|
|
452
|
+
setupEDL() {
|
|
453
|
+
if (!this.renderer || !this.map) return;
|
|
454
|
+
const width = this.map.getCanvas().width;
|
|
455
|
+
const height = this.map.getCanvas().height;
|
|
456
|
+
this.colorTarget = new THREE.WebGLRenderTarget(width, height, {
|
|
457
|
+
minFilter: THREE.LinearFilter,
|
|
458
|
+
magFilter: THREE.LinearFilter,
|
|
459
|
+
format: THREE.RGBAFormat
|
|
460
|
+
});
|
|
461
|
+
this.depthTarget = new THREE.WebGLRenderTarget(width, height, {
|
|
462
|
+
minFilter: THREE.NearestFilter,
|
|
463
|
+
magFilter: THREE.NearestFilter,
|
|
464
|
+
format: THREE.RGBAFormat,
|
|
465
|
+
depthBuffer: true,
|
|
466
|
+
depthTexture: new THREE.DepthTexture(width, height)
|
|
467
|
+
});
|
|
468
|
+
this.edlMaterial = new THREE.ShaderMaterial({
|
|
469
|
+
uniforms: {
|
|
470
|
+
tColor: { value: this.colorTarget.texture },
|
|
471
|
+
tDepth: { value: this.depthTarget.depthTexture },
|
|
472
|
+
screenSize: { value: new THREE.Vector2(width, height) },
|
|
473
|
+
edlStrength: { value: this.options.edlStrength },
|
|
474
|
+
radius: { value: this.options.edlRadius }
|
|
475
|
+
},
|
|
476
|
+
vertexShader: edl_vert_default,
|
|
477
|
+
fragmentShader: edl_frag_default
|
|
478
|
+
});
|
|
479
|
+
this.edlQuadScene = new THREE.Scene();
|
|
480
|
+
this.edlQuadCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
|
|
481
|
+
const geometry = new THREE.PlaneGeometry(2, 2);
|
|
482
|
+
const quad = new THREE.Mesh(geometry, this.edlMaterial);
|
|
483
|
+
this.edlQuadScene.add(quad);
|
|
484
|
+
}
|
|
485
|
+
updateEDLSize() {
|
|
486
|
+
if (!this.map || !this.colorTarget || !this.depthTarget || !this.edlMaterial) return;
|
|
487
|
+
const width = this.map.getCanvas().width;
|
|
488
|
+
const height = this.map.getCanvas().height;
|
|
489
|
+
if (width === this._lastEdlWidth && height === this._lastEdlHeight) return;
|
|
490
|
+
this._lastEdlWidth = width;
|
|
491
|
+
this._lastEdlHeight = height;
|
|
492
|
+
this.colorTarget.setSize(width, height);
|
|
493
|
+
this.depthTarget.setSize(width, height);
|
|
494
|
+
this.edlMaterial.uniforms.screenSize.value.set(width, height);
|
|
495
|
+
}
|
|
496
|
+
createPointMaterial() {
|
|
497
|
+
if (this.options.enableEDL) return new THREE.ShaderMaterial({
|
|
498
|
+
uniforms: {
|
|
499
|
+
size: { value: this.options.pointSize },
|
|
500
|
+
scale: { value: window.devicePixelRatio },
|
|
501
|
+
useVertexColors: { value: this.options.colorMode !== "white" },
|
|
502
|
+
pointColor: { value: new THREE.Color(16777215) }
|
|
503
|
+
},
|
|
504
|
+
vertexShader: points_vert_default,
|
|
505
|
+
fragmentShader: points_frag_default,
|
|
506
|
+
vertexColors: this.options.colorMode !== "white",
|
|
507
|
+
depthTest: this.options.depthTest,
|
|
508
|
+
depthWrite: this.options.depthTest,
|
|
509
|
+
transparent: false
|
|
510
|
+
});
|
|
511
|
+
const material = new THREE.PointsMaterial({
|
|
512
|
+
vertexColors: this.options.colorMode !== "white",
|
|
513
|
+
size: this.options.pointSize,
|
|
514
|
+
depthTest: this.options.depthTest,
|
|
515
|
+
depthWrite: this.options.depthTest,
|
|
516
|
+
sizeAttenuation: true
|
|
517
|
+
});
|
|
518
|
+
if (this.options.colorMode === "white") material.color.setHex(16777215);
|
|
519
|
+
return material;
|
|
520
|
+
}
|
|
521
|
+
getPointSize() {
|
|
522
|
+
return this.options.pointSize;
|
|
523
|
+
}
|
|
524
|
+
getColorMode() {
|
|
525
|
+
return this.options.colorMode;
|
|
526
|
+
}
|
|
527
|
+
getSseThreshold() {
|
|
528
|
+
return this.options.sseThreshold;
|
|
529
|
+
}
|
|
530
|
+
getDepthTest() {
|
|
531
|
+
return this.options.depthTest;
|
|
532
|
+
}
|
|
533
|
+
getOptions() {
|
|
534
|
+
return { ...this.options };
|
|
535
|
+
}
|
|
536
|
+
isLoading() {
|
|
537
|
+
return this.pendingRequests.size > 0 || !this.workerInitialized;
|
|
538
|
+
}
|
|
539
|
+
getNodeStats() {
|
|
540
|
+
return {
|
|
541
|
+
loaded: this.cacheManager.size(),
|
|
542
|
+
visible: this.visibleNodes.length
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
clearCache() {
|
|
546
|
+
this.cacheManager.clear();
|
|
547
|
+
this.updateVisibleNodes();
|
|
548
|
+
}
|
|
549
|
+
setEDLEnabled(enabled) {
|
|
550
|
+
this.options.enableEDL = enabled;
|
|
551
|
+
if (enabled && !this.edlMaterial) this.setupEDL();
|
|
552
|
+
this.rebuildAllMaterials();
|
|
553
|
+
this.map?.triggerRepaint();
|
|
554
|
+
}
|
|
555
|
+
updateEDLParameters(params) {
|
|
556
|
+
if (!this.edlMaterial) return;
|
|
557
|
+
if (params.strength !== void 0) {
|
|
558
|
+
this.options.edlStrength = params.strength;
|
|
559
|
+
this.edlMaterial.uniforms.edlStrength.value = params.strength;
|
|
560
|
+
}
|
|
561
|
+
if (params.radius !== void 0) {
|
|
562
|
+
this.options.edlRadius = params.radius;
|
|
563
|
+
this.edlMaterial.uniforms.radius.value = params.radius;
|
|
564
|
+
}
|
|
565
|
+
this.map?.triggerRepaint();
|
|
566
|
+
}
|
|
567
|
+
getEDLParameters() {
|
|
568
|
+
return {
|
|
569
|
+
enabled: this.options.enableEDL,
|
|
570
|
+
strength: this.options.edlStrength,
|
|
571
|
+
radius: this.options.edlRadius
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
//#endregion
|
|
576
|
+
//#region src/globe-control.ts
|
|
577
|
+
/**
|
|
578
|
+
* A MapLibre control that toggles between Mercator and Globe projections.
|
|
579
|
+
*/
|
|
580
|
+
var GlobeControl = class {
|
|
581
|
+
map;
|
|
582
|
+
container;
|
|
583
|
+
button;
|
|
584
|
+
isGlobe = false;
|
|
585
|
+
onAdd(map) {
|
|
586
|
+
this.map = map;
|
|
587
|
+
this.container = document.createElement("div");
|
|
588
|
+
this.container.classList.add("maplibregl-ctrl", "maplibregl-ctrl-group");
|
|
589
|
+
this.button = document.createElement("button");
|
|
590
|
+
this.button.type = "button";
|
|
591
|
+
this.button.title = "Toggle Globe view";
|
|
592
|
+
this.button.setAttribute("aria-label", "Toggle Globe view");
|
|
593
|
+
this.button.style.cssText = "display:flex;align-items:center;justify-content:center;";
|
|
594
|
+
this.updateIcon();
|
|
595
|
+
this.button.addEventListener("click", this.toggle);
|
|
596
|
+
this.container.appendChild(this.button);
|
|
597
|
+
return this.container;
|
|
598
|
+
}
|
|
599
|
+
onRemove() {
|
|
600
|
+
this.button?.removeEventListener("click", this.toggle);
|
|
601
|
+
this.container?.remove();
|
|
602
|
+
this.map = void 0;
|
|
603
|
+
this.container = void 0;
|
|
604
|
+
this.button = void 0;
|
|
605
|
+
}
|
|
606
|
+
getDefaultPosition() {
|
|
607
|
+
return "top-right";
|
|
608
|
+
}
|
|
609
|
+
toggle = () => {
|
|
610
|
+
if (!this.map) return;
|
|
611
|
+
this.isGlobe = !this.isGlobe;
|
|
612
|
+
const projection = this.isGlobe ? "globe" : "mercator";
|
|
613
|
+
this.map.setProjection({ type: projection });
|
|
614
|
+
this.updateIcon();
|
|
615
|
+
};
|
|
616
|
+
updateIcon() {
|
|
617
|
+
if (!this.button) return;
|
|
618
|
+
if (this.isGlobe) this.button.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="12" y1="3" x2="12" y2="21"/></svg>`;
|
|
619
|
+
else this.button.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/><path d="M2 12h20"/></svg>`;
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
//#endregion
|
|
623
|
+
//#region src/worker/sse.ts
|
|
624
|
+
function distance(a, b) {
|
|
625
|
+
const dx = a[0] - b[0];
|
|
626
|
+
const dy = a[1] - b[1];
|
|
627
|
+
const dz = a[2] - b[2];
|
|
628
|
+
return Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Calculates Screen Space Error (SSE) for a point cloud node.
|
|
632
|
+
* Higher SSE = more visible error = should render with higher detail.
|
|
633
|
+
*
|
|
634
|
+
* @param maxDistance - Optional maximum effective distance. When the camera is
|
|
635
|
+
* extremely far away (e.g. Globe View at low zoom), the distance is capped
|
|
636
|
+
* to this value so that SSE does not approach 0.
|
|
637
|
+
*/
|
|
638
|
+
function computeScreenSpaceError(cameraCenter, center, fov, geometricError, screenHeight, maxDistance) {
|
|
639
|
+
let dist = distance(cameraCenter, center);
|
|
640
|
+
if (maxDistance !== void 0 && dist > maxDistance) dist = maxDistance;
|
|
641
|
+
const fovRad = fov * (Math.PI / 180);
|
|
642
|
+
return geometricError * screenHeight / (2 * dist * Math.tan(fovRad / 2));
|
|
643
|
+
}
|
|
644
|
+
//#endregion
|
|
645
|
+
export { CacheManager, CopcLayer, GlobeControl, computeScreenSpaceError };
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "maplibre-copc-layer",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"description": "A library for loading and rendering Cloud-Optimized Point Cloud (COPC) data in MapLibre GL JS using Three.js.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./dist/index.mjs",
|
|
9
|
+
"./package.json": "./package.json"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"src"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "vp pack",
|
|
17
|
+
"build:demo": "vp build --outDir demo --base ./",
|
|
18
|
+
"dev": "vp dev",
|
|
19
|
+
"test": "vp test",
|
|
20
|
+
"typecheck": "tsc --noEmit",
|
|
21
|
+
"release": "bumpp",
|
|
22
|
+
"prepublishOnly": "pnpm run build"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"copc": "^0.0.8",
|
|
26
|
+
"proj4": "^2.15.0"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"maplibre-gl": ">=5.0.0",
|
|
30
|
+
"three": ">=0.150.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@cloudflare/kv-asset-handler": "^0.4.2",
|
|
34
|
+
"@types/node": "^25.5.0",
|
|
35
|
+
"@types/proj4": "^2.5.6",
|
|
36
|
+
"@types/three": "^0.173.0",
|
|
37
|
+
"@typescript/native-preview": "7.0.0-dev.20260316.1",
|
|
38
|
+
"bumpp": "^11.0.1",
|
|
39
|
+
"maplibre-gl": "^5.1.1",
|
|
40
|
+
"three": "^0.173.0",
|
|
41
|
+
"typescript": "^5.9.3",
|
|
42
|
+
"vite-plus": "latest",
|
|
43
|
+
"vitest": "npm:@voidzero-dev/vite-plus-test@latest"
|
|
44
|
+
},
|
|
45
|
+
"pnpm": {
|
|
46
|
+
"overrides": {
|
|
47
|
+
"vite": "npm:@voidzero-dev/vite-plus-core@latest",
|
|
48
|
+
"vitest": "npm:@voidzero-dev/vite-plus-test@latest"
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"packageManager": "pnpm@10.32.1"
|
|
52
|
+
}
|