FluxViewport.js

'use strict';

import * as THREE from 'three';
import EdgesHelper from './EdgesHelper.js';
import FluxRenderer from './FluxRenderer.js';
import FluxCameras from './FluxCameras.js';
import * as FluxJsonToThree from 'flux-json-to-three';
import {scene} from 'flux-modelingjs';
import * as constants from './controls/constants.js';
import * as print from './utils/debugPrint.js';

/**
 * UI widget to render 3D geometry.
 * This class provides all the interface you need to add the flux web
 * viewer to your SDK app. This allows you to interpret Flux json data
 * and render that geometry. You may also create any number of viewports
 * and they will share the finite number of WebGL contexts available
 * from the browser.<br>
 * The most commonly used functions are <a href="#setGeometryEntity">setGeometryEntity</a> (to set geometry to render)
 * and <a href="#.isKnownGeom">isKnownGeom</a> (determine if JSON is geometry) so you might want to start reading there. <br>
 * Note: If you are using <a href="https://community.flux.io/content/kbentry/2718/materials-1.html">Flux materials</a> that have the parameter roughness
 * set then you will need to configure your server to have a <a href="https://content-security-policy.com">content security
 * policy</a> that allows content from https://object-library.storage.googleapis.com
 * so that our environment texture images can be loaded.
 * @class FluxViewport
 * @param {Element}   domParent     The div container for the canvas
 * @param {Object}    optionalParams Object containing all other parameters
 * @param {Number}    optionalParams.width         The width of the canvas
 * @param {Number}    optionalParams.height        The height of the canvas
 * @param {String}    optionalParams.tessUrl       The url for making brep tessellation requests (overrides projectId) (Used when server is not flux.io)
 * @param {Enumeration}    optionalParams.selection       Whether to enable user selection
 * @param {String}    optionalParams.projectId     Id of a flux project (required to render breps)
 * @param {String}    optionalParams.token         The current flux auth token (required to render breps)
 */
export default function FluxViewport (domParent, optionalParams) {
    var width;
    var height;
    var tessUrl;
    var token;
    var selection = constants.Selection.NONE;
    if (optionalParams) {
        width = optionalParams.width;
        height = optionalParams.height;
        if (optionalParams.tessUrl) {
            tessUrl = optionalParams.tessUrl;
        } else if (optionalParams.projectId) {
            tessUrl = _getTessUrl(optionalParams.projectId);
        }
        token = optionalParams.token;
        if (optionalParams.selection) {
            selection = optionalParams.selection;
        }
    }

    var renderWidth = 100;//px
    if (width == null) {
        renderWidth = domParent.clientWidth;
    } else {
        renderWidth = Math.max(renderWidth, width);
    }

    var renderHeight = 100;//px
    if (height == null) {
        renderHeight = domParent.clientHeight;
    } else {
        renderHeight = Math.max(renderHeight, height);
    }

    if (!domParent) {
        throw new Error('domParent must be specified to FluxViewport');
    }

    this._sceneBuilder = new FluxJsonToThree.SceneBuilder(tessUrl, token);
    // Only allow merging when selection is disabled
    this._sceneBuilder.setAllowMerge(constants.Selection.NONE === selection);

    this._renderer = new FluxRenderer(domParent, renderWidth, renderHeight, selection);
    this._initCallback();

    // Make sure to render on mouse over in case the renderer has swapped contexts
    var _this = this;
    domParent.addEventListener('mouseenter', function(){
        _this.render();
    });

    // Cache of the Flux entity objects for downloading
    this._entities = null;

    this._latestSceneResults = null;

    // Track the last blob that was downloaded for memory cleanup
    this._downloadUrl = null;

    // Whether the viewport is locked on the current geometry and will automatically focus on new geometry when updating the entities
    this._autoFocus = true;

    // Track whether geometry is being converted, so we don't try two at once
    this.running = false;
}

FluxViewport.prototype = Object.create( THREE.EventDispatcher.prototype );
FluxViewport.prototype.constructor = FluxViewport;

/**
 * Build the url to use for tessellation requests to flux.
 * @private
 * @param  {String} projectId Hashed project id to use in url
 * @return {String}           Url to use for tessellation requests
 */
function _getTessUrl(projectId) {
    return 'https://flux.io/p/'+projectId+
           '/api/blockexec?block=flux-internal/parasolid/Parasolid';
}

/**
 * @summary Enumeration of edges rendering modes.
 * This determines whether edges will be shown when rendering the front face, back face or both.
 * Front edge rendering can be used to achieve hidden line rendering.<br>
 * Options are NONE, FRONT, BACK, BOTH
 * @return {Object} enumeration
 */
FluxViewport.getEdgesModes = function () {
    return EdgesHelper.EDGES_MODES;
};

/**
 * @summary Enumeration of selection modes.
 * This determines when selection events occur in response to mouse events.
 * Options are NONE, CLICK, HOVER
 * @return {Object} enumeration
 */
FluxViewport.getSelectionModes = function () {
   return constants.Selection;
};

/**
 * Name of the event fired when the camera changes
 *
 * This can be used to observe those changes and register a callback.<br>
 * For example:
 * fluxViewport.addEventListener(FluxViewport.getChangeEvent(), function() {});
 * @return {String} Event name
 */
FluxViewport.getChangeEvent = function () {
    return constants.Events.CHANGE;
};

/**
 * Enumeration of different event types
 * This can be used to differentiate events in the EventListener.
 * fluxViewport.addEventListener(FluxViewport.getChangeEvent(), function(e) {
 * FluxViewport.getEvents().SELECT === e.event;});
 * @return {Object} Enumeration object
 */
FluxViewport.getEvents = function () {
    return constants.Events;
};

/**
 * Determines whether the entity or list of entities contains geometry
 * @param  {Array.<Object>|Object}  entities Geometry data
 * @return {Boolean}          True for objects/lists containing geometry
 */
FluxViewport.isKnownGeom = function (entities) {
    return scene.isGeometry(entities);
};

//---- Class member functions

/**
 * Set up the callback to render when the camera changes
 * @private
 */
FluxViewport.prototype._initCallback = function() {
    var _this = this;
    this._renderer.addEventListener(constants.Events.CHANGE, function(event) {
        _this.dispatchEvent(event);
        _this.render();
    });
};

/**
 * Add a new plugin for user interaction controls.
 * See ViewportControls.js for more information.
 * @param  {ViewportControls} CustomControls A constructor that implements the controls interface.
 * @return {CustomControls}                The new instance
 */
FluxViewport.prototype.addControls = function(CustomControls) {
    return this._renderer.addControls(CustomControls);
};


/**
 * Get an object that maps from id string to Three.Object3D in the current scene
 * @return {Object}  Id to object scene map
 */
FluxViewport.prototype.getObjectMap = function() {
    return this._latestSceneResults.getObjectMap();
};

/**
 * Get the currently selected geometry as a list of id strings
 * @return {Array.<String>}  Current selection
 */
FluxViewport.prototype.getSelection = function() {
    var selectionMap = this._renderer.getSelection();
    var keys = Object.keys(selectionMap);
    var selection = [];
    for (var i=0;i<keys.length;i++) {
        var obj = selectionMap[keys[i]];
        if (obj != null) {
            selection.push(obj.userData.id);
        }
    }
    return selection;
};

/**
 * Define the material that is applied on selected objects
 * @param  {Object} data Flux json description of a material
 */
FluxViewport.prototype.setSelectionMaterial = function(data) {
    this._renderer.setSelectionMaterial(data);
};

/**
 * Set the currently selected geometry
 * @param {Array.<String>}  ids New selection
 */
FluxViewport.prototype.setSelection = function(ids) {
    if (ids == null || ids.constructor !== Array) return;
    var map = this._latestSceneResults.getObjectMap();
    var objects = ids.map(function (id) { return map[id];});
    this._renderer.setSelection(objects);
};

/**
 * Get the JSON representation of the objects with the given ids
 * @param  {Array.<String>} ids List of object ids
 * @return {Array.<Object>}     List of Flux JSON objects
 */
FluxViewport.prototype.getJson = function(ids) {
    if (ids == null || ids.constructor !== Array) return [];
    var map = this._latestSceneResults.getObjectMap();
    return ids.map(function (id) { return map[id].userData.data;});
};

/**
 * Actually render the geometry!<br>
 * This is called automatically when the camera changes.
 * You may call it on demand as needed when changing properties.
 * @memberOf FluxViewport
 */
FluxViewport.prototype.render = function() {
    this._renderer.doRender();
};

/**
 * Focus the camera on the current geometry
 * @param  {THREE.Object3D}  [obj] Object to focus on
 */
FluxViewport.prototype.focus = function(obj) {
    this._renderer.focus(obj);
};

/**
 * Restore the camera to a default location
 */
FluxViewport.prototype.homeCamera = function() {
    this._renderer.homeCamera();
};

/**
 * Whether to draw helpers (axis and grid)
 *
 * @param  {Boolean} visible False to hide them
 */
FluxViewport.prototype.setHelpersVisible = function(visible) {
    this._renderer.setHelpersVisible(visible);
};

/**
 * Set the viewport geomtery from a JSON string
 * @param {String} dataString The geometry to render formatted as JSON containing Flux entities
 * @return {Object} Promise to resolve after geometry is created
 */
FluxViewport.prototype.setGeometryJson = function(dataString) {
    var dataObj = JSON.parse(dataString);
    return this.setGeometryEntity(dataObj);
};

/**
 * Set the viewport geometry from a data object containing Flux entities.
 * See documentation for more explanation of <a href="https://community.flux.io/content/kbentry/2579/geometric-primitives.html">entities</a>
 * and <a href="https://community.flux.io/content/kbentry/3087/what-is-a-scene.html">scene primitives</a>
 * @param {Object} data The geometry entities to render
 * @return {Object} Promise to resolve after geometry is created
 */
FluxViewport.prototype.setGeometryEntity = function(data) {
    var _this = this;
    // The flow sends the same value twice, so we assume that requests
    // sent while there is already one pending are redundant
    // TODO(Kyle): This is a hack that we can remove once there are not always duplicate requests
    return new Promise(function (resolve, reject) {
        if (!_this.running) {
            _this.running = true;
            return _this._sceneBuilder.convert(data).then(function (results) {
                var object = results.getObject();
                _this._entities = object ? data : null;
                _this._updateModel(object);
                _this._latestSceneResults = results;
                _this.running = false;
                resolve(results);
            }).catch(function (err) {
                console.warn(err); // eslint-disable-line no-console
                _this.running = false;
            });
        } else {
            reject(new Error('Already running. You can only convert one entity at a time.'));
        }
    }).catch(function (err) {
        if (err.message.indexOf('running') === -1) {
            console.log(err); // eslint-disable-line no-console
        }
        throw err;
    });
};

/**
 * Change the geometry being rendered
 * @private
 * @param  {THREE.Object3D} newModel The new model to render
 * @param  {THREE.Object3D} oldModel The old model to remove
 */
FluxViewport.prototype._updateModel = function(newModel) {
    this._renderer.setModel(newModel);
    if (this._autoFocus) {
        this.focus(); // changing the controls will trigger a render
        this._autoFocus = false;
    } else {
        this.render();
    }
};

/**
 * Make serializable by pruning all references and building an object property tree
 * @return {Object} Data to stringify
 */
FluxViewport.prototype.toJSON = function() {
    var serializableState = {
        entities: this._entities,
        renderer: this._renderer.toJSON(),
        autoFocus: this._autoFocus
    };
    return serializableState;
};

/**
 * Take a data object and use it to update the viewport's internal state<br>
 * Warning this is async when it sets entities
 * @param  {Object} state The properties to set
 * @return {Promise} Completion promise
 */
FluxViewport.prototype.fromJSON = function(state) {
    if (!state) return Promise.resolve();
    var _this = this;
    if (state.entities) {
        return this.setGeometryEntity(state.entities).then(function () {
            _this._fromJSONHelper(state);
        });
    } else {
        this._fromJSONHelper(state);
        return Promise.resolve();
    }
};

/**
 * Rehydrate everything but the entities.
 * @private
 * @param  {Object} state Parameter data
 */
FluxViewport.prototype._fromJSONHelper = function(state) {
    if (state.renderer != null) {
        this._renderer.fromJSON(state.renderer);
    }
    if (state.autoFocus != null) {
        this._autoFocus = state.autoFocus;
    }
};

/**
 * Download all the geometry settings and raster image that are the state of this viewport.
 * Used for QA testing.
 * @param  {String} prefix File name prefix for download path
 */
FluxViewport.prototype.downloadState = function(prefix) {
    this._downloadJson(this.toJSON(), prefix);
    this._download(this._renderer.getGlCanvas().toDataURL('image/png'), prefix+'.png');
};

/**
 * Helper function to download some data from a url
 * @private
 * @param  {DOMString} dataUrl  The url containing the data to download
 * @param  {String} filename The name of the file when it downloads
 */
FluxViewport.prototype._download = function(dataUrl, filename) {
    var a = document.createElement('a');
    a.href = dataUrl;
    a.download = filename;
    a.click();
};

/**
 * Create a link and a temporary blob url to use to download from.
 * @private
 * @param  {Object} data   The serializable data to write as JSON
 * @param  {String} prefix The file name prefix
 */
FluxViewport.prototype._downloadJson = function(data, prefix) {
    if (this._downloadUrl) {
        window.URL.revokeObjectURL(this._downloadUrl);
    }
    var jsonString = JSON.stringify(data, null, 2);
    this._downloadUrl = window.URL.createObjectURL(new Blob([jsonString]), {type: 'text/json'});
    this._download(this._downloadUrl, prefix+'.json');
};

/**
 * Create a default 3 light rig on the renderer's scene.
 */
FluxViewport.prototype.setupDefaultLighting = function() {
    var lighting = new THREE.Object3D();
    lighting.name = 'Lights';

    //TODO(Kyle) non static lighting
    this._keyLight = new THREE.DirectionalLight();
    this._keyLight.position.set(60, 80, 50);
    this._keyLight.intensity = 2.95;
    lighting.add(this._keyLight);

    var backLight = new THREE.DirectionalLight();
    backLight.position.set(-250, 50, -200);
    backLight.intensity = 1.7;
    lighting.add(backLight);

    var fillLight = new THREE.DirectionalLight();
    fillLight.position.set(-500, -500, 0);
    fillLight.intensity = 0.9;
    lighting.add(fillLight);

    this._renderer.setLights(lighting);
};

//---- Pass through functions

/**
 * Set the size of the render canvas
 * @param {Number} width  Pixels
 * @param {Number} height Pixels
 */
FluxViewport.prototype.setSize = function(width, height) {
    this._renderer.setSize(width, height);
};

/**
 * Set the background color of the render canvas
 * @param {THREE.color} color Background color
 * @param {Number} alpha Opacity
 */
FluxViewport.prototype.setClearColor = function(color, alpha) {
    this._renderer.setClearColor(color, alpha);
};

/**
 * Set which camera view to use (ex perspective, top etc.).
 * @param {FluxCameras.VIEWS} view The new view mode
 */
FluxViewport.prototype.setView = function(view) {
    this._renderer.setView(view);
    this.focus();
};

/**
 * Return the views enumeration.
 * Allows you to change the view to perspective, top, etc.
 * @return {Object} Enumeration of view options for cameras
 */
FluxViewport.getViews = function() {
    return FluxCameras.VIEWS;
};

/**
 * Set the density of the exponential fog. Generally on the order of 0.0001
 * @param {Number} density How much fog
 */
FluxViewport.prototype.setFogDensity = function(density) {
    this._renderer._fog.density = density;
};

/**
 * Set the url of the tessellation service.
 * This can be used to replace the value if you did not set it on the constructor.
 * @param {String} newUrl The url of the tessellation server
 */
FluxViewport.prototype.setTessUrl = function(newUrl) {
    this._sceneBuilder.setTessUrl(newUrl);
};

/**
 * Set whether the viewport should focus the geometry when it is changed
 * @param {Boolean} focus Whether to auto focus
 */
FluxViewport.prototype.setAutoFocus = function(focus) {
    this._autoFocus = focus;
};

/**
 * Get whether the viewport will focus the geometry when it is changed
 * @return {Boolean} Whether to auto focus
 */
FluxViewport.prototype.getAutoFocus = function() {
    return this._autoFocus;
};

/**
 * Set the edges rendering mode for hidden line rendering
 * @param  {FluxViewport.EDGES_MODES} mode Whether to render front, back, both or none
 */
FluxViewport.prototype.setEdgesMode = function(mode) {
    this._renderer.setEdgesMode(mode);
};

/**
 * Get the canvas for use in QA scripts
 * @return {Canvas} WebGL canvas dom element
 */
FluxViewport.prototype.getGlCanvas = function() {
    return this._renderer.getGlCanvas();
};

/**
 * Turn on shadow rendering (not implemented)
 */
FluxViewport.prototype.activateShadows = function() {
    print.warn('Shadows are not implemented yet');
    // https://vannevar.atlassian.net/browse/LIB3D-97
};