Source: renderer.js

const THREE = require('three');
const ResizeSensor = require('css-element-queries/src/ResizeSensor');
/**
 * Create a Zinc 3D renderer in the container provided.
 * The primary function of a Zinc 3D renderer is to display the current
 * scene (@link Scene} set to the renderer and each scene may contain as 
 * many geometries, glyphset and other primitives as the system can support.
 * Zinc.Renderer also allows additional scenes to be displayed.
 * 
 * @param {Object} containerIn - Container to create the renderer on.
 * @class
 * @author Alan Wu
 * @return {Renderer}
 */
exports.Renderer = function (containerIn) {

	let container = containerIn;
	
	const stats = 0;
	
	let renderer = undefined;
	let currentScene = undefined;

	//myGezincGeometriestains a tuple of the threejs mesh, timeEnabled, morphColour flag, unique id and morph
	const clock = new THREE.Clock(false);
	this.playAnimation = true;
  /* default animation update rate, rate is 1000 and duration 
    is default to 6000, 6s to finish a full animation */
	let playRate = 1000;
	let preRenderCallbackFunctions = [];
  let preRenderCallbackFunctions_id = 0;
  let postRenderCallbackFunctions = [];
	let postRenderCallbackFunctions_id  = 0;
	let animated_id = undefined;
	let cameraOrtho = undefined, sceneOrtho = undefined, logoSprite = undefined;
	let sceneMap = [];
	let additionalActiveScenes = [];
	let scenesGroup = new THREE.Group();
	let canvas = undefined;
  let sensor = undefined;
  let isRendering = false;
	const _this = this;
	const currentSize = [0, 0];
	const currentOffset = [0, 0];
	
	this.getDrawingWidth = () => {
		if (container) {
			return container.clientWidth;
		} else if (canvas)
			if (typeof canvas.clientWidth !== 'undefined')
				return Math.round(canvas.clientWidth);
			else
				return Math.round(canvas.width);
		return 0;
	}
	
	this.getDrawingHeight = () => {
		if (container) {
			return container.clientHeight;
		} else if (canvas)
			if (typeof canvas.clientHeight !== 'undefined')
				return Math.round(canvas.clientHeight);
			else
				return Math.round(canvas.height);
		return 0;
	}
	
	/** 
	 * Call this to resize the renderer, this is normally call automatically.
	 */
	this.onWindowResize = () => {
		currentScene.onWindowResize();
		const width = this.getDrawingWidth();
		const height = this.getDrawingHeight();
		if (renderer != undefined) {
			let localRect = undefined;
			if (container) {
				localRect = container.getBoundingClientRect();
				renderer.setSize(width, height);
			} else if (canvas) {
				if (typeof canvas.getBoundingClientRect !== 'undefined') {
					localRect = canvas.getBoundingClientRect();
					canvas.width = width;
					canvas.height = height;
					renderer.setSize(width, height, false);
				} else {
					renderer.setSize(width, height, false);
					
				}
			}
			if (localRect) {
				currentOffset[0] = localRect.left;
				currentOffset[1] = localRect.top;
			}
			const target = new THREE.Vector2();
			renderer.getSize(target);
			currentSize[0] = target.x;
			currentSize[1] = target.y;
		}
	}
	
	/**
	 * Initialise the renderer and its visualisations.
	 */
	this.initialiseVisualisation = parameters => {
	  parameters = parameters || {};
	  if (parameters['antialias'] === undefined) {
      let onMobile = false;
      try {
        if( /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ) {
          onMobile = true;
        }
      }
      catch(err) {
        onMobile = false;
      }
      if (onMobile)
        parameters['antialias'] = false;
      else
        parameters['antialias'] = true;
	  }
	  if (parameters["canvas"]) {
		  container = undefined;
		  canvas = parameters["canvas"];
	  }
    renderer = new THREE.WebGLRenderer(parameters);
    if (container !== undefined) {
		  container.appendChild( renderer.domElement );
    }
	  renderer.setClearColor( 0xffffff, 1);
	  if (canvas && canvas.style) {
		  canvas.style.height = "100%";
		  canvas.style.width = "100%";
    }
    renderer.autoClear = false;
	  const scene = this.createScene("default");
	  this.setCurrentScene(scene);
	}
	
	/**
	 * Get the current scene on display.
	 * @return {Zinc.Scene};
	 */
	this.getCurrentScene = () => {
		return currentScene;
	}
	
	/**
	 * Set the current scene on display.
	 * 
	 * @param {Zinc.Scene} sceneIn - The scene to be set, only scene created by this instance
	 * of ZincRenderer is supported currently.
	 */
	this.setCurrentScene = sceneIn => {
		if (sceneIn) {
			this.removeActiveScene(sceneIn);
			const oldScene = currentScene;
			currentScene = sceneIn;
			if (oldScene) {
				oldScene.setInteractiveControlEnable(false);
			}
			currentScene.setInteractiveControlEnable(true);
			currentScene.setAdditionalScenesGroup(scenesGroup);
			this.onWindowResize();
		}
	}
	
	/**
	 * Return scene with the matching name if scene with that name has been created.
	 * 
	 * @param {String} name - Name to match
	 * @return {Zinc.Scene}
	 */
	this.getSceneByName = name => {
		return sceneMap[name];
	}
	
	/**
	 * Create a new scene with the provided name if scene with the same name exists,
	 * return undefined.
	 * 
	 * @param {String} name - Name of the scene to be created.
	 * @return {Zinc.Scene}
	 */
	this.createScene = name => {
		if (sceneMap[name] != undefined){
			return undefined;
		} else {
			let new_scene = undefined;
			if (canvas)
				new_scene = new (require('./scene').Scene)(canvas, renderer);
			else
				new_scene = new (require('./scene').Scene)(container, renderer);
			sceneMap[name] = new_scene;
			new_scene.sceneName = name;
			return new_scene;
		}
	}
	
	const updateOrthoScene = () => {
		if (logoSprite != undefined) {
			const material = logoSprite.material;
			if (material.map) {
				const width = this.getDrawingWidth();
				const height = this.getDrawingHeight();
				const calculatedWidth = (width - material.map.image.width)/2;
				const calculatedHeight = (-height + material.map.image.height)/2;
				logoSprite.position.set(calculatedWidth, calculatedHeight, 1 );
			}
		}
	};
	
	const updateOrthoCamera = () => {
		if (cameraOrtho != undefined) {
			const width = this.getDrawingWidth();
			const height = this.getDrawingHeight();
			cameraOrtho.left = -width / 2;
			cameraOrtho.right = width / 2;
			cameraOrtho.top =  height / 2;
			cameraOrtho.bottom = -height / 2;
			cameraOrtho.updateProjectionMatrix();
		}
	};
	
	/**
	 * Reset the viewport of the current scene to its original state.
	 */
	this.resetView = () => {
		currentScene.resetView();
	}
	
	/**
	 * Adjust zoom distance to include all primitives in scene and also the additional scenes
	 * but the lookat direction and up vectors will remain constant.
	 */
	this.viewAll = () => {
		if (currentScene) {	
			const boundingBox = currentScene.getBoundingBox();
			if (boundingBox) {
			    for(let i = 0; i < additionalActiveScenes.length; i++) {
			        const boundingBox2 = additionalActiveScenes[i].getBoundingBox();
			        if (boundingBox2) {
			        	boundingBox.union(boundingBox2);
			        }
			    }
				currentScene.viewAllWithBoundingBox(boundingBox);
			}
		}
	}
	
	/**
	 * Load a legacy model(s) format with the provided URLs and parameters. This only loads the geometry
	 * without any of the metadata. Therefore, extra parameters should be provided. This should be
	 * called from {@link Zinc.Scene}.
	 * 
	 * @deprecated
	 */
	this.loadModelsURL = (urls, colours, opacities, timeEnabled, morphColour, finishCallback) => {
		currentScene.loadModelsURL(urls, colours, opacities, timeEnabled, morphColour, finishCallback);
	}
	
	const loadView = viewData => {
		currentScene.loadView(viewData);
	};
	
	/**
	 * Load the viewport from an external location provided by the url. This should be
	 * called from {@link Zinc.Scene};
	 * @param {String} URL - address to the file containing viewport information.
	 * @deprecated
	 */
	this.loadViewURL = url => {
		currentScene.loadViewURL(url);
	}
	
	/**
	 * Load a legacy file format containing the viewport and its model file from an external 
	 * location provided by the url. Use the new metadata format with
	 * {@link Zinc.Scene#loadMetadataURL} instead. This should be
	 * called from {@link Zinc.Scene};
	 * 
	 * @param {String} URL - address to the file containing viewport and model information.
	 * @deprecated
	 */
	this.loadFromViewURL = (jsonFilePrefix, finishCallback) => {
		currentScene.loadFromViewURL(jsonFilePrefix, finishCallback);
	}
	
	this.updateDirectionalLight = () => {
		currentScene.updateDirectionalLight();
	}
  
  let runAnimation = () => {
    if (isRendering) {
      animated_id = requestAnimationFrame( runAnimation );
      this.render();
    } else {
      cancelAnimationFrame(animated_id);
      animated_id = undefined;
    }
  }

	/**
	 * Stop the animation and renderer to get into the render loop.
	 */
	this.stopAnimate = () => {
    if (isRendering) {
      clock.stop();
      isRendering = false;
    }
	}

	/**
	 * Start the animation and begin the rendering loop.
	 */
	this.animate = () => {
    if (!isRendering) {
      clock.start();
      isRendering = true;
      runAnimation();
    }
	}

	const prevTime = Date.now();
	
	/**
	 * Add a callback function which will be called everytime before the renderer renders its scene.
	 * @param {Function} callbackFunction - callbackFunction to be added.
	 * 
	 * @return {Number}
	 */
	this.addPreRenderCallbackFunction = callbackFunction => {
		preRenderCallbackFunctions_id = preRenderCallbackFunctions_id + 1;
		preRenderCallbackFunctions[preRenderCallbackFunctions_id] = callbackFunction;
		return preRenderCallbackFunctions_id;
	}
	
	/**
	 * Remove a callback function that is previously added to the scene.
	 * @param {Number} id - identifier of the previously added callback function.
	 */
	this.removePreRenderCallbackFunction = id => {
		if (id in preRenderCallbackFunctions) {
   			delete preRenderCallbackFunctions[id];
		}
	}
  
	/**
	 * Add a callback function which will be called everytime after the renderer renders its scene.
	 * @param {Function} callbackFunction - callbackFunction to be added.
	 * 
	 * @return {Number}
	 */
	this.addPostRenderCallbackFunction = callbackFunction => {
		postRenderCallbackFunctions_id = postRenderCallbackFunctions_id + 1;
		postRenderCallbackFunctions[postRenderCallbackFunctions_id] = callbackFunction;
		return postRenderCallbackFunctions_id;
	}
	
	/**
	 * Remove a callback function that is previously added to the scene.
	 * @param {Number} id - identifier of the previously added callback function.
	 */
	this.removePostRenderCallbackFunction = id => {
		if (id in postRenderCallbackFunctions) {
   			delete postRenderCallbackFunctions[id];
		}
	}

	/**
	 * Get the current play rate, playrate affects how fast an animated object animates.
	 * Also see {@link Zinc.Scene#duration}.
	 */
	this.getPlayRate = () => {
		return playRate;
	}
	
	/**
	 * Set the current play rate, playrate affects how fast an animated object animates.
	 * @param {Number} PlayRateIn - value to set the playrate to.
	 * Also see {@link Zinc.Scene#duration}.
	 */
	this.setPlayRate = playRateIn => {
		playRate = playRateIn;
	}
	
	this.getCurrentTime = () => {
		return currentScene.getCurrentTime();
	}
	
	
	/**
	 * Get the current play rate, playrate affects how fast an animated object animates.
	 * Also see {@link Zinc.Scene#duration}.
	 */
	this.setMorphsTime = time => {
		currentScene.setMorphsTime(time);
	}
	
	/**
	 * Get {Zinc.Geoemtry} by its id. This should be called from {@link Zinc.Scene};
	 * 
	 * @depreacted
	 * @return {Zinc.Geometry}
	 */
	this.getZincGeometryByID = id => {
		return currentScene.getZincGeometryByID(id);
	}	
	
	/**
	 * Add {Three.Object} to the current scene.
	 */
	this.addToScene = object => {
		currentScene.addObject(object)
	}
	
	/**
	 * Add {Three.Object} to the ortho scene, objects added to the ortho scene are rendered in
	 * normalised coordinates and overlay on top of current scene.  
	 * 
	 */
	this.addToOrthoScene = object => {
		if (sceneOrtho == undefined)
			sceneOrtho = new THREE.Scene();
		if (cameraOrtho == undefined) {
			const width = this.getDrawingWidth();
			const height = this.getDrawingHeight();
			cameraOrtho = new THREE.OrthographicCamera( -width / 2,
					width / 2, height/ 2, -height / 2, 1, 10 );
			cameraOrtho.position.z = 10;
		}
		sceneOrtho.add(object)
	}
	
	const createHUDSprites = logoSprite => {
		return texture => {
			texture.needsUpdate = true;
			const material = new THREE.SpriteMaterial( { map: texture } );
			const imagewidth = material.map.image.width;
			const imageheight = material.map.image.height;
			logoSprite.material = material;
			logoSprite.scale.set( imagewidth, imageheight, 1 );
			const width = this.getDrawingWidth();
			const height = this.getDrawingHeight();
			logoSprite.position.set( (width - imagewidth)/2, (-height + imageheight)/2, 1 );
			this.addToOrthoScene(logoSprite);
		};
	};
	
	this.addLogo = () => {
		logoSprite = new THREE.Sprite();
		const logo = THREE.ImageUtils.loadTexture(
				"images/abi_big_logo_transparent_small.png", undefined, createHUDSprites(logoSprite));
	}
	
	/**
	 * Render the current and all additional scenes. It will first update all geometries and glyphsets
	 * in scenes, clear depth buffer and render the ortho scene, call the preRenderCallbackFunctions stack
	 * , render the scenes then postRenderCallback.
	 */
	this.render = () => {
		if (!sensor) {
			if (container) {
				if (container.clientWidth > 0 && container.clientHeight > 0)
					sensor = new ResizeSensor(container, this.onWindowResize);
			} else if (canvas) {
				if (canvas.width > 0 && canvas.height > 0)
					sensor = new ResizeSensor(canvas, this.onWindowResize);
			}
		}
		const delta = clock.getDelta();
		currentScene.renderGeometries(playRate, delta, this.playAnimation);
	    for(let i = 0; i < additionalActiveScenes.length; i++) {
	        const sceneItem = additionalActiveScenes[i];
	        sceneItem.renderGeometries(playRate, delta, this.playAnimation);
	    }
		if (cameraOrtho != undefined && sceneOrtho != undefined) {
			renderer.clearDepth();
			renderer.render( sceneOrtho, cameraOrtho );
		}
    for (let key in preRenderCallbackFunctions) {
      if (preRenderCallbackFunctions.hasOwnProperty(key)) {
        preRenderCallbackFunctions[key].call();
      }
    }
    currentScene.render(renderer);
    for (let key in postRenderCallbackFunctions) {
      if (postRenderCallbackFunctions.hasOwnProperty(key)) {
        postRenderCallbackFunctions[key].call();
      }
    }
	}
	
	/**
	 * Get the internal {@link Three.Renderer}, to gain access to ThreeJS APIs.
	 */
	this.getThreeJSRenderer = () => {
		return renderer;
	}
	
	/**
	 * Check if a scene is currently active.
	 * @param {Zinc.Scene} sceneIn - Scene to check if it is currently
	 * rendered.
	 */
	this.isSceneActive = sceneIn => {
		if (currentScene === sceneIn) {
			return true;
		} else {
		    for(let i = 0; i < additionalActiveScenes.length; i++) {
		        const sceneItem = additionalActiveScenes[i];
		        if (sceneItem === sceneIn)
		        	return true;
		    }
		}
	  return false;
	} 
	
	/**
	 * Add additional active scene for rendering, this scene will also be rendered but 
	 * viewport of the currentScene will be used. 
	 * @param {Zinc.Scene} additionalScene - Scene to be added to the rendering.
	 */
	this.addActiveScene = additionalScene => {
		if (!this.isSceneActive(additionalScene)) {
			additionalActiveScenes.push(additionalScene);
			scenesGroup.add(additionalScene.getThreeJSScene());
		}
	}
	
	/**
	 * Remove a currenrtly active scene from the renderer, this scene will also be rendered but 
	 * viewport of the currentScene will be used. 
	 * @param {Zinc.Scene} additionalScene - Scene to be removed from rendering.
	 */
	this.removeActiveScene = additionalScene => {
	    for(let i = 0; i < additionalActiveScenes.length; i++) {
	        const sceneItem = additionalActiveScenes[i];
	        if (sceneItem === additionalScene) {
	        	additionalActiveScenes.splice(i, 1);
	        	scenesGroup.remove(additionalScene.getThreeJSScene());
	        	return;
	        }
	    }
	}
	
	/**
	 * Clear all additional scenes from rendering except for curentScene.
	 */
	this.clearAllActiveScene = () => {
		for (let i = 0; i < additionalActiveScenes.length; i++) {
			scenesGroup.remove(additionalActiveScenes[i].getThreeJSScene());
		}
		additionalActiveScenes.splice(0,additionalActiveScenes.length);
	}
	
	/**
	 * Dispose all memory allocated, this will effetively destroy all scenes.
	 */
	this.dispose = () => {
    if (isRendering)
      cancelAnimationFrame(animated_id);
	  for (const key in sceneMap) {
	    if (sceneMap.hasOwnProperty(key)) {
	      sceneMap[key].clearAll();
	    }
	  }
	  sceneMap = [];
	  additionalActiveScenes = [];
	  scenesGroup = new THREE.Group();
	  this.stopAnimate();
	  preRenderCallbackFunctions = [];
	  preRenderCallbackFunctions_id = 0;
	  cameraOrtho = undefined;
	  sceneOrtho = undefined;
	  logoSprite = undefined;
	  const scene = this.createScene("default");
	  this.setCurrentScene(scene);
	  sensor = undefined;
	}
	
	/**
	 * Transition from the current viewport to the endingScene's viewport in the specified duration.
	 * 
	 * @param {Zinc.Scene} endingScene - Viewport of this scene will be used as the destination.
	 * @param {Number} duration - Amount of time to transition from current viewport to the 
	 * endingScene's viewport.
	 */
	this.transitionScene = (endingScene, duration) => {
		if (currentScene) {
			const currentCamera = currentScene.getZincCameraControls();
			const boundingBox = endingScene.getBoundingBox();
			if (boundingBox) {
				const radius = boundingBox.min.distanceTo(boundingBox.max)/2.0;
				const centreX = (boundingBox.min.x + boundingBox.max.x) / 2.0;
				const centreY = (boundingBox.min.y + boundingBox.max.y) / 2.0;
				const centreZ = (boundingBox.min.z + boundingBox.max.z) / 2.0;
				const clip_factor = 4.0;
				const endingViewport = currentCamera.getViewportFromCentreAndRadius(centreX, centreY, centreZ, radius, 40, radius * clip_factor );
				const startingViewport = currentCamera.getCurrentViewport();
				currentCamera.cameraTransition(startingViewport, endingViewport, duration);
				currentCamera.enableCameraTransition();
			}
		}
	}

  this.isWebGL2 = () => {
    if (renderer)
      return renderer.capabilities.isWebGL2;
    return false;
  }
};