Home Reference Source

viewer/bimserverviewer.js

import * as mat4 from "./glmatrix/mat4.js";
import * as vec3 from "./glmatrix/vec3.js";

import { GeometryLoader } from "./geometryloader.js";
import { BimserverGeometryLoader } from "./bimservergeometryloader.js";
import { Viewer } from "./viewer.js";
import { DefaultRenderLayer } from "./defaultrenderlayer.js";
import { TilingRenderLayer } from "./tilingrenderlayer.js";
import { VertexQuantization } from "./vertexquantization.js";
import { Executor } from "./executor.js";
import { Stats } from "./stats.js";
import { DefaultSettings } from "./defaultsettings.js";
import { Utils } from "./utils.js";
import { DataInputStream } from "./datainputstream.js";
import { AbstractViewer } from "./abstractviewer.js";

/*
 * The main class you instantiate when creating a viewer that will be loading data from a BIMserver.
 * This will eventually become a public API
 */

/**
 * @ignore
 */
export class BimServerViewer extends AbstractViewer {
	constructor(settings, canvas, width, height, stats) {
		super(settings, canvas, width, height, stats);
	}

	loadRevisionByRoid(api, roid) {
        return this.loadRevisionsByRoids(api, [roid]);
	}

	unloadRevisionByRoid(roid) {
		const layerSet = this.layers.get(roid);
		if (layerSet) {
			for (var layer of layerSet) {
				this.viewer.renderLayers.delete(layer);
			}
		}
		this.layers.delete(roid);
		this.viewer.dirty = 2;

		// TODO probably a good idea to also shrink the model bounds
	}

	loadRevisionsByRoids(api, roids) {
		return this.viewer.init().then(() => {
			return new Promise((resolve, reject) => {
				api.call("ServiceInterface", "listBoundingBoxes", {
					roids: roids
				}, (bbs) => {
					if (bbs.length > 1) {
						this.settings.regionSelector(bbs).then((bb) => {
							this.genDensityThreshold(api, roids, bb).then(resolve);
						});
					} else {
						this.genDensityThreshold(api, roids, bbs[0]).then(resolve);
					}
				});
			});
		});
	}

	loadRevision(api, revision) {
		return this.loadRevisionByRoid(api, revision.oid);
	}

	/*
	 * This will load a BIMserver project. The given argument must be a Project object that is returned by the BIMserver JavaScript API.
	 * 
	 * In later stages much more control will be given to the user, for now the stategy is:
	 * - If this project has no subprojects, we will simply load the latest revision of the project (if available)
	 * - If this project has subprojects, all latest revisions of all subprojects _that have no subprojects_ will be loaded
	 * 
	 */
	loadModel(api, project, gltfBuffer) {

		var defaultRenderLayer = new DefaultRenderLayer(this.viewer, this.geometryDataIdsToReuse);

		return new Promise((resolve, reject) => {
			this.totalStart = performance.now();

			this.viewer.init().then(() => {
				api.call("ServiceInterface", "listBoundingBoxes", {
					roids: [project.lastRevisionId]
				}, (bbs) => {
					if (bbs.length > 1) {
						this.settings.regionSelector(bbs).then((bb) => {
							this.genDensityThreshold(api, [project.lastRevisionId], bb).then(resolve);
						});
					} else {
						this.genDensityThreshold(api, [project.lastRevisionId], bbs[0]).then(resolve);
					}
				});
			});
		});
	}

	genDensityThreshold(api, roids, bb) {
		var roid = roids[0];
		return new Promise((resolve, reject) => {
			api.call("ServiceInterface", "getDensityThreshold", {
				roids: roids,
				nrTriangles: this.viewer.settings.triangleThresholdDefaultLayer,
				excludedTypes: this.settings.excludedTypes
			}, (densityAtThreshold) => {
				this.densityAtThreshold = densityAtThreshold;
				this.densityThreshold = densityAtThreshold.density;
				var nrPrimitivesBelow = densityAtThreshold.trianglesBelow;
				var nrPrimitivesAbove = densityAtThreshold.trianglesAbove;

				api.call("ServiceInterface", "getRevision", {
					roid: roid
				}, (revision) => {
					this.internalLoadRevision(api, revision, nrPrimitivesBelow, nrPrimitivesAbove).then(resolve);
				});
			});
		});
	}

	/*
	 * Private method
	 */
	internalLoadRevision(api, revision, nrPrimitivesBelow, nrPrimitivesAbove) {
		return new Promise((resolve, reject) => {

			this.revisionId = revision.oid;

			this.viewer.stats.setParameter("Models", "Models to load", 1);

			var requests = [
				["ServiceInterface", "getTotalBounds", {
					roids: [revision.oid]
				}],
				["ServiceInterface", "getTotalUntransformedBounds", {
					roids: [revision.oid]
				}]
			];

			if (this.settings.gpuReuse) {
				requests.push(["ServiceInterface", "getGeometryDataToReuse", {
					roids: [revision.oid],
					excludedTypes: this.settings.excludedTypes,
					trianglesToSave: 0
				}]);
			}

			for (var croid of revision.concreteRevisions) {
				requests.push(["ServiceInterface", "getModelBoundsUntransformedForConcreteRevision", {
					croid: croid
				}]);
			}
			for (var croid of revision.concreteRevisions) {
				requests.push(["ServiceInterface", "getModelBoundsForConcreteRevision", {
					croid: croid
				}]);
			}

			api.multiCall(requests, (responses) => {

				var totalBounds = responses[0].result;
				var totalBoundsUntransformed = responses[1].result;
			
				if (this.settings.gpuReuse) {
					this.geometryDataIdsToReuse = new Set(responses[2].result);
				} else {
					this.geometryDataIdsToReuse = new Set(); // TODO later make this null, nicer
				}


				var add = this.settings.gpuReuse ? 3 : 2;
				var modelBoundsUntransformed = new Map();
				for (var i = 0; i < (responses.length - add) / 2; i++) {
					modelBoundsUntransformed.set(revision.concreteRevisions[i], responses[i + add].result);
				}
				var modelBoundsTransformed = new Map();
				for (var i = 0; i < (responses.length - add) / 2; i++) {
					modelBoundsTransformed.set(revision.concreteRevisions[i], responses[(responses.length - add) / 2 + i + add].result);
				}

				var bounds = [
					totalBounds.min.x,
					totalBounds.min.y,
					totalBounds.min.z,
					totalBounds.max.x,
					totalBounds.max.y,
					totalBounds.max.z,
				];

				// globalTranslationVector is a translation vector that puts the complete model close to 0, 0, 0
				if (this.viewer.globalTranslationVector == null) {
					this.viewer.globalTranslationVector = vec3.fromValues(
						-(bounds[0] + (bounds[3] - bounds[0]) / 2),
						-(bounds[1] + (bounds[4] - bounds[1]) / 2),
						-(bounds[2] + (bounds[5] - bounds[2]) / 2));
				}

				if (this.settings.quantizeVertices || this.settings.loaderSettings.quantizeVertices) {
					if (this.viewer.vertexQuantization == null) {
						this.viewer.vertexQuantization = new VertexQuantization(this.settings);
					}
					for (var croid of modelBoundsUntransformed.keys()) {
						this.viewer.vertexQuantization.generateUntransformedMatrices(croid, modelBoundsUntransformed.get(croid));
					}
					this.viewer.vertexQuantization.generateMatrices(totalBounds, totalBoundsUntransformed, this.viewer.globalTranslationVector);
				}

				this.viewer.stats.inc("Primitives", "Primitives to load (L1)", nrPrimitivesBelow);
				this.viewer.stats.inc("Primitives", "Primitives to load (L2)", nrPrimitivesAbove);

				var min = vec3.fromValues(bounds[0], bounds[1], bounds[2]);
				var max = vec3.fromValues(bounds[3], bounds[4], bounds[5]);
				vec3.add(min, min, this.viewer.globalTranslationVector);
				vec3.add(max, max, this.viewer.globalTranslationVector);
				this.viewer.setModelBounds([min[0], min[1], min[2], max[0], max[1], max[2]]);

				// TODO This is very BIMserver specific, clutters the code, should move somewhere else (maybe BimserverGeometryLoader)
				var fieldsToInclude = ["indices"];
				fieldsToInclude.push("colorPack");
				if (this.settings.loaderSettings.quantizeNormals) {
					if (this.settings.loaderSettings.prepareBuffers) {
						fieldsToInclude.push("normals");
						fieldsToInclude.push("normalsQuantized");
					} else {
						fieldsToInclude.push("normalsQuantized");
					}
				} else {
					fieldsToInclude.push("normals");
				}
				if (this.settings.loaderSettings.quantizeVertices) {
					if (this.settings.loaderSettings.prepareBuffers) {
						fieldsToInclude.push("vertices");
						fieldsToInclude.push("verticesQuantized");
					} else {
						fieldsToInclude.push("verticesQuantized");
					}
				} else {
					fieldsToInclude.push("vertices");
				}
				if (this.settings.loaderSettings.generateLineRenders) {
					fieldsToInclude.push("lineIndices");
				}
				if (!this.settings.loaderSettings.useObjectColors) {
					fieldsToInclude.push("colorsQuantized");
					fieldsToInclude.push("colorsQuantized");
				}

				var promise = Promise.resolve();

				const layerSet = new Set();
				this.layers.set(revision.oid, layerSet);

				if (this.viewer.settings.defaultLayerEnabled && nrPrimitivesBelow) {
					var defaultRenderLayer = new DefaultRenderLayer(this.viewer, this.geometryDataIdsToReuse);
					layerSet.add(defaultRenderLayer);
					this.viewer.renderLayers.add(defaultRenderLayer);

					defaultRenderLayer.setProgressListener((nrTrianglesLoaded, nrLinesLoaded) => {
						var percentage = 100 * nrTrianglesLoaded / nrPrimitivesBelow;
						this.updateProgress(percentage);
					});

					promise = this.loadDefaultLayer(api, defaultRenderLayer, revision.oid, fieldsToInclude);
				}

				promise.then(() => {
					this.viewer.dirty = 2;
					var tilingPromise = Promise.resolve();
					if (this.viewer.settings.tilingLayerEnabled && nrPrimitivesAbove > 0) {
						var tilingRenderLayer = new TilingRenderLayer(this.viewer, this.geometryDataIdsToReuse, bounds);
						layerSet.add(tilingRenderLayer);
						this.viewer.renderLayers.add(tilingRenderLayer);

						tilingPromise = this.loadTilingLayer(api, tilingRenderLayer, revision, bounds, fieldsToInclude);
					}
					tilingPromise.then(() => {
						this.viewer.stats.setParameter("Loading time", "Total", performance.now() - this.totalStart);
						if (this.viewer.bufferSetPool != null) {
							this.viewer.bufferSetPool.cleanup();
						}
						this.viewer.dirty = 2;

						resolve();
					});
				});
			});
		});
	}

	loadDefaultLayer(api, defaultRenderLayer, roid, fieldsToInclude) {
		//		document.getElementById("progress").style.display = "block";
		var startLayer1 = performance.now();
		//debugger;

		var start = performance.now();
		var executor = new Executor(4);

		const loaderSettings = JSON.parse(JSON.stringify(this.settings.loaderSettings)); // copy

		loaderSettings.globalTranslationVector = Utils.toArray(this.viewer.globalTranslationVector);

		var query = {
			doublebuffer: false,
			type: {
				name: "IfcProduct",
				includeAllSubTypes: true,
				exclude: this.settings.excludedTypes
			},
			tiles: {
				ids: [0],
				densityLowerThreshold: this.densityThreshold,
				densityUpperThreshold: -1,
				reuseLowerThreshold: -1,
				geometryDataToReuse: this.geometryDataIdsToReuse ? Array.from(this.geometryDataIdsToReuse) : null,
				maxDepth: 0
			},
			include: {
				type: "IfcProduct",
				field: "geometry",
				include: {
					type: "GeometryInfo",
					field: "data",
					include: {
						type: "GeometryData",
						fieldsDirect: fieldsToInclude
					}
				}
			},
			loaderSettings: loaderSettings
		};

		if (this.settings.loaderSettings.quantizeVertices) {
			query.loaderSettings.vertexQuantizationMatrix = this.viewer.vertexQuantization.vertexQuantizationMatrixWithGlobalTranslation;
		}

		var geometryLoader = new BimserverGeometryLoader(0, api, defaultRenderLayer, [roid], this.settings.loaderSettings, null, this.stats, this.settings, query, null, defaultRenderLayer.gpuBufferManager);
		if (this.settings.loaderSettings.quantizeVertices) {
			geometryLoader.unquantizationMatrix = this.viewer.vertexQuantization.inverseVertexQuantizationMatrixWithGlobalTranslation;
		}
		defaultRenderLayer.registerLoader(geometryLoader.loaderId);
		executor.add(geometryLoader).then(() => {
			defaultRenderLayer.done(geometryLoader.loaderId);

			this.viewer.stats.inc("Models", "Models loaded", 1);
			
		});

		executor.awaitTermination().then(() => {
			this.viewer.stats.setParameter("Loading time", "Layer 1", performance.now() - start);
			defaultRenderLayer.completelyDone();
			this.viewer.stats.requestUpdate();
			this.viewer.dirty = 2;
		});
		return executor.awaitTermination();
	}
}