@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
package/.babelrc ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "presets": [
3
+ [
4
+ "@nx/react/babel",
5
+ {
6
+ "runtime": "automatic",
7
+ "useBuiltIns": "usage"
8
+ }
9
+ ]
10
+ ],
11
+ "plugins": []
12
+ }
@@ -0,0 +1,20 @@
1
+ import type { StorybookConfig } from '@storybook/react-vite';
2
+
3
+ const config: StorybookConfig = {
4
+ stories: ['../src/**/*.@(mdx|stories.@(js|jsx|ts|tsx))'],
5
+ addons: [],
6
+ framework: {
7
+ name: '@storybook/react-vite',
8
+ options: {
9
+ builder: {
10
+ viteConfigPath: 'vite.config.ts',
11
+ },
12
+ },
13
+ },
14
+ };
15
+
16
+ export default config;
17
+
18
+ // To customize your Vite configuration you can use the viteFinal field.
19
+ // Check https://storybook.js.org/docs/react/builders/vite#configuration
20
+ // and https://nx.dev/recipes/storybook/custom-builder-configs
File without changes
package/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # @mapcomponents/three
2
+
3
+ This library provides React components to easily integrate [Three.js](https://threejs.org/) 3D content into [MapLibre GL JS](https://maplibre.org/) maps using [@mapcomponents/react-maplibre](https://github.com/mapcomponents/react-map-components-maplibre).
4
+
5
+ ## Installation
6
+
7
+ Install the package and its peer dependencies:
8
+
9
+ ```bash
10
+ npm install @mapcomponents/three @mapcomponents/react-maplibre
11
+ ```
12
+
13
+ ## Getting Started
14
+
15
+ To use `@mapcomponents/three`, you need to wrap your 3D layers with the `ThreeProvider` component. This provider initializes the Three.js scene, camera, and renderer, and registers a custom layer within the MapLibre map.
16
+
17
+ ### Basic Usage
18
+
19
+ Here is a simple example of how to render a 3D model on a map:
20
+
21
+ ```tsx
22
+ import React from 'react';
23
+ import { MapComponentsProvider, MapLibreMap } from '@mapcomponents/react-maplibre';
24
+ import { ThreeProvider, MlThreeModelLayer } from '@mapcomponents/three';
25
+
26
+ const App = () => {
27
+ return (
28
+ <MapComponentsProvider>
29
+ <ThreeProvider id="three-scene-1">
30
+ <MapLibreMap
31
+ options={{
32
+ style: 'https://demotiles.maplibre.org/style.json',
33
+ center: [13.404954, 52.520008],
34
+ zoom: 15,
35
+ pitch: 60
36
+ }}
37
+ />
38
+
39
+ <MlThreeModelLayer
40
+ url="https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/master/2.0/Duck/glTF-Binary/Duck.glb"
41
+ mapPosition={[13.404954, 52.520008]}
42
+ scale={10}
43
+ />
44
+ </ThreeProvider>
45
+ </MapComponentsProvider>
46
+ );
47
+ };
48
+
49
+ export default App;
50
+ ```
51
+
52
+ ## Running unit tests
53
+
54
+ Run `nx test @mapcomponents/three` to execute the unit tests via [Vitest](https://vitest.dev/).
@@ -0,0 +1,13 @@
1
+ import { nxComponentTestingPreset } from '@nx/react/plugins/component-testing';
2
+ import { defineConfig } from 'cypress';
3
+
4
+ export default defineConfig({
5
+ component: {
6
+ ...nxComponentTestingPreset(__filename, {
7
+ bundler: 'vite',
8
+ buildTarget: '@mapcomponents/three:build',
9
+ }),
10
+ supportFile: 'src/cypress/support/component.ts',
11
+ indexHtmlFile: 'src/cypress/support/component-index.html',
12
+ },
13
+ });
@@ -0,0 +1,12 @@
1
+ import nx from '@nx/eslint-plugin';
2
+ import baseConfig from '../../eslint.config.mjs';
3
+
4
+ export default [
5
+ ...baseConfig,
6
+ ...nx.configs['flat/react'],
7
+ {
8
+ files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
9
+ // Override or add rules here
10
+ rules: {},
11
+ },
12
+ ];
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@mapcomponents/three",
3
+ "version": "1.7.2",
4
+ "main": "./index.js",
5
+ "types": "./index.d.ts",
6
+ "exports": {
7
+ ".": {
8
+ "import": "./index.mjs",
9
+ "require": "./index.js"
10
+ }
11
+ },
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "dependencies": {
16
+ "@mui/material": "^7.3.2",
17
+ "maplibre-gl": "^5.7.0",
18
+ "three": "^0.182.0",
19
+ "@mapcomponents/react-maplibre": "1.7.2"
20
+ },
21
+ "devDependencies": {
22
+ "@types/three": "^0.182.0"
23
+ }
24
+ }
package/project.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "@mapcomponents/three",
3
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
4
+ "sourceRoot": "packages/three/src",
5
+ "projectType": "library",
6
+ "tags": [],
7
+ "// targets": "to see all targets run: nx show project @mapcomponents/three --web",
8
+ "targets": {
9
+ "storybook": {
10
+ "options": {
11
+ "port": 4403
12
+ }
13
+ }
14
+ }
15
+ }
@@ -0,0 +1,63 @@
1
+ import React, { useEffect } from 'react';
2
+ import { MapComponentsProvider, MapLibreMap, useMap } from '@mapcomponents/react-maplibre';
3
+ import { ThreeProvider } from '../ThreeProvider';
4
+ import MlThreeModelLayer from './MlThreeModelLayer';
5
+
6
+ const MapExposer = () => {
7
+ const { map } = useMap({ mapId: 'map_1' });
8
+ useEffect(() => {
9
+ if (map) {
10
+ (window as any)._map = map;
11
+ }
12
+ }, [map]);
13
+ return null;
14
+ };
15
+
16
+ const TestComponent = ({ onDone }: { onDone: () => void }) => {
17
+ return (
18
+ <MapComponentsProvider>
19
+ <MapExposer />
20
+ <ThreeProvider id="three-provider" mapId="map_1">
21
+ <MlThreeModelLayer
22
+ url="assets/3D/godzilla_simple.glb"
23
+ mapPosition={[13.404954, 52.520008]}
24
+ scale={10}
25
+ onDone={onDone}
26
+ />
27
+ </ThreeProvider>
28
+ <MapLibreMap
29
+ options={{
30
+ style: { version: 8, sources: {}, layers: [] },
31
+ center: [13.404954, 52.520008],
32
+ zoom: 15,
33
+ pitch: 60,
34
+ }}
35
+ mapId="map_1"
36
+ />
37
+ </MapComponentsProvider>
38
+ );
39
+ };
40
+
41
+ describe('<MlThreeModelLayer />', () => {
42
+ it('renders', () => {
43
+ const onDoneSpy = cy.spy().as('onDoneSpy');
44
+ cy.mount(<TestComponent onDone={onDoneSpy} />);
45
+
46
+ // Wait for map to load
47
+ cy.get('.maplibregl-canvas').should('exist');
48
+
49
+ // Wait for the model to load first (this confirms map and three provider are working)
50
+ cy.get('@onDoneSpy', { timeout: 15000 }).should('have.been.called');
51
+
52
+ // Check if map instance is available and has the custom layer
53
+ cy.window().should('have.property', '_map');
54
+ cy.window().then((win: any) => {
55
+ const map = win._map;
56
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
57
+ expect(map).to.exist;
58
+ // Check for the layer added by ThreeProvider
59
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
60
+ expect(map.getLayer('three-provider')).to.exist;
61
+ });
62
+ });
63
+ });
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "MlThreeModelLayer",
3
+ "title": "3D Model",
4
+ "description": "Layer Component, that makes it possible to show 3D Models on the map.",
5
+ "i18n": {
6
+ "de": {
7
+ "title": "3D Modelle",
8
+ "description": "Layer Component, das es ermöglicht 3D Modelle auf der Karte darzustellen."
9
+ }
10
+ },
11
+ "tags": ["Map layer"],
12
+ "category": "layer",
13
+ "type": "component",
14
+ "demos": [
15
+ {
16
+ "name": "Demo",
17
+ "url": "https://mapcomponents.github.io/react-map-components-maplibre/three/iframe.html?id=mapcomponents-mlthreemodellayer--example-config&viewMode=story"
18
+ }
19
+ ],
20
+ "thumbnail": "https://mapcomponents.github.io/react-map-components-maplibre/three/thumbnails/MlThreeModelLayer.png"
21
+ }
@@ -0,0 +1,161 @@
1
+ import { useRef, useState, useEffect } from 'react';
2
+ import Button from '@mui/material/Button';
3
+ import MlThreeModelLayer from './MlThreeModelLayer';
4
+ import { useMap, TopToolbar, Sidebar } from '@mapcomponents/react-maplibre';
5
+ import ThreejsContextDecorator from '../../decorators/ThreejsContextDecorator';
6
+ import { useThree } from '../ThreeContext';
7
+ import { ThreeObjectControls } from '../ThreeObjectControls';
8
+ import ThreejsUtils from '../../lib/ThreejsUtils';
9
+ import * as THREE from 'three';
10
+
11
+ const storyoptions = {
12
+ title: 'MapComponents/MlThreeModelLayer',
13
+ component: MlThreeModelLayer,
14
+ argTypes: {
15
+ options: {
16
+ control: {
17
+ type: 'object',
18
+ },
19
+ },
20
+ },
21
+ decorators: ThreejsContextDecorator,
22
+ };
23
+ export default storyoptions;
24
+
25
+ const Lights = () => {
26
+ const { scene } = useThree();
27
+ const lightsRef = useRef<THREE.Light[]>([]);
28
+
29
+ useEffect(() => {
30
+ if (!scene) return;
31
+
32
+ const directionalLight = new THREE.DirectionalLight(0xffffff);
33
+ directionalLight.position.set(0, -70, 100).normalize();
34
+ scene.add(directionalLight);
35
+
36
+ const directionalLight2 = new THREE.DirectionalLight(0xff2255);
37
+ directionalLight2.position.set(0, 70, 100).normalize();
38
+ scene.add(directionalLight2);
39
+
40
+ lightsRef.current = [directionalLight, directionalLight2];
41
+
42
+ return () => {
43
+ lightsRef.current.forEach((light) => scene.remove(light));
44
+ };
45
+ }, [scene]);
46
+
47
+ return null;
48
+ };
49
+
50
+ const Template: any = () => {
51
+ const { worldMatrix } = useThree();
52
+ const [showLayer, setShowLayer] = useState(true);
53
+ const [scale, setScale] = useState(1);
54
+ const [rotation, setRotation] = useState({ x: 90, y: 90, z: 0 });
55
+ const [useMapCoords, setUseMapCoords] = useState(true);
56
+ const [mapPosition, setMapPosition] = useState({ lng: 7.097, lat: 50.7355 });
57
+ const [altitude, setAltitude] = useState(0);
58
+ const [position, setPosition] = useState({ x: 0, y: 0, z: 0 });
59
+ const [enableTransformControls, setEnableTransformControls] = useState(false);
60
+ const [transformMode, setTransformMode] = useState<'translate' | 'rotate' | 'scale'>('translate');
61
+ const [sidebarOpen, setSidebarOpen] = useState(true);
62
+
63
+ const mapHook = useMap({ mapId: 'map_1' });
64
+ useEffect(() => {
65
+ if (!mapHook.map) return;
66
+ mapHook.map?.setZoom(15.5);
67
+ mapHook.map?.setPitch(44.5);
68
+ mapHook.map?.setCenter([7.097, 50.7355]);
69
+ }, [mapHook.map]);
70
+
71
+ // Center map on position when switching coordinate modes
72
+ useEffect(() => {
73
+ if (!mapHook.map) return;
74
+ if (useMapCoords) {
75
+ mapHook.map.setCenter([mapPosition.lng, mapPosition.lat]);
76
+ }
77
+ // eslint-disable-next-line react-hooks/exhaustive-deps
78
+ }, [useMapCoords, mapHook.map]);
79
+
80
+ const handleTransformChange = (object: THREE.Object3D) => {
81
+ setRotation({
82
+ x: (object.rotation.x * 180) / Math.PI,
83
+ y: (object.rotation.y * 180) / Math.PI,
84
+ z: (object.rotation.z * 180) / Math.PI,
85
+ });
86
+ setScale(object.scale.x);
87
+
88
+ if (useMapCoords && worldMatrix) {
89
+ const [lng, lat, alt] = ThreejsUtils.toMapPosition(worldMatrix, object.position);
90
+ setMapPosition({ lng, lat });
91
+ setAltitude(alt);
92
+ } else {
93
+ setPosition({ x: object.position.x, y: object.position.y, z: object.position.z });
94
+ }
95
+ };
96
+
97
+ return (
98
+ <>
99
+ <Lights />
100
+ {showLayer && (
101
+ <MlThreeModelLayer
102
+ url="assets/3D/godzilla_simple.glb"
103
+ rotation={{
104
+ x: (rotation.x * Math.PI) / 180,
105
+ y: (rotation.y * Math.PI) / 180,
106
+ z: (rotation.z * Math.PI) / 180,
107
+ }}
108
+ scale={scale}
109
+ enableTransformControls={enableTransformControls}
110
+ transformMode={transformMode}
111
+ onTransformChange={handleTransformChange}
112
+ {...(useMapCoords
113
+ ? {
114
+ mapPosition: [mapPosition.lng, mapPosition.lat],
115
+ altitude: altitude,
116
+ }
117
+ : {
118
+ position: position,
119
+ })}
120
+ />
121
+ )}
122
+
123
+ <TopToolbar
124
+ unmovableButtons={
125
+ <Button
126
+ variant={sidebarOpen ? 'contained' : 'outlined'}
127
+ onClick={() => setSidebarOpen(!sidebarOpen)}
128
+ >
129
+ Sidebar
130
+ </Button>
131
+ }
132
+ />
133
+ <Sidebar open={sidebarOpen} setOpen={setSidebarOpen} name="3D Model Config">
134
+ <ThreeObjectControls
135
+ showLayer={showLayer}
136
+ setShowLayer={setShowLayer}
137
+ scale={scale}
138
+ setScale={setScale}
139
+ rotation={rotation}
140
+ setRotation={setRotation}
141
+ useMapCoords={useMapCoords}
142
+ setUseMapCoords={setUseMapCoords}
143
+ mapPosition={mapPosition}
144
+ setMapPosition={setMapPosition}
145
+ altitude={altitude}
146
+ setAltitude={setAltitude}
147
+ position={position}
148
+ setPosition={setPosition}
149
+ enableTransformControls={enableTransformControls}
150
+ setEnableTransformControls={setEnableTransformControls}
151
+ transformMode={transformMode}
152
+ setTransformMode={setTransformMode}
153
+ layerName="Model"
154
+ />
155
+ </Sidebar>
156
+ </>
157
+ );
158
+ };
159
+
160
+ export const ExampleConfig = Template.bind({});
161
+ ExampleConfig.parameters = {};
@@ -0,0 +1,153 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import * as THREE from 'three';
3
+ import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
4
+ import { OBJLoader } from 'three/addons/loaders/OBJLoader.js';
5
+ import { LngLatLike } from 'maplibre-gl';
6
+ import { useThree } from '../ThreeContext';
7
+ import ThreejsUtils from '../../lib/ThreejsUtils';
8
+ import MlTransformControls from '../MlTransformControls';
9
+
10
+ /**
11
+ * Renders obj or gltf 3D Models on the MapLibreMap referenced by props.mapId
12
+ *
13
+ * @component
14
+ */
15
+
16
+ export interface MlThreeModelLayerProps {
17
+ mapId?: string;
18
+ url: string;
19
+ position?: { x: number; y: number; z: number };
20
+ mapPosition?: LngLatLike;
21
+ altitude?: number;
22
+ rotation?: { x: number; y: number; z: number };
23
+ scale?: { x: number; y: number; z: number } | number;
24
+ enableTransformControls?: boolean;
25
+ transformMode?: 'translate' | 'rotate' | 'scale';
26
+ onTransformChange?: (object: THREE.Object3D) => void;
27
+ init?: () => void;
28
+ onDone?: () => void;
29
+ }
30
+
31
+ const MlThreeModelLayer = (props: MlThreeModelLayerProps) => {
32
+ const {
33
+ url,
34
+ position,
35
+ mapPosition,
36
+ altitude,
37
+ rotation,
38
+ scale,
39
+ enableTransformControls,
40
+ transformMode,
41
+ onTransformChange,
42
+ init,
43
+ onDone,
44
+ } = props;
45
+ const { scene, worldMatrixInv } = useThree();
46
+ const modelRef = useRef<THREE.Object3D | undefined>(undefined);
47
+ const [model, setModel] = useState<THREE.Object3D | undefined>(undefined);
48
+
49
+ // Use refs for callbacks to avoid re-triggering the effect when they change
50
+ const initRef = useRef(init);
51
+ const onDoneRef = useRef(onDone);
52
+ initRef.current = init;
53
+ onDoneRef.current = onDone;
54
+
55
+ const transformRef = useRef({ position, mapPosition, altitude, rotation, scale });
56
+ transformRef.current = { position, mapPosition, altitude, rotation, scale };
57
+ const worldMatrixInvRef = useRef(worldMatrixInv);
58
+ worldMatrixInvRef.current = worldMatrixInv;
59
+
60
+ useEffect(() => {
61
+ if (!scene) return;
62
+
63
+ if (typeof initRef.current === 'function') {
64
+ initRef.current();
65
+ }
66
+
67
+ const extension = url.split('.').pop()?.toLowerCase();
68
+
69
+ const onLoad = (object: THREE.Object3D) => {
70
+ const { position, mapPosition, altitude, rotation, scale } = transformRef.current;
71
+ const worldMatrixInv = worldMatrixInvRef.current;
72
+
73
+ if (mapPosition && worldMatrixInv) {
74
+ const scenePos = ThreejsUtils.toScenePosition(worldMatrixInv, mapPosition, altitude);
75
+ object.position.set(scenePos.x, scenePos.y, scenePos.z);
76
+ } else if (position) {
77
+ object.position.set(position.x, position.y, position.z);
78
+ }
79
+
80
+ if (rotation) {
81
+ object.rotation.set(rotation.x, rotation.y, rotation.z);
82
+ }
83
+ if (scale) {
84
+ if (typeof scale === 'number') {
85
+ object.scale.set(scale, scale, scale);
86
+ } else {
87
+ object.scale.set(scale.x, scale.y, scale.z);
88
+ }
89
+ }
90
+
91
+ modelRef.current = object;
92
+ scene.add(object);
93
+ setModel(object);
94
+ if (typeof onDoneRef.current === 'function') {
95
+ onDoneRef.current();
96
+ }
97
+ };
98
+
99
+ if (extension === 'glb' || extension === 'gltf') {
100
+ const loader = new GLTFLoader();
101
+ loader.load(url, (gltf) => {
102
+ onLoad(gltf.scene);
103
+ });
104
+ } else if (extension === 'obj') {
105
+ const loader = new OBJLoader();
106
+ loader.load(url, (obj) => {
107
+ onLoad(obj);
108
+ });
109
+ } else {
110
+ console.warn('MlThreeModelLayer: Unsupported file extension', extension);
111
+ }
112
+
113
+ return () => {
114
+ if (modelRef.current) {
115
+ scene.remove(modelRef.current);
116
+ modelRef.current = undefined;
117
+ setModel(undefined);
118
+ }
119
+ };
120
+ }, [scene, url]);
121
+
122
+ useEffect(() => {
123
+ if (!model) return;
124
+
125
+ // Handle position: mapPosition takes precedence over position
126
+ if (mapPosition && worldMatrixInv) {
127
+ const scenePos = ThreejsUtils.toScenePosition(worldMatrixInv, mapPosition, altitude);
128
+ model.position.set(scenePos.x, scenePos.y, scenePos.z);
129
+ } else if (position) {
130
+ model.position.set(position.x, position.y, position.z);
131
+ }
132
+
133
+ if (rotation) {
134
+ model.rotation.set(rotation.x, rotation.y, rotation.z);
135
+ }
136
+ if (scale) {
137
+ if (typeof scale === 'number') {
138
+ model.scale.set(scale, scale, scale);
139
+ } else {
140
+ model.scale.set(scale.x, scale.y, scale.z);
141
+ }
142
+ }
143
+ }, [model, position, mapPosition, altitude, rotation, scale, worldMatrixInv]);
144
+
145
+ if (enableTransformControls && model) {
146
+ return (
147
+ <MlTransformControls target={model} mode={transformMode} onObjectChange={onTransformChange} />
148
+ );
149
+ }
150
+ return null;
151
+ };
152
+
153
+ export default MlThreeModelLayer;
@@ -0,0 +1,62 @@
1
+ import React, { useEffect } from 'react';
2
+ import { MapComponentsProvider, MapLibreMap, useMap } from '@mapcomponents/react-maplibre';
3
+ import { ThreeProvider } from '../ThreeProvider';
4
+ import MlThreeSplatLayer from './MlThreeSplatLayer';
5
+
6
+ const MapExposer = () => {
7
+ const { map } = useMap({ mapId: 'map_1' });
8
+ useEffect(() => {
9
+ if (map) {
10
+ (window as any)._map = map;
11
+ }
12
+ }, [map]);
13
+ return null;
14
+ };
15
+
16
+ const TestComponent = ({ onDone }: { onDone: () => void }) => {
17
+ return (
18
+ <MapComponentsProvider>
19
+ <MapExposer />
20
+ <ThreeProvider id="three-provider" mapId="map_1">
21
+ <MlThreeSplatLayer
22
+ url="assets/splats/output.splat"
23
+ mapPosition={[13.404954, 52.520008]}
24
+ scale={1}
25
+ onDone={onDone}
26
+ />
27
+ </ThreeProvider>
28
+ <MapLibreMap
29
+ options={{
30
+ style: { version: 8, sources: {}, layers: [] },
31
+ center: [13.404954, 52.520008],
32
+ zoom: 15,
33
+ pitch: 60,
34
+ }}
35
+ mapId="map_1"
36
+ />
37
+ </MapComponentsProvider>
38
+ );
39
+ };
40
+
41
+ describe('<MlThreeSplatLayer />', () => {
42
+ it('renders', () => {
43
+ const onDoneSpy = cy.spy().as('onDoneSpy');
44
+ cy.mount(<TestComponent onDone={onDoneSpy} />);
45
+
46
+ // Wait for map to load
47
+ cy.get('.maplibregl-canvas').should('exist');
48
+
49
+ // Wait for the splat to load first
50
+ cy.get('@onDoneSpy', { timeout: 15000 }).should('have.been.called');
51
+
52
+ // Check if map instance is available and has the custom layer
53
+ cy.window().should('have.property', '_map');
54
+ cy.window().then((win: any) => {
55
+ const map = win._map;
56
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
57
+ expect(map).to.exist;
58
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
59
+ expect(map.getLayer('three-provider')).to.exist;
60
+ });
61
+ });
62
+ });
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "MlThreeSplatLayer",
3
+ "title": "3D Splat Model",
4
+ "description": "Layer Component, that makes it possible to show 3D Gaussian Splatting Models on the map.",
5
+ "i18n": {
6
+ "de": {
7
+ "title": "3D Splat Modelle",
8
+ "description": "Layer Component, das es ermöglicht 3D Gaussian Splatting Modelle auf der Karte darzustellen."
9
+ }
10
+ },
11
+ "tags": ["Map layer", "3D", "Splat"],
12
+ "category": "layer",
13
+ "type": "component",
14
+ "demos": [
15
+ {
16
+ "name": "Demo",
17
+ "url": "https://mapcomponents.github.io/react-map-components-maplibre/three/iframe.html?id=mapcomponents-mlthreesplatlayer--default&viewMode=story"
18
+ }
19
+ ],
20
+ "thumbnail": "https://mapcomponents.github.io/react-map-components-maplibre/three/thumbnails/MlThreeSplatLayer.png"
21
+ }