src/webComponent/webComponent.js
import { BIMViewer } from '../../src/BIMViewer.js';
import { Server } from '../../src/server/Server.js';
import faRegularWoff from '@fortawesome/fontawesome-free/webfonts/fa-regular-400.woff2';
import faSolidWoff from '@fortawesome/fontawesome-free/webfonts/fa-solid-900.woff2';
import faBrandsWoff from '@fortawesome/fontawesome-free/webfonts/fa-brands-400.woff2';
import faRegularTtf from '@fortawesome/fontawesome-free/webfonts/fa-regular-400.ttf';
import faSolidTtf from '@fortawesome/fontawesome-free/webfonts/fa-solid-900.ttf';
import faBrandsTtf from '@fortawesome/fontawesome-free/webfonts/fa-brands-400.ttf';
import fontStyle from '@fortawesome/fontawesome-free/css/all.css';
import bimViewerStyles from '../../xeokit-bim-viewer.css';
const headStyleInnerHtml = `
@font-face {
font-family: 'Font Awesome 6 Free';
font-display: block;
font-weight: 900;
src: url(${faSolidWoff}) format("woff2"), url(${faSolidTtf}) format("truetype");
}
@font-face {
font-family: 'Font Awesome 6 Free';
font-display: block;
font-weight: 400;
src: url(${faRegularWoff}) format("woff2"), url(${faRegularTtf}) format("truetype");
}
@font-face {
font-family: 'Font Awesome 5 Free';
font-display: block;
font-weight: 900;
src: url(${faSolidWoff}) format("woff2"), url(${faSolidTtf}) format("truetype");
}
@font-face {
font-family: 'Font Awesome 5 Free';
font-display: block;
font-weight: 400;
src: url(${faRegularWoff}) format("woff2"), url(${faRegularTtf}) format("truetype");
}
@font-face {
font-family: 'Font Awesome 6 Brands';
font-display: block;
font-weight: 400;
src: url(${faBrandsWoff}) format("woff2"), url(${faBrandsTtf}) format("truetype");
}
@font-face {
font-family: 'Font Awesome 5 Brands';
font-display: block;
font-weight: 400;
src: url(${faBrandsWoff}) format("woff2"), url(${faBrandsTtf}) format("truetype");
}
`
const innerHtml = `
<input type="checkbox" id="explorer_toggle" />
<label for="explorer_toggle" class="xeokit-i18n explorer_toggle_label xeokit-btn fas fa-2x fa-sitemap"
data-xeokit-i18ntip="toolbar.toggleExplorer" data-tippy-content="Toggle explorer"></label>
<input type="checkbox" id="inspector_toggle" />
<label id="inspector_toggle_label" for="inspector_toggle"
class="xeokit-i18n inspector_toggle_label xeokit-btn fas fa-info-circle fa-2x"
data-xeokit-i18ntip="toolbar.toggleProperties" data-tippy-content="Toggle properties"></label>
<div id="myExplorer"></div>
<div id="myToolbar"></div>
<div id="myInspector"></div>
<div id="myViewer">
<canvas id="myCanvas"></canvas>
<canvas id="myNavCubeCanvas"></canvas>
</div>
<div class="xeokit-marker"></div>
<style type="text/css">
${fontStyle}
${bimViewerStyles}
:host {
display: block;
position: relative;
width: 100%;
height: 100%;
font-family: "Roboto", sans-serif;
font-size: 14px;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
margin: 0;
overflow: clip;
background: #f2f2f2;
}
:host #myViewer {
display: flex;
width: 100%;
height: 100%;
align-items: stretch;
z-index: 10000;
overflow: clip;
position: absolute;
left: 0;
right: 0;
--left: 0;
--right: 0;
transition: all 300ms ease-in-out;
}
:host #myToolbar {
font-family: 'Font Awesome 6 Free';
min-width: 400px;
top: 0;
align-items: center;
justify-content: center;
padding: 0;
z-index: 100000;
pointer-events: none;
position: absolute;
left: 95px;
transition: all 300ms ease-in-out;
}
:host #myCanvas {
width: 100%;
height: 100%;
cursor: default;
pointer-events: all;
margin: 0;
position: relative;
}
:host #myNavCubeCanvas {
position: absolute;
width: 200px;
height: 200px;
bottom: 0;
right: 0;
z-index: 200000;
}
:host #myExplorer {
position: absolute;
height: 100%;
color: #fff;
background: #03103f;
overflow: auto;
border-right: 2px solid #212529;
padding: 0px;
padding-bottom: 100px;
padding-left: 15px;
user-select: none;
top: 0;
left: -460px;
z-index: 10;
width: 460px;
transition: all 300ms ease-in-out;
}
:host #explorer_toggle {
display: none;
}
:host .explorer_toggle_label {
position: absolute;
top: 10px;
left: 20px;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
flex: 1 1 auto;
color: #03103f;
background-color: white;
text-align: center;
vertical-align: middle;
border: 2px solid #1d2453;
padding: 0.375rem 0.75rem;
border-radius: 0.25rem;
-webkit-appearance: button;
overflow: visible;
margin: 0 2px 0 0;
box-sizing: border-box;
align-items: flex-start;
pointer-events: all;
z-index: 100000;
transition: all 300ms ease-in-out;
}
:host .explorer_toggle_label:hover {
cursor: pointer;
}
:host #explorer_toggle:checked + .explorer_toggle_label {
left: 480px;
color: #fff;
background-color: #03103f;
border-color: #03103f;
}
:host #explorer_toggle:checked ~ #myExplorer {
left: 0;
}
:host #explorer_toggle:checked ~ #myViewer {
left: 460px;
--left: 460;
width: calc(100% - (var(--left) + var(--right)));
}
:host #explorer_toggle:checked ~ #myToolbar {
left: 555px;
}
:host #explorer_toggle:not(:checked) + .explorer_toggle_label {
left: 20px;
color: #fff;
background-color: #03103f;
border-color: #03103f;
}
:host #explorer_toggle:not(:checked) ~ #myExplorer {
left: -460px;
}
:host #explorer_toggle:not(:checked) ~ #myViewer {
left: 0;
--left: 0;
width: calc(100% - var(--right));
}
:host #explorer_toggle:not(:checked) ~ #myToolbar {
left: 95px;
}
:host #myInspector {
position: absolute;
height: 100%;
color: #fff;
background: #03103f;
overflow: auto;
border-left: 2px solid #212529;
padding: 0;
top: 0;
right: -360px;
z-index: 40;
width: 358px;
transition: all 300ms ease-in-out;
}
:host #inspector_toggle {
display: none;
}
:host .inspector_toggle_label {
position: absolute;
top: 10px;
right: 20px;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
flex: 1 1 auto;
color: #03103f;
background-color: white;
text-align: center;
vertical-align: middle;
border: 2px solid #1d2453;
padding: 0.375rem 0.75rem; /* FIXME */
border-radius: 0.25rem;
-webkit-appearance: button;
overflow: visible;
margin: 0 2px 0 0; /* FIXME */
box-sizing: border-box;
align-items: flex-start;
pointer-events: all;
z-index: 100000;
transition: all 300ms ease-in-out;
}
:host .inspector_toggle_label:hover {
cursor: pointer;
}
:host #inspector_toggle:checked + .inspector_toggle_label {
right: 380px;
color: #fff;
background-color: #03103f;
border-color: #03103f;
}
:host #inspector_toggle:checked ~ #myViewer {
--right: 360;
right: 360px;
width: calc(100% - (var(--left) + var(--right)));
}
:host #inspector_toggle:checked ~ #myInspector {
right: 0;
--right: 0;
}
:host #inspector_toggle:not(:checked) ~ #myInspector {
right: -380px;
--right: -380px;
}
:host #inspector_toggle:not(:checked) ~ #myViewer {
--right: 0;
right: 0;
width: calc(100% - var(--left));
}
:host .xeokit-camera-pivot-marker {
display: none !important;
}
:host .xeokit-marker {
color: #ffffff;
position: absolute;
width: 15px;
height: 15px;
border-radius: 100%;
border: 1px solid #ebebeb;
background: #444444;
box-shadow: 3px 3px 5px 1px #212529;
z-index: 10000;
pointer-events: none;
display: none;
}
</style>
`
class BimViewerWebComponent extends HTMLElement {
static get observedAttributes() {
return [
"projectid",
"modelid",
"datadir",
"tab",
"configs",
"openexplorer",
"enableeditmodels"
];
}
constructor() {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.innerHTML = innerHtml;
this.bimViewer = null;
this.projectId = "";
this.modelId = "";
this.dataDir = "/app/data";
this.tab = "";
this.configs = "";
this.openExplorer = false;
this.enableEditModels = false;
this._debounceTimeout = null;
this._pendingAttributes = new Map();
const canvas = this.shadowRoot.getElementById("myCanvas");
canvas.addEventListener("wheel", (e) => {
e.preventDefault();
});
}
/**
* Register a custom HTMLElement.
*
* @param {String} name Element's name, defaults to 'xeokit-bim-viewer', must be lowercase and contain at least one hyphen.
*/
static register(name = "xeokit-bim-viewer") {
customElements.define(name, this);
}
dispatchEvent(event) {
super.dispatchEvent(event);
const attrName = 'on' + event.type;
const handler = this.getAttribute(attrName);
if (handler) {
try {
const func = new Function('event', handler);
func.call(this, event);
} catch (e) {
console.error('Error executing inline event handler:', e);
}
}
}
connectedCallback() {
const style = document.createElement('style');
style.innerHTML = headStyleInnerHtml;
document.getElementsByTagName('head')[0].appendChild(style);
this.projectId = this.getAttribute("projectId");
this.modelId = this.getAttribute("modelId");
this.openExplorer = this.getAttribute("openExplorer");
this.setExplorerOpen(this.openExplorer === "true");
this.enableEditModels = this.getAttribute("enableEditModels") === "true";
this.dataDir = this.getAttribute("dataDir") || "./app/data";
const server = new Server({
dataDir: this.dataDir
});
const bimViewer = new BIMViewer(server, {
enableMeasurements: true,
canvasElement: this.shadowRoot.getElementById("myCanvas"), // WebGL canvas
keyboardEventsElement: this.shadowRoot, // Optional, defaults to this.shadowRoot
explorerElement: this.shadowRoot.getElementById("myExplorer"), // Left panel
toolbarElement: this.shadowRoot.getElementById("myToolbar"), // Toolbar
inspectorElement: this.shadowRoot.getElementById("myInspector"), // Right panel
navCubeCanvasElement: this.shadowRoot.getElementById("myNavCubeCanvas"),
busyModelBackdropElement: this.shadowRoot.getElementById("myViewer"),
enableEditModels: this.enableEditModels,
containerElement: this.shadowRoot
});
bimViewer.viewer.cameraControl.on("picked", (pickResult) => {
if (pickResult && pickResult.entity) {
const objectId = pickResult.entity.id;
const metaObject = bimViewer.viewer.metaScene.metaObjects[objectId];
if (metaObject) {
this.dispatchEvent(new CustomEvent('objectSelected', {
detail: metaObject,
bubbles: true,
composed: true
}));
}
}
});
bimViewer.setConfigs({
"showSpaces": false, // Default
"selectedGlowThrough": true,
"highlightGlowThrough": true,
"dtxEnabled": true // Enable data texture scene representation for models - may be slow on low-spec GPUs
});
bimViewer.on("openExplorer", () => {
this.setExplorerOpen(true);
});
bimViewer.on("openInspector", () => {
this.setInspectorOpen(true);
});
bimViewer.on("addModel", (event) => { // "Add" selected in Models tab's context menu
console.log("addModel: " + JSON.stringify(event, null, "\t"));
});
bimViewer.on("editModel", (event) => { // "Edit" selected in Models tab's context menu
console.log("editModel: " + JSON.stringify(event, null, "\t"));
});
bimViewer.on("deleteModel", (event) => { // "Delete" selected in Models tab's context menu
console.log("deleteModel: " + JSON.stringify(event, null, "\t"));
});
this.bimViewer = bimViewer;
this.configs = this.getAttribute("configs");
if (this.configs) {
const configNameVals = this.configs.split(/,(?![^\[\]]*\])/);
for (let i = 0, len = configNameVals.length; i < len; i++) {
const configNameValStr = configNameVals[i];
const configNameVal = configNameValStr.split(":");
const configName = configNameVal[0];
const configVal = configNameVal[1];
bimViewer.setConfig(configName, configVal);
}
}
if (!this.projectId) {
console.log("No projectId attribute found");
return;
}
bimViewer.loadProject(this.projectId, () => {
this.modelId = this.getAttribute("modelId");
if (this.modelId) {
bimViewer.loadModel(this.modelId);
}
this.tab = this.getAttribute("tab");
if (this.tab) {
bimViewer.openTab(this.tab);
}
},
(errorMsg) => {
console.error(errorMsg);
});
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === "projectid") {
console.log("projectid changed: " + newValue);
this.projectId = newValue;
if (!this.bimViewer || !this.projectId) {
return;
}
this.bimViewer.loadProject(this.projectId, () => {
this.modelId = this.getAttribute("modelId");
if (this.modelId) {
this.bimViewer.loadModel(this.modelId);
}
this.tab = this.getAttribute("tab");
if (this.tab) {
this.bimViewer.openTab(this.tab);
}
},
(errorMsg) => {
console.error(errorMsg);
});
} else if (name === "modelid") {
this.modelId = newValue;
if (!this.bimViewer || !this.modelId || !this.projectId) {
return;
}
this.bimViewer.loadProject(this.projectId, () => {
this.bimViewer.loadModel(this.modelId);
this.tab = this.getAttribute("tab");
if (this.tab) {
this.bimViewer.openTab(this.tab);
}
},
(errorMsg) => {
console.error(errorMsg);
});
}
}
setExplorerOpen(explorerOpen) {
const toggle = this.shadowRoot.getElementById("explorer_toggle");
if (toggle) {
toggle.checked = explorerOpen;
}
}
setInspectorOpen(inspectorOpen) {
const toggle = this.shadowRoot.getElementById("inspector_toggle");
if (toggle) {
toggle.checked = inspectorOpen;
}
}
}
export default BimViewerWebComponent;