Skip to content

Commit

Permalink
add ifc document viewer (#1486)
Browse files Browse the repository at this point in the history
  • Loading branch information
allyoucanmap authored Aug 8, 2023
1 parent b7c20ef commit 156b026
Show file tree
Hide file tree
Showing 7 changed files with 168 additions and 10 deletions.
6 changes: 4 additions & 2 deletions geonode_mapstore_client/client/devServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,15 @@ module.exports = (devServerDefault, projectConfig) => {
{
context: [
'/static/mapstore/ms-translations/**',
'/docs/**'
'/docs/**',
'/static/mapstore/dist/js/web-ifc/**'
],
target: `${protocol}://${devServerHost}:8081`,
secure: false,
changeOrigin: true,
pathRewrite: {
'/static/mapstore/ms-translations': '/node_modules/mapstore/web/client/translations'
'/static/mapstore/ms-translations': '/node_modules/mapstore/web/client/translations',
'/static/mapstore/dist/js/web-ifc': '/node_modules/web-ifc'
}
}
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const mediaMap = {
pdf: PdfViewer,
gltf: Scene3DViewer,
pcd: Scene3DViewer,
ifc: Scene3DViewer,
unsupported: UnsupportedViewer
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@
* LICENSE file in the root directory of this source tree.
*/

import React, { Suspense, useEffect, useState } from 'react';
import React, { Suspense, useEffect, useState, useRef } from 'react';
import * as THREE from 'three';
import isString from 'lodash/isString';
import isNumber from 'lodash/isNumber';
import { Canvas, useLoader } from '@react-three/fiber';
import { OrbitControls, PerspectiveCamera, Environment, Html, useProgress } from '@react-three/drei';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
import { PCDLoader } from 'three/examples/jsm/loaders/PCDLoader';
import { parseDevHostname } from '@js/utils/APIUtils';
import { parseDevHostname, getGeoNodeLocalConfig } from '@js/utils/APIUtils';
import { IFCLoader } from 'web-ifc-three/IFCLoader';

function Loader() {
const { progress } = useProgress();
Expand Down Expand Up @@ -42,7 +45,9 @@ function GLTFModel({ src, onChange }) {
const { radius, center } = computeBoundingSphereFromGLTF(gltf);

useEffect(() => {
onChange({ center: [center.x || 0, center.y || 0, center.z || 0], radius });
if (center) {
onChange({ center: [center.x || 0, center.y || 0, center.z || 0], radius });
}
}, [radius, center?.x, center?.y, center?.z]);

return gltf?.scene ? <primitive object={gltf.scene} /> : null;
Expand All @@ -58,14 +63,93 @@ function PCDModel({ src, onChange }) {
}
const { radius, center } = pcd?.geometry?.boundingSphere || {};
useEffect(() => {
onChange({ center: [center.x || 0, center.y || 0, center.z || 0], radius });
if (center) {
onChange({ center: [center.x || 0, center.y || 0, center.z || 0], radius });
}
}, [radius, center?.x, center?.y, center?.z]);
return pcd ? <primitive object={pcd} /> : null;
}

const highlight = new THREE.MeshLambertMaterial({
transparent: true,
opacity: 0.6,
color: 0xff0000,
depthTest: false
});

// references for the IFC Model implementation
// https://github.com/sebastianoscarlopez/ifc-react-fiber/tree/main
// https://github.com/IFCjs/hello-world/pull/54
function IFCModel({ src, sessionId, onChange, onUpdateInfo = () => {} }) {
const ifc = useLoader(IFCLoader, parseDevHostname(`${src}?_v_=${sessionId}`), (loader) => {
loader.ifcManager.setWasmPath(getGeoNodeLocalConfig('staticPath', '/static/') + 'mapstore/dist/js/web-ifc/');
loader.ifcManager.state.api.isWasmPathAbsolute = true;
});
useEffect(() => {
return () => {
if (ifc?.ifcManager?.dispose) {
ifc.ifcManager.dispose();
}
};
}, []);
const { radius, center } = ifc?.geometry?.boundingSphere || {};
useEffect(() => {
if (center) {
onChange({ center: [center.x || 0, center.y || 0, center.z || 0], radius });
}
}, [radius, center?.x, center?.y, center?.z]);
return ifc ? (
<primitive
object={ifc}
onPointerLeave={() => {
ifc.ifcManager.removeSubset(ifc.modelID, highlight);
onUpdateInfo({});
}}
onPointerMove={(event) => {
const { intersections = [] } = event || {};
const intersected = intersections[0];
const manager = intersected?.object?.ifcManager;
if (manager && intersected?.object?.geometry) {
const index = intersected.faceIndex;
const geometry = intersected.object.geometry;
const modelID = intersected.object.modelID;
const expressId = manager.getExpressId(geometry, index);
manager.createSubset({
modelID,
ids: [ expressId ],
material: highlight,
scene: intersected?.object?.parent,
removePrevious: true
});
manager.getItemProperties(modelID, expressId).
then((props) => {
onUpdateInfo({
x: event.x,
y: event.y,
properties: Object.keys(props).reduce((acc, key) => {
const value = isString(props[key]) || isNumber(props[key])
? props[key]
: props[key]?.value;
if (value === undefined || value === null) {
return acc;
}
return {
...acc,
[key]: value
};
}, {})
});
});
}
}}
/>
) : null;
}

const modelTypes = {
gltf: GLTFModel,
pcd: PCDModel
pcd: PCDModel,
ifc: IFCModel
};

function Scene3DViewer({
Expand All @@ -75,20 +159,51 @@ function Scene3DViewer({
// https://polyhaven.com/a/studio_small_03
environmentFiles = '/static/mapstore/img/studio_small_03_1k.hdr'
}) {
// for ifc model we need to append the sessionId to the src
// to avoid caching of the url
const [sessionId] = useState(Date.now());
const container = useRef();
const [boundingSphere, setBoundingSphere] = useState({
radius: 10,
center: [0, 0, 0]
});
const [info, setInfo] = useState({});
const Model = modelTypes[mediaType];
const containetClientRect = container?.current?.getBoundingClientRect();
function handleInfoPosition() {
const minSize = 768;
if (containetClientRect.width < minSize || containetClientRect.height < minSize) {
return {
left: 0,
top: 0,
transform: 'translateX(1rem) translateY(1rem)'
};
}
const left = info?.x ? info.x - containetClientRect.left : 0;
const top = info?.y ? info.y - containetClientRect.top : 0;
const translateX = left > containetClientRect.width / 2 ? 'translateX(calc(-100% - 1rem))' : 'translateX(1rem)';
const translateY = top > containetClientRect.height / 2 ? 'translateY(calc(-100% - 1rem))' : 'translateY(1rem)';
return {
left,
top,
transform: `${translateX} ${translateY}`
};
}
function getMaxPropertyWidth() {
const maxKeyLength = Object.keys(info.properties || {}).reduce((previous, current) => previous.length > current.length ? previous : current);
return maxKeyLength.length ? `${maxKeyLength.length * 0.5}rem` : 'auto';
}
return (
<div className="gn-media-scene-3d">
<div ref={container} className="gn-media-scene-3d">
<Suspense fallback={null}>
<Canvas>
<ambientLight intensity={0.5} />
<directionalLight color="white" intensity={0.5} position={[10, 10, 10]} />
<Suspense fallback={null}>
<Environment files={environmentFiles} />
</Suspense>
<Suspense fallback={<Loader />}>
<Model src={src} onChange={setBoundingSphere}/>
<Model sessionId={sessionId} src={src} onChange={setBoundingSphere} onUpdateInfo={(newInfo) => setInfo(newInfo)}/>
</Suspense>
<OrbitControls
makeDefault
Expand All @@ -100,6 +215,37 @@ function Scene3DViewer({
<PerspectiveCamera makeDefault fov={65} far={boundingSphere.radius * 12} position={[boundingSphere.center[0], boundingSphere.center[1], boundingSphere.radius * 2]}/>
</Canvas>
</Suspense>
{info?.properties && (
<div
className="shadow gn-media-scene-3d-info gn-details-info-fields"
style={{
position: 'absolute',
zIndex: 10,
padding: '0.25rem',
pointerEvents: 'none',
maxWidth: containetClientRect.width * 3 / 2,
wordBreak: 'break-word',
transition: '0.3s all',
minWidth: 300,
userSelect: 'none',
...handleInfoPosition()
}}
>
<div className="gn-media-scene-3d-info-bg" style={{ opacity: 0.85, position: 'absolute', top: 0, left: 0, width: '100%', height: '100%' }}/>
{Object.keys(info.properties).map((key) => {
return (
<div key={key} className="gn-details-info-row">
<div className="gn-details-info-label" style={{ width: getMaxPropertyWidth() }}>
{key}
</div>
<div className="gn-details-info-value" style={{ maxWidth: 'none'}}>
{info.properties[key]}
</div>
</div>
);
})}
</div>
)}
</div>
);
}
Expand Down
2 changes: 2 additions & 0 deletions geonode_mapstore_client/client/js/utils/FileUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const imageExtensions = ['jpg', 'jpeg', 'png'];
export const videoExtensions = ['mp4', 'mpg', 'avi', 'm4v', 'mp2', '3gp', 'flv', 'vdo', 'afl', 'mpga', 'webm'];
export const gltfExtensions = ['glb', 'gltf'];
export const pcdExtensions = ['pcd'];
export const ifcExtensions = ['ifc'];

/**
* check if a resource extension is supported for display in the media viewer
Expand All @@ -39,6 +40,7 @@ export const determineResourceType = extension => {
if (videoExtensions.includes(extension)) return 'video';
if (gltfExtensions.includes(extension)) return 'gltf';
if (pcdExtensions.includes(extension)) return 'pcd';
if (ifcExtensions.includes(extension)) return 'ifc';
return 'unsupported';
};

Expand Down
3 changes: 2 additions & 1 deletion geonode_mapstore_client/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@
"react-helmet": "6.1.0",
"react-intl": "2.3.0",
"three": "0.138.3",
"three-stdlib": "2.8.9"
"three-stdlib": "2.8.9",
"web-ifc-three": "0.0.125"
},
"mapstore": {
"output": "dist",
Expand Down
3 changes: 3 additions & 0 deletions geonode_mapstore_client/client/postCompile.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,14 @@ const distDirectory = 'dist';
});

// copy compiled files
fs.copySync(path.resolve(appDirectory, 'node_modules', 'web-ifc'), path.resolve(appDirectory, distDirectory, 'js', 'web-ifc'));
message.title('copy ifc files in dist folder');
fs.moveSync(path.resolve(appDirectory, distDirectory, 'ms-translations'), path.resolve(appDirectory, staticPath, 'ms-translations'), { overwrite: true });
message.title('copy ms-translations from MapStore Core');
fs.moveSync(path.resolve(appDirectory, distDirectory), path.resolve(appDirectory, staticPath, distDirectory), { overwrite: true });
message.title('copy dist folder to static/mapstore directory');


// create new version file
const versionString = `${name}-v${version}-${commit}`;
fs.writeFileSync(path.resolve(appDirectory, 'version.txt'), versionString);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
.background-color-var(@theme-vars[success]);
}
}
.gn-media-scene-3d-info-bg {
.background-color-var(@theme-vars[main-variant-bg]);
}
}

// **************
Expand Down

0 comments on commit 156b026

Please sign in to comment.