/**
A **Viewer** is a WebGL-based 3D viewer for the visualisation and evaluation of BIM models.
## Overview
<ul>
<li></li>
</ul>
## Example
In the example below we'll create a Viewer with a {{#crossLink "Camera"}}{{/crossLink}},
a {{#crossLink "CameraControl"}}{{/crossLink}} and a {{#crossLink "TeapotGeometry"}}{{/crossLink}},
which is used by an {{#crossLink "Object"}}{{/crossLink}}.
<br>Finally, we make the {{#crossLink "Camera"}}{{/crossLink}} orbit on each "tick" event emitted by the Viewer.
<iframe style="width: 600px; height: 400px" src="../../examples/viewer_Viewer.html"></iframe>
````javascript
// Create a Viewer
var viewer = new BIMSURFER.Viewer({
// ID of the DIV element
element: "myDiv"
});
// Create a Camera
var camera = new BIMSURFER.Camera(viewer, {
eye: [5, 5, -5]
});
// Create a CameraControl to control our Camera with mouse and keyboard
var cameraControl = new BIMSURFER.CameraControl(viewer, {
camera: camera
});
// Create a Geometry
var geometry = new BIMSURFER.TeapotGeometry(viewer, {
id: "myGeometry"
});
// Create an Object that uses the Geometry
var object1 = new BIMSURFER.Object(viewer, {
id: "myObject1",
type: "IfcCovering",
geometries: [ geometry ]
});
// Spin the camera
viewer.on("tick", function () {
camera.rotateEyeY(0.2);
});
````
@class Viewer
@module BIMSURFER
@constructor
@param [cfg] {*} Configs
@param [cfg.id] {String} Optional ID, unique among all components in the parent viewer, generated automatically when omitted.
@param [cfg.meta] {String:Object} Optional map of user-defined metadata to attach to this Object.
@param cfg.element {String|HTMLElement} ID or instance of a DIV element in the page.
@param cfg.bimServerApi {*} The BIMServer API.
*/
(function () {
"use strict";
BIMSURFER.Viewer = function (cfg) {
var self = this;
this.className = "BIMSURFER.Viewer";
// Event management
// Pub/sub
this._handleMap = new BIMSURFER.utils.Map(); // Subscription handle pool
this._locSubs = {}; // A [handle -> callback] map for each location name
this._handleLocs = {}; // Maps handles to loc names
this.props = {}; // Maps locations to publications
// Check arguments
cfg = cfg || {};
var element = cfg.element;
if (!element) {
throw "Param expected: element";
}
if (typeof element == 'string') {
element = document.getElementById(element);
}
/**
* The HTML element ocupied by the Viewer
*
* @property element
* @final
* @type {HTMLElement}
*/
this.element = element;
/**
* The BIMServer API
*
* @property bimServerApi
* @final
* @type {Object}
*/
this.bimServerApi = cfg.bimServerApi;
this.SYSTEM = this;
var canvasId = "canvas-" + BIMSURFER.math.createUUID();
var body = document.getElementsByTagName("body")[0];
var div = document.createElement('div');
var style = div.style;
style.height = "100%";
style.width = "100%";
style.padding = "0";
style.margin = "0";
style.background = "black";
style.float = "left";
//style.left = "0";
//style.top = "0";
// style.position = "absolute";
// style["z-index"] = "10000";
div.innerHTML += '<canvas id="' + canvasId + '" style="width: 100%; height: 100%; float: left; margin: 0; padding: 0;"></canvas>';
element.appendChild(div);
/**
* The HTML Canvas that this Viewer renders to. This is inserted into the element we configured this Viewer with.
* @property canvas
* @final
* @type {HTMLCanvasElement}
* @final
*/
this._canvas = document.getElementById(canvasId);
/**
* The SceneJS scene graph that renders 3D content for this Viewer.
* @property scene
* @final
* @type {SceneJS.Scene}
* @final
*/
this.scene = SceneJS.createScene({
canvasId: canvasId,
// Transparent canvas
// Less work for the GPU rendering all those background fragments.
// Let CSS do that work.
transparent: true,
nodes: [
// Node library, where we keep sharable
// asset nodes, such as geometries
{
type: "library",
id: "library"
},
// Viewing transform
{
type: "lookAt",
id: "theLookat",
nodes: [
// Projection transform
{
type: "camera",
id: "theCamera",
nodes: [
// Light sources
{
id: "lightsRoot",
lights: [],
nodes: [
// Origin translation
{
type: "translate",
id: "theOrigin",
nodes: [
// Content is appended below this node
{
id: "contentRoot"
}
]
}
]
}
]
}
]
}
]
});
/**
* ID of this Viewer
*
* @property id
* @final
* @type {String}
*/
this.id = this.scene.getId();
// Init events
var canvas = this.scene.getCanvas();
this.scene.on('tick',
function (params) {
self.fire('tick', {
time: params.time * 0.001,
elapsed: (params.time - params.prevTime) * 0.001
});
});
this._lookatNode = this.scene.getNode('theLookat');
this._lookatNode.on("matrix",
function (matrix) {
self.fire('viewMatrix', matrix);
});
this._cameraNode = this.scene.getNode('theCamera');
this._cameraNode.on("matrix",
function (matrix) {
self.fire('projMatrix', matrix);
});
this._originNode = this.scene.getNode('theOrigin');
// Pool where we'll keep all component IDs
this._componentIDMap = new BIMSURFER.utils.Map();
/**
* The {{#crossLink "Component"}}Components{{/crossLink}} within this Viewer, mapped to their IDs.
* @property components
* @final
* @type {{String:Component}}
*/
this.components = {};
/**
* Map of components that have an 'exclusive' property. This is used to ensure that
* only one of these component types is active within this Viewer at a time.
*/
this._onComponentActive = {};
/**
* The {{#crossLink "Component"}}Components{{/crossLink}} within this Viewer, mapped to their class names.
* @property classes
* @final
* @type {{String:{String:Component}}}
*/
this.classes = {};
/**
* The {{#crossLink "Component"}}Components{{/crossLink}} within this Viewer, mapped to their IFC type names.
* @property types
* @final
* @type {{String:{String:Component}}}
*/
this.types = {};
// Add components
var components = cfg.components;
if (components) {
var component;
var className;
var constructor;
for (var i = 0, len = components.length; i < len; i++) {
component = components[i];
className = component.className;
if (className) {
constructor = window[className];
if (constructor) {
// Adds component to this Viewer via #_addComponent
new constructor(this, component);
}
}
}
}
if (BIMSURFER.utils.isset(cfg, cfg.autoStart)) {
if (!BIMSURFER.Util.isset(cfg.autoStart.serverUrl, cfg.autoStart.serverUsername, cfg.autoStart.serverPassword, cfg.autoStart.projectOid)) {
console.error('Some autostart parameters are missing');
return;
}
var _this = this;
var BIMServer = new BIMSURFER.Server(this, cfg.autoStart.serverUrl, cfg.autoStart.serverUsername, cfg.autoStart.serverPassword, false, true, true, function () {
if (BIMServer.loginStatus != 'loggedin') {
_this.element.innerHTML = 'Something went wrong while connecting';
console.error('Something went wrong while connecting');
return;
}
var project = BIMServer.getProjectByOid(cfg.autoStart.projectOid);
project.loadScene((BIMSURFER.Util.isset(cfg.autoStart.revisionOid) ? cfg.autoStart.revisionOid : null), true);
});
}
/**
* Geometry loaders
* @property geometryLoaders
* @type {Array of }
* @final
*/
this.geometryLoaders = [];
// Start the loading loop
// This just runs forever, polling any loaders that exist on this viewer
this.scene.on("tick",
function () {
self.geometryLoaders.forEach(
function (geometryLoader) {
geometryLoader.process();
});
});
// Add components here
/**
* Canvas manager for this Viewer.
* @property canvas
* @final
* @type {BIMSURFER.Canvas}
*/
this.canvas = new BIMSURFER.Canvas(this);
/**
* Input handling for this Viewer.
* @property input
* @final
* @type {BIMSURFER.Input}
*/
this.input = new BIMSURFER.Input(this);
/**
* Cursor icon control for this Viewer.
* @property cursor
* @final
* @type {BIMSURFER.Cursor}
*/
this.cursor = new BIMSURFER.Cursor(this);
/**
* The default {{#crossLink "Camera"}}{{/crossLink}} for this Viewer.
*
* This {{#crossLink "Camera"}}{{/crossLink}} is active by default, and becomes inactive
* as soon as you activate some other {{#crossLink "Camera"}}{{/crossLink}} in this Viewer.
*
* Any components that you create for this Viewer, that require a {{#crossLink "Camera"}}{{/crossLink}},
* will fall back on this one by default.
*
* @property camera
* @final
* @type {BIMSURFER.Camera}
*/
this.camera = new BIMSURFER.Camera(this);
/**
* The number of {{#crossLink "Objects"}}{{/crossLink}} within this ObjectSet.
*
* @property numObjects
* @type Number
*/
this.numObjects = 0;
this._boundary = {xmin: 0.0, ymin: 0.0, zmin: 0.0, xmax: 0.0, ymax: 0.0, zmax: 0.0};
this._center = [0, 0, 0];
this._boundaryDirty = true;
this.origin = cfg.origin;
};
/**
* Adds a {{#crossLink "Component"}}{{/crossLink}} to this viewer.
*
* This is called within the constructors of {{#crossLink "Component"}}{{/crossLink}} subclasses.
*
* The {{#crossLink "Component"}}{{/crossLink}} is assigned a
* unique {{#crossLink "Component/id:property"}}{{/crossLink}} if it does not yet have one.
*
* @private
* @param {BIMSURFER.Component} component The Component to add.
*/
BIMSURFER.Viewer.prototype._addComponent = function (component) {
var id = component.id;
var className = component.className;
// Check for ID clash
if (id) {
if (this.components[id]) {
this.error("A component with this ID already exists in this Viewer: " + id);
return;
}
} else {
id = component.id = this._componentIDMap.addItem({});
}
// Add component to ID map
this.components[id] = component;
// Add component to className map
var classComponents = this.classes[className];
if (!classComponents) {
classComponents = this.classes[className] = {};
}
classComponents[id] = component;
// Add component to type map
if (component.type) {
var type = component.type;
var typeComponents = this.types[type];
if (!typeComponents) {
typeComponents = this.types[type] = {};
}
typeComponents[id] = component;
}
var self = this;
// When the component has an 'exclusive' property set true, then only one instance of that component
// type may be active within the Viewer at a time. When a component is activated, that has a true value
// for this flag, then any other active component of the same type will be deactivated first.
if (component.exclusive === true) {
if (component.active) {
self.deactivateOthers(component);
}
this._onComponentActive[component.id] = component.on("active",
function (active) {
if (active) {
self._deactivateOthers(component);
}
});
}
this._boundaryDirty = true;
/**
* Fired whenever a Component has been created within this Viewer.
* @event componentCreated
* @param {Component} value The component that was created
*/
this.fire("componentCreated", component, true);
};
// Deactivates all other components within this Viewer, that have same className as that given.
BIMSURFER.Viewer.prototype._deactivateOthers = function (component) {
this.withClasses([component.className],
function (otherComponent) {
if (otherComponent.id !== component.id) {
otherComponent.active = false;
}
});
};
/**
* Removes a {{#crossLink "Component"}}{{/crossLink}} from this Viewer.
*
* This is called within the destructors of {{#crossLink "Component"}}{{/crossLink}} subclasses.
*
* @private
* @param {BIMSURFER.Component} component The component to remove
*/
BIMSURFER.Viewer.prototype._removeComponent = function (component) {
var id = component.id;
var className = component.className;
if (!this.components[id]) {
console.warn("BIMSURFER.Viewer._removeComponent - Component with this ID is not within Viewer: " + id);
return;
}
delete this.components[id];
delete this.classes[className][id];
if (component.type) {
delete this.types[component.type][id];
}
this._boundaryDirty = true;
this._componentIDMap.removeItem(id);
if (component.exclusive === true) {
component.off(this._onComponentActive[component.id]);
delete this._onComponentActive[component.id];
}
/**
* Fired whenever a component within this Viewer has been destroyed.
* @event componentDestroyed
* @param {Component} value The component that was destroyed
*/
this.fire("componentDestroyed", component, true);
};
/**
* World-space origin.
*
* @property origin
* @final
* @type {*}
*/
Object.defineProperty(BIMSURFER.Viewer.prototype, "origin", {
get: function () {
return this._origin;
},
set: function (origin) {
this._origin = origin || [0, 0, 0];
this._originNode.setXYZ(this._origin);
this._boundaryDirty = true;
},
enumerable: true
});
/**
* This Viewer's view transformation matrix.
*
* @property viewMatrix
* @final
* @default [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]
* @type {Array of Number}
*/
Object.defineProperty(BIMSURFER.Viewer.prototype, "viewMatrix", {
get: function () {
return this._lookatNode.getMatrix();
},
enumerable: true
});
/**
* This Viewer's projection transformation matrix.
*
* @property projMatrix
* @final
* @default [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]
* @type {Array of Number}
*/
Object.defineProperty(BIMSURFER.Viewer.prototype, "projMatrix", {
get: function () {
return this._cameraNode.getMatrix();
},
enumerable: true
});
/**
* Boundary of all bounded components in this Viewer.
*
* @property boundary
* @final
* @type {*}
*/
Object.defineProperty(BIMSURFER.Viewer.prototype, "boundary", {
get: function () {
if (this._boundaryDirty) {
this._rebuildBoundary();
}
return this._boundary;
},
enumerable: true
});
/**
* Center of all bounded components in this Viewer.
*
* @property center
* @final
* @type {*}
*/
Object.defineProperty(BIMSURFER.Viewer.prototype, "center", {
get: function () {
if (this._boundaryDirty) {
this._rebuildBoundary();
}
return this._center;
},
enumerable: true
});
BIMSURFER.Viewer.prototype._rebuildBoundary = function () {
if (!this._boundaryDirty) {
return;
}
// For an empty selection, boundary is zero volume and centered at the origin
if (this.numObjects === 0) {
this._boundary.xmin = -1.0;
this._boundary.ymin = -1.0;
this._boundary.zmin = -1.0;
this._boundary.xmax = 1.0;
this._boundary.ymax = 1.0;
this._boundary.zmax = 1.0;
} else {
// Set boundary inside-out, ready to expand by each selected object
this._boundary.xmin = 1000000.0;
this._boundary.ymin = 1000000.0;
this._boundary.zmin = 1000000.0;
this._boundary.xmax = -1000000.0;
this._boundary.ymax = -1000000.0;
this._boundary.zmax = -1000000.0;
var component;
var boundary;
for (var componentId in this.components) {
if (this.components.hasOwnProperty(componentId)) {
component = this.components[componentId];
boundary = component.boundary;
if (boundary) {
if (boundary.xmin < this._boundary.xmin) {
this._boundary.xmin = boundary.xmin;
}
if (boundary.ymin < this._boundary.ymin) {
this._boundary.ymin = boundary.ymin;
}
if (boundary.zmin < this._boundary.zmin) {
this._boundary.zmin = boundary.zmin;
}
if (boundary.xmax > this._boundary.xmax) {
this._boundary.xmax = boundary.xmax;
}
if (boundary.ymax > this._boundary.ymax) {
this._boundary.ymax = boundary.ymax;
}
if (boundary.zmax > this._boundary.zmax) {
this._boundary.zmax = boundary.zmax;
}
}
}
}
}
this._center[0] = (this._boundary.xmax + this._boundary.xmin) * 0.5;
this._center[1] = (this._boundary.ymax + this._boundary.ymin) * 0.5;
this._center[2] = (this._boundary.zmax + this._boundary.zmin) * 0.5;
this._boundaryDirty = false;
};
/**
*
*/
BIMSURFER.Viewer.prototype.pick = function (x, y, options) {
var hit = this.scene.pick(x, y, options);
if (hit) {
var objectId = hit.name;
var object = this.components[objectId];
if (object) {
return {
object: object,
canvasPos: hit.canvasPos,
worldPos: hit.worldPos
}
}
}
};
/**
* Resizes the viewport and updates the aspect ratio
*
* @param {Number} width The new width in px
* @param {Number} height The new height in px
*/
BIMSURFER.Viewer.prototype.resize = function (width, height) {
return;
if (!this.canvas) {
// TODO: log
return;
}
jQuery(this.canvas).width(width).height(height);
if (BIMSURFER.Util.isset(this.canvas[0])) {
this.canvas[0].width = width;
this.canvas[0].height = height;
}
var cameraNode = this.scene.getNode("theCamera");
var optics = cameraNode.getOptics();
optics.aspect = this.canvas.width() / this.canvas.height();
cameraNode.setOptics(optics);
};
/**
* Iterates with a callback over Components of the given classes
*
* @param {String} classNames List of class names
* @param {Function} callback Callback called for each Component of the given classes
*/
BIMSURFER.Viewer.prototype.withClasses = function (classNames, callback) {
var className;
for (var i = 0, len = classNames.length; i < len; i++) {
className = classNames[i];
var components = this.classes[className];
if (components) {
for (var id in components) {
if (components.hasOwnProperty(id)) {
callback(components[id]);
}
}
}
}
};
/**
* Iterates with a callback over Components of the given IFC types
*
* @param {String} typeNames List of type names
* @param {Function} callback Callback called for each Component of the given types
*/
BIMSURFER.Viewer.prototype.withTypes = function (typeNames, callback) {
var typeName;
for (var i = 0, len = typeNames.length; i < len; i++) {
typeName = typeNames[i];
var components = this.types[typeName];
if (components) {
for (var id in components) {
if (components.hasOwnProperty(id)) {
callback(components[id]);
}
}
}
}
};
/**
* Shows an IFC type of a revision.
*
* @param {Array of String} typeNames Names of types to hide
* @param {BIMSURFER.ProjectRevision instance} revision The revision
*/
BIMSURFER.Viewer.prototype.showTypes = function (typeNames, revision) {
this.withTypes(typeNames,
function (component) {
component.active = true;
});
};
/**
* Hides an IFC type of a revision.
*
* @param {Array of String} typeNames Names of types to hide
* @param {BIMSURFER.ProjectRevision instance} revision The revision
*/
BIMSURFER.Viewer.prototype.hideTypes = function (typeNames, revision) {
this.withTypes(typeNames,
function (component) {
component.active = false;
});
};
/**
* Hides all the types of a revision
*
* @param {BIMSURFER.ProjectRevision} revision The revision to hide
*/
BIMSURFER.Viewer.prototype.hideRevision = function (revision) {
// var visibleTypes = revision.visibleTypes.slice(0);
// for (var i = 0; i < visibleTypes.length; i++) {
// this.hideType(visibleTypes[i], revision);
// }
};
/**
* Shows a revision
*
* @param {BIMSURFER.ProjectRevision} revision The revision to show
* @param {Array} [types] The types to show (default = BIMSURFER.constants.defaultTypes)
*/
BIMSURFER.Viewer.prototype.showRevision = function (revision, types) {
if (!types) {
types = [];
var defaultTypes = BIMSURFER.constants.defaultTypes;
if (!defaultTypes) {
this.warn("Property expected in BIMSURFER.constants: defaultTypes");
} else {
for (var i = 0; i < revision.ifcTypes.length; i++) {
if (defaultTypes.indexOf(revision.ifcTypes[i]) != -1) {
types.push(revision.ifcTypes[i]);
}
}
}
}
this.showType(types, revision);
};
/**
* Fires an event on this Viewer.
*
* Notifies existing subscribers to the event, retains the event to give to
* any subsequent notifications on that location as they are made.
*
* @method fire
* @param {String} event The event type name
* @param {Object} value The event
* @param {Boolean} [forget=false] When true, does not retain for subsequent subscribers
*/
BIMSURFER.Viewer.prototype.fire = function (event, value, forget) {
if (forget !== true) {
this.props[event] = value; // Save notification
}
var subsForLoc = this._locSubs[event];
var sub;
if (subsForLoc) { // Notify subscriptions
for (var handle in subsForLoc) {
if (subsForLoc.hasOwnProperty(handle)) {
sub = subsForLoc[handle];
sub.callback.call(sub.scope, value);
}
}
}
};
/**
* Subscribes to an event on this Viewer.
*
* The callback is be called with this Viewer as scope.
*
* @method on
* @param {String} event Publication event
* @param {Function} callback Called when fresh data is available at the event
* @param {Object} [scope=this] Scope for the callback
* @return {String} Handle to the subscription, which may be used to unsubscribe with {@link #off}.
*/
BIMSURFER.Viewer.prototype.on = function (event, callback, scope) {
var subsForLoc = this._locSubs[event];
if (!subsForLoc) {
subsForLoc = {};
this._locSubs[event] = subsForLoc;
}
var handle = this._handleMap.addItem(); // Create unique handle
subsForLoc[handle] = {
scope: scope || this,
callback: callback
};
this._handleLocs[handle] = event;
var value = this.props[event];
if (value) { // A publication exists, notify callback immediately
callback.call(scope || this, value);
}
return handle;
};
/**
* Cancels an event subscription that was previously made with {{#crossLink "Viewer/on:method"}}{{/crossLink}} or
* {{#crossLink "Viewer/once:method"}}{{/crossLink}}.
*
* @method off
* @param {String} handle Publication handle
*/
BIMSURFER.Viewer.prototype.off = function (handle) {
var event = this._handleLocs[handle];
if (event) {
delete this._handleLocs[handle];
var locSubs = this._locSubs[event];
if (locSubs) {
delete locSubs[handle];
}
this._handleMap.removeItem(handle); // Release handle
}
};
/**
* Subscribes to the next occurrence of the given event on this Viewer, then un-subscribes as soon as the event is handled.
*
* @method once
* @param {String} event Data event to listen to
* @param {Function(data)} callback Called when fresh data is available at the event
* @param {Object} [scope=this] Scope for the callback
*/
BIMSURFER.Viewer.prototype.once = function (event, callback, scope) {
var self = this;
var handle = this.on(event,
function (value) {
self.off(handle);
callback(value);
},
scope);
};
/**
* Logs a console debugging message for this View.
*
* The console message will have this format: *````[LOG] BIMSERVER.Viewer: <message>````*
*
* @method log
* @param {String} message The message to log
*/
BIMSURFER.Viewer.prototype.log = function (message) {
window.console.log("[LOG] BIMSERVER.Viewer: " + message);
};
/**
* Logs an error for this View to the JavaScript console.
*
* The console message will have this format: *````[ERROR] BIMSERVER.Viewer: <message>````*
*
* @method error
* @param {String} message The message to log
*/
BIMSURFER.Viewer.prototype.error = function (message) {
window.console.error("[ERROR] BIMSERVER.Viewer: " + message);
};
})();