@mapcomponents/three 1.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/.babelrc +12 -0
  2. package/.storybook/main.ts +20 -0
  3. package/.storybook/preview.ts +0 -0
  4. package/README.md +54 -0
  5. package/cypress.config.ts +13 -0
  6. package/eslint.config.mjs +12 -0
  7. package/package.json +24 -0
  8. package/project.json +15 -0
  9. package/public/assets/3D/godzilla_simple.glb +0 -0
  10. package/public/assets/splats/output.splat +0 -0
  11. package/src/components/MlThreeModelLayer/MlThreeModelLayer.cy.tsx +63 -0
  12. package/src/components/MlThreeModelLayer/MlThreeModelLayer.meta.json +21 -0
  13. package/src/components/MlThreeModelLayer/MlThreeModelLayer.stories.tsx +161 -0
  14. package/src/components/MlThreeModelLayer/MlThreeModelLayer.tsx +153 -0
  15. package/src/components/MlThreeSplatLayer/MlThreeSplatLayer.cy.tsx +62 -0
  16. package/src/components/MlThreeSplatLayer/MlThreeSplatLayer.meta.json +21 -0
  17. package/src/components/MlThreeSplatLayer/MlThreeSplatLayer.stories.tsx +151 -0
  18. package/src/components/MlThreeSplatLayer/MlThreeSplatLayer.tsx +158 -0
  19. package/src/components/MlTransformControls.tsx +112 -0
  20. package/src/components/ThreeContext.tsx +26 -0
  21. package/src/components/ThreeObjectControls.tsx +197 -0
  22. package/src/components/ThreeProvider.tsx +149 -0
  23. package/src/cypress/support/commands.ts +1 -0
  24. package/src/cypress/support/component-index.html +13 -0
  25. package/src/cypress/support/component.ts +13 -0
  26. package/src/decorators/ThreejsContextDecorator.tsx +42 -0
  27. package/src/decorators/style.css +33 -0
  28. package/src/index.ts +7 -0
  29. package/src/lib/ThreejsSceneHelper.ts +250 -0
  30. package/src/lib/ThreejsSceneRenderer.ts +73 -0
  31. package/src/lib/ThreejsUtils.ts +62 -0
  32. package/src/lib/splats/GaussianSplattingMesh.ts +848 -0
  33. package/src/lib/splats/GaussianSplattingShaders.ts +266 -0
  34. package/src/lib/splats/loaders/PlySplatLoader.ts +537 -0
  35. package/src/lib/splats/loaders/SplatLoader.ts +52 -0
  36. package/src/lib/utils/coroutine.ts +121 -0
  37. package/tsconfig.json +21 -0
  38. package/tsconfig.lib.json +27 -0
  39. package/tsconfig.storybook.json +24 -0
  40. package/vite.config.ts +49 -0
@@ -0,0 +1,149 @@
1
+ import React, { useEffect, useState, useRef } from 'react';
2
+ import { useMap } from '@mapcomponents/react-maplibre';
3
+ import { Scene, PerspectiveCamera, Group, Matrix4 } from 'three';
4
+ import { LngLatLike, CustomLayerInterface } from 'maplibre-gl';
5
+ import ThreejsSceneHelper from '../lib/ThreejsSceneHelper';
6
+ import ThreejsSceneRenderer from '../lib/ThreejsSceneRenderer';
7
+ import ThreejsUtils from '../lib/ThreejsUtils';
8
+ import { ThreeContext } from './ThreeContext';
9
+
10
+ export interface ThreeProviderProps {
11
+ mapId?: string;
12
+ id: string;
13
+ refCenter?: LngLatLike;
14
+ envTexture?: string;
15
+ envIntensity?: number;
16
+ createLight?: boolean;
17
+ children?: React.ReactNode;
18
+ /**
19
+ * Id of an existing layer in the MapLibre instance to help specify the layer order.
20
+ * The Three.js layer will be rendered visually beneath the layer with the specified id.
21
+ */
22
+ beforeId?: string;
23
+ }
24
+
25
+ export const ThreeProvider: React.FC<ThreeProviderProps> = ({
26
+ mapId,
27
+ id,
28
+ refCenter,
29
+ envTexture,
30
+ envIntensity = 1,
31
+ createLight = true,
32
+ children,
33
+ beforeId,
34
+ }) => {
35
+ const { map } = useMap({ mapId, waitForLayer: beforeId });
36
+ const [scene, setScene] = useState<Scene>();
37
+ const [camera, setCamera] = useState<PerspectiveCamera>();
38
+ const [renderer, setRenderer] = useState<ThreejsSceneRenderer>();
39
+ const [sceneRoot, setSceneRoot] = useState<Group>();
40
+ const [worldMatrix, setWorldMatrix] = useState<Matrix4>();
41
+ const [worldMatrixInv, setWorldMatrixInv] = useState<Matrix4>();
42
+
43
+ const helperRef = useRef(new ThreejsSceneHelper());
44
+ const worldMatrixRef = useRef<Matrix4>(new Matrix4());
45
+ const worldMatrixInvRef = useRef<Matrix4>(new Matrix4());
46
+ const rendererRef = useRef<ThreejsSceneRenderer | undefined>(undefined);
47
+
48
+ useEffect(() => {
49
+ if (!map) return;
50
+
51
+ const helper = helperRef.current;
52
+ const threeScene = helper.createScene(createLight);
53
+ const root = helper.createGroup(threeScene, 'scene-root');
54
+ const threeCamera = helper.createCamera(root, 'camera-for-render');
55
+
56
+ const customLayer: CustomLayerInterface = {
57
+ id: id,
58
+ type: 'custom',
59
+ renderingMode: '3d',
60
+ onAdd: (mapInstance, gl) => {
61
+ const threeRenderer = new ThreejsSceneRenderer(mapInstance, gl as WebGL2RenderingContext);
62
+ rendererRef.current = threeRenderer;
63
+ setRenderer(threeRenderer);
64
+
65
+ const center = refCenter || mapInstance.getCenter();
66
+ worldMatrixRef.current = ThreejsUtils.updateWorldMatrix(mapInstance, center);
67
+ worldMatrixInvRef.current = worldMatrixRef.current.clone().invert();
68
+ setWorldMatrix(worldMatrixRef.current);
69
+ setWorldMatrixInv(worldMatrixInvRef.current);
70
+
71
+ if (envTexture) {
72
+ helper.createEnvTexture(envTexture, threeScene);
73
+ }
74
+ threeScene.environmentIntensity = envIntensity;
75
+
76
+ mapInstance.triggerRepaint();
77
+ },
78
+ render: (gl, matrix) => {
79
+ if (!rendererRef.current || !threeScene || !threeCamera) return;
80
+
81
+ helper.updateCameraForRender(
82
+ threeCamera,
83
+ map.map,
84
+ matrix,
85
+ worldMatrixRef.current,
86
+ worldMatrixInvRef.current
87
+ );
88
+
89
+ rendererRef.current.render(threeScene, threeCamera);
90
+ map.triggerRepaint();
91
+ },
92
+ onRemove: () => {
93
+ if (rendererRef.current) {
94
+ rendererRef.current.dispose();
95
+ rendererRef.current = undefined;
96
+ }
97
+ setRenderer(undefined);
98
+ },
99
+ };
100
+
101
+ if (!map.getLayer(id)) {
102
+ map.addLayer(customLayer, beforeId);
103
+ }
104
+
105
+ setScene(threeScene);
106
+ setCamera(threeCamera);
107
+ setSceneRoot(root);
108
+
109
+ return () => {
110
+ if (map.getLayer(id)) {
111
+ map.removeLayer(id);
112
+ }
113
+ // Cleanup is handled in onRemove
114
+ };
115
+ // eslint-disable-next-line react-hooks/exhaustive-deps
116
+ }, [map, id]); // Re-run if map or id changes.
117
+
118
+ // Handle dynamic prop changes
119
+ useEffect(() => {
120
+ if (scene && envTexture) {
121
+ helperRef.current.createEnvTexture(envTexture, scene);
122
+ }
123
+ }, [scene, envTexture]);
124
+
125
+ useEffect(() => {
126
+ if (scene) {
127
+ scene.environmentIntensity = envIntensity;
128
+ }
129
+ }, [scene, envIntensity]);
130
+
131
+ // Handle refCenter change
132
+ useEffect(() => {
133
+ if (map && refCenter) {
134
+ worldMatrixRef.current = ThreejsUtils.updateWorldMatrix(map.map, refCenter);
135
+ worldMatrixInvRef.current = worldMatrixRef.current.clone().invert();
136
+ setWorldMatrix(worldMatrixRef.current);
137
+ setWorldMatrixInv(worldMatrixInvRef.current);
138
+ map.triggerRepaint();
139
+ }
140
+ }, [map, refCenter]);
141
+
142
+ return (
143
+ <ThreeContext.Provider
144
+ value={{ scene, camera, renderer, map: map?.map, sceneRoot, worldMatrix, worldMatrixInv }}
145
+ >
146
+ {children}
147
+ </ThreeContext.Provider>
148
+ );
149
+ };
@@ -0,0 +1 @@
1
+ /// <reference types="cypress" />
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
6
+ <meta name="viewport" content="width=device-width,initial-scale=1.0" />
7
+ <title>@mapcomponents/three Components App</title>
8
+ <link href="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.css" rel="stylesheet" />
9
+ </head>
10
+ <body>
11
+ <div data-cy-root></div>
12
+ </body>
13
+ </html>
@@ -0,0 +1,13 @@
1
+ import { mount } from 'cypress/react';
2
+ import './commands';
3
+
4
+ declare global {
5
+ // eslint-disable-next-line @typescript-eslint/no-namespace
6
+ namespace Cypress {
7
+ interface Chainable {
8
+ mount: typeof mount;
9
+ }
10
+ }
11
+ }
12
+
13
+ Cypress.Commands.add('mount', mount);
@@ -0,0 +1,42 @@
1
+ import React, { useMemo } from 'react';
2
+ import { ThreeProvider } from '../components/ThreeProvider';
3
+ import {
4
+ MapComponentsProvider,
5
+ MapLibreMap,
6
+ MlNavigationTools,
7
+ getTheme,
8
+ } from '@mapcomponents/react-maplibre';
9
+ import { ThemeProvider as MUIThemeProvider } from '@mui/material/styles';
10
+ import './style.css';
11
+
12
+ const decorators = [
13
+ (Story: any, context: any) => {
14
+ const theme = useMemo(() => getTheme(context?.globals?.theme), [context?.globals?.theme]);
15
+ return (
16
+ <div className="fullscreen_map">
17
+ <MapComponentsProvider>
18
+ <MUIThemeProvider theme={theme}>
19
+ <ThreeProvider mapId="map_1" id="three-scene-layer" beforeId="water_name_line">
20
+ <Story />
21
+ </ThreeProvider>
22
+ <MapLibreMap
23
+ options={{
24
+ zoom: 14.5,
25
+ style: 'https://wms.wheregroup.com/tileserver/style/osm-liberty.json',
26
+ center: [7.099771581806502, 50.73395746209983],
27
+ }}
28
+ mapId="map_1"
29
+ />
30
+ <MlNavigationTools
31
+ sx={{ bottom: '25px', right: '5px' }}
32
+ showZoomButtons={false}
33
+ mapId="map_1"
34
+ />
35
+ </MUIThemeProvider>
36
+ </MapComponentsProvider>
37
+ </div>
38
+ );
39
+ },
40
+ ];
41
+
42
+ export default decorators;
@@ -0,0 +1,33 @@
1
+ #root {
2
+ background-color: #000;
3
+ position: absolute;
4
+ min-height: 400px;
5
+ top: 0;
6
+ bottom: 0;
7
+ left: 0;
8
+ right: 0;
9
+ }
10
+ .docs-story {
11
+ min-height: 400px;
12
+ display: flex;
13
+ align-items: stretch;
14
+ }
15
+ .docs-story > div:first-child {
16
+ width: 100%;
17
+ }
18
+
19
+ .App {
20
+ position: absolute;
21
+ top: 0;
22
+ right: 0;
23
+ bottom: 0;
24
+ left: 0;
25
+ }
26
+ .fullscreen_map .mapContainer {
27
+ position: absolute;
28
+ top: 0;
29
+ right: 0;
30
+ left: 0;
31
+ bottom: 0;
32
+ z-index: 100;
33
+ }
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ export * from './lib/ThreejsUtils';
2
+ export * from './lib/ThreejsSceneHelper';
3
+ export * from './lib/ThreejsSceneRenderer';
4
+ export * from './components/ThreeContext';
5
+ export * from './components/ThreeProvider';
6
+ export { default as MlThreeModelLayer } from './components/MlThreeModelLayer/MlThreeModelLayer';
7
+ export { default as MlThreeSplatLayer } from './components/MlThreeSplatLayer/MlThreeSplatLayer';
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Derived from mapbox-3d-tiles by Jianshun Yang (MIT License)
3
+ * https://github.com/yangjs6/mapbox-3d-tiles
4
+ */
5
+
6
+ import { type Map as MaplibreMap } from 'maplibre-gl';
7
+ import {
8
+ Scene,
9
+ PerspectiveCamera,
10
+ Matrix4,
11
+ Group,
12
+ EquirectangularReflectionMapping,
13
+ DirectionalLight,
14
+ AmbientLight,
15
+ Vector3,
16
+ Quaternion,
17
+ Euler,
18
+ } from 'three';
19
+ import { HDRLoader } from 'three/examples/jsm/loaders/HDRLoader.js';
20
+ import ThreejsUtils from './ThreejsUtils';
21
+
22
+ export default class ThreejsSceneHelper {
23
+ createScene(createLight = true): Scene {
24
+ const scene = new Scene();
25
+
26
+ if (createLight) {
27
+ const dirLight = new DirectionalLight(0xffffff, 4);
28
+ dirLight.position.set(1, 2, 3);
29
+ scene.add(dirLight);
30
+
31
+ const ambLight = new AmbientLight(0xffffff, 0.2);
32
+ scene.add(ambLight);
33
+ }
34
+
35
+ return scene;
36
+ }
37
+
38
+ createGroup(parent: Scene | Group, name: string): Group {
39
+ const group = new Group();
40
+ group.name = name;
41
+ parent.add(group);
42
+ return group;
43
+ }
44
+
45
+ createCamera(sceneRoot: Group, name: string): PerspectiveCamera {
46
+ const camera = new PerspectiveCamera();
47
+ camera.name = name;
48
+
49
+ const group = new Group();
50
+ group.name = `${name}-parent`;
51
+ group.add(camera);
52
+
53
+ sceneRoot.add(group);
54
+ return camera;
55
+ }
56
+
57
+ private buildPerspectiveMatrix(
58
+ fov: number,
59
+ aspect: number,
60
+ near: number,
61
+ far: number
62
+ ): Float64Array {
63
+ const f = 1.0 / Math.tan(fov / 2);
64
+ const nf = 1.0 / (near - far);
65
+
66
+ return new Float64Array([
67
+ f / aspect,
68
+ 0,
69
+ 0,
70
+ 0,
71
+ 0,
72
+ f,
73
+ 0,
74
+ 0,
75
+ 0,
76
+ 0,
77
+ (far + near) * nf,
78
+ -1,
79
+ 0,
80
+ 0,
81
+ 2 * far * near * nf,
82
+ 0,
83
+ ]);
84
+ }
85
+
86
+ private buildOrthographicMatrix(
87
+ left: number,
88
+ right: number,
89
+ bottom: number,
90
+ top: number,
91
+ near: number,
92
+ far: number
93
+ ): Float64Array {
94
+ const lr = 1 / (left - right);
95
+ const bt = 1 / (bottom - top);
96
+ const nf = 1 / (near - far);
97
+
98
+ return new Float64Array([
99
+ -2 * lr,
100
+ 0,
101
+ 0,
102
+ 0,
103
+ 0,
104
+ -2 * bt,
105
+ 0,
106
+ 0,
107
+ 0,
108
+ 0,
109
+ 2 * nf,
110
+ 0,
111
+ (left + right) * lr,
112
+ (top + bottom) * bt,
113
+ (far + near) * nf,
114
+ 1,
115
+ ]);
116
+ }
117
+
118
+ private calcProjectionMatrix(transform: any, fov: number, nearZ: number, farZ: number): Matrix4 {
119
+ const offset = transform.centerOffset;
120
+ const aspect = transform.width / transform.height;
121
+
122
+ const perspective = this.buildPerspectiveMatrix(fov, aspect, nearZ, farZ);
123
+ perspective[8] = (-offset.x * 2) / transform.width;
124
+ perspective[9] = (offset.y * 2) / transform.height;
125
+
126
+ if (!transform.isOrthographic) {
127
+ return new Matrix4().fromArray(perspective);
128
+ }
129
+
130
+ const cameraToCenterDistance = (0.5 * transform.height) / Math.tan(fov / 2.0);
131
+ const halfHeight = cameraToCenterDistance * Math.tan(fov * 0.5);
132
+ const halfWidth = halfHeight * aspect;
133
+
134
+ const ortho = this.buildOrthographicMatrix(
135
+ -halfWidth - offset.x,
136
+ halfWidth - offset.x,
137
+ -halfHeight + offset.y,
138
+ halfHeight + offset.y,
139
+ nearZ,
140
+ farZ
141
+ );
142
+
143
+ const transitionPitch = 15;
144
+ const t = Math.min(transform.pitch / transitionPitch, 1.0);
145
+ const eased = t * t * t * t * t;
146
+
147
+ const blended = new Float64Array(16);
148
+ for (let i = 0; i < 16; i++) {
149
+ blended[i] = (1 - eased) * ortho[i] + eased * perspective[i];
150
+ }
151
+
152
+ return new Matrix4().fromArray(blended);
153
+ }
154
+
155
+ updateCameraForRender(
156
+ camera: PerspectiveCamera,
157
+ map: MaplibreMap,
158
+ matrix: any,
159
+ worldMatrix: Matrix4,
160
+ worldMatrixInv: Matrix4
161
+ ): void {
162
+ const transform = map.transform;
163
+
164
+ const { fov, nearZ, farZ, aspect } = this.extractCameraParams(matrix, transform);
165
+
166
+ camera.fov = ThreejsUtils.radToDeg(fov);
167
+ camera.aspect = aspect;
168
+ camera.near = nearZ;
169
+ camera.far = farZ;
170
+
171
+ const cleanProjection = this.buildPerspectiveMatrix(fov, aspect, nearZ, farZ);
172
+ (camera as any)._cleanProjectionMatrix = cleanProjection;
173
+
174
+ const mvpMatrix = this.extractMVPMatrix(matrix, worldMatrix);
175
+ const projectionMatrix = this.calcProjectionMatrix(transform, fov, nearZ, farZ);
176
+
177
+ camera.projectionMatrix.copy(projectionMatrix);
178
+ camera.projectionMatrixInverse.copy(projectionMatrix).invert();
179
+
180
+ const viewMatrix = new Matrix4().multiplyMatrices(camera.projectionMatrixInverse, mvpMatrix);
181
+
182
+ camera.matrixWorld.copy(viewMatrix).invert();
183
+ camera.matrixWorldInverse.copy(viewMatrix);
184
+ camera.matrixAutoUpdate = false;
185
+ camera.matrixWorldAutoUpdate = false;
186
+
187
+ this.updateCameraTransform(camera);
188
+ }
189
+
190
+ private extractCameraParams(matrix: any, transform: any) {
191
+ const aspect = transform.width / transform.height;
192
+
193
+ if (matrix.fov !== undefined) {
194
+ return {
195
+ fov: matrix.fov,
196
+ nearZ: matrix.nearZ,
197
+ farZ: matrix.farZ,
198
+ aspect,
199
+ };
200
+ }
201
+
202
+ const cameraToCenterDistance = transform.cameraToCenterDistance;
203
+ const fov =
204
+ cameraToCenterDistance && transform.height
205
+ ? 2 * Math.atan(transform.height / 2 / cameraToCenterDistance)
206
+ : 0.6435;
207
+
208
+ return {
209
+ fov,
210
+ nearZ: transform.nearZ || 0.1,
211
+ farZ: transform.farZ || 10000,
212
+ aspect,
213
+ };
214
+ }
215
+
216
+ private extractMVPMatrix(matrix: any, worldMatrix: Matrix4): Matrix4 {
217
+ let baseMatrix: Matrix4;
218
+
219
+ if (matrix.defaultProjectionData?.mainMatrix) {
220
+ baseMatrix = new Matrix4().fromArray(Object.values(matrix.defaultProjectionData.mainMatrix));
221
+ } else if (matrix.modelViewProjectionMatrix) {
222
+ baseMatrix = new Matrix4().fromArray(matrix.modelViewProjectionMatrix);
223
+ } else {
224
+ const arr = Array.isArray(matrix) ? matrix : Array.from(matrix);
225
+ baseMatrix = new Matrix4().fromArray(arr);
226
+ }
227
+
228
+ return new Matrix4().multiplyMatrices(baseMatrix, worldMatrix);
229
+ }
230
+
231
+ private updateCameraTransform(camera: PerspectiveCamera): void {
232
+ const position = new Vector3();
233
+ const quaternion = new Quaternion();
234
+ const scale = new Vector3();
235
+
236
+ camera.matrixWorld.decompose(position, quaternion, scale);
237
+ camera.position.copy(position);
238
+ camera.rotation.copy(new Euler().setFromQuaternion(quaternion, 'YXZ'));
239
+ }
240
+
241
+ createEnvTexture(envTexture: string, scene: Scene): void {
242
+ if (!envTexture?.endsWith('.hdr')) return;
243
+
244
+ new HDRLoader().load(envTexture, (texture) => {
245
+ texture.mapping = EquirectangularReflectionMapping;
246
+ scene.environment = texture;
247
+ scene.environmentRotation.x = Math.PI / 2;
248
+ });
249
+ }
250
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Derived from mapbox-3d-tiles by Jianshun Yang (MIT License)
3
+ * https://github.com/yangjs6/mapbox-3d-tiles
4
+ */
5
+
6
+ import { type Map as MaplibreMap } from 'maplibre-gl';
7
+ import { WebGLRenderer, Scene, Camera } from 'three';
8
+ import { CSS2DRenderer } from 'three/examples/jsm/renderers/CSS2DRenderer.js';
9
+
10
+ export default class ThreejsSceneRenderer {
11
+ private renderer: WebGLRenderer;
12
+ private labelRenderer: CSS2DRenderer;
13
+
14
+ /**
15
+ * Creates a ThreejsSceneRenderer instance.
16
+ *
17
+ * @param map - The MapLibre map instance
18
+ * @param gl - The WebGL2 rendering context from MapLibre
19
+ */
20
+ constructor(map: MaplibreMap, gl: WebGL2RenderingContext) {
21
+ if (!gl || gl.isContextLost()) {
22
+ throw new Error('WebGL context is lost or invalid');
23
+ }
24
+
25
+ this.renderer = new WebGLRenderer({
26
+ alpha: true,
27
+ antialias: true,
28
+ canvas: map.getCanvas(),
29
+ context: gl,
30
+ });
31
+ this.renderer.autoClear = false;
32
+
33
+ this.renderer.shadowMap.enabled = true;
34
+ this.labelRenderer = this.createLabelRenderer(map);
35
+ }
36
+
37
+ private createLabelRenderer(map: MaplibreMap): CSS2DRenderer {
38
+ const container = map.getContainer();
39
+ const labelRenderer = new CSS2DRenderer();
40
+
41
+ labelRenderer.setSize(container.clientWidth, container.clientHeight);
42
+ labelRenderer.domElement.style.position = 'absolute';
43
+ labelRenderer.domElement.style.top = '0px';
44
+ labelRenderer.domElement.style.pointerEvents = 'none';
45
+
46
+ map._container.appendChild(labelRenderer.domElement);
47
+ map.on('resize', () => {
48
+ const { clientWidth, clientHeight } = map.getContainer();
49
+ labelRenderer.setSize(clientWidth, clientHeight);
50
+ });
51
+
52
+ return labelRenderer;
53
+ }
54
+
55
+ getRenderer(): WebGLRenderer {
56
+ return this.renderer;
57
+ }
58
+
59
+ render(scene: Scene, camera: Camera): void {
60
+ // Reset WebGL state to avoid conflicts with MapLibre
61
+ // but DO NOT clear the depth buffer - we want to preserve MapLibre's depth
62
+ // information so Three.js objects can be properly occluded by MapLibre 3D
63
+ // content (fill-extrusion buildings, terrain, etc.) and vice versa.
64
+ this.renderer.resetState();
65
+ this.renderer.render(scene, camera);
66
+ this.labelRenderer.render(scene, camera);
67
+ }
68
+
69
+ dispose(): void {
70
+ this.labelRenderer.domElement?.parentNode?.removeChild(this.labelRenderer.domElement);
71
+ this.renderer?.dispose();
72
+ }
73
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Derived from mapbox-3d-tiles by Jianshun Yang (MIT License)
3
+ * https://github.com/yangjs6/mapbox-3d-tiles
4
+ */
5
+
6
+ import { type Map as MaplibreMap, MercatorCoordinate, LngLatLike } from 'maplibre-gl';
7
+ import { Vector3, Quaternion, Matrix4 } from 'three';
8
+
9
+ type Position = number[];
10
+
11
+ const DEG_TO_RAD = Math.PI / 180;
12
+ const RAD_TO_DEG = 180 / Math.PI;
13
+
14
+ export default class ThreejsUtils {
15
+ static updateWorldMatrix(map: MaplibreMap | null, refCenter: LngLatLike | null = null): Matrix4 {
16
+ if (!map) return new Matrix4();
17
+
18
+ const center = refCenter ?? map.getCenter();
19
+ const origin = MercatorCoordinate.fromLngLat(center);
20
+ const scale = origin.meterInMercatorCoordinateUnits();
21
+
22
+ return new Matrix4().compose(
23
+ new Vector3(origin.x, origin.y, origin.z),
24
+ new Quaternion(),
25
+ new Vector3(scale, -scale, scale)
26
+ );
27
+ }
28
+
29
+ static toScenePositionMercator(worldMatrixInv: Matrix4, mercator: MercatorCoordinate): Vector3 {
30
+ return new Vector3(mercator.x, mercator.y, mercator.z).applyMatrix4(worldMatrixInv);
31
+ }
32
+
33
+ static toMapPositionMercator(worldMatrix: Matrix4, position: Vector3): MercatorCoordinate {
34
+ const transformed = position.clone().applyMatrix4(worldMatrix);
35
+ return new MercatorCoordinate(transformed.x, transformed.y, transformed.z);
36
+ }
37
+
38
+ static toScenePosition(
39
+ worldMatrixInv: Matrix4,
40
+ position: LngLatLike,
41
+ altitude?: number
42
+ ): Vector3 {
43
+ return this.toScenePositionMercator(
44
+ worldMatrixInv,
45
+ MercatorCoordinate.fromLngLat(position, altitude)
46
+ );
47
+ }
48
+
49
+ static toMapPosition(worldMatrix: Matrix4, position: Vector3): Position {
50
+ const mercator = this.toMapPositionMercator(worldMatrix, position);
51
+ const lngLat = mercator.toLngLat();
52
+ return [lngLat.lng, lngLat.lat, mercator.toAltitude()];
53
+ }
54
+
55
+ static degToRad(degrees: number): number {
56
+ return degrees * DEG_TO_RAD;
57
+ }
58
+
59
+ static radToDeg(radians: number): number {
60
+ return radians * RAD_TO_DEG;
61
+ }
62
+ }