src/explorer/ModelsExplorer.js
import {math, XKTLoaderPlugin} from "@xeokit/xeokit-sdk/dist/xeokit-sdk.es.js";
import {Controller} from "../Controller.js";
import {ModelsContextMenu} from "../contextMenus/ModelsContextMenu.js";
const tempVec3a = math.vec3();
/**
* Custom data access strategy for {@link XKTLoaderPlugin}.
* @private
*/
class BIMViewerDataSource {
constructor(server) {
this._server = server;
}
setProjectId(projectId) {
this._projectId = projectId;
}
setModelId(modelId) {
this._modelId = modelId;
}
getManifest(src, ok, error) {
this._server.getSplitModelManifest(this._projectId, this._modelId, src, ok, error);
}
getMetaModel(src, ok, error) {
this._server.getSplitModelMetadata(this._projectId, this._modelId, src, ok, error);
}
getXKT(src, ok, error) {
this._server.getSplitModelGeometry(this._projectId, this._modelId, src, ok, error)
}
}
/** @private */
class ModelsExplorer extends Controller {
constructor(parent, cfg) {
super(parent, cfg);
if (!cfg.modelsTabElement) {
throw "Missing config: modelsTabElement";
}
if (!cfg.unloadModelsButtonElement) {
throw "Missing config: unloadModelsButtonElement";
}
if (!cfg.modelsElement) {
throw "Missing config: modelsElement";
}
this._enableAddModels = !!cfg.enableEditModels;
this._modelsTabElement = cfg.modelsTabElement;
this._loadModelsButtonElement = cfg.loadModelsButtonElement;
this._unloadModelsButtonElement = cfg.unloadModelsButtonElement;
this._addModelButtonElement = cfg.addModelButtonElement;
this._modelsElement = cfg.modelsElement;
this._modelsTabButtonElement = this._modelsTabElement.querySelector(".xeokit-tab-btn");
if (!this._modelsTabButtonElement) {
throw "Missing DOM element: ,xeokit-tab-btn";
}
this._dataSource = new BIMViewerDataSource(this.server);
this._xktLoader = new XKTLoaderPlugin(this.viewer, {
dataSource: this._dataSource
});
this._modelsContextMenu = new ModelsContextMenu({
enableEditModels: cfg.enableEditModels,
hideOnAction: true
});
this._modelsInfo = {};
this._numModels = 0;
this._numModelsLoaded = 0;
this._projectId = null;
}
setObjectColors(objectColors) {
this._xktLoader.objectDefaults = objectColors;
}
loadProject(projectId, done, error) {
this.server.getProject(projectId, (projectInfo) => {
this.unloadProject();
this._projectId = projectId;
this._modelsInfo = {};
this._numModels = 0;
this._parseProject(projectInfo, done);
if (this._numModelsLoaded < this._numModels) {
this._loadModelsButtonElement.classList.remove("disabled");
}
if (this._numModelsLoaded > 0) {
this._unloadModelsButtonElement.classList.remove("disabled");
}
if (this._enableAddModels) {
this._addModelButtonElement.classList.remove("disabled");
}
}, (errMsg) => {
this.error(errMsg);
if (error) {
error(errMsg);
}
});
}
_parseProject(projectInfo, done) {
this._buildModelsMenu(projectInfo);
this._parseViewerConfigs(projectInfo);
this._parseViewerContent(projectInfo, () => {
this._parseViewerState(projectInfo, () => {
done();
});
});
}
_buildModelsMenu(projectInfo) {
var html = "";
const modelsInfo = projectInfo.models || [];
this._modelsInfo = {};
this._numModels = modelsInfo.length;
for (let i = 0, len = modelsInfo.length; i < len; i++) {
const modelInfo = modelsInfo[i];
this._modelsInfo[modelInfo.id] = modelInfo;
html += "<div class='xeokit-form-check'>";
html += "<input id='" + modelInfo.id + "' type='checkbox' value=''><span id='span-" + modelInfo.id + "' class='disabled'>" + modelInfo.name + "</span>";
html += "</div>";
}
this._modelsElement.innerHTML = html;
for (let i = 0, len = modelsInfo.length; i < len; i++) {
const modelInfo = modelsInfo[i];
const modelId = modelInfo.id;
const checkBox = document.getElementById("" + modelId);
const span = document.getElementById("span-" + modelId);
checkBox.addEventListener("click", () => {
if (checkBox.checked) {
this.loadModel(modelId);
} else {
this.unloadModel(modelInfo.id);
}
});
span.addEventListener("click", () => {
const model = this.viewer.scene.models[modelId];
const modelLoaded = (!!model);
if (!modelLoaded) {
this.loadModel(modelId);
} else {
this.unloadModel(modelInfo.id);
}
});
span.oncontextmenu = (e) => {
this._modelsContextMenu.context = {
bimViewer: this.bimViewer,
viewer: this.viewer,
modelId: modelId
};
this._modelsContextMenu.show(e.pageX, e.pageY);
e.preventDefault();
};
}
}
_parseViewerConfigs(projectInfo) {
const viewerConfigs = projectInfo.viewerConfigs;
if (viewerConfigs) {
this.bimViewer.setConfigs(viewerConfigs);
}
}
_parseViewerContent(projectInfo, done) {
const viewerContent = projectInfo.viewerContent;
if (!viewerContent) {
done();
return;
}
this._parseModelsLoaded(viewerContent, () => {
done();
});
}
_parseModelsLoaded(viewerContent, done) {
const modelsLoaded = viewerContent.modelsLoaded;
if (!modelsLoaded || (modelsLoaded.length === 0)) {
done();
return;
}
this._loadNextModel(modelsLoaded.slice(0), done);
}
_loadNextModel(modelsLoaded, done) {
if (modelsLoaded.length === 0) {
done();
return;
}
const modelId = modelsLoaded.pop();
this.loadModel(modelId,
() => { // Done
this._loadNextModel(modelsLoaded, done);
},
() => { // Error - recover and attempt to load next model
this._loadNextModel(modelsLoaded, done);
});
}
_parseViewerState(projectInfo, done) {
const viewerState = projectInfo.viewerState;
if (!viewerState) {
done();
return;
}
this.bimViewer.setViewerState(viewerState, done);
}
unloadProject() {
if (!this._projectId) {
return;
}
const models = this.viewer.scene.models;
for (var modelId in models) {
if (models.hasOwnProperty(modelId)) {
const model = models[modelId];
model.destroy();
}
}
this._modelsElement.innerHTML = "";
this._numModelsLoaded = 0;
this._loadModelsButtonElement.classList.add("disabled");
this._unloadModelsButtonElement.classList.add("disabled");
if (this._enableAddModels) {
this._addModelButtonElement.classList.add("disabled");
}
const lastProjectId = this._projectId;
this._projectId = null;
this.fire("projectUnloaded", {
projectId: lastProjectId
});
}
getLoadedProjectId() {
return this._projectId;
}
getModelIds() {
return Object.keys(this._modelsInfo);
}
loadModel(modelId, done, error) {
if (!this._projectId) {
const errMsg = "No project currently loaded";
this.error(errMsg);
if (error) {
error(errMsg);
}
return;
}
const modelInfo = this._modelsInfo[modelId];
if (!modelInfo) {
const errMsg = "Model not in currently loaded project";
this.error(errMsg);
if (error) {
error(errMsg);
}
return;
}
this.bimViewer._busyModal.show(`${this.viewer.localeService.translate("busyModal.loading") || "Loading"} ${modelInfo.name}`);
const externalMetadata = this.bimViewer.getConfig("externalMetadata");
if (externalMetadata && !modelInfo.manifest) {
this.server.getMetadata(this._projectId, modelId, (json) => {
this._loadGeometry(modelId, modelInfo, json, done, error);
},
(errMsg) => {
this.bimViewer._busyModal.hide();
this.error(errMsg);
if (error) {
error(errMsg);
}
});
} else {
this._loadGeometry(modelId, modelInfo, null, done, error);
}
}
_loadGeometry(modelId, modelInfo, json, done, error) {
const modelLoaded = () => {
const checkbox = document.getElementById("" + modelId);
checkbox.checked = true;
this._numModelsLoaded++;
this._unloadModelsButtonElement.classList.remove("disabled");
if (this._numModelsLoaded < this._numModels) {
this._loadModelsButtonElement.classList.remove("disabled");
} else {
this._loadModelsButtonElement.classList.add("disabled");
}
if (this._numModelsLoaded === 1) { // Jump camera to view-fit first model loaded
this._jumpToInitialCamera();
this.fire("modelLoaded", modelId);
this.bimViewer._busyModal.hide();
if (done) {
done();
}
} else {
this.fire("modelLoaded", modelId);
this.bimViewer._busyModal.hide();
if (done) {
done();
}
}
};
const loadError = (errMsg) => {
this.bimViewer._busyModal.hide();
this.error(errMsg);
if (error) {
error(errMsg);
}
};
if (modelInfo.manifest) {
// Load multi-part split model;
// Uses the BIMViewerDataSource, which then uses the BIMViewer's Server strategy
this._dataSource.setProjectId(this._projectId);
this._dataSource.setModelId(modelId);
const model = this._xktLoader.load({
id: modelId,
manifestSrc: modelInfo.manifest,
excludeUnclassifiedObjects: true,
origin: modelInfo.origin || modelInfo.position,
scale: modelInfo.scale,
rotation: modelInfo.rotation,
matrix: modelInfo.matrix,
edges: (modelInfo.edges !== false),
saoEnabled: modelInfo.saoEnabled,
pbrEnabled: modelInfo.pbrEnabled,
backfaces: modelInfo.backfaces,
globalizeObjectIds: modelInfo.globalizeObjectIds,
reuseGeometries: (modelInfo.reuseGeometries !== false)
});
model.on("loaded", modelLoaded);
model.on("error", loadError);
} else {
// Load single XKT/Metamodel file model;
// Uses the BIMViewer's Server strategy directly
this.server.getGeometry(this._projectId, modelId, (arraybuffer) => {
const model = this._xktLoader.load({
id: modelId,
metaModelData: json,
xkt: arraybuffer,
excludeUnclassifiedObjects: true,
origin: modelInfo.origin || modelInfo.position,
scale: modelInfo.scale,
rotation: modelInfo.rotation,
matrix: modelInfo.matrix,
edges: (modelInfo.edges !== false),
saoEnabled: modelInfo.saoEnabled,
pbrEnabled: modelInfo.pbrEnabled,
backfaces: modelInfo.backfaces,
globalizeObjectIds: modelInfo.globalizeObjectIds,
reuseGeometries: (modelInfo.reuseGeometries !== false)
});
model.on("loaded", modelLoaded);
model.on("error", loadError);
}, loadError);
}
}
_jumpToInitialCamera() {
const viewer = this.viewer;
const scene = viewer.scene;
const aabb = scene.getAABB(scene.visibleObjectIds);
const diag = math.getAABB3Diag(aabb);
const center = math.getAABB3Center(aabb, tempVec3a);
const camera = scene.camera;
const fitFOV = camera.perspective.fov;
const dist = Math.abs(diag / Math.tan(45 * math.DEGTORAD));
const dir = math.normalizeVec3((camera.yUp) ? [-0.5, -0.7071, -0.5] : [-1, 1, -1]);
const up = math.normalizeVec3((camera.yUp) ? [-0.5, 0.7071, -0.5] : [-1, 1, 1]);
viewer.cameraControl.pivotPos = center;
viewer.cameraControl.planView = false;
viewer.cameraFlight.jumpTo({
look: center,
eye: [center[0] - (dist * dir[0]), center[1] - (dist * dir[1]), center[2] - (dist * dir[2])],
up: up,
orthoScale: diag * 1.1
});
}
unloadModel(modelId) {
const model = this.viewer.scene.models[modelId];
if (!model) {
this.error("Model not loaded: " + modelId);
return;
}
model.destroy();
const checkbox = document.getElementById("" + modelId);
checkbox.checked = false;
const span = document.getElementById("span-" + modelId);
this._numModelsLoaded--;
if (this._numModelsLoaded > 0) {
this._unloadModelsButtonElement.classList.remove("disabled");
} else {
this._unloadModelsButtonElement.classList.add("disabled");
}
if (this._numModelsLoaded < this._numModels) {
this._loadModelsButtonElement.classList.remove("disabled");
} else {
this._loadModelsButtonElement.classList.add("disabled");
}
this.fire("modelUnloaded", modelId);
}
unloadAllModels() {
const models = this.viewer.scene.models;
const modelIds = Object.keys(models);
for (var i = 0, len = modelIds.length; i < len; i++) {
const modelId = modelIds[i];
this.unloadModel(modelId);
}
}
getNumModelsLoaded() {
return this._numModelsLoaded;
}
_getLoadedModelIds() {
return Object.keys(this.viewer.scene.models);
}
isModelLoaded(modelId) {
return (!!this.viewer.scene.models[modelId]);
}
getModelsInfo() {
return this._modelsInfo;
}
getModelInfo(modelId) {
return this._modelsInfo[modelId];
}
setEnabled(enabled) {
if (!enabled) {
this._modelsTabButtonElement.classList.add("disabled");
this._unloadModelsButtonElement.classList.add("disabled");
} else {
this._modelsTabButtonElement.classList.remove("disabled");
this._unloadModelsButtonElement.classList.remove("disabled");
}
}
/** @private */
destroy() {
super.destroy();
this._xktLoader.destroy();
}
}
export {ModelsExplorer};