@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,151 @@
1
+ import { useState, useEffect } from 'react';
2
+ import Button from '@mui/material/Button';
3
+ import Typography from '@mui/material/Typography';
4
+ import Link from '@mui/material/Link';
5
+ import MlThreeSplatLayer from './MlThreeSplatLayer';
6
+ import { useMap, TopToolbar, Sidebar } from '@mapcomponents/react-maplibre';
7
+ import MlThreeJsContextDecorator from '../../decorators/ThreejsContextDecorator';
8
+ import { ThreeObjectControls } from '../ThreeObjectControls';
9
+ import { useThree } from '../ThreeContext';
10
+ import ThreejsUtils from '../../lib/ThreejsUtils';
11
+ import * as THREE from 'three';
12
+
13
+ const storyoptions = {
14
+ title: 'MapComponents/MlThreeSplatLayer',
15
+ component: MlThreeSplatLayer,
16
+ argTypes: {
17
+ options: {
18
+ control: {
19
+ type: 'object',
20
+ },
21
+ },
22
+ },
23
+ decorators: MlThreeJsContextDecorator,
24
+ };
25
+ export default storyoptions;
26
+
27
+ const Template: any = () => {
28
+ const { worldMatrix } = useThree();
29
+ const [showLayer, setShowLayer] = useState(true);
30
+ const [scale, setScale] = useState(100);
31
+ const [rotation, setRotation] = useState({ x: 270, y: 0, z: 5 });
32
+ const [useMapCoords, setUseMapCoords] = useState(true);
33
+ const [mapPosition, setMapPosition] = useState({ lng: 7.0968, lat: 50.736 });
34
+ const [altitude, setAltitude] = useState(30);
35
+ const [position, setPosition] = useState({ x: 0, y: 0, z: 100 });
36
+ const [enableTransformControls, setEnableTransformControls] = useState(false);
37
+ const [transformMode, setTransformMode] = useState<'translate' | 'rotate' | 'scale'>('translate');
38
+ const [sidebarOpen, setSidebarOpen] = useState(true);
39
+
40
+ const mapHook = useMap({ mapId: 'map_1' });
41
+ useEffect(() => {
42
+ if (!mapHook.map) return;
43
+ mapHook.map?.setZoom(17.5);
44
+ mapHook.map?.setPitch(44.5);
45
+ mapHook.map?.setCenter([7.096614581535903, 50.736500960686556]);
46
+ }, [mapHook.map]);
47
+
48
+ // Center map on position when switching coordinate modes
49
+ useEffect(() => {
50
+ if (!mapHook.map) return;
51
+ if (useMapCoords) {
52
+ mapHook.map.setCenter([mapPosition.lng, mapPosition.lat]);
53
+ }
54
+ // eslint-disable-next-line react-hooks/exhaustive-deps
55
+ }, [useMapCoords, mapHook.map]);
56
+
57
+ const handleTransformChange = (object: THREE.Object3D) => {
58
+ setRotation({
59
+ x: (object.rotation.x * 180) / Math.PI,
60
+ y: (object.rotation.y * 180) / Math.PI,
61
+ z: (object.rotation.z * 180) / Math.PI,
62
+ });
63
+ setScale(object.scale.x);
64
+
65
+ if (useMapCoords && worldMatrix) {
66
+ const [lng, lat, alt] = ThreejsUtils.toMapPosition(worldMatrix, object.position);
67
+ setMapPosition({ lng, lat });
68
+ setAltitude(parseFloat(alt.toFixed(2)));
69
+ } else {
70
+ setPosition({ x: object.position.x, y: object.position.y, z: object.position.z });
71
+ }
72
+ };
73
+
74
+ return (
75
+ <>
76
+ {showLayer && (
77
+ <MlThreeSplatLayer
78
+ url="assets/splats/output.splat"
79
+ rotation={{
80
+ x: (rotation.x * Math.PI) / 180,
81
+ y: (rotation.y * Math.PI) / 180,
82
+ z: (rotation.z * Math.PI) / 180,
83
+ }}
84
+ scale={scale}
85
+ enableTransformControls={enableTransformControls}
86
+ transformMode={transformMode}
87
+ onTransformChange={handleTransformChange}
88
+ {...(useMapCoords
89
+ ? {
90
+ mapPosition: [mapPosition.lng, mapPosition.lat],
91
+ altitude: altitude,
92
+ }
93
+ : {
94
+ position: position,
95
+ })}
96
+ />
97
+ )}
98
+
99
+ <TopToolbar
100
+ unmovableButtons={
101
+ <Button
102
+ variant={sidebarOpen ? 'contained' : 'outlined'}
103
+ onClick={() => setSidebarOpen(!sidebarOpen)}
104
+ >
105
+ Sidebar
106
+ </Button>
107
+ }
108
+ />
109
+ <Sidebar open={sidebarOpen} setOpen={setSidebarOpen} name="Splat Config">
110
+ <ThreeObjectControls
111
+ showLayer={showLayer}
112
+ setShowLayer={setShowLayer}
113
+ scale={scale}
114
+ setScale={setScale}
115
+ rotation={rotation}
116
+ setRotation={setRotation}
117
+ useMapCoords={useMapCoords}
118
+ setUseMapCoords={setUseMapCoords}
119
+ mapPosition={mapPosition}
120
+ setMapPosition={setMapPosition}
121
+ altitude={altitude}
122
+ setAltitude={setAltitude}
123
+ position={position}
124
+ setPosition={setPosition}
125
+ enableTransformControls={enableTransformControls}
126
+ setEnableTransformControls={setEnableTransformControls}
127
+ transformMode={transformMode}
128
+ setTransformMode={setTransformMode}
129
+ layerName="Splat"
130
+ />
131
+ <Typography
132
+ variant="body2"
133
+ sx={{ mt: 2, p: 1, backgroundColor: 'rgba(255,255,255,0.1)', borderRadius: 1 }}
134
+ >
135
+ The splat used is from{' '}
136
+ <Link
137
+ href="https://www.patreon.com/posts/cluster-fly-141866089"
138
+ target="_blank"
139
+ rel="noopener"
140
+ >
141
+ Cluster Fly
142
+ </Link>{' '}
143
+ by Dany Bittel published under CC.
144
+ </Typography>
145
+ </Sidebar>
146
+ </>
147
+ );
148
+ };
149
+
150
+ export const Default = Template.bind({});
151
+ Default.parameters = {};
@@ -0,0 +1,158 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import * as THREE from 'three';
3
+ import { LngLatLike } from 'maplibre-gl';
4
+ import { useThree } from '../ThreeContext';
5
+ import { SplatLoader } from '../../lib/splats/loaders/SplatLoader';
6
+ import { PlySplatLoader } from '../../lib/splats/loaders/PlySplatLoader';
7
+ import ThreejsUtils from '../../lib/ThreejsUtils';
8
+ import MlTransformControls from '../MlTransformControls';
9
+
10
+ /**
11
+ * Renders splat 3D Models on the MapLibreMap referenced by props.mapId
12
+ *
13
+ * @component
14
+ */
15
+
16
+ export interface MlThreeSplatLayerProps {
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 MlThreeSplatLayer = (props: MlThreeSplatLayerProps) => {
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
+ object.updateMatrixWorld(true);
91
+
92
+ modelRef.current = object;
93
+ scene.add(object);
94
+ setModel(object);
95
+ if (typeof onDoneRef.current === 'function') {
96
+ onDoneRef.current();
97
+ }
98
+ };
99
+
100
+ if (extension === 'splat') {
101
+ const loader = new SplatLoader();
102
+ loader.load(url, (splatMesh) => {
103
+ onLoad(splatMesh);
104
+ });
105
+ } else if (extension === 'ply') {
106
+ const loader = new PlySplatLoader();
107
+ loader.load(url, (splatMesh) => {
108
+ onLoad(splatMesh);
109
+ });
110
+ } else {
111
+ console.warn('MlThreeSplatLayer: Unsupported file extension', extension);
112
+ }
113
+
114
+ return () => {
115
+ if (modelRef.current) {
116
+ scene.remove(modelRef.current);
117
+ if ('dispose' in modelRef.current && typeof modelRef.current.dispose === 'function') {
118
+ (modelRef.current as any).dispose();
119
+ }
120
+ modelRef.current = undefined;
121
+ setModel(undefined);
122
+ }
123
+ };
124
+ }, [scene, url]);
125
+
126
+ useEffect(() => {
127
+ if (!model) return;
128
+
129
+ // Handle position: mapPosition takes precedence over position
130
+ if (mapPosition && worldMatrixInv) {
131
+ const scenePos = ThreejsUtils.toScenePosition(worldMatrixInv, mapPosition, altitude);
132
+ model.position.set(scenePos.x, scenePos.y, scenePos.z);
133
+ } else if (position) {
134
+ model.position.set(position.x, position.y, position.z);
135
+ }
136
+
137
+ if (rotation) {
138
+ model.rotation.set(rotation.x, rotation.y, rotation.z);
139
+ }
140
+ if (scale) {
141
+ if (typeof scale === 'number') {
142
+ model.scale.set(scale, scale, scale);
143
+ } else {
144
+ model.scale.set(scale.x, scale.y, scale.z);
145
+ }
146
+ }
147
+ model.updateMatrixWorld(true);
148
+ }, [model, position, mapPosition, altitude, rotation, scale, worldMatrixInv]);
149
+
150
+ if (enableTransformControls && model) {
151
+ return (
152
+ <MlTransformControls target={model} mode={transformMode} onObjectChange={onTransformChange} />
153
+ );
154
+ }
155
+ return null;
156
+ };
157
+
158
+ export default MlThreeSplatLayer;
@@ -0,0 +1,112 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import * as THREE from 'three';
3
+ import { TransformControls } from 'three/examples/jsm/controls/TransformControls.js';
4
+ import { useThree } from './ThreeContext';
5
+
6
+ export interface MlTransformControlsProps {
7
+ target?: THREE.Object3D;
8
+ mode?: 'translate' | 'rotate' | 'scale';
9
+ enabled?: boolean;
10
+ space?: 'world' | 'local';
11
+ size?: number;
12
+ onObjectChange?: (object: THREE.Object3D) => void;
13
+ }
14
+
15
+ const MlTransformControls = (props: MlTransformControlsProps) => {
16
+ const { target, mode, enabled, space, size, onObjectChange } = props;
17
+ const { scene, camera, renderer, map, sceneRoot } = useThree();
18
+ const controlsRef = useRef<TransformControls | null>(null);
19
+
20
+ useEffect(() => {
21
+ if (!scene || !camera || !renderer || !map || !sceneRoot) return;
22
+
23
+ const domElement = renderer.getRenderer().domElement;
24
+ const controls = new TransformControls(camera, domElement);
25
+ controlsRef.current = controls;
26
+
27
+ // Set initial mode
28
+ controls.setMode(mode || 'translate');
29
+ controls.setSpace(space || 'world');
30
+ if (size) {
31
+ controls.setSize(size);
32
+ }
33
+
34
+ // Add TransformControls root object to the sceneRoot
35
+ // TransformControls has an internal _root that is the actual Object3D
36
+ sceneRoot.add((controls as any)._root);
37
+
38
+ // When transform controls are active, disable map interaction
39
+ const onDraggingChanged = (event: any) => {
40
+ if (event.value) {
41
+ // Disable map dragging when using transform controls
42
+ map.dragPan.disable();
43
+ map.scrollZoom.disable();
44
+ } else {
45
+ // Re-enable map dragging
46
+ map.dragPan.enable();
47
+ map.scrollZoom.enable();
48
+ }
49
+ };
50
+
51
+ controls.addEventListener('dragging-changed', onDraggingChanged);
52
+
53
+ // Trigger callback on object change
54
+ if (onObjectChange) {
55
+ const handleObjectChange = () => {
56
+ if (controls.object) {
57
+ onObjectChange(controls.object);
58
+ }
59
+ };
60
+ controls.addEventListener('objectChange', handleObjectChange);
61
+ }
62
+
63
+ return () => {
64
+ controls.removeEventListener('dragging-changed', onDraggingChanged);
65
+ controls.detach();
66
+ sceneRoot.remove((controls as any)._root);
67
+ controls.dispose();
68
+ controlsRef.current = null;
69
+ };
70
+ // eslint-disable-next-line react-hooks/exhaustive-deps
71
+ }, [scene, camera, renderer, map, sceneRoot]);
72
+
73
+ // Update target object
74
+ useEffect(() => {
75
+ if (!controlsRef.current) return;
76
+
77
+ if (target) {
78
+ controlsRef.current.attach(target);
79
+ } else {
80
+ controlsRef.current.detach();
81
+ }
82
+ }, [target]);
83
+
84
+ // Update mode
85
+ useEffect(() => {
86
+ if (!controlsRef.current) return;
87
+ // Directly set the mode to avoid detach/reattach cycle
88
+ (controlsRef.current as any).mode = mode || 'translate';
89
+ }, [mode]);
90
+
91
+ // Update enabled state
92
+ useEffect(() => {
93
+ if (!controlsRef.current) return;
94
+ controlsRef.current.enabled = enabled !== false;
95
+ }, [enabled]);
96
+
97
+ // Update space
98
+ useEffect(() => {
99
+ if (!controlsRef.current) return;
100
+ controlsRef.current.setSpace(space || 'world');
101
+ }, [space]);
102
+
103
+ // Update size
104
+ useEffect(() => {
105
+ if (!controlsRef.current || !size) return;
106
+ controlsRef.current.setSize(size);
107
+ }, [size]);
108
+
109
+ return null;
110
+ };
111
+
112
+ export default MlTransformControls;
@@ -0,0 +1,26 @@
1
+ import { createContext, useContext } from 'react';
2
+ import { Scene, PerspectiveCamera, Group, Matrix4 } from 'three';
3
+ import { Map as MapboxMap } from 'maplibre-gl';
4
+ import ThreejsSceneRenderer from '../lib/ThreejsSceneRenderer';
5
+
6
+ export interface ThreeContextType {
7
+ scene: Scene | undefined;
8
+ camera: PerspectiveCamera | undefined;
9
+ renderer: ThreejsSceneRenderer | undefined;
10
+ map: MapboxMap | undefined;
11
+ sceneRoot: Group | undefined;
12
+ worldMatrix: Matrix4 | undefined;
13
+ worldMatrixInv: Matrix4 | undefined;
14
+ }
15
+
16
+ export const ThreeContext = createContext<ThreeContextType>({
17
+ scene: undefined,
18
+ camera: undefined,
19
+ renderer: undefined,
20
+ map: undefined,
21
+ sceneRoot: undefined,
22
+ worldMatrix: undefined,
23
+ worldMatrixInv: undefined,
24
+ });
25
+
26
+ export const useThree = () => useContext(ThreeContext);
@@ -0,0 +1,197 @@
1
+ import Button from '@mui/material/Button';
2
+ import ButtonGroup from '@mui/material/ButtonGroup';
3
+ import Slider from '@mui/material/Slider';
4
+ import Typography from '@mui/material/Typography';
5
+ import Box from '@mui/material/Box';
6
+
7
+ export interface ThreeObjectControlsProps {
8
+ showLayer: boolean;
9
+ setShowLayer: (show: boolean) => void;
10
+ scale: number;
11
+ setScale: (scale: number) => void;
12
+ rotation: { x: number; y: number; z: number };
13
+ setRotation: (rotation: { x: number; y: number; z: number }) => void;
14
+ useMapCoords: boolean;
15
+ setUseMapCoords: (use: boolean) => void;
16
+ mapPosition: { lng: number; lat: number };
17
+ setMapPosition: (position: { lng: number; lat: number }) => void;
18
+ altitude: number;
19
+ setAltitude: (altitude: number) => void;
20
+ position: { x: number; y: number; z: number };
21
+ setPosition: (position: { x: number; y: number; z: number }) => void;
22
+ enableTransformControls?: boolean;
23
+ setEnableTransformControls?: (enable: boolean) => void;
24
+ transformMode?: 'translate' | 'rotate' | 'scale';
25
+ setTransformMode?: (mode: 'translate' | 'rotate' | 'scale') => void;
26
+ layerName?: string;
27
+ }
28
+
29
+ export const ThreeObjectControls = ({
30
+ showLayer,
31
+ setShowLayer,
32
+ scale,
33
+ setScale,
34
+ rotation,
35
+ setRotation,
36
+ useMapCoords,
37
+ setUseMapCoords,
38
+ mapPosition,
39
+ setMapPosition,
40
+ altitude,
41
+ setAltitude,
42
+ position,
43
+ setPosition,
44
+ enableTransformControls,
45
+ setEnableTransformControls,
46
+ transformMode,
47
+ setTransformMode,
48
+ layerName = 'Layer',
49
+ }: ThreeObjectControlsProps) => {
50
+ return (
51
+ <Box sx={{ padding: '10px' }}>
52
+ <Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap', marginBottom: 2 }}>
53
+ <Button
54
+ color="primary"
55
+ variant={showLayer ? 'contained' : 'outlined'}
56
+ onClick={() => setShowLayer(!showLayer)}
57
+ size="small"
58
+ >
59
+ {showLayer ? 'Hide' : 'Show'} {layerName}
60
+ </Button>
61
+ <Button
62
+ color="secondary"
63
+ variant={useMapCoords ? 'contained' : 'outlined'}
64
+ onClick={() => setUseMapCoords(!useMapCoords)}
65
+ size="small"
66
+ >
67
+ {useMapCoords ? 'Map Coords' : 'Scene Coords'}
68
+ </Button>
69
+ {setEnableTransformControls && (
70
+ <Button
71
+ color="info"
72
+ variant={enableTransformControls ? 'contained' : 'outlined'}
73
+ onClick={() => setEnableTransformControls(!enableTransformControls)}
74
+ size="small"
75
+ >
76
+ 3D Gizmo
77
+ </Button>
78
+ )}
79
+ </Box>
80
+
81
+ {setTransformMode && enableTransformControls && (
82
+ <Box sx={{ marginBottom: 2 }}>
83
+ <ButtonGroup variant="outlined" size="small" fullWidth aria-label="transform mode">
84
+ <Button
85
+ variant={transformMode === 'translate' ? 'contained' : 'outlined'}
86
+ onClick={() => setTransformMode('translate')}
87
+ >
88
+ Move
89
+ </Button>
90
+ <Button
91
+ variant={transformMode === 'rotate' ? 'contained' : 'outlined'}
92
+ onClick={() => setTransformMode('rotate')}
93
+ >
94
+ Rotate
95
+ </Button>
96
+ <Button
97
+ variant={transformMode === 'scale' ? 'contained' : 'outlined'}
98
+ onClick={() => setTransformMode('scale')}
99
+ >
100
+ Scale
101
+ </Button>
102
+ </ButtonGroup>
103
+ </Box>
104
+ )}
105
+ <Typography gutterBottom>Scale: {scale.toFixed(2)}</Typography>
106
+ <Slider
107
+ value={scale}
108
+ onChange={(e, newValue) => setScale(newValue as number)}
109
+ min={0.01}
110
+ max={150}
111
+ step={0.01}
112
+ valueLabelDisplay="auto"
113
+ />
114
+ <Typography gutterBottom>Rotation X: {rotation.x}°</Typography>
115
+ <Slider
116
+ value={rotation.x}
117
+ onChange={(e, newValue) => setRotation({ ...rotation, x: newValue as number })}
118
+ min={0}
119
+ max={360}
120
+ valueLabelDisplay="auto"
121
+ />
122
+ <Typography gutterBottom>Rotation Y: {rotation.y}°</Typography>
123
+ <Slider
124
+ value={rotation.y}
125
+ onChange={(e, newValue) => setRotation({ ...rotation, y: newValue as number })}
126
+ min={0}
127
+ max={360}
128
+ valueLabelDisplay="auto"
129
+ />
130
+ <Typography gutterBottom>Rotation Z: {rotation.z}°</Typography>
131
+ <Slider
132
+ value={rotation.z}
133
+ onChange={(e, newValue) => setRotation({ ...rotation, z: newValue as number })}
134
+ min={0}
135
+ max={360}
136
+ valueLabelDisplay="auto"
137
+ />
138
+ {useMapCoords ? (
139
+ <>
140
+ <Typography gutterBottom>Longitude: {mapPosition.lng.toFixed(6)}</Typography>
141
+ <Slider
142
+ value={mapPosition.lng}
143
+ onChange={(e, newValue) => setMapPosition({ ...mapPosition, lng: newValue as number })}
144
+ min={7.09}
145
+ max={7.11}
146
+ step={0.0001}
147
+ valueLabelDisplay="auto"
148
+ />
149
+ <Typography gutterBottom>Latitude: {mapPosition.lat.toFixed(6)}</Typography>
150
+ <Slider
151
+ value={mapPosition.lat}
152
+ onChange={(e, newValue) => setMapPosition({ ...mapPosition, lat: newValue as number })}
153
+ min={50.73}
154
+ max={50.74}
155
+ step={0.0001}
156
+ valueLabelDisplay="auto"
157
+ />
158
+ <Typography gutterBottom>Altitude: {altitude} m</Typography>
159
+ <Slider
160
+ value={altitude}
161
+ onChange={(e, newValue) => setAltitude(newValue as number)}
162
+ min={-100}
163
+ max={500}
164
+ valueLabelDisplay="auto"
165
+ />
166
+ </>
167
+ ) : (
168
+ <>
169
+ <Typography gutterBottom>Position X: {position.x}</Typography>
170
+ <Slider
171
+ value={position.x}
172
+ onChange={(e, newValue) => setPosition({ ...position, x: newValue as number })}
173
+ min={-100}
174
+ max={100}
175
+ valueLabelDisplay="auto"
176
+ />
177
+ <Typography gutterBottom>Position Y: {position.y}</Typography>
178
+ <Slider
179
+ value={position.y}
180
+ onChange={(e, newValue) => setPosition({ ...position, y: newValue as number })}
181
+ min={-100}
182
+ max={100}
183
+ valueLabelDisplay="auto"
184
+ />
185
+ <Typography gutterBottom>Position Z: {position.z}</Typography>
186
+ <Slider
187
+ value={position.z}
188
+ onChange={(e, newValue) => setPosition({ ...position, z: newValue as number })}
189
+ min={-500}
190
+ max={100}
191
+ valueLabelDisplay="auto"
192
+ />
193
+ </>
194
+ )}
195
+ </Box>
196
+ );
197
+ };