Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions packages/three/package.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
{
"name": "@mapcomponents/three",
"version": "1.7.2",
"main": "./index.js",
"types": "./index.d.ts",
"license": "MIT",
"main": "dist/index.cjs.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"exports": {
".": {
"import": "./index.mjs",
"require": "./index.js"
"import": "dist/index.mjs",
"require": "dist/index.cjs.js"
}
},
"publishConfig": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useEffect, useRef } from 'react';
import * as THREE from 'three';
import { TransformControls } from 'three/examples/jsm/controls/TransformControls.js';
import { useThree } from './ThreeContext';
import { useThree } from '../contexts/ThreeContext';

export interface MlTransformControlsProps {
export interface MlThreeGizmoProps {
target?: THREE.Object3D;
mode?: 'translate' | 'rotate' | 'scale';
enabled?: boolean;
Expand All @@ -12,7 +12,7 @@ export interface MlTransformControlsProps {
onObjectChange?: (object: THREE.Object3D) => void;
}

const MlTransformControls = (props: MlTransformControlsProps) => {
const MlThreeGizmo = (props: MlThreeGizmoProps) => {
const { target, mode, enabled, space, size, onObjectChange } = props;
const { scene, camera, renderer, map, sceneRoot } = useThree();
const controlsRef = useRef<TransformControls | null>(null);
Expand Down Expand Up @@ -109,4 +109,4 @@ const MlTransformControls = (props: MlTransformControlsProps) => {
return null;
};

export default MlTransformControls;
export default MlThreeGizmo;
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useEffect } from 'react';
import { MapComponentsProvider, MapLibreMap, useMap } from '@mapcomponents/react-maplibre';
import { ThreeProvider } from '../ThreeProvider';
import { ThreeProvider } from '../../contexts/ThreeProvider';
import MlThreeModelLayer from './MlThreeModelLayer';

const MapExposer = () => {
Expand All @@ -20,8 +20,7 @@ const TestComponent = ({ onDone }: { onDone: () => void }) => {
<ThreeProvider id="three-provider" mapId="map_1">
<MlThreeModelLayer
url="assets/3D/godzilla_simple.glb"
mapPosition={[13.404954, 52.520008]}
scale={10}
position={[13.404954, 52.520008]}
onDone={onDone}
/>
</ThreeProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@ import Button from '@mui/material/Button';
import MlThreeModelLayer from './MlThreeModelLayer';
import { useMap, TopToolbar, Sidebar } from '@mapcomponents/react-maplibre';
import ThreejsContextDecorator from '../../decorators/ThreejsContextDecorator';
import { useThree } from '../ThreeContext';
import { ThreeObjectControls } from '../ThreeObjectControls';
import ThreejsUtils from '../../lib/ThreejsUtils';
import { useThree } from '../../contexts/ThreeContext';
import { MlThreeObjectControls } from '../MlThreeObjectControls';
import * as THREE from 'three';

const storyoptions = {
Expand Down Expand Up @@ -48,17 +47,14 @@ const Lights = () => {
};

const Template: any = () => {
const { worldMatrix } = useThree();
const [showLayer, setShowLayer] = useState(true);
const [scale, setScale] = useState(1);
const [rotation, setRotation] = useState({ x: 90, y: 90, z: 0 });
const [useMapCoords, setUseMapCoords] = useState(true);
const [mapPosition, setMapPosition] = useState({ lng: 7.097, lat: 50.7355 });
const [altitude, setAltitude] = useState(0);
const [position, setPosition] = useState({ x: 0, y: 0, z: 0 });
const [sidebarOpen, setSidebarOpen] = useState(true);
const [enableTransformControls, setEnableTransformControls] = useState(false);
const [transformMode, setTransformMode] = useState<'translate' | 'rotate' | 'scale'>('translate');
const [sidebarOpen, setSidebarOpen] = useState(true);

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

// Center map on position when switching coordinate modes
useEffect(() => {
if (!mapHook.map) return;
if (useMapCoords) {
mapHook.map.setCenter([mapPosition.lng, mapPosition.lat]);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [useMapCoords, mapHook.map]);

const handleTransformChange = (object: THREE.Object3D) => {
setRotation({
x: (object.rotation.x * 180) / Math.PI,
y: (object.rotation.y * 180) / Math.PI,
z: (object.rotation.z * 180) / Math.PI,
});
setScale(object.scale.x);

if (useMapCoords && worldMatrix) {
const [lng, lat, alt] = ThreejsUtils.toMapPosition(worldMatrix, object.position);
setMapPosition({ lng, lat });
setAltitude(alt);
} else {
setPosition({ x: object.position.x, y: object.position.y, z: object.position.z });
}
};

return (
<>
<Lights />
{showLayer && (
<MlThreeModelLayer
url="assets/3D/godzilla_simple.glb"
rotation={{
x: (rotation.x * Math.PI) / 180,
y: (rotation.y * Math.PI) / 180,
z: (rotation.z * Math.PI) / 180,
position={[mapPosition.lng, mapPosition.lat]}
transform={{
rotation: {
x: (rotation.x * Math.PI) / 180,
y: (rotation.y * Math.PI) / 180,
z: (rotation.z * Math.PI) / 180,
},
scale: scale,
position: position,
}}
scale={scale}
enableTransformControls={enableTransformControls}
transformMode={transformMode}
onTransformChange={handleTransformChange}
{...(useMapCoords
? {
mapPosition: [mapPosition.lng, mapPosition.lat],
altitude: altitude,
}
: {
position: position,
})}
/>
)}

Expand All @@ -131,26 +94,22 @@ const Template: any = () => {
}
/>
<Sidebar open={sidebarOpen} setOpen={setSidebarOpen} name="3D Model Config">
<ThreeObjectControls
<MlThreeObjectControls
showLayer={showLayer}
setShowLayer={setShowLayer}
scale={scale}
setScale={setScale}
rotation={rotation}
setRotation={setRotation}
useMapCoords={useMapCoords}
setUseMapCoords={setUseMapCoords}
mapPosition={mapPosition}
setMapPosition={setMapPosition}
altitude={altitude}
setAltitude={setAltitude}
position={position}
setPosition={setPosition}
layerName="Model"
enableTransformControls={enableTransformControls}
setEnableTransformControls={setEnableTransformControls}
transformMode={transformMode}
setTransformMode={setTransformMode}
layerName="Model"
/>
</Sidebar>
</>
Expand Down
163 changes: 30 additions & 133 deletions packages/three/src/components/MlThreeModelLayer/MlThreeModelLayer.tsx
Original file line number Diff line number Diff line change
@@ -1,152 +1,49 @@
import { useEffect, useRef, useState } from 'react';
import * as THREE from 'three';
import { useMemo } from 'react';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { OBJLoader } from 'three/addons/loaders/OBJLoader.js';
import { LngLatLike } from 'maplibre-gl';
import { useThree } from '../ThreeContext';
import ThreejsUtils from '../../lib/ThreejsUtils';
import MlTransformControls from '../MlTransformControls';
import { useThreeModel, UseThreeModelProps, ModelLoader } from '../../hooks/useThreeModel';

/**
* Renders obj or gltf 3D Models on the MapLibreMap referenced by props.mapId
* Renders obj or gltf 3D Models on the MapLibreMap
*
* @component
*/

export interface MlThreeModelLayerProps {
export type MlThreeModelLayerProps = Omit<UseThreeModelProps, 'loaders'> & {
mapId?: string;
url: string;
position?: { x: number; y: number; z: number };
mapPosition?: LngLatLike;
altitude?: number;
rotation?: { x: number; y: number; z: number };
scale?: { x: number; y: number; z: number } | number;
enableTransformControls?: boolean;
transformMode?: 'translate' | 'rotate' | 'scale';
onTransformChange?: (object: THREE.Object3D) => void;
init?: () => void;
onDone?: () => void;
}
};

const MlThreeModelLayer = (props: MlThreeModelLayerProps) => {
const {
const { url, position, transform, init, onDone, customLoaders } = props;

const loaders = useMemo<Record<string, ModelLoader>>(
() => ({
gltf: (url, onLoad) => {
const loader = new GLTFLoader();
loader.load(url, (gltf) => onLoad(gltf.scene));
},
glb: (url, onLoad) => {
const loader = new GLTFLoader();
loader.load(url, (gltf) => onLoad(gltf.scene));
},
obj: (url, onLoad) => {
const loader = new OBJLoader();
loader.load(url, (obj) => onLoad(obj));
},
}),
[]
);

useThreeModel({
url,
position,
mapPosition,
altitude,
rotation,
scale,
enableTransformControls,
transformMode,
onTransformChange,
transform,
init,
onDone,
} = props;
const { scene, worldMatrixInv } = useThree();
const modelRef = useRef<THREE.Object3D | undefined>(undefined);
const [model, setModel] = useState<THREE.Object3D | undefined>(undefined);

// Use refs for callbacks to avoid re-triggering the effect when they change
const initRef = useRef(init);
const onDoneRef = useRef(onDone);
initRef.current = init;
onDoneRef.current = onDone;

const transformRef = useRef({ position, mapPosition, altitude, rotation, scale });
transformRef.current = { position, mapPosition, altitude, rotation, scale };
const worldMatrixInvRef = useRef(worldMatrixInv);
worldMatrixInvRef.current = worldMatrixInv;

useEffect(() => {
if (!scene) return;

if (typeof initRef.current === 'function') {
initRef.current();
}

const extension = url.split('.').pop()?.toLowerCase();

const onLoad = (object: THREE.Object3D) => {
const { position, mapPosition, altitude, rotation, scale } = transformRef.current;
const worldMatrixInv = worldMatrixInvRef.current;

if (mapPosition && worldMatrixInv) {
const scenePos = ThreejsUtils.toScenePosition(worldMatrixInv, mapPosition, altitude);
object.position.set(scenePos.x, scenePos.y, scenePos.z);
} else if (position) {
object.position.set(position.x, position.y, position.z);
}

if (rotation) {
object.rotation.set(rotation.x, rotation.y, rotation.z);
}
if (scale) {
if (typeof scale === 'number') {
object.scale.set(scale, scale, scale);
} else {
object.scale.set(scale.x, scale.y, scale.z);
}
}

modelRef.current = object;
scene.add(object);
setModel(object);
if (typeof onDoneRef.current === 'function') {
onDoneRef.current();
}
};

if (extension === 'glb' || extension === 'gltf') {
const loader = new GLTFLoader();
loader.load(url, (gltf) => {
onLoad(gltf.scene);
});
} else if (extension === 'obj') {
const loader = new OBJLoader();
loader.load(url, (obj) => {
onLoad(obj);
});
} else {
console.warn('MlThreeModelLayer: Unsupported file extension', extension);
}

return () => {
if (modelRef.current) {
scene.remove(modelRef.current);
modelRef.current = undefined;
setModel(undefined);
}
};
}, [scene, url]);

useEffect(() => {
if (!model) return;

// Handle position: mapPosition takes precedence over position
if (mapPosition && worldMatrixInv) {
const scenePos = ThreejsUtils.toScenePosition(worldMatrixInv, mapPosition, altitude);
model.position.set(scenePos.x, scenePos.y, scenePos.z);
} else if (position) {
model.position.set(position.x, position.y, position.z);
}

if (rotation) {
model.rotation.set(rotation.x, rotation.y, rotation.z);
}
if (scale) {
if (typeof scale === 'number') {
model.scale.set(scale, scale, scale);
} else {
model.scale.set(scale.x, scale.y, scale.z);
}
}
}, [model, position, mapPosition, altitude, rotation, scale, worldMatrixInv]);
loaders,
customLoaders,
});

if (enableTransformControls && model) {
return (
<MlTransformControls target={model} mode={transformMode} onObjectChange={onTransformChange} />
);
}
return null;
};

Expand Down
Loading
Loading