Home Reference Source

scripts/three/edge.js

import {EventDispatcher, TextureLoader, RepeatWrapping, Vector2, Vector3, MeshBasicMaterial, FrontSide, DoubleSide, BackSide, Shape, Path, ShapeGeometry, Mesh, Geometry, Face3, } from 'three';
import {Utils} from '../core/utils.js';
import {EVENT_REDRAW, EVENT_CAMERA_MOVED, EVENT_CAMERA_ACTIVE_STATUS} from '../core/events.js';

export class Edge extends EventDispatcher
{
	constructor(scene, edge, controls)
	{
		super();
		this.name = 'edge';
		this.scene = scene;
		this.edge = edge;
		this.controls = controls;

		this.wall = edge.wall;
		this.front = edge.front;

		this.planes = [];
		this.phantomPlanes = [];
		this.basePlanes = []; // always visible
		this.texture = new TextureLoader();

		this.lightMap = new TextureLoader().load('rooms/textures/walllightmap.png');
		this.fillerColor = 0xdddddd;
		this.sideColor = 0xcccccc;
		this.baseColor = 0xdddddd;
		this.visible = false;

		var scope = this;

		this.redrawevent = ()=>{scope.redraw();};
		this.visibilityevent = ()=>{scope.updateVisibility();};
		this.showallevent =  ()=>{scope.showAll();};

		this.visibilityfactor = true;
		this.init();
	}

	remove()
	{
		this.edge.removeEventListener(EVENT_REDRAW, this.redrawevent);
		this.controls.removeEventListener(EVENT_CAMERA_MOVED, this.visibilityevent);
		this.controls.removeEventListener(EVENT_CAMERA_ACTIVE_STATUS, this.showallevent);
		this.removeFromScene();
	}

	init()
	{
		this.edge.addEventListener(EVENT_REDRAW, this.redrawevent);
		this.controls.addEventListener(EVENT_CAMERA_MOVED, this.visibilityevent);
		this.controls.addEventListener(EVENT_CAMERA_ACTIVE_STATUS, this.showallevent);

		this.updateTexture();
		this.updatePlanes();
		this.addToScene();
	}

	redraw()
	{
		this.removeFromScene();
		this.updateTexture();
		this.updatePlanes();
		this.addToScene();
	}

	removeFromScene()
	{
		var scope = this;
		scope.planes.forEach((plane) => {
			scope.scene.remove(plane);
		});
		scope.basePlanes.forEach((plane) => {
			scope.scene.remove(plane);
		});
		scope.planes = [];
		scope.basePlanes = [];
	}

	addToScene()
	{
		var scope = this;
		this.planes.forEach((plane) => {
			scope.scene.add(plane);
		});
		this.basePlanes.forEach((plane) => {
			scope.scene.add(plane);
		});
		this.updateVisibility();
	}

	showAll()
	{
		var scope = this;
		scope.visible = true;
		scope.planes.forEach((plane) =>
		{
			plane.material.transparent = !scope.visible;
			plane.material.opacity = 1.0;
			plane.visible = scope.visible;
		});

		this.wall.items.forEach((item) => {
			item.updateEdgeVisibility(scope.visible, scope.front);
		});
		this.wall.onItems.forEach((item) => {
			item.updateEdgeVisibility(scope.visible, scope.front);
		});
	}

	switchWireframe(flag)
	{
		var scope = this;
		scope.visible = true;
		scope.planes.forEach((plane) =>
		{
			plane.material.wireframe = flag;
		});
	}

	updateVisibility()
	{
		var scope = this;
		// finds the normal from the specified edge
		var start = scope.edge.interiorStart();
		var end = scope.edge.interiorEnd();
		var x = end.x - start.x;
		var y = end.y - start.y;
		// rotate 90 degrees CCW
		var normal = new Vector3(-y, 0, x);
		normal.normalize();

		// setup camera: scope.controls.object refers to the camera of the scene
		var position = scope.controls.object.position.clone();
		var focus = new Vector3((start.x + end.x) / 2.0,0,(start.y + end.y) / 2.0);
		var direction = position.sub(focus).normalize();

		// find dot
		var dot = normal.dot(direction);
		// update visible
		scope.visible = (dot >= 0);
		// show or hide planes
		scope.planes.forEach((plane) => {
			plane.material.transparent = !scope.visible;
			plane.material.opacity = (scope.visible)? 1.0 : 0.3;
//			plane.visible = scope.visible;
		});
		scope.updateObjectVisibility();
	}

	updateObjectVisibility()
	{
//		var scope = this;
//		this.wall.items.forEach((item) => {
//			item.updateEdgeVisibility(scope.visible, scope.front);
//		});
//		this.wall.onItems.forEach((item) => {
//			item.updateEdgeVisibility(scope.visible, scope.front);
//		});
	}

	updateTexture(callback)
	{
		var scope = this;
		// callback is fired when texture loads
		callback = callback || function () {scope.scene.needsUpdate = true;};
		var textureData = this.edge.getTexture();
		var stretch = textureData.stretch;
		var url = textureData.url;
		var scale = textureData.scale;
		this.texture = new TextureLoader().load(url, callback);

		if (!stretch)
		{
			var height = this.wall.height;
			var width = this.edge.interiorDistance();
			this.texture.wrapT = RepeatWrapping;
			this.texture.wrapS = RepeatWrapping;
			this.texture.repeat.set(width / scale, height / scale);
			this.texture.needsUpdate = true;
		}
	}

	updatePlanes()
	{
		var extStartCorner = this.wall.getClosestCorner(this.edge.exteriorStart());
		var extEndCorner = this.wall.getClosestCorner(this.edge.exteriorEnd());

		if(extStartCorner == null || extEndCorner == null)
		{
				//Maybe this is an orphan wall. Let the garbage collector clean this up later
				return;
		}

		var color = 0xFFFFFF;
		var wallMaterial = new MeshBasicMaterial({
			color: color,
			side: FrontSide,
			map: this.texture,
			transparent: true,
			lightMap: this.lightMap,
			opacity: 1.0,
			wireframe: false,
		});
		var fillerMaterial = new MeshBasicMaterial({
			color: this.fillerColor,
			side: DoubleSide,
			map: this.texture,
			transparent: true,
			opacity: 1.0,
			wireframe: false,
		});

		// exterior plane for real exterior walls
		//If the walls have corners that have more than one room attached
		//Then there is no need to construct an exterior wall
		if(this.edge.wall.start.getAttachedRooms().length < 2 || this.edge.wall.end.getAttachedRooms().length < 2)
		{
			this.planes.push(this.makeWall(this.edge.exteriorStart(), this.edge.exteriorEnd(), this.edge.exteriorTransform, this.edge.invExteriorTransform, fillerMaterial));
		}
		// interior plane
		this.planes.push(this.makeWall(this.edge.interiorStart(), this.edge.interiorEnd(), this.edge.interiorTransform, this.edge.invInteriorTransform, wallMaterial));
		// bottom
		// put into basePlanes since this is always visible
		this.basePlanes.push(this.buildFillerUniformHeight(this.edge, 0, BackSide, this.baseColor));
		if(this.edge.wall.start.getAttachedRooms().length < 2 || this.edge.wall.end.getAttachedRooms().length < 2)
		{
			this.planes.push(this.buildFillerVaryingHeights(this.edge, DoubleSide, this.fillerColor));
		}

		// sides
		this.planes.push(this.buildSideFillter(this.edge.interiorStart(), this.edge.exteriorStart(), extStartCorner.elevation, this.sideColor));
		this.planes.push(this.buildSideFillter(this.edge.interiorEnd(), this.edge.exteriorEnd(), extEndCorner.elevation, this.sideColor));
	}

	// start, end have x and y attributes (i.e. corners)
	makeWall(start, end, transform, invTransform, material)
	{
		var v1 = this.toVec3(start);
		var v2 = this.toVec3(end);
		var v3 = v2.clone();
		var v4 = v1.clone();
		v3.y = this.wall.getClosestCorner(end).elevation;
		v4.y = this.wall.getClosestCorner(start).elevation;
		var points = [v1.clone(), v2.clone(), v3.clone(), v4.clone()];

		points.forEach((p) => {p.applyMatrix4(transform);});

		var spoints = [new Vector2(points[0].x, points[0].y),new Vector2(points[1].x, points[1].y),new Vector2(points[2].x, points[2].y),new Vector2(points[3].x, points[3].y)];
		var shape = new Shape(spoints);

		// add holes for each wall item
		this.wall.items.forEach((item) => {
			var pos = item.position.clone();
			pos.applyMatrix4(transform);
			var halfSize = item.halfSize;
			var min = halfSize.clone().multiplyScalar(-1);
			var max = halfSize.clone();
			min.add(pos);
			max.add(pos);

			var holePoints = [new Vector2(min.x, min.y),new Vector2(max.x, min.y),new Vector2(max.x, max.y),new Vector2(min.x, max.y)];
			shape.holes.push(new Path(holePoints));
		});

		var geometry = new ShapeGeometry(shape);
		geometry.vertices.forEach((v) => {
			v.applyMatrix4(invTransform);
		});

		// make UVs
		var totalDistance = Utils.distance(new Vector2(v1.x, v1.z), new Vector2(v2.x, v2.z));
		var height = this.wall.height;
		geometry.faceVertexUvs[0] = [];

		geometry.faces.forEach((face) => {
			var vertA = geometry.vertices[face.a];
			var vertB = geometry.vertices[face.b];
			var vertC = geometry.vertices[face.c];
			geometry.faceVertexUvs[0].push([vertexToUv(vertA),vertexToUv(vertB),vertexToUv(vertC)]);
		});

		geometry.faceVertexUvs[1] = geometry.faceVertexUvs[0];
		geometry.computeFaceNormals();
		geometry.computeVertexNormals();

		function vertexToUv(vertex)
		{
			var x = Utils.distance(new Vector2(v1.x, v1.z), new Vector2(vertex.x, vertex.z)) / totalDistance;
			var y = vertex.y / height;
			return new Vector2(x, y);
		}

		var mesh = new Mesh(geometry, material);
		mesh.name = 'wall';
		return mesh;
	}

	buildSideFillter(p1, p2, height, color)
	{
		var points = [this.toVec3(p1), this.toVec3(p2), this.toVec3(p2, height), this.toVec3(p1, height) ];

		var geometry = new Geometry();
		points.forEach((p) => {
			geometry.vertices.push(p);
		});
		geometry.faces.push(new Face3(0, 1, 2));
		geometry.faces.push(new Face3(0, 2, 3));

		var fillerMaterial = new MeshBasicMaterial({color: color,side: DoubleSide});
		var filler = new Mesh(geometry, fillerMaterial);
		return filler;
	}

	buildFillerVaryingHeights(edge, side, color)
	{
		var a = this.toVec3(edge.exteriorStart(), this.wall.getClosestCorner(edge.exteriorStart()).elevation);
		var b = this.toVec3(edge.exteriorEnd(), this.wall.getClosestCorner(edge.exteriorEnd()).elevation);
		var c = this.toVec3(edge.interiorEnd(), this.wall.getClosestCorner(edge.interiorEnd()).elevation);
		var d = this.toVec3(edge.interiorStart(), this.wall.getClosestCorner(edge.interiorStart()).elevation);
		var fillerMaterial = new MeshBasicMaterial({color: color,side: side});

		var geometry = new Geometry();
		geometry.vertices.push(a,b,c,d);
		geometry.faces.push(new Face3(0, 1, 2));
		geometry.faces.push(new Face3(0, 2, 3));

		var filler = new Mesh(geometry, fillerMaterial);
		return filler;
	}

	buildFillerUniformHeight(edge, height, side, color)
	{
		var points = [this.toVec2(edge.exteriorStart()), this.toVec2(edge.exteriorEnd()), this.toVec2(edge.interiorEnd()),this.toVec2(edge.interiorStart())];

		var fillerMaterial = new MeshBasicMaterial({color: color,side: side});
		var shape = new Shape(points);
		var geometry = new ShapeGeometry(shape);
		var filler = new Mesh(geometry, fillerMaterial);
		filler.rotation.set(Math.PI / 2, 0, 0);
		filler.position.y = height;
		return filler;
	}

	toVec2(pos)
	{
		return new Vector2(pos.x, pos.y);
	}

	toVec3(pos, height)
	{
		height = height || 0;
		return new Vector3(pos.x, height, pos.y);
	}
}