Home Reference Source

scripts/model/half_edge.js

import {EventDispatcher, Vector2, Vector3, Matrix4, Face3, Mesh, Geometry, MeshBasicMaterial, Box3} from 'three';
import {EVENT_REDRAW} from '../core/events.js';
import {Utils} from '../core/utils.js';


/**
 * Half Edges are created by Room.
 *
 * Once rooms have been identified, Half Edges are created for each interior wall.
 *
 * A wall can have two half edges if it is visible from both sides.
 */
export class HalfEdge extends EventDispatcher
{
	/**
	 * Constructs a half edge.
	 * @param {Room} room The associated room. Instance of Room
	 * @param {Wall} wall The corresponding wall. Instance of Wall
	 * @param {boolean} front True if front side. Boolean value
	 */
	constructor(room, wall, front)
	{
		super();

		/**  The minimum point in space calculated from the bounds
		 * @property {Vector3} min  The minimum point in space calculated from the bounds
		 * @type {Vector3}
		 * @see https://threejs.org/docs/#api/en/math/Vector3
		**/
		this.min = null;
		
		/**
		 * The maximum point in space calculated from the bounds
		 * @property {Vector3} max	 The maximum point in space calculated from the bounds
		 * @type {Vector3}
		 * @see https://threejs.org/docs/#api/en/math/Vector3
		**/
		this.max = null;

		/**
		 * The center of this half edge
		 * @property {Vector3} center The center of this half edge
		 * @type {Vector3}
		 * @see https://threejs.org/docs/#api/en/math/Vector3
		**/
		this.center = null;

		/**
		 * Reference to a Room instance
		 * @property {Room} room Reference to a Room instance
		 * @type {Room}
		**/
		this.room = room;
		
		/** 
		 *  Reference to a Wall instance
		 * @property {Wall} room Reference to a Wall instance
		 * @type {Wall}
		**/
		this.wall = wall;
		
		/**
		 * Reference to the next halfedge instance connected to this
		 * @property {HalfEdge} next Reference to the next halfedge instance connected to this
		 * @type {HalfEdge}
		**/
		this.next = null;
		
		/**
		 * Reference to the previous halfedge instance connected to this
		 * @property {HalfEdge} prev Reference to the previous halfedge instance connected to this
		 * @type {HalfEdge}
		**/
		this.prev = null;
		
		/** 
		 * The offset to maintain for the front and back walls from the midline of a wall
		 * @property {Number} offset The offset to maintain for the front and back walls from the midline of a wall
		 * @type {Number}
		**/
		this.offset = 0.0;

		/**
		 *  The height of a wall
		 * @property {Number} height The height of a wall
		 * @type {Number}
		**/
		this.height = 0.0;
		
		/**
		 * The plane mesh that will be used for checking intersections of wall items
		 * @property {Mesh} plane The plane mesh that will be used for checking intersections of wall items
		 * @type {Mesh}
		 * @see https://threejs.org/docs/#api/en/objects/Mesh
		 */
		this.plane = null;
		
		/**
		 * The interior transformation matrix that contains the homogeneous transformation of the plane based on the two corner positions of the wall
		 * @property {Matrix4} interiorTransform The interior transformation matrix that contains the homogeneous transformation of the plane based on the two corner positions of the wall
		 * @type {Matrix4} 
		 * @see https://threejs.org/docs/#api/en/math/Matrix4
		 */
		this.interiorTransform = new Matrix4();
		
		/**
		 * The inverse of the interior transformation matrix that contains the homogeneous transformation of the plane based on the two corner positions of the wall
		 * @property {Matrix4} invInteriorTransform The inverse of the interior transformation matrix that contains the homogeneous transformation of the plane based on the two corner positions of the wall
		 * @type {Matrix4}
		 * @see https://threejs.org/docs/#api/en/math/Matrix4
		 */
		this.invInteriorTransform = new Matrix4();
		
		/**
		 * The exterior transformation matrix that contains the homogeneous transformation of the plane based on the two corner positions of the wall
		 * @property {Matrix4} exteriorTransform The exterior transformation matrix that contains the homogeneous transformation of the plane based on the two corner positions of the wall
		 * @type {Matrix4} 
		 * @see https://threejs.org/docs/#api/en/math/Matrix4
		 */
		this.exteriorTransform = new Matrix4();
		
		/**
		 * The inverse of the exterior transformation matrix that contains the homogeneous transformation of the plane based on the two corner positions of the wall
		 * @property {Matrix4} invExteriorTransform The inverse of the exterior transformation matrix that contains the homogeneous transformation of the plane based on the two corner positions of the wall
		 * @type {Matrix4}
		 * @see https://threejs.org/docs/#api/en/math/Matrix4
		 */
		this.invExteriorTransform = new Matrix4();
		
		/**
		 * This is an array of callbacks to be call when redraw happens
		 * @depreceated 
		 */
		this.redrawCallbacks = null;
		
		/**
		 * Is this is the front edge or the back edge
		 * @property {boolean} front Is this is the front edge or the back edge
		 * @type {boolean}
		 */
		this.front = front || false;
		
		this.offset = wall.thickness / 2.0;
		this.height = wall.height;

		if (this.front)
		{
			this.wall.frontEdge = this;
		}
		else
		{
			this.wall.backEdge = this;
		}

	}

	/**
	 * Two separate textures are used for the walls. Based on which side of the wall this {HalfEdge} refers the texture is returned
	 * @return {Object} front/back Two separate textures are used for the walls. Based on which side of the wall this {@link HalfEdge} refers the texture is returned
	 */
	getTexture()
	{
		if (this.front)
		{
			return this.wall.frontTexture;
		}
		else
		{
			return this.wall.backTexture;
		}
	}

	/**
	 * Set a Texture to the wall. Based on the edge side as front or back the texture is applied appropriately to the wall
	 * @param {String} textureUrl The path to the texture image
	 * @param {boolean} textureStretch Can the texture stretch? If not it will be repeated
	 * @param {Number} textureScale The scale value using which the number of repetitions of the texture image is calculated
	 * @emits {EVENT_REDRAW}
	 */
	setTexture(textureUrl, textureStretch, textureScale)
	{
		var texture = {url: textureUrl, stretch: textureStretch, scale: textureScale};
		if (this.front)
		{
			this.wall.frontTexture = texture;
		}
		else
		{
			this.wall.backTexture = texture;
		}

		//this.redrawCallbacks.fire();
		this.dispatchEvent({type:EVENT_REDRAW, item: this});
	}
	
	/**
	 * Emit the redraw event
	 * @emits {EVENT_REDRAW}
	 */
	dispatchRedrawEvent()
	{
		this.dispatchEvent({type:EVENT_REDRAW, item: this});
	}
	
	/**
	 * Transform the {@link Corner} instance to a Vector3 instance using the x and y position returned as x and z
	 * @param {Corner} corner
	 * @return {Vector3}
	 * @see https://threejs.org/docs/#api/en/math/Vector3
	 */
	transformCorner(corner)
	{
		return new Vector3(corner.x, 0, corner.y);
	}


	/**
	 * This generates the invisible planes in the scene that are used for interesection testing for the wall items
	 */
	generatePlane()
	{
		var geometry = new Geometry();
		var v1 = this.transformCorner(this.interiorStart());
		var v2 = this.transformCorner(this.interiorEnd());
		var v3 = v2.clone();
		var v4 = v1.clone();

		// v3.y = this.wall.height;
		// v4.y = this.wall.height;

		v3.y = this.wall.getStart().elevation;
		v4.y = this.wall.getEnd().elevation;

		geometry.vertices = [v1, v2, v3, v4];
		geometry.faces.push(new Face3(0, 1, 2));
		geometry.faces.push(new Face3(0, 2, 3));
		geometry.computeFaceNormals();
		geometry.computeBoundingBox();


		this.plane = new Mesh(geometry, new MeshBasicMaterial({visible:false}));
		//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.visible = true;
		this.plane.edge = this; // js monkey patch


		this.computeTransforms(this.interiorTransform, this.invInteriorTransform, this.interiorStart(), this.interiorEnd());
		this.computeTransforms(this.exteriorTransform, this.invExteriorTransform, this.exteriorStart(), this.exteriorEnd());

		var b3 = new Box3();
		b3.setFromObject(this.plane);
		this.min = b3.min.clone();
		this.max = b3.max.clone();
		this.center = this.max.clone().sub(this.min).multiplyScalar(0.5).add(this.min);
	}
	
	/**
	 * Calculate the transformation matrix for the edge (front/back) baesd on the parameters. 
	 * @param {Matrix4} transform The matrix reference in which the transformation is stored
	 * @param {Matrix4} invTransform The inverse of the transform that is stored in the invTransform
	 * @param {Vector2} start The starting point location
	 * @param {Vector2} end The ending point location
	 * @see https://threejs.org/docs/#api/en/math/Matrix4
	 * @see https://threejs.org/docs/#api/en/math/Vector2
	 */
	computeTransforms(transform, invTransform, start, end)
	{
		var v1 = start;
		var v2 = end;

		var angle = Utils.angle(new Vector2(1, 0), new Vector2(v2.x - v1.x, v2.y - v1.y));

		var tt = new Matrix4();
		var tr = new Matrix4();

		tt.makeTranslation(-v1.x, 0, -v1.y);
		tr.makeRotationY(-angle);
		transform.multiplyMatrices(tr, tt);
		invTransform.getInverse(transform);
	}

	/** Gets the distance from specified point.
	 * @param {Number} x X coordinate of the point.
	 * @param {Number} y Y coordinate of the point.
	 * @returns {Number} The distance.
	 */
	distanceTo(x, y)
	{
		// x, y, x1, y1, x2, y2
		return Utils.pointDistanceFromLine(new Vector2(x, y), this.interiorStart(), this.interiorEnd());
	}
	
	/**
	 * Get the starting corner of the wall this instance represents
	 * @return {Corner} The starting corner
	 */
	getStart()
	{
		if (this.front)
		{
			return this.wall.getStart();
		}
		else
		{
			return this.wall.getEnd();
		}
	}
	
	/**
	 * Get the ending corner of the wall this instance represents
	 * @return {Corner} The ending corner
	 */
	getEnd()
	{
		if (this.front)
		{
			return this.wall.getEnd();
		}
		else
		{
			return this.wall.getStart();
		}
	}
	
	/**
	 * If this is the front edge then return the back edge. 
	 * For example in a wall there are two halfedges, i.e one for front and one back. Based on which side this halfedge lies return the opposite {@link HalfEdge}
	 * @return {HalfEdge} The other HalfEdge
	 */
	getOppositeEdge()
	{
		if (this.front)
		{
			return this.wall.backEdge;
		}
		else
		{
			return this.wall.frontEdge;
		}
	}
	
	/**
	 * Return the 2D interior location that is at the end. 
	 * @return {Vector2} Return an object with attributes x, y
	 * @see https://threejs.org/docs/#api/en/math/Vector2
	 */
	// 
	interiorEnd()
	{
		var vec = this.halfAngleVector(this, this.next);
		return new Vector2(this.getEnd().x + vec.x, this.getEnd().y + vec.y);
		// return {x:this.getEnd().x + vec.x, y:this.getEnd().y + vec.y};
	}
	
	/**
	 * Return the 2D interior location that is at the start. 
	 * @return {Vector2} Return an object with attributes x, y
	 * @see https://threejs.org/docs/#api/en/math/Vector2
	 */
	interiorStart()
	{
		var vec = this.halfAngleVector(this.prev, this);
		return new Vector2(this.getStart().x + vec.x, this.getStart().y + vec.y);
		// return {x:this.getStart().x + vec.x, y:this.getStart().y + vec.y};
	}
	
	/**
	 * Return the 2D interior location that is at the center/middle. 
	 * @return {Vector2} Return an object with attributes x, y
	 * @see https://threejs.org/docs/#api/en/math/Vector2
	 */
	interiorCenter()
	{
		return new Vector2((this.interiorStart().x + this.interiorEnd().x) / 2.0, (this.interiorStart().y + this.interiorEnd().y) / 2.0);
	}
	
	/**
	 * Return the interior distance of the interior wall 
	 * @return {Number} The distance
	 */
	interiorDistance()
	{
		var start = this.interiorStart();
		var end = this.interiorEnd();
		return Utils.distance(start, end);
	}
	
	/**
	 * Return the 2D exterior location that is at the end. 
	 * @return {Vector2} Return an object with attributes x, y
	 * @see https://threejs.org/docs/#api/en/math/Vector2
	 */
	exteriorEnd()
	{
		var vec = this.halfAngleVector(this, this.next);
		return new Vector2(this.getEnd().x - vec.x, this.getEnd().y - vec.y);
	}
	
	/**
	 * Return the 2D exterior location that is at the start. 
	 * @return {Vector2} Return an object with attributes x, y
	 * @see https://threejs.org/docs/#api/en/math/Vector2
	 */
	exteriorStart()
	{
		var vec = this.halfAngleVector(this.prev, this);
		return new Vector2(this.getStart().x - vec.x, this.getStart().y - vec.y);
	}
	
	/**
	 * Return the 2D exterior location that is at the center/middle. 
	 * @return {Vector2} Return an object with attributes x, y
	 * @see https://threejs.org/docs/#api/en/math/Vector2
	 */
	exteriorCenter()
	{
			return new Vector2((this.exteriorStart().x + this.exteriorEnd().x) / 2.0, (this.exteriorStart().y + this.exteriorEnd().y) / 2.0);
	}
	
	/**
	 * Return the exterior distance of the exterior wall 
	 * @return {Number} The distance
	 */
	exteriorDistance()
	{
		var start = this.exteriorStart();
		var end = this.exteriorEnd();
		return Utils.distance(start, end);
	}

	/** Get the corners of the half edge.
	 * @returns {Corner[]} An array of x,y pairs.
	 */
	corners()
	{
		return [this.interiorStart(), this.interiorEnd(), this.exteriorEnd(), this.exteriorStart()];
	}

	/**
	 * Gets CCW angle from v1 to v2
	 * @param {Vector2} v1 The point a
	 * @param {Vector2} v1 The point b
	 * @return {Object} contains keys x and y with number representing the halfAngles
	 */
	halfAngleVector(v1, v2)
	{
		var v1startX=0.0, v1startY=0.0, v1endX=0.0, v1endY=0.0;
		var v2startX=0.0, v2startY=0.0, v2endX=0.0, v2endY=0.0;

		// make the best of things if we dont have prev or next
		if (!v1)
		{
			v1startX = v2.getStart().x - (v2.getEnd().x - v2.getStart().x);
			v1startY = v2.getStart().y - (v2.getEnd().y - v2.getStart().y);

			v1endX = v2.getStart().x;
			v1endY = v2.getStart().y;
		}
		else
		{
			v1startX = v1.getStart().x;
			v1startY = v1.getStart().y;
			v1endX = v1.getEnd().x;
			v1endY = v1.getEnd().y;
		}

		if (!v2)
		{
			v2startX = v1.getEnd().x;
			v2startY = v1.getEnd().y;
			v2endX = v1.getEnd().x + (v1.getEnd().x - v1.getStart().x);
			v2endY = v1.getEnd().y + (v1.getEnd().y - v1.getStart().y);
		}
		else
		{
			v2startX = v2.getStart().x;
			v2startY = v2.getStart().y;
			v2endX = v2.getEnd().x;
			v2endY = v2.getEnd().y;
		}

		// CCW angle between edges
		var theta = Utils.angle2pi( new Vector2(v1startX - v1endX, v1startY - v1endY), new Vector2(v2endX - v1endX, v2endY - v1endY));

		// cosine and sine of half angle
		var cs = Math.cos(theta / 2.0);
		var sn = Math.sin(theta / 2.0);

		// rotate v2
		var v2dx = v2endX - v2startX;
		var v2dy = v2endY - v2startY;

		var vx = v2dx * cs - v2dy * sn;
		var vy = v2dx * sn + v2dy * cs;

		// normalize
		var mag = Utils.distance(new Vector2(0, 0), new Vector2(vx, vy));
		var desiredMag = (this.offset) / sn;
		var scalar = desiredMag / mag;

		var halfAngleVector = {x:vx * scalar, y:vy * scalar};//new Vector2(vx * scalar, vy * scalar);
		return halfAngleVector;
	}
}