Skip to content
Open
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
102 changes: 102 additions & 0 deletions components/Layers/Model/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/* eslint-disable import/no-extraneous-dependencies */
import * as THREE from 'three';
import maplibregl from 'maplibre-gl';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
import { COORDS_EKATERINBURG } from 'constants/coords';

// parameters to ensure the model is georeferenced correctly on the map
const modelOrigin = COORDS_EKATERINBURG;
const modelAltitude = 0;
const modelRotate = [Math.PI / 2, 0, 0];

const modelAsMercatorCoordinate = maplibregl.MercatorCoordinate.fromLngLat(
modelOrigin,
modelAltitude,
);

// transformation parameters to position, rotate and scale the 3D model onto the map
const modelTransform = {
translateX: modelAsMercatorCoordinate.x,
translateY: modelAsMercatorCoordinate.y,
translateZ: modelAsMercatorCoordinate.z,
rotateX: modelRotate[0],
rotateY: modelRotate[1],
rotateZ: modelRotate[2],
/* Since our 3D model is in real world meters, a scale transform needs to be
* applied since the CustomLayerInterface expects units in MercatorCoordinates.
*/
scale: modelAsMercatorCoordinate.meterInMercatorCoordinateUnits(),
};

// configuration of the custom layer for a 3D model per the CustomLayerInterface
export const getModelLayer = (id: string, path: string) => ({
id,
type: 'custom',
renderingMode: '3d',
onAdd(map, gl) {
this.camera = new THREE.Camera();
this.scene = new THREE.Scene();

// create two three.js lights to illuminate the model
const directionalLight = new THREE.DirectionalLight(0xffffff);
directionalLight.position.set(0, -70, 100).normalize();
this.scene.add(directionalLight);

const directionalLight2 = new THREE.DirectionalLight(0xffffff);
directionalLight2.position.set(0, 70, 100).normalize();
this.scene.add(directionalLight2);

// use the three.js GLTF loader to add the 3D model to the three.js scene
const loader = new GLTFLoader();
loader.load(path, (gltf) => {
this.scene.add(gltf.scene);
});
this.map = map;

// use the MapLibre GL JS map canvas for three.js
this.renderer = new THREE.WebGLRenderer({
canvas: map.getCanvas(),
context: gl,
antialias: true,
});

this.renderer.autoClear = false;
},
render(gl, matrix) {
const rotationX = new THREE.Matrix4().makeRotationAxis(
new THREE.Vector3(1, 0, 0),
modelTransform.rotateX,
);
const rotationY = new THREE.Matrix4().makeRotationAxis(
new THREE.Vector3(0, 1, 0),
modelTransform.rotateY,
);
const rotationZ = new THREE.Matrix4().makeRotationAxis(
new THREE.Vector3(0, 0, 1),
modelTransform.rotateZ,
);

const m = new THREE.Matrix4().fromArray(matrix);
const l = new THREE.Matrix4()
.makeTranslation(
modelTransform.translateX,
modelTransform.translateY,
modelTransform.translateZ,
)
.scale(
new THREE.Vector3(
modelTransform.scale,
-modelTransform.scale,
modelTransform.scale,
),
)
.multiply(rotationX)
.multiply(rotationY)
.multiply(rotationZ);

this.camera.projectionMatrix = m.multiply(l);
this.renderer.resetState();
this.renderer.render(this.scene, this.camera);
this.map.triggerRepaint();
},
});
4 changes: 3 additions & 1 deletion components/Map/Map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { DesignCodeSource } from './layers/DesignCodeSource';
import { MapContext } from './providers/MapProvider';

import 'maplibre-gl/dist/maplibre-gl.css';
import { ModelSource } from './layers/ModelSource';

function MapLayers() {
return (
Expand All @@ -23,6 +24,7 @@ function MapLayers() {
<DtpSource />
<LinesSource />
<DesignCodeSource />
<ModelSource />
</>
);
}
Expand All @@ -40,7 +42,7 @@ export function Map() {
pitch: 30,
}}
minZoom={11}
maxZoom={20}
maxZoom={23}
// hash
style={{ width: '100vw', height: '100vh', color: 'black' }}
mapStyle="https://map-backend.netlify.app/style.json"
Expand Down
24 changes: 24 additions & 0 deletions components/Map/layers/ModelSource.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react';
import { useMap } from 'react-map-gl';
import { getModelLayer } from 'components/Layers/Model';

export function ModelSource() {
const ekbMap = useMap();

return (
<input
style={{ position: 'absolute', top: 0, left: 0, zIndex: 1000 }}
type="file"
onChange={(e) => {
const map = ekbMap?.current?.getMap?.();
map.addLayer(
// @ts-ignore
getModelLayer(
`3d-model-${Math.round(Math.random() * 1000)}`,
URL.createObjectURL(e.target.files[0]),
),
);
}}
/>
);
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@
"@types/lodash": "^4.14.197",
"@types/node": "20.5.4",
"@types/react": "^18.2.21",
"@types/three": "^0.155.1",
"classnames": "^2.3.2",
"ekb": "^1.1.4",
"framer-motion": "^10.16.1",
"hls.js": "^1.4.10",
"lodash": "^4.17.21",
"mapbox-gl": "npm:empty-npm-package@1.0.0",
"maplibre-gl": "^3.3.0",
Expand All @@ -36,6 +36,7 @@
"react-modal-sheet": "^2.0.0",
"react-redux": "^8.1.2",
"redux": "^4.2.1",
"three": "^0.156.0",
"typescript": "^5.1.6"
},
"devDependencies": {
Expand Down
Loading