Home Reference Source

scripts/three/main.js

import $ from 'jquery';
import {EventDispatcher, Vector2, Vector3, WebGLRenderer,ImageUtils, PerspectiveCamera, OrthographicCamera} from 'three';
import {Plane} from 'three';
import {PCFSoftShadowMap} from 'three';
import {Clock} from 'three';
// import {FirstPersonControls} from './first-person-controls.js';
import {PointerLockControls} from './pointerlockcontrols.js';

import {EVENT_UPDATED, EVENT_WALL_CLICKED, EVENT_NOTHING_CLICKED, EVENT_FLOOR_CLICKED, EVENT_ITEM_SELECTED, EVENT_ITEM_UNSELECTED, EVENT_GLTF_READY} from '../core/events.js';
import {EVENT_CAMERA_ACTIVE_STATUS, EVENT_FPS_EXIT, EVENT_CAMERA_VIEW_CHANGE} from '../core/events.js';
import {VIEW_TOP, VIEW_FRONT, VIEW_RIGHT, VIEW_LEFT, VIEW_ISOMETRY} from '../core/constants.js';

import {OrbitControls} from './orbitcontrols.js';

// import {Controls} from './controls.js';
import {Controller} from './controller.js';
import {HUD} from './hud.js';
import {Floorplan3D} from './floorPlan.js';
import {Lights} from './lights.js';
import {Skybox} from './skybox.js';

export class Main extends EventDispatcher
{
	constructor(model, element, canvasElement, opts)
	{
		super();
		var options = {resize: true,pushHref: false,spin: true,spinSpeed: .00002,clickPan: true,canMoveFixedItems: false};
		for (var opt in options)
		{
			if (options.hasOwnProperty(opt) && opts.hasOwnProperty(opt))
			{
				options[opt] = opts[opt];
			}
		}

		this.pauseRender = true;
		this.model = model;
		this.scene = model.scene;
		this.element = $(element);
		this.canvasElement = canvasElement;
		this.options = options;

		this.domElement = null;
		this.orthocamera = null;
		this.perspectivecamera = null;
		this.camera = null;
		this.savedcameraposition = null;
		this.fpscamera = null;

		this.cameraNear = 10;
		this.cameraFar = 10000;

		this.controls = null;
		this.fpscontrols = null;
		this.fpsclock = new Clock(true);
		this.firstpersonmode = false;

		this.renderer = null;
		this.controller = null;

		this.needsUpdate = false;
		this.lastRender = Date.now();

		this.mouseOver = false;
		this.hasClicked = false;

		this.hud = null;

		this.heightMargin = null;
		this.widthMargin = null;
		this.elementHeight = null;
		this.elementWidth = null;


		this.itemSelectedCallbacks = $.Callbacks(); // item
		this.itemUnselectedCallbacks = $.Callbacks();

		this.wallClicked = $.Callbacks(); // wall
		this.floorClicked = $.Callbacks(); // floor
		this.nothingClicked = $.Callbacks();

		this.floorplan = null;

		var scope = this;
		this.updatedevent = ()=>{scope.centerCamera();};
		this.gltfreadyevent = (o)=>{scope.gltfReady(o);};

		this.clippingPlaneActive = new Plane(new Vector3(0, 0, 1), 0.0);
		this.clippingPlaneActive2 = new Plane(new Vector3(0, 0, -1), 0.0);
		this.globalClippingPlane = [this.clippingPlaneActive, this.clippingPlaneActive2];
		this.clippingEmpty = Object.freeze([]);
		this.clippingEnabled = false;

//		console.log('THIS ON MOBILE DEVICE ::: ', isMobile, isTablet);

		this.init();
	}

	getARenderer()
	{
// scope.renderer = new WebGLRenderer({antialias: true, preserveDrawingBuffer:
// true, alpha:true}); // preserveDrawingBuffer:true - required to support
// .toDataURL()
		var renderer = new WebGLRenderer({antialias: true, alpha:true});

// scope.renderer.autoClear = false;
		renderer.shadowMap.enabled = true;
		renderer.shadowMapSoft = true;
		renderer.shadowMap.type = PCFSoftShadowMap;
		renderer.setClearColor( 0xFFFFFF, 1 );
		renderer.clippingPlanes = this.clippingEmpty;
		renderer.localClippingEnabled = false;
//		renderer.setPixelRatio(window.devicePixelRatio);
// renderer.sortObjects = false;

		return renderer;
	}

	init()
	{
		var scope = this;
		ImageUtils.crossOrigin = '';

		var orthoScale = 100;
		var orthoWidth = window.innerWidth;
		var orthoHeight = window.innerHeight;

		scope.domElement = scope.element.get(0);

		scope.fpscamera = new PerspectiveCamera(60, 1, 1, 10000 );
		scope.perspectivecamera = new PerspectiveCamera(45, 10, scope.cameraNear, scope.cameraFar);
		scope.orthocamera = new OrthographicCamera(orthoWidth / -orthoScale, orthoWidth /orthoScale, orthoHeight /orthoScale, orthoHeight / -orthoScale, scope.cameraNear, scope.cameraFar);

		scope.camera = scope.perspectivecamera;
// scope.camera = scope.orthocamera;

		scope.renderer = scope.getARenderer();
		scope.domElement.appendChild(scope.renderer.domElement);

		scope.skybox = new Skybox(scope.scene, scope.renderer);

		scope.controls = new OrbitControls(scope.camera, scope.domElement);
		scope.controls.autoRotate = this.options['spin'];
		scope.controls.enableDamping = true;
		scope.controls.dampingFactor = 0.5;
		scope.controls.maxPolarAngle = Math.PI * 0.5;
		scope.controls.maxDistance = 3000;
		scope.controls.minZoom = 0.9;
		scope.controls.screenSpacePanning = true;

		scope.fpscontrols = new PointerLockControls(scope.fpscamera);
		scope.fpscontrols.characterHeight = 160;

		this.scene.add(scope.fpscontrols.getObject());
		this.fpscontrols.getObject().position.set(0, 200, 0);

		this.fpscontrols.addEventListener('unlock', function(){
			scope.switchFPSMode(false);
			scope.dispatchEvent({type:EVENT_FPS_EXIT});
		});


		scope.hud = new HUD(scope, scope.scene);
		scope.controller = new Controller(scope, scope.model, scope.camera, scope.element, scope.controls, scope.hud);

		// handle window resizing
		scope.updateWindowSize();

		if (scope.options.resize)
		{
			$(window).resize(() => {scope.updateWindowSize();});
		}
		// setup camera nicely
		scope.centerCamera();

		scope.model.floorplan.addEventListener(EVENT_UPDATED, this.updatedevent);
		scope.model.addEventListener(EVENT_GLTF_READY, this.gltfreadyevent);

		scope.lights = new Lights(scope.scene, scope.model.floorplan);
		scope.floorplan = new Floorplan3D(scope.scene, scope.model.floorplan, scope.controls);

		function animate()
		{
//			requestAnimationFrame(animate);
			scope.renderer.setAnimationLoop(function(){scope.render();});
			scope.render();
		}
		scope.switchFPSMode(false);
		animate();

		scope.element.mouseenter(function () {scope.mouseOver = true;}).mouseleave(function () {scope.mouseOver = false;}).click(function () {scope.hasClicked = true;});
	}
	exportForBlender()
	{
		this.skybox.setEnabled(false);
		this.controller.showGroundPlane(false);
		this.model.exportForBlender();
	}

	gltfReady(o)
	{
		this.dispatchEvent({type:EVENT_GLTF_READY, item: this, gltf: o.gltf});
		this.skybox.setEnabled(true);
		this.controller.showGroundPlane(true);
	}

	itemIsSelected(item)
	{
		this.dispatchEvent({type:EVENT_ITEM_SELECTED, item:item});
	}

	itemIsUnselected()
	{
		this.dispatchEvent({type:EVENT_ITEM_UNSELECTED});
	}

	wallIsClicked(wall)
	{
		this.dispatchEvent({type:EVENT_WALL_CLICKED, item:wall, wall:wall});
	}

	floorIsClicked(item)
	{
		this.dispatchEvent({type:EVENT_FLOOR_CLICKED, item:item});
	}

	nothingIsClicked()
	{
		this.dispatchEvent({type:EVENT_NOTHING_CLICKED});
	}

	spin()
	{
		var scope = this;
		scope.controls.autoRotate = scope.options.spin && !scope.mouseOver && !scope.hasClicked;
	}

	dataUrl()
	{
		var dataUrl = this.renderer.domElement.toDataURL('image/png');
		return dataUrl;
	}

	stopSpin()
	{
		this.hasClicked = true;
		this.controls.autoRotate = false;
	}

	options()
	{
		return this.options;
	}

	getModel()
	{
		return this.model;
	}

	getScene()
	{
		return this.scene;
	}

	getController()
	{
		return this.controller;
	}

	getCamera()
	{
		return this.camera;
	}


	/*
	 * This method name conflicts with a variable so changing it to a different
	 * name needsUpdate() { this.needsUpdate = true; }
	 */

	ensureNeedsUpdate()
	{
		this.needsUpdate = true;
	}

	rotatePressed()
	{
		this.controller.rotatePressed();
	}

	rotateReleased()
	{
		this.controller.rotateReleased();
	}

	setCursorStyle(cursorStyle)
	{
		this.domElement.style.cursor = cursorStyle;
	}

	updateWindowSize()
	{
		var scope = this;

		scope.heightMargin = scope.element.offset().top;
		scope.widthMargin = scope.element.offset().left;
		scope.elementWidth = scope.element.innerWidth();

		if (scope.options.resize)
		{
			scope.elementHeight = window.innerHeight - scope.heightMargin;
		}
		else
		{
			scope.elementHeight = scope.element.innerHeight();
		}

		scope.orthocamera.left = -window.innerWidth / 1.0;
		scope.orthocamera.right = window.innerWidth / 1.0;
		scope.orthocamera.top = window.innerHeight / 1.0;
		scope.orthocamera.bottom = -window.innerHeight / 1.0;
		scope.orthocamera.updateProjectionMatrix();

		scope.perspectivecamera.aspect = scope.elementWidth / scope.elementHeight;
		scope.perspectivecamera.updateProjectionMatrix();

		scope.fpscamera.aspect = scope.elementWidth / scope.elementHeight;
		scope.fpscamera.updateProjectionMatrix();

		scope.renderer.setSize(scope.elementWidth, scope.elementHeight);
		scope.needsUpdate = true;
	}

	centerCamera()
	{
		var scope = this;
		var yOffset = 150.0;
		var pan = scope.model.floorplan.getCenter();
		pan.y = yOffset;
		scope.controls.target = pan;
		var distance = scope.model.floorplan.getSize().z * 1.5;
		var offset = pan.clone().add(new Vector3(0, distance, distance));
		// scope.controls.setOffset(offset);
		scope.camera.position.copy(offset);
		scope.controls.update();
	}

	// projects the object's center point into x,y screen coords
	// x,y are relative to top left corner of viewer
	projectVector(vec3, ignoreMargin)
	{
		var scope = this;
		ignoreMargin = ignoreMargin || false;
		var widthHalf = scope.elementWidth / 2;
		var heightHalf = scope.elementHeight / 2;
		var vector = new Vector3();
		vector.copy(vec3);
		vector.project(scope.camera);

		var vec2 = new Vector2();
		vec2.x = (vector.x * widthHalf) + widthHalf;
		vec2.y = - (vector.y * heightHalf) + heightHalf;
		if (!ignoreMargin)
		{
			vec2.x += scope.widthMargin;
			vec2.y += scope.heightMargin;
		}
		return vec2;
	}

	sceneGraph(obj)
	{
		console.group( ' <%o> ' + obj.name, obj );
		obj.children.forEach( this.sceneGraph );
		console.groupEnd();
	}

	switchWireframe(flag)
	{
		this.model.switchWireframe(flag);
		this.floorplan.switchWireframe(flag);
		this.render(true);
	}

	pauseTheRendering(flag)
	{
		this.pauseRender = flag;
	}

	switchView(viewpoint)
	{
		var center = this.model.floorplan.getCenter();
		var size = this.model.floorplan.getSize();
		var distance = this.controls.object.position.distanceTo(this.controls.target);
		this.controls.target.copy(center);

		switch(viewpoint)
		{
		case VIEW_TOP:
			center.y = 1000;
			this.dispatchEvent({type:EVENT_CAMERA_VIEW_CHANGE, view: VIEW_TOP});
			break;
		case VIEW_FRONT:
			center.z = center.z - (size.z*0.5) - distance;
			this.dispatchEvent({type:EVENT_CAMERA_VIEW_CHANGE, view: VIEW_FRONT});
			break;
		case VIEW_RIGHT:
			center.x = center.x + (size.x*0.5) + distance;
			this.dispatchEvent({type:EVENT_CAMERA_VIEW_CHANGE, view: VIEW_RIGHT});
			break;
		case VIEW_LEFT:
			center.x = center.x - (size.x*0.5) - distance;
			this.dispatchEvent({type:EVENT_CAMERA_VIEW_CHANGE, view: VIEW_LEFT});
			break;
		case VIEW_ISOMETRY:
		default:
			center.x += distance;
			center.y += distance;
			center.z += distance;
			this.dispatchEvent({type:EVENT_CAMERA_VIEW_CHANGE, view: VIEW_ISOMETRY});
		}
		this.camera.position.copy(center);
		this.controls.dispatchEvent({type:EVENT_CAMERA_ACTIVE_STATUS});
		this.controls.needsUpdate = true;
		this.controls.update();
		this.render(true);
	}

	lockView(locked)
	{
		this.controls.enableRotate = locked;
		this.render(true);
	}

	// Send in a value between -1 to 1
	changeClippingPlanes(clipRatio, clipRatio2)
	{
		var size = this.model.floorplan.getSize();
		size.z = size.z + (size.z * 0.25);
		size.z = size.z * 0.5;
		this.clippingPlaneActive.constant = (this.model.floorplan.getSize().z * clipRatio);
		this.clippingPlaneActive2.constant = (this.model.floorplan.getSize().z * clipRatio2);

		if(!this.clippingEnabled)
		{
			this.clippingEnabled = true;
			this.renderer.clippingPlanes = this.globalClippingPlane;
		}
		this.controls.dispatchEvent({type:EVENT_CAMERA_ACTIVE_STATUS});
		this.controls.needsUpdate = true;
		this.controls.update();
		this.render(true);
	}

	resetClipping()
	{
		this.clippingEnabled = false;
		this.renderer.clippingPlanes = this.clippingEmpty;
		this.controls.needsUpdate = true;
		this.controls.update();
		this.render(true);
	}

	switchOrthographicMode(flag)
	{
		if(flag)
		{
			this.camera = this.orthocamera;
			this.camera.position.copy(this.perspectivecamera.position.clone());
			this.controls.object = this.camera;
			this.controller.changeCamera(this.camera);
			this.controls.needsUpdate = true;
			this.controls.update();
			this.render(true);
			return;
		}

		this.camera = this.perspectivecamera;
		this.camera.position.copy(this.orthocamera.position.clone());
		this.controls.object = this.camera;
		this.controller.changeCamera(this.camera);
		this.controls.needsUpdate = true;
		this.controls.update();
		this.render(true);
	}

	switchFPSMode(flag)
	{
		this.firstpersonmode = flag;
		this.fpscontrols.enabled = flag;
		this.controls.enabled = !flag;
		this.controller.enabled = !flag;
		this.controls.dispatchEvent({type:EVENT_CAMERA_ACTIVE_STATUS});

		if(flag)
		{
			this.skybox.toggleEnvironment(true);
			this.fpscontrols.lock();
		}
		else
		{
			this.skybox.toggleEnvironment(false);
			this.fpscontrols.unlock();
		}

		this.model.switchWireframe(false);
		this.floorplan.switchWireframe(false);
		this.render(true);
	}

	shouldRender()
	{
		var scope = this;
		// Do we need to draw a new frame
		if (scope.controls.needsUpdate || scope.controller.needsUpdate || scope.needsUpdate || scope.model.scene.needsUpdate)
		{
			scope.controls.needsUpdate = false;
			scope.controller.needsUpdate = false;
			scope.needsUpdate = false;
			scope.model.scene.needsUpdate = false;
			return true;
		}
		else
		{
			return false;
		}
	}

	rendervr()
	{

	}

	render(forced)
	{
		var scope = this;
		forced = (forced)? forced : false;
		if(this.pauseRender && !forced)
		{
			return;
		}

		scope.spin();
		if(scope.firstpersonmode)
		{
			scope.fpscontrols.update(scope.fpsclock.getDelta());
			scope.renderer.render(scope.scene.getScene(), scope.fpscamera);

		}
		else
		{
			if(this.shouldRender() || forced)
			{
				scope.renderer.render(scope.scene.getScene(), scope.camera);
			}
		}
		scope.lastRender = Date.now();
	}
}