Skip to content

Commit 51eaf52

Browse files
authored
Refactor/three package (#251)
- streamline MlThreeModelLayer and MlThreeSplatLayer props for transform handling - refactor MlThreeGizmo and add an export for it - fix build
1 parent 812cf2e commit 51eaf52

16 files changed

+573
-600
lines changed

packages/three/package.json

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
{
22
"name": "@mapcomponents/three",
33
"version": "1.7.2",
4-
"main": "./index.js",
5-
"types": "./index.d.ts",
4+
"license": "MIT",
5+
"main": "dist/index.cjs.js",
6+
"module": "dist/index.mjs",
7+
"types": "dist/index.d.ts",
68
"exports": {
79
".": {
8-
"import": "./index.mjs",
9-
"require": "./index.js"
10+
"import": "dist/index.mjs",
11+
"require": "dist/index.cjs.js"
1012
}
1113
},
1214
"publishConfig": {

packages/three/src/components/MlTransformControls.tsx renamed to packages/three/src/components/MlThreeGizmo.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { useEffect, useRef } from 'react';
22
import * as THREE from 'three';
33
import { TransformControls } from 'three/examples/jsm/controls/TransformControls.js';
4-
import { useThree } from './ThreeContext';
4+
import { useThree } from '../contexts/ThreeContext';
55

6-
export interface MlTransformControlsProps {
6+
export interface MlThreeGizmoProps {
77
target?: THREE.Object3D;
88
mode?: 'translate' | 'rotate' | 'scale';
99
enabled?: boolean;
@@ -12,7 +12,7 @@ export interface MlTransformControlsProps {
1212
onObjectChange?: (object: THREE.Object3D) => void;
1313
}
1414

15-
const MlTransformControls = (props: MlTransformControlsProps) => {
15+
const MlThreeGizmo = (props: MlThreeGizmoProps) => {
1616
const { target, mode, enabled, space, size, onObjectChange } = props;
1717
const { scene, camera, renderer, map, sceneRoot } = useThree();
1818
const controlsRef = useRef<TransformControls | null>(null);
@@ -109,4 +109,4 @@ const MlTransformControls = (props: MlTransformControlsProps) => {
109109
return null;
110110
};
111111

112-
export default MlTransformControls;
112+
export default MlThreeGizmo;

packages/three/src/components/MlThreeModelLayer/MlThreeModelLayer.cy.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { useEffect } from 'react';
22
import { MapComponentsProvider, MapLibreMap, useMap } from '@mapcomponents/react-maplibre';
3-
import { ThreeProvider } from '../ThreeProvider';
3+
import { ThreeProvider } from '../../contexts/ThreeProvider';
44
import MlThreeModelLayer from './MlThreeModelLayer';
55

66
const MapExposer = () => {
@@ -20,8 +20,7 @@ const TestComponent = ({ onDone }: { onDone: () => void }) => {
2020
<ThreeProvider id="three-provider" mapId="map_1">
2121
<MlThreeModelLayer
2222
url="assets/3D/godzilla_simple.glb"
23-
mapPosition={[13.404954, 52.520008]}
24-
scale={10}
23+
position={[13.404954, 52.520008]}
2524
onDone={onDone}
2625
/>
2726
</ThreeProvider>

packages/three/src/components/MlThreeModelLayer/MlThreeModelLayer.stories.tsx

Lines changed: 14 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@ import Button from '@mui/material/Button';
33
import MlThreeModelLayer from './MlThreeModelLayer';
44
import { useMap, TopToolbar, Sidebar } from '@mapcomponents/react-maplibre';
55
import ThreejsContextDecorator from '../../decorators/ThreejsContextDecorator';
6-
import { useThree } from '../ThreeContext';
7-
import { ThreeObjectControls } from '../ThreeObjectControls';
8-
import ThreejsUtils from '../../lib/ThreejsUtils';
6+
import { useThree } from '../../contexts/ThreeContext';
7+
import { MlThreeObjectControls } from '../MlThreeObjectControls';
98
import * as THREE from 'three';
109

1110
const storyoptions = {
@@ -48,17 +47,14 @@ const Lights = () => {
4847
};
4948

5049
const Template: any = () => {
51-
const { worldMatrix } = useThree();
5250
const [showLayer, setShowLayer] = useState(true);
5351
const [scale, setScale] = useState(1);
5452
const [rotation, setRotation] = useState({ x: 90, y: 90, z: 0 });
55-
const [useMapCoords, setUseMapCoords] = useState(true);
5653
const [mapPosition, setMapPosition] = useState({ lng: 7.097, lat: 50.7355 });
57-
const [altitude, setAltitude] = useState(0);
5854
const [position, setPosition] = useState({ x: 0, y: 0, z: 0 });
55+
const [sidebarOpen, setSidebarOpen] = useState(true);
5956
const [enableTransformControls, setEnableTransformControls] = useState(false);
6057
const [transformMode, setTransformMode] = useState<'translate' | 'rotate' | 'scale'>('translate');
61-
const [sidebarOpen, setSidebarOpen] = useState(true);
6258

6359
const mapHook = useMap({ mapId: 'map_1' });
6460
useEffect(() => {
@@ -68,55 +64,22 @@ const Template: any = () => {
6864
mapHook.map?.setCenter([7.097, 50.7355]);
6965
}, [mapHook.map]);
7066

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-
9767
return (
9868
<>
9969
<Lights />
10070
{showLayer && (
10171
<MlThreeModelLayer
10272
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,
73+
position={[mapPosition.lng, mapPosition.lat]}
74+
transform={{
75+
rotation: {
76+
x: (rotation.x * Math.PI) / 180,
77+
y: (rotation.y * Math.PI) / 180,
78+
z: (rotation.z * Math.PI) / 180,
79+
},
80+
scale: scale,
81+
position: position,
10782
}}
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-
})}
12083
/>
12184
)}
12285

@@ -131,26 +94,22 @@ const Template: any = () => {
13194
}
13295
/>
13396
<Sidebar open={sidebarOpen} setOpen={setSidebarOpen} name="3D Model Config">
134-
<ThreeObjectControls
97+
<MlThreeObjectControls
13598
showLayer={showLayer}
13699
setShowLayer={setShowLayer}
137100
scale={scale}
138101
setScale={setScale}
139102
rotation={rotation}
140103
setRotation={setRotation}
141-
useMapCoords={useMapCoords}
142-
setUseMapCoords={setUseMapCoords}
143104
mapPosition={mapPosition}
144105
setMapPosition={setMapPosition}
145-
altitude={altitude}
146-
setAltitude={setAltitude}
147106
position={position}
148107
setPosition={setPosition}
108+
layerName="Model"
149109
enableTransformControls={enableTransformControls}
150110
setEnableTransformControls={setEnableTransformControls}
151111
transformMode={transformMode}
152112
setTransformMode={setTransformMode}
153-
layerName="Model"
154113
/>
155114
</Sidebar>
156115
</>

packages/three/src/components/MlThreeModelLayer/MlThreeModelLayer.tsx

Lines changed: 30 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -1,152 +1,49 @@
1-
import { useEffect, useRef, useState } from 'react';
2-
import * as THREE from 'three';
1+
import { useMemo } from 'react';
32
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
43
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';
4+
import { useThreeModel, UseThreeModelProps, ModelLoader } from '../../hooks/useThreeModel';
95

106
/**
11-
* Renders obj or gltf 3D Models on the MapLibreMap referenced by props.mapId
7+
* Renders obj or gltf 3D Models on the MapLibreMap
128
*
139
* @component
1410
*/
1511

16-
export interface MlThreeModelLayerProps {
12+
export type MlThreeModelLayerProps = Omit<UseThreeModelProps, 'loaders'> & {
1713
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-
}
14+
};
3015

3116
const MlThreeModelLayer = (props: MlThreeModelLayerProps) => {
32-
const {
17+
const { url, position, transform, init, onDone, customLoaders } = props;
18+
19+
const loaders = useMemo<Record<string, ModelLoader>>(
20+
() => ({
21+
gltf: (url, onLoad) => {
22+
const loader = new GLTFLoader();
23+
loader.load(url, (gltf) => onLoad(gltf.scene));
24+
},
25+
glb: (url, onLoad) => {
26+
const loader = new GLTFLoader();
27+
loader.load(url, (gltf) => onLoad(gltf.scene));
28+
},
29+
obj: (url, onLoad) => {
30+
const loader = new OBJLoader();
31+
loader.load(url, (obj) => onLoad(obj));
32+
},
33+
}),
34+
[]
35+
);
36+
37+
useThreeModel({
3338
url,
3439
position,
35-
mapPosition,
36-
altitude,
37-
rotation,
38-
scale,
39-
enableTransformControls,
40-
transformMode,
41-
onTransformChange,
40+
transform,
4241
init,
4342
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]);
43+
loaders,
44+
customLoaders,
45+
});
14446

145-
if (enableTransformControls && model) {
146-
return (
147-
<MlTransformControls target={model} mode={transformMode} onObjectChange={onTransformChange} />
148-
);
149-
}
15047
return null;
15148
};
15249

0 commit comments

Comments
 (0)