Home Reference Source

scripts/three/controller.js

//import $ from 'jquery';
import {EventDispatcher, Vector2, Vector3, Mesh, PlaneGeometry, MeshBasicMaterial, Raycaster } from 'three';
import {EVENT_ITEM_REMOVED, EVENT_ITEM_LOADED} from '../core/events.js';
import { Utils } from '../core/utils.js';

export const states = {UNSELECTED: 0, SELECTED: 1, DRAGGING: 2, ROTATING: 3,  ROTATING_FREE: 4, PANNING: 5};

// Controller is the class that maintains the items, floors, walls selection in
// the 3d scene
export class Controller extends EventDispatcher
{
	constructor(three, model, camera, element, controls, hud)
	{
		super();
		this.three = three;
		this.model = model;
		this.camera = camera;
		this.element = element;
		this.controls = controls;
		this.hud = hud;

		this.enabled = true;
		this.scene = model.scene;

		this.plane = null;
		this.mouse = new Vector2(0, 0);
		this.alternateMouse = new Vector2(0, 0);

		this.intersectedObject = null;
		this.mouseoverObject = null;
		this.selectedObject = null;

		this.mouseDown = false;
		this.mouseMoved = false; // has mouse moved since down click
		this.rotateMouseOver = false;

		this.state = states.UNSELECTED;
		this.needsUpdate = true;

		var scope = this;
		this.itemremovedevent = (o) => {scope.itemRemoved(o.item);};
		this.itemloadedevent = (o) => {scope.itemLoaded(o.item);};

		this.mousedownevent = (event)=> {scope.mouseDownEvent(event);};
		this.mouseupevent = (event)=> {scope.mouseUpEvent(event);};
		this.mousemoveevent = (event)=> {scope.mouseMoveEvent(event);};
		this.init();
	}


	init()
	{

//		this.element.mousedown(this.mousedownevent);
//		this.element.mouseup(this.mouseupevent);
//		this.element.mousemove(this.mousemoveevent);

		this.element.bind('touchstart mousedown', this.mousedownevent);
		this.element.bind('touchmove mousemove', this.mousemoveevent);
		this.element.bind('touchend mouseup', this.mouseupevent);

		this.scene.addEventListener(EVENT_ITEM_REMOVED, this.itemremovedevent);
		this.scene.addEventListener(EVENT_ITEM_LOADED, this.itemloadedevent);
		this.setGroundPlane();
	}

	itemRemoved(item)
	{
		// invoked as a callback to event in Scene
		if (item === this.selectedObject)
		{
			this.selectedObject.setUnselected();
			this.selectedObject.mouseOff();
			this.setSelectedObject(null);
		}
	}

	// invoked via callback when item is loaded
	itemLoaded(item)
	{
		var scope = this;
		if (!item.position_set)
		{
			scope.setSelectedObject(item);
			scope.switchState(states.DRAGGING);
			var pos = item.position.clone();
			pos.y = 0;
			var vec = scope.three.projectVector(pos);
			scope.clickPressed(vec);
		}
		item.position_set = true;
	}

	clickPressed(vec2)
	{
		this.mouse = vec2 || this.mouse;
		var intersection = this.itemIntersection(this.mouse, this.selectedObject);
		if (intersection)
		{
			this.selectedObject.clickPressed(intersection);
		}
	}

	clickDragged(vec2)
	{
		var scope = this;
		this.mouse = vec2 || this.mouse;
		var intersection = scope.itemIntersection(this.mouse, this.selectedObject);
		if (intersection)
		{
			if (scope.isRotating())
			{
				this.selectedObject.rotate(intersection);
			}
			else
			{
				this.selectedObject.clickDragged(intersection);
			}
		}
	}

	showGroundPlane(flag)
	{
		this.plane.visible = flag;
	}

	setGroundPlane()
	{
		// ground plane used to find intersections
		var size = 10000;

		// The below line was originally setting the plane visibility to false
		// Now its setting visibility to true. This is necessary to be detected
		// with the raycaster objects to click walls and floors.
		this.plane = new Mesh(new PlaneGeometry(size, size), new MeshBasicMaterial({visible:false}));
		this.plane.rotation.x = -Math.PI / 2;
		this.plane.visible = true;
		this.scene.add(this.plane);
	}

	checkWallsAndFloors()
	{
		// double click on a wall or floor brings up texture change modal
		if (this.state == states.UNSELECTED && this.mouseoverObject == null)
		{
			// check walls
			var wallEdgePlanes = this.model.floorplan.wallEdgePlanes();
			var wallIntersects = this.getIntersections(this.mouse, wallEdgePlanes, true);
			if (wallIntersects.length > 0)
			{
				var wall = wallIntersects[0].object.edge;
				// three.wallClicked.fire(wall);
				this.three.wallIsClicked(wall);
				return;
			}

			// check floors
			var floorPlanes = this.model.floorplan.floorPlanes();
			var floorIntersects = this.getIntersections(this.mouse, floorPlanes, false);
			if (floorIntersects.length > 0)
			{
				var room = floorIntersects[0].object.room;
				// this.three.floorClicked.fire(room);
				this.three.floorIsClicked(room);
				return;
			}
			// three.nothingClicked.fire();
			this.three.nothingIsClicked();
		}
	}

	isRotating()
	{
		return (this.state == states.ROTATING || this.state == states.ROTATING_FREE);
	}


	mouseDownEvent(event)
	{
		if (this.enabled)
		{
			event.preventDefault();

			this.mouseMoved = false;
			this.mouseDown = true;

			if(event.touches)
			{
				//In case if this is a touch device do the necessary to click and drag items
				this.mouse.x = event.touches[0].clientX;
				this.mouse.y = event.touches[0].clientY;
				this.alternateMouse.x = event.touches[0].clientX;
				this.alternateMouse.y = event.touches[0].clientY;
				this.updateIntersections();
				this.checkWallsAndFloors();
			}

			switch (this.state)
			{
			case states.SELECTED:
				if (this.rotateMouseOver)
				{
					this.switchState(states.ROTATING);
				}
				else if (this.intersectedObject != null)
				{
					this.setSelectedObject(this.intersectedObject);
					if (!this.intersectedObject.fixed)
					{
						this.switchState(states.DRAGGING);
					}
				}
				break;
			case states.UNSELECTED:
				if (this.intersectedObject != null)
				{
					this.setSelectedObject(this.intersectedObject);
					if (!this.intersectedObject.fixed)
					{
						this.switchState(states.DRAGGING);
					}
				}
				break;
			case states.DRAGGING:
			case states.ROTATING:
				break;
			case states.ROTATING_FREE:
				this.switchState(states.SELECTED);
				break;
			}
		}
	}

	mouseMoveEvent(event)
	{
		if (this.enabled)
		{
			event.preventDefault();
			this.mouseMoved = true;

			this.mouse.x = event.clientX;
			this.mouse.y = event.clientY;
			this.alternateMouse.x = event.clientX;
			this.alternateMouse.y = event.clientY;

			if(event.touches)
			{
				this.mouse.x = event.touches[0].clientX;
				this.mouse.y = event.touches[0].clientY;
				this.alternateMouse.x = event.touches[0].clientX;
				this.alternateMouse.y = event.touches[0].clientY;
			}

			if (!this.mouseDown)
			{
				this.updateIntersections();
			}

			switch (this.state)
			{
			case states.UNSELECTED:
				this.updateMouseover();
				break;
			case states.SELECTED:
				this.updateMouseover();
				break;
			case states.DRAGGING:
			case states.ROTATING:
			case states.ROTATING_FREE:
				this.clickDragged();
				this.hud.update();
				this.needsUpdate = true;
				break;
			}
		}
	}

	mouseUpEvent()
	{
		if (this.enabled)
		{
			this.mouseDown = false;

			switch (this.state)
			{
			case states.DRAGGING:
				this.selectedObject.clickReleased();
				this.switchState(states.SELECTED);
				break;
			case states.ROTATING:
				if (!this.mouseMoved) {
					this.switchState(states.ROTATING_FREE);
				}
				else {
					this.switchState(states.SELECTED);
				}
				break;
			case states.UNSELECTED:
				if (!this.mouseMoved) {
					this.checkWallsAndFloors();
				}
				break;
			case states.SELECTED:
				if (this.intersectedObject == null && !this.mouseMoved)
				{
					this.switchState(states.UNSELECTED);
					this.checkWallsAndFloors();
				}
				break;
			case states.ROTATING_FREE:
				break;
			}
		}
	}

	switchState(newState)
	{
		if (newState != this.state)
		{
			this.onExit(this.state);
			this.onEntry(newState);
		}
		this.state = newState;
		this.hud.setRotating(this.isRotating());
	}

	onEntry(state)
	{
		switch (state)
		{
		case states.UNSELECTED:
			this.setSelectedObject(null);
			break;
		case states.SELECTED:
			this.controls.enabled = true;
			break;
		case states.ROTATING:
		case states.ROTATING_FREE:
			this.controls.enabled = false;
			break;
		case states.DRAGGING:
			this.three.setCursorStyle('move');
			this.clickPressed();
			this.controls.enabled = false;
			break;
		}
	}

	onExit(state)
	{
		switch (state)
		{
		case states.UNSELECTED:
		case states.SELECTED:
			break;
		case states.DRAGGING:
			if (this.mouseoverObject)
			{
				this.three.setCursorStyle('pointer');
			}
			else {
				this.three.setCursorStyle('auto');
			}
			break;
		case states.ROTATING:
		case states.ROTATING_FREE:
			break;
		}
	}

	selectedObject()
	{
		return this.selectedObject;
	}

	// updates the vector of the intersection with the plane of a given
	// mouse position, and the intersected object
	// both may be set to null if no intersection found
	updateIntersections()
	{
		// check the rotate arrow
		var hudObject = this.hud.getObject();
		if (hudObject != null)
		{
			var hudIntersects = this.getIntersections(this.mouse, hudObject, false, false, true);
			if (hudIntersects.length > 0)
			{
				this.rotateMouseOver = true;
				this.hud.setMouseover(true);
				this.intersectedObject = null;
				return;
			}
		}
		this.rotateMouseOver = false;
		this.hud.setMouseover(false);

		// check objects
		var items = this.model.scene.getItems();
		var intersects = this.getIntersections(this.mouse, items, false, true);
		if (intersects.length > 0)
		{
			this.intersectedObject = intersects[0].object;
		}
		else
		{
			this.intersectedObject = null;
		}
	}

	// returns the first intersection object
	itemIntersection(vec2, item)
	{
		var customIntersections = item.customIntersectionPlanes();
		var intersections = null;
		if (customIntersections && customIntersections.length > 0)
		{
			intersections = this.getIntersections(vec2, customIntersections, true);
		}
		else
		{
			intersections = this.getIntersections(vec2, this.plane);
		}
		if (intersections.length > 0)
		{
			return intersections[0];
		}
		else
		{
			return null;
		}
	}

	// sets coords to -1 to 1
	normalizeVector2(vec2)
	{
		var retVec = new Vector2();
		retVec.x = ((vec2.x - this.three.widthMargin) / (window.innerWidth - this.three.widthMargin)) * 2 - 1;
		retVec.y = -((vec2.y - this.three.heightMargin) / (window.innerHeight - this.three.heightMargin)) * 2 + 1;
		return retVec;
	}

	//
	mouseToVec3(vec2)
	{
		var normVec2 = this.normalizeVector2(vec2);
		var vector = new Vector3(normVec2.x, normVec2.y, 0.5);
		vector.unproject(this.camera);
		return vector;
	}

	// filter by normals will only return objects facing the camera
	// objects can be an array of objects or a single object
	getIntersections(vec2, objects, filterByNormals, onlyVisible, recursive, linePrecision)
	{
		var vector = this.mouseToVec3(vec2);
		onlyVisible = onlyVisible || false;
		filterByNormals = filterByNormals || false;
		recursive = recursive || false;
		linePrecision = linePrecision || 20;

		var direction = vector.sub(this.camera.position).normalize();
		var raycaster = new Raycaster(this.camera.position, direction);
		raycaster.linePrecision = linePrecision;

		raycaster = new Raycaster();
		raycaster.setFromCamera( this.normalizeVector2(this.alternateMouse), this.camera );

		var intersections;

		if (objects instanceof Array)
		{
			intersections = raycaster.intersectObjects(objects, recursive);
		}
		else
		{
			intersections = raycaster.intersectObject(objects, recursive);
		}
		// filter by visible, if true
		if (onlyVisible)
		{
			intersections = Utils.removeIf(intersections, function (intersection) {return !intersection.object.visible;});
		}

		// filter by normals, if true
		if (filterByNormals)
		{
			intersections = Utils.removeIf(intersections, function (intersection) {var dot = intersection.face.normal.dot(direction);return (dot > 0);});
		}
		return intersections;
	}

	// manage the selected object
	setSelectedObject(object)
	{
		if (this.state === states.UNSELECTED)
		{
			this.switchState(states.SELECTED);
		}
		if (this.selectedObject != null)
		{
			this.selectedObject.setUnselected();
		}
		if (object != null)
		{
			this.selectedObject = object;
			this.selectedObject.setSelected();
// three.itemSelectedCallbacks.fire(object);
			this.three.itemIsSelected(object);
		}
		else
		{
			this.selectedObject = null;
// three.itemUnselectedCallbacks.fire();
			this.three.itemIsUnselected();
		}
		this.needsUpdate = true;
	}

	// TODO: there MUST be simpler logic for expressing this
	updateMouseover()
	{
		if (this.intersectedObject != null)
		{
			if (this.mouseoverObject != null)
			{
				if (this.mouseoverObject !== this.intersectedObject)
				{
					this.mouseoverObject.mouseOff();
					this.mouseoverObject = this.intersectedObject;
					this.mouseoverObject.mouseOver();
					this.needsUpdate = true;
				}
				else
				{
					// do nothing, mouseover already set
				}
			}
			else
			{
				this.mouseoverObject = this.intersectedObject;
				this.mouseoverObject.mouseOver();
				this.three.setCursorStyle('pointer');
				this.needsUpdate = true;
			}
		}
		else if (this.mouseoverObject != null)
		{
			this.mouseoverObject.mouseOff();
			this.three.setCursorStyle('auto');
			this.mouseoverObject = null;
			this.needsUpdate = true;
		}
	}

	changeCamera(newCamera)
	{
		this.camera = newCamera;
	}
}