A Rudimentary Mesh Editor in JavaScript

The code below implements a rudimentary editor for three-dimensional forms called “meshes”. To see it in action, copy it into an .html file and open that file in a web browser that runs JavaScript. Or, for an online version, visit “https://thiscouldbebetter.neocities.org/mesheditor.html“.

MeshEditor.png


<html>
<body>

<div id="divMain"></div>
<div id="divUI">
	<div id="divView" style="border:1px solid">
		<label>View:</label>
		<div style="border:1px solid">
			<label>Camera:</label>
			<button onclick="buttonViewUp_Clicked();">^</button>
			<button onclick="buttonViewDown_Clicked();">v</button>
			<button onclick="buttonViewLeft_Clicked();">&lt;</button>
			<button onclick="buttonViewRight_Clicked();">&gt;</button>
			<button onclick="buttonViewIn_Clicked();">+</button>
			<button onclick="buttonViewOut_Clicked();">-</button>
			<button onclick="buttonViewFront_Clicked();">Front</button>	
			<button onclick="buttonViewSide_Clicked();">Side</button>	
			<button onclick="buttonViewTop_Clicked();">Top</button>			
			<button onclick="buttonViewSelected_Clicked();">Selected</button>			
		</div>
		<div style="border:1px solid">
			
			<label>Highlight:</label>
			<label>Vertices:</label>
			<input id="checkboxHighlightVertices" type="checkbox" checked="true" onchange="checkboxHighlightVertices_Changed();"></input>
			<label>Faces:</label>
			<input id="checkboxHighlightFaces" type="checkbox" checked="true" onchange="checkboxHighlightFaces_Changed();"></input>
		</div>
	</div>
	<div id="divSelect" style="border:1px solid">
		<label>Select:</label>
		<button onclick="buttonSelectAtCursor_Clicked();">At Cursor</button>
		<button onclick="buttonSelectAll_Clicked();">All</button>
		<button onclick="buttonSelectNone_Clicked();">None</button>
		<button onclick="buttonDeselectAtCursor_Clicked();">Deselect At Cursor</button>
	</div>
	<div id="divVertexOperations" style="border:1px solid">
		<label>Vertices:</label>
		<button onclick="buttonVertexAddAtCursor_Clicked();">Add at Cursor</button>
		<button onclick="buttonVertexDeleteSelected_Clicked();">Delete Selected</button>	
		<button onclick="buttonVertexConnectSelected_Clicked();">Connect Selected</button>		
		<button onclick="buttonVertexFaceBuildWithSelected_Clicked();">Face from Selected</button>				
	</div>
	
	<div id="divTransform" style="border:1px solid">
		<label>Transform:</label>
		<div id="divTransformParameters">
			<div>
				<label>Relative to:</label>
				<select id="selectTransformCenter">
					<option>Origin</option>				
					<option>Median of Selected</option>
					<option>Cursor</option>
				</select>
			</div>
			<div>
				<label>Axes:</label>
				<select id="selectTransformAxes">
					<option>All</option>
					<option>X</option>
					<option>Y</option>
					<option>Z</option>
					<option>Camera Forward</option>
				</select>
			</div>
			<div>
				<label>Operation:</label>
				<select id="selectTransformOperation">
					<option>Translate</option>
					<option>Rotate</option>
					<option>Scale</option>
				</select>
			</div>
			<div>
				<label>Amount:</label>
				<input id="inputTransformAmount" type="number" value="0"></input>
			</div>
			<button onclick="buttonTransform_Clicked();">Transform Selected</button>
		</div>
	</div>
	<div style="border:1px solid">
		<label>File:</label>
		<button onclick="buttonFileSave_Clicked();">Save</button>
		<button onclick="buttonFileLoad_Clicked();">Load:</button>
		<input id="inputFileToLoad" type="file"></input>
	</div>
</div>

<script type="text/javascript">

// ui events

function buttonDeselectAtCursor_Clicked()
{
	Globals.Instance.session.deselectAtCursor();
}

function buttonFileLoad_Clicked()
{
	var inputFileToLoad = document.getElementById("inputFileToLoad");
	var fileToLoad = inputFileToLoad.files[0];
	if (fileToLoad == null)
	{
		alert("No file specified!")
	}
	else
	{
		Globals.Instance.session.sceneLoadFromFile(fileToLoad);
	}
}

function buttonFileSave_Clicked()
{
	Globals.Instance.session.sceneSaveToFile();
}

function buttonSelectAll_Clicked()
{
	Globals.Instance.session.selectAll();
}

function buttonSelectAtCursor_Clicked()
{
	Globals.Instance.session.selectAtCursor();
}

function buttonSelectNone_Clicked()
{
	Globals.Instance.session.selectNone();
}

function buttonTransform_Clicked()
{
	var session = Globals.Instance.session;

	var centerName = document.getElementById("selectTransformCenter").value;

	var center;
	if (centerName == "Origin")
	{
		center = new Coords(0, 0, 0);
	}
	else if (centerName == "Median of Selected")
	{
		var scene = session.scene;
		center = scene.selection.medianForMesh(scene.mesh);
	}
	else if (centerName == "Cursor")
	{
		center = session.scene.cursor.pos;
	}
	
	var axes = document.getElementById("selectTransformAxes").value;
	var operation = document.getElementById("selectTransformOperation").value;
	var amount = parseFloat(document.getElementById("inputTransformAmount").value);
	if (isNaN(amount) == true)
	{
		amount = 0;
	}
	
	var amountAsCoords;
	var axis;
	if (axes == "All")
	{
		amountAsCoords = new Coords(1, 1, 1);
		axis = null;
	}
	else if (axes == "X")
	{
		amountAsCoords = new Coords(1, 0, 0);
		axis = new Coords(1, 0, 0);		
	}
	else if (axes == "Y")
	{
		amountAsCoords = new Coords(0, 1, 0);
		axis = new Coords(0, 1, 0);		
	}
	else if (axes == "Z")
	{
		amountAsCoords = new Coords(0, 0, 1);
		axis = new Coords(0, 0, 1);
	}
	else if (axes == "Camera Forward")
	{
		var camera = session.scene.camera;
		var cameraForward = camera.loc.orientation.forward;
		amountAsCoords = cameraForward.clone().normalize();	
		axis = cameraForward.clone();
	}	
	
	amountAsCoords.multiplyScalar(amount);

	var transform = null;
	if (operation == "Translate")
	{
		if (axes != "All")
		{
			transform = new Transform_Translate(amountAsCoords);
		}
	}
	else if (operation == "Rotate")
	{
		if (axes != "All")
		{
			transform = new Transform_Rotate(axis, amount);
		}
	}
	else if (operation == "Scale")
	{
		transform = new Transform_Scale(amountAsCoords);
	}

	if (transform == null)
	{
		alert("Invalid parameters for transform!");
	}
	else
	{
		session.transformWithCenterApplyToSelected(transform, center);
		session.update();
	}
}

function buttonVertexAddAtCursor_Clicked()
{
	var session = Globals.Instance.session;
	var scene = session.scene;
	var vertexNew = scene.cursor.pos.clone();
	var mesh = scene.mesh;
	mesh.vertices.push(vertexNew);
	session.selectNone();
	session.selectAtCursor();
	session.update();
}

function buttonVertexConnectSelected_Clicked()
{
	var session = Globals.Instance.session;
	var scene = session.scene;
	var mesh = scene.mesh;
	var selection = scene.selection;
	mesh.verticesConnectWithEdge(selection.vertexIndices);
	session.update();
}

function buttonVertexDeleteSelected_Clicked()
{
	var session = Globals.Instance.session;
	var scene = session.scene;
	var mesh = scene.mesh;
	var verticesToRemove = scene.selection.verticesForMesh(mesh);
	mesh.verticesRemove(verticesToRemove);
	session.selectNone();
	session.update();
}

function buttonVertexFaceFromSelected_Clicked()
{
	var session = Globals.Instance.session;
	var scene = session.scene;
	var mesh = scene.mesh;
	var selection = scene.selection;
	mesh.edgesConnectWithFace(selection.vertexIndices);
	session.update();
}

function buttonViewDown_Clicked()
{
	var session = Globals.Instance.session;
	session.viewMoveDown();
	session.update();
}

function buttonViewFront_Clicked()
{
	var session = Globals.Instance.session;
	session.viewSetFront();
	session.update();
}

function buttonViewIn_Clicked()
{
	var session = Globals.Instance.session;
	session.viewMoveIn();
	session.update();	
}

function buttonViewLeft_Clicked()
{
	var session = Globals.Instance.session;
	session.viewMoveLeft();
	session.update();
}

function buttonViewOut_Clicked()
{
	var session = Globals.Instance.session;
	session.viewMoveOut();
	session.update();
}

function buttonViewRight_Clicked()
{
	var session = Globals.Instance.session;
	session.viewMoveRight();
	session.update();
}

function buttonViewSelected_Clicked()
{
	var session = Globals.Instance.session;
	session.viewSetSelected();
	session.update();
}

function buttonViewSide_Clicked()
{
	var session = Globals.Instance.session;
	session.viewSetSide();
	session.update();
}

function buttonViewTop_Clicked()
{
	var session = Globals.Instance.session;
	session.viewSetTop();
	session.update();
}

function buttonViewUp_Clicked()
{
	var session = Globals.Instance.session;
	session.viewMoveUp();
	session.update();	
}

function checkboxHighlightVertices_Changed()
{
	var highlightVertices = document.getElementById("checkboxHighlightVertices").checked;	
	var session = Globals.Instance.session;
	session.scene.highlightVertices = highlightVertices;
	session.update();	
}

function checkboxHighlightFaces_Changed()
{
	var highlightFaces = document.getElementById("checkboxHighlightFaces").checked;	
	var session = Globals.Instance.session;
	session.scene.highlightFaces = highlightFaces;
	session.update();	
}


// main

function main()
{
	var camera = new Camera
	(
		new Coords(100, 100, 0), // viewSize
		50, // focalLength
		new Location
		(
			new Coords(-2, -2, -2), // pos
			Orientation.fromForwardAndDown
			(
				new Coords(1, 1, 1), // forward
				new Coords(0, 0, 1) // down
			)
		)
	);

	var display = new Display
	(
		camera.viewSize
	);
	
	var mesh = MeshBuilder.cube();
	
	var scene = new Scene
	(
		camera,
		mesh
	);

	Globals.Instance.initialize
	(
		display,
		scene
	);
}

// extensions

function ArrayExtensions()
{
	// extension class
}
{
	Array.prototype.contains = function(element)
	{
		return (this.indexOf(element) >= 0);
	}

	Array.prototype.remove = function(element)
	{
		this.splice(this.indexOf(element), 1);
		return this;
	}
	
	Array.prototype.removeAt = function(index)
	{
		this.splice(index, 1);
		return this;
	}
	
}

// classes

function Bounds(min, max)
{
	this.min = min;
	this.max = max;
	this.size = new Coords();
	this.sizeRecalculate();
}
{
	// static methods
	
	Bounds.ofPoints = function(pointsToFindBoundsOf)
	{
		var point0 = pointsToFindBoundsOf[0];
		var min = point0.clone();
		var max = point0.clone();
		
		for (var i = 1; i < pointsToFindBoundsOf.length; i++)
		{
			var point = pointsToFindBoundsOf[i];
			if (point.x < min.x)
			{
				min.x = point.x;
			}
			else if (point.x > max.x)
			{
				max.x = point.x;
			}

			if (point.y < min.y)
			{
				min.y = point.y;
			}
			else if (point.y > max.y)
			{
				max.y = point.y;
			}

			if (point.z < min.z)
			{
				min.z = point.z;
			}
			else if (point.z > max.z)
			{
				max.z = point.z;
			}
		}
		
		var returnValue = new Bounds(min, max);
		
		return returnValue;
	}

	// instance methods
	
	Bounds.prototype.center = function()
	{
		return this.min.clone().add(this.max).divideScalar(2);
	}

	Bounds.prototype.sizeRecalculate = function()
	{
		this.size.overwriteWith(this.max).subtract(this.min);
	}

	Bounds.prototype.trimCoords = function(coordsToTrim)
	{
		if (coordsToTrim.x < this.min.x)
		{
			coordsToTrim.x = this.min.x;
		}
		else if (coordsToTrim.x > this.max.x)
		{
			coordsToTrim.x = this.max.x;
		}
		
		if (coordsToTrim.y < this.min.y)
		{
			coordsToTrim.y = this.min.y;
		}
		else if (coordsToTrim.y > this.max.y)
		{
			coordsToTrim.y = this.max.y;
		}

		if (coordsToTrim.z < this.min.z)
		{
			coordsToTrim.z = this.min.z;
		}
		else if (coordsToTrim.z > this.max.z)
		{
			coordsToTrim.z = this.max.z;
		}		
		
		return coordsToTrim;
	}
}

function Camera(viewSize, focalLength, loc)
{
	this.viewSize = viewSize;
	this.focalLength = focalLength;
	this.loc = loc;
	
	this.viewSizeHalf = this.viewSize.clone().divideScalar(2);
			
	this.constraints = 
	[
		new Constraint_Upright(),	
		new Constraint_LookAt(new Coords(0, 0, 0)),
		new Constraint_KeepDistance(new Coords(0, 0, 0), this.loc.pos.magnitude()),
	];
	
	this.reinitialize();
}
{
	Camera.prototype.constraintsApply = function()
	{
		for (var i = 0; i < this.constraints.length; i++)
		{
			var constraint = this.constraints[i];
			constraint.constrainLoc(this.loc)
		}
	}
		
	Camera.prototype.transformCoordsWorldToView = function(coordsToTransform)
	{
		return this.transformWorldToView.transformCoords(coordsToTransform);
	}
	
	Camera.prototype.transformCoordsViewToWorld = function(coordsToTransform)
	{
		return this.transformViewToWorld.transformCoords(coordsToTransform);
	}	

	Camera.prototype.transformDistanceWorldToView = function(distanceToTransform, distanceFromCamera)
	{
		var returnValue = distanceToTransform * this.focalLength * this.focalLength / distanceFromCamera;
		return returnValue;
	}
	
	// serialization
	
	Camera.prototype.reinitialize = function()
	{
		// hack
	
		this.loc.orientation.axesReset();
	
		this.transformWorldToView = new Transform_Multiple
		([
			new Transform_TranslateInverse(this.loc.pos),
			new Transform_Orient(this.loc.orientation),
			new Transform_Perspective(this.focalLength),
			new Transform_Translate(this.viewSizeHalf),
		]);
		
		this.transformViewToWorld = new Transform_Multiple
		([
			new Transform_TranslateInverse(this.viewSizeHalf),	
			new Transform_PerspectiveInverse(this.focalLength),
			new Transform_OrientInverse(this.loc.orientation),
			new Transform_Translate(this.loc.pos),
		]);		
	}

	
}

function Constraint_KeepDistance(targetPos, distanceToMaintain)
{
	this.targetPos = targetPos;
	this.distanceToMaintain = distanceToMaintain;
}
{
	Constraint_KeepDistance.prototype.constrainLoc = function(loc)
	{
		var displacement = loc.pos.subtract // No clone needed.
		(
			this.targetPos
		);
		
		displacement.normalize().multiplyScalar
		(
			this.distanceToMaintain
		).add
		(
			this.targetPos
		);		
	}
}


function Constraint_LookAt(targetPos)
{
	this.targetPos = targetPos;
}
{
	Constraint_LookAt.prototype.constrainLoc = function(loc)
	{
		var orientation = loc.orientation;
		orientation.forward.overwriteWith
		(
			this.targetPos
		).subtract
		(
			loc.pos
		)
		
		orientation.orthogonalizeAxes().normalizeAxes();		
	}
}

function Constraint_Upright()
{
	// do nothing
}
{
	Constraint_Upright.prototype.constrainLoc = function(loc)
	{
		var orientation = loc.orientation;
		if (orientation.forward.equals(Coords.Instances.ZeroZeroOne) == false)
		{
			orientation.down.overwriteWithXYZ(0, 0, 1);
		}
	}
}

function Coords(x, y, z)
{
	this.x = x;
	this.y = y;
	this.z = z;
}
{
	// instances
	
	Coords.Instances = new Coords_Instances()
	
	function Coords_Instances()
	{
		this.Ones = new Coords(1, 1, 1);
		this.ZeroZeroOne = new Coords(0, 0, 1);	
		this.Zeroes = new Coords(0, 0, 0);
	}

	// methods

	Coords.prototype.add = function(other)
	{
		this.x += other.x;
		this.y += other.y;
		this.z += other.z;
		return this;
	}

	Coords.prototype.clone = function()
	{
		return new Coords(this.x, this.y, this.z);
	}
	
	Coords.prototype.crossProduct = function(other)
	{
		return this.overwriteWithXYZ 
		(
			this.y * other.z - this.z * other.y,
			this.z * other.x - this.x * other.z,
			this.x * other.y - this.y * other.x
		);
	}
	
	Coords.prototype.divide = function(other)
	{
		this.x /= other.x;
		this.y /= other.y;
		this.z /= other.z;
		return this;
	}
	
	Coords.prototype.divideScalar = function(scalar)
	{
		this.x /= scalar;
		this.y /= scalar;
		this.z /= scalar;
		return this;
	}
	
	Coords.prototype.dotProduct = function(other)
	{
		return this.x * other.x + this.y * other.y + this.z * other.z;
	}
	
	Coords.prototype.equals = function(other)
	{
		var returnValue = 
		(
			this.x == other.x
			&& this.y == other.y
			&& this.z == other.z
		);
		
		return returnValue;
	}
	
	Coords.prototype.invert = function()
	{
		return this.multiplyScalar(-1);
	}

	Coords.prototype.magnitude = function()
	{
		return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
	}
	
	Coords.prototype.multiply = function(other)
	{
		this.x *= other.x;
		this.y *= other.y;
		this.z *= other.z;
		return this;
	}
	
	Coords.prototype.multiplyScalar = function(scalar)
	{
		this.x *= scalar;
		this.y *= scalar;
		this.z *= scalar;
		return this;
	}	
	
	Coords.prototype.normalize = function()
	{
		return this.divideScalar(this.magnitude());
	}
	
	Coords.prototype.overwriteWith = function(other)
	{
		this.x = other.x;
		this.y = other.y;
		this.z = other.z;
		return this;
	}	
	
	Coords.prototype.overwriteWithXYZ = function(x, y, z)
	{
		this.x = x;
		this.y = y;
		this.z = z;
		return this;
	}		
	
	Coords.prototype.subtract = function(other)
	{
		this.x -= other.x;
		this.y -= other.y;
		this.z -= other.z;
		return this;
	}
}

function Cursor()
{
	this.radius = 5;
	this.pos = new Coords(0, 0, 0);
}
{
	Cursor.prototype.drawToDisplayForCamera = function(display, camera)
	{
		var drawPos = display.drawPos;
		camera.transformCoordsWorldToView
		(
			drawPos.overwriteWith
			(
				this.pos
			)
		);
		display.drawCircle(drawPos, this.radius, null, "Black");
	}
}

function Display(viewSize)
{
	this.viewSize = viewSize;
	
	this.colorFore = "LightGray";
	this.colorBack = "White";
	
	// helper variables
	this.drawPos = new Coords();
}
{
	Display.prototype.clear = function()
	{
		this.drawRectangle(Coords.Instances.Zeroes, this.viewSize, this.colorBack, this.colorFore);
	}

	Display.prototype.drawCircle = function(center, radius, colorFill, colorBorder)
	{
		this.graphics.beginPath();
		
		this.graphics.arc
		(
			center.x, center.y,
			radius,
			0, Math.PI * 2 // start and stop angles
		);
		
		if (colorFill != null)
		{
			this.graphics.fillStyle = colorFill;
			this.graphics.fill();
		}
		
		if (colorBorder != null)
		{
			this.graphics.strokeStyle = colorBorder;		
			this.graphics.stroke();
		}
	}

	Display.prototype.drawLine = function(fromPos, toPos)
	{
		this.graphics.beginPath();
		this.graphics.moveTo(fromPos.x, fromPos.y);
		this.graphics.lineTo(toPos.x, toPos.y);
		this.graphics.stroke();
	}
	
	Display.prototype.drawRectangle = function(pos, size, colorFill, colorBorder)
	{
		if (colorFill != null)
		{
			this.graphics.fillStyle = colorFill;
			this.graphics.fillRect
			(
				pos.x, pos.y,
				size.x, size.y
			);
		}
		
		if (colorBorder != null)
		{
			this.graphics.strokeStyle = colorBorder;
			this.graphics.strokeRect
			(
				pos.x, pos.y,
				size.x, size.y
			);
		}
	}
	
	Display.prototype.initialize = function()
	{
		this.canvas = document.createElement("canvas");
		this.canvas.width = this.viewSize.x;
		this.canvas.height = this.viewSize.y;
		
		this.graphics = this.canvas.getContext("2d");
		
		var divMain = document.getElementById("divMain");
		divMain.appendChild(this.canvas);
	}
}

function Edge(vertexIndices)
{
	this.vertexIndices = vertexIndices;
}
{
	// helper variables

	Edge.drawPosFrom = new Coords();
	Edge.drawPosTo = new Coords();

	// methods

	Edge.prototype.vertices = function(mesh)
	{
		var returnValues = [];
	
		var vertices = mesh.vertices;
		for (var i = 0; i < this.vertexIndices.length; i++)
		{
			var vertexIndex = this.vertexIndices[i];
			var vertex = vertices[vertexIndex];
			returnValues.push(vertex);
		}
		
		return returnValues;	
	}
	
	// drawable
	
	Edge.prototype.drawToDisplayForCameraAndMesh = function(display, camera, mesh)
	{
		var fromPos = Edge.drawPosFrom;
		var toPos = Edge.drawPosTo;
		
		var vertices = this.vertices(mesh);
		
		fromPos.overwriteWith(vertices[0]);
		camera.transformCoordsWorldToView(fromPos);
		
		toPos.overwriteWith(vertices[1]);
		camera.transformCoordsWorldToView(toPos);
		
		display.drawLine(fromPos, toPos);
	}
}

function Face(vertexIndices)
{
	this.vertexIndices = vertexIndices;
}
{
	Face.prototype.medianForMesh = function(mesh)
	{
		var vertices = this.vertices(mesh);
		var median = vertices[0].clone();
		for (var i = 1; i < vertices.length; i++)
		{
			var vertex = vertices[i];
			median.add(vertex);
		}
		median.divideScalar(vertices.length);
		
		return median;
	}

	Face.prototype.plane = function(mesh)
	{
		var vertices = this.vertices(mesh);
		var plane = Plane.fromPoints(vertices);
		return plane;
	}

	Face.prototype.vertices = function(mesh)
	{
		var returnValues = [];
	
		var vertices = mesh.vertices;
		for (var i = 0; i < this.vertexIndices.length; i++)
		{
			var vertexIndex = this.vertexIndices[i];
			var vertex = vertices[vertexIndex];
			returnValues.push(vertex);
		}
		
		return returnValues;
	}
	
	// drawable
	
	Face.prototype.drawToDisplayForCameraAndMesh = function(display, camera, mesh)
	{
		var median = this.medianForMesh(mesh);
		var normal = this.plane(mesh).normal;

		var fromPos = median;

		var normalIndicatorLength = 1;
		var toPos = normal.multiplyScalar(normalIndicatorLength).add(median);

		camera.transformCoordsWorldToView(fromPos);
		camera.transformCoordsWorldToView(toPos);
		
		display.drawRectangle(fromPos, Coords.Instances.Ones, null, display.colorFore);
		display.drawLine(fromPos, toPos);
	}

}

function FileHelper()
{
	// do nothing
}
{
    FileHelper.prototype.loadFileAsBinaryString = function(systemFileToLoad, callback, contextForCallback)
    {
        var fileReader = new FileReader();
        fileReader.systemFile = systemFileToLoad;
        fileReader.callback = callback;
        fileReader.contextForCallback = contextForCallback;
        fileReader.onload = this.loadFile_FileLoaded.bind(this);
        fileReader.readAsBinaryString(systemFileToLoad);
    }
	
    FileHelper.prototype.loadFileAsText = function(systemFileToLoad, callback, contextForCallback)
    {
        var fileReader = new FileReader();
        fileReader.systemFile = systemFileToLoad;
        fileReader.callback = callback;
        fileReader.contextForCallback = contextForCallback;
        fileReader.onload = this.loadFile_FileLoaded.bind(this);
        fileReader.readAsText(systemFileToLoad);
    }
	 
    FileHelper.prototype.loadFile_FileLoaded = function(fileLoadedEvent)
    {
        var fileReader = fileLoadedEvent.target;
        var contentsOfFileLoaded = fileReader.result;
        var fileName = fileReader.systemFile.name;
 
        var callback = fileReader.callback;
        var contextForCallback = fileReader.contextForCallback;
        callback.call(contextForCallback, contentsOfFileLoaded);
    }
 
    FileHelper.prototype.saveBinaryStringToFileWithName = function(fileAsBinaryString, fileName)
    {
        var fileAsArrayBuffer = new ArrayBuffer(fileAsBinaryString.length);
        var fileAsArrayUnsigned = new Uint8Array(fileAsArrayBuffer);
        for (var i = 0; i < fileAsBinaryString.length; i++) 
        {
            fileAsArrayUnsigned[i] = fileAsBinaryString.charCodeAt(i);
        }
 
        var fileAsBlob = new Blob([fileAsArrayBuffer], {type:'unknown/unknown'});
 
        var link = document.createElement("a");
        link.href = window.URL.createObjectURL(fileAsBlob);
        link.download = fileName;
        link.click();
    }
	
	FileHelper.prototype.saveTextStringToFileWithName = function(textToSave, fileNameToSaveAs)
	{
		var textToSaveAsBlob = new Blob([textToSave], {type:"text/plain"});	 
        var link = document.createElement("a");
        link.href = window.URL.createObjectURL(textToSaveAsBlob);
        link.download = fileNameToSaveAs;
        link.click();
	}
}

function Globals()
{
	// do nothing
}
{
	// instance
	
	Globals.Instance = new Globals();
	
	// methods

	Globals.prototype.initialize = function(display, scene)
	{
		this.display = display;
		this.session = new Session(scene);
		
		this.inputHelper = new InputHelper();
		
		this.display.initialize();		
		this.session.initialize();		
		this.update();
		
		this.inputHelper.initialize();
	}
	
	Globals.prototype.update = function()
	{
		this.session.update();
	}
}

function InputHelper()
{
	this.keyPressed = null;
	this.isMouseButtonPressed = false;
	this.mouseClickPos = new Coords();
}
{
	InputHelper.prototype.initialize = function()
	{
		document.body.onkeydown = this.handleEventKeyDown.bind(this);
		document.body.onkeyup = this.handleEventKeyUp.bind(this);

		var canvas = Globals.Instance.display.canvas;
		canvas.onmousedown = this.handleEventMouseDown.bind(this);
		canvas.onmouseup = this.handleEventMouseUp.bind(this);
		
		var divMainBounds = divMain.getBoundingClientRect();
		this.mouseClickPosOffset = new Coords
		(
			divMainBounds.left,
			divMainBounds.top,
			0
		);
	}
	
	// events
	
	InputHelper.prototype.handleEventKeyDown = function(event)
	{
		this.keyPressed = event.key;
		Globals.Instance.update();
	}
	
	InputHelper.prototype.handleEventKeyUp = function(event)
	{
		this.keyPressed = null;
	}
	
	InputHelper.prototype.handleEventMouseDown = function(event)
	{
		this.isMouseButtonPressed = true;
		this.mouseClickPos.overwriteWithXYZ
		(
			event.x, event.y, 0
		).subtract
		(
			this.mouseClickPosOffset
		);
		Globals.Instance.update();
	}
	
	InputHelper.prototype.handleEventMouseUp = function(event)
	{
		this.isMouseButtonPressed = false;
	}
	
	
}

function Location(pos, orientation)
{
	this.pos = pos;
	this.orientation = orientation;
}

function Mesh(vertices, faces)
{
	this.vertices = vertices;
	this.faces = faces;
	
	this.edgesBuild();
}
{
	// helper variables
	
	Mesh.drawPos = new Coords();

	// instance methods

	Mesh.prototype.edgesBuild = function()
	{
		this.edges = [];
		var edgeLookup = [];
		
		for (var f = 0; f < this.faces.length; f++)
		{
			var face = this.faces[f];
			var faceVertexIndices = face.vertexIndices;
			
			for (var vi = 0; vi < faceVertexIndices.length; vi++)
			{			
				var vertexIndex = faceVertexIndices[vi];
				var vertex = this.vertices[vertexIndex];

				var viNext = NumberHelper.wrapValueToRangeMax(vi + 1, faceVertexIndices.length);
				var vertexIndexNext = faceVertexIndices[viNext];
				var vertexNext = this.vertices[vertexIndexNext];
				
				var vertexIndicesSorted;
				if (vertexIndex < vertexIndexNext)
				{
					vertexIndicesSorted = [vertexIndex, vertexIndexNext];
				}
				else
				{
					vertexIndicesSorted = [vertexIndexNext, vertexIndex];
				}
				
				var vertexIndexLesser = vertexIndicesSorted[0];
				var vertexIndexGreater = vertexIndicesSorted[1];
				
				var edgesWithVertexIndexLesser = edgeLookup[vertexIndexLesser];
				if (edgesWithVertexIndexLesser == null)
				{
					edgesWithVertexIndexLesser = [];
					edgeLookup[vertexIndexLesser] = edgesWithVertexIndexLesser;
				}
				var edgeExisting = edgesWithVertexIndexLesser[vertexIndexGreater];
				if (edgeExisting == null)
				{
					var edgeNew = new Edge([vertexIndexLesser, vertexIndexGreater])
					edgesWithVertexIndexLesser.push(edgeNew);
					
					this.edges.push(edgeNew);
				}
				else
				{
					// todo
				}
			}
		}
	}
	
	Mesh.prototype.edgesConnectWithFace = function(vertexIndicesForEdges)
	{
		if (vertexIndicesToConnect.length < 3 || vertexIndicesToConnect.length > 4)
		{
			alert("Either 3 or 4 vertices must be selected!");
		}
		else
		{
			var face = new Face(vertexIndicesToConnect.slice(0));
			this.faces.add(face);
			this.edgesBuild();
		}
	}
	

	Mesh.prototype.vertexRemove = function(vertexToRemove)
	{
		// todo
		var vertexIndexBeingRemoved = this.vertices.indexOf(vertexToRemove);
		this.vertices.remove(vertexToRemove);
		
		var facesToRemove = [];
		
		for (var f = 0; f < this.faces.length; f++)
		{
			var face = this.faces[f];
			var faceVertexIndices = face.vertexIndices;
			for (var vi = 0; vi < faceVertexIndices.length; vi++)
			{
				var faceVertexIndex = faceVertexIndices[vi];
				if (faceVertexIndex == vertexIndexBeingRemoved)
				{
					facesToRemove.push(face);
					break;
				}
				else if (faceVertexIndex > vertexIndexBeingRemoved)
				{
					faceVertexIndices[vi] = faceVertexIndex - 1;
				}
			}
		}
		
		for (var fi = 0; fi < facesToRemove.length; fi++)
		{
			var faceToRemove = facesToRemove[fi];
			this.faces.remove(faceToRemove);
		}
		
		this.edgesBuild();
	}
	
	Mesh.prototype.verticesConnectWithEdge = function(vertexIndicesToConnect)
	{
		if (vertexIndicesToConnect.length != 2)
		{
			alert("Exactly 2 vertices must be selected!");
		}
		else
		{
			var edge = new Edge(vertexIndicesToConnect);
			this.edges.push(edge);
		}
	}
	
	Mesh.prototype.verticesRemove = function(verticesToRemove)
	{
		for (var i = 0; i < verticesToRemove.length; i++)
		{
			var vertex = verticesToRemove[i];
			
			this.vertexRemove(vertex);
		}
	}
		
	// drawable
	
	Mesh.prototype.drawToDisplayForScene = function(display, scene)
	{
		var camera = scene.camera;
	
		for (var i = 0; i < this.edges.length; i++)
		{
			var edge = this.edges[i];
			edge.drawToDisplayForCameraAndMesh(display, camera, this);
		}
		
		var drawPos = Mesh.drawPos;
		var vertexHandleRadiusActual = .25;
		
		if (scene.highlightVertices == true)
		{
			for (var i = 0; i < this.vertices.length; i++)
			{
				var vertex = this.vertices[i];
				drawPos.overwriteWith(vertex);
				camera.transformCoordsWorldToView(drawPos);
				var vertexHandleRadiusApparent = camera.transformDistanceWorldToView
				(
					vertexHandleRadiusActual, drawPos.z
				);
				display.drawCircle(drawPos, vertexHandleRadiusApparent, null, display.colorFore);
			}
		}
		
		if (scene.highlightFaces == true)
		{
			for (var i = 0; i < this.faces.length; i++)
			{
				var face = this.faces[i];
				face.drawToDisplayForCameraAndMesh(display, camera, this);
			}
		}
	}
}

function MeshBuilder()
{
	// static class
}
{
	MeshBuilder.cube = function()
	{
		var returnValue = new Mesh
		(
			// vertices
			[
				// top
				new Coords(-1, -1, -1), // 0 - nw
				new Coords(1, -1, -1), // 1 - ne
				new Coords(1, 1, -1), // 2 - se
				new Coords(-1, 1, -1), // 3 - sw
				
				// bottom
				new Coords(-1, -1, 1), // 4 - nw
				new Coords(1, -1, 1), // 5 - ne
				new Coords(1, 1, 1), // 6 - se
				new Coords(-1, 1, 1), // 7 - sw
			],
			// faces
			[
				new Face([0, 3, 2, 1]), // top
				new Face([1, 2, 6, 5]), // east
				new Face([2, 3, 7, 6]), // south 
				new Face([3, 0, 4, 7]), // west
				new Face([0, 1, 5, 4]), // north
				new Face([4, 5, 6, 7]), // bottom
			]
		);
		
		return returnValue;
	}
}

function NumberHelper()
{
	// static class
}
{
	NumberHelper.wrapValueToRangeMax = function(valueToWrap, max)
	{
		while (valueToWrap < 0)
		{
			valueToWrap += max; // rangeSize == max
		}
	
		while (valueToWrap >= max)
		{
			valueToWrap -= max;
		}
		
		return valueToWrap;
	}
}

function Orientation(forward, right, down)
{
	this.forward = forward;
	this.right = right;
	this.down = down;
	this.axesReset();
}
{
	// static methods

	Orientation.fromForwardAndDown = function(forward, down)
	{
		var returnValue = new Orientation(forward.clone(), new Coords(), down.clone());
		returnValue.orthogonalizeAxes().normalizeAxes();
		return returnValue;
	}
	
	Orientation.prototype.axesReset = function()
	{
		this.axes = [this.forward, this.right, this.down];	
	}
	
	// instance methods
	
	Orientation.prototype.normalizeAxes = function()
	{
		for (var i = 0; i < this.axes.length; i++)
		{
			this.axes[i].normalize();
		}
		
		return this;
	}
	
	Orientation.prototype.orthogonalizeAxes = function()
	{
		this.right.overwriteWith(this.down).crossProduct(this.forward);
		this.down.overwriteWith(this.forward).crossProduct(this.right);
		return this;
	}
	
	Orientation.prototype.overwriteWith = function(other)
	{
		this.forward.overwriteWith(other.forward);
		this.right.overwriteWith(other.right);
		this.down.overwriteWith(other.down);
		return this;
	}
}

function Plane(normal, distanceFromOrigin)
{
	this.normal = normal;
	this.distanceFromOrigin = distanceFromOrigin;
}
{
	// helper variables
	Plane.displacementFromPoint0To1 = new Coords();
	Plane.displacementFromPoint1To2 = new Coords();

	// static methods

	Plane.fromPoints = function(points)
	{
		var displacementFromPoint0To1 = Plane.displacementFromPoint0To1;
		var displacementFromPoint1To2 = Plane.displacementFromPoint1To2;
		
		var point0 = points[0];
		var point1 = points[1];
		var point2 = points[2];
		
		displacementFromPoint0To1.overwriteWith(point1).subtract(point0);
		displacementFromPoint1To2.overwriteWith(point2).subtract(point1);
		
		var normal = new Coords().overwriteWith
		(
			displacementFromPoint0To1
		).crossProduct
		(
			displacementFromPoint1To2
		).normalize();
		
		var distanceFromOrigin = normal.dotProduct(point0);
		
		var returnValue = new Plane(normal, distanceFromOrigin);
		
		return returnValue;
	}
}

function Scene(camera, mesh)
{
	this.camera = camera;
	this.mesh = mesh;

	this.selection = new Selection();
	this.cursor = new Cursor();
	
	this.highlightVertices = true;
	this.highlightFaces = true;
}
{
	Scene.prototype.drawToDisplay = function(display)
	{
		this.mesh.drawToDisplayForScene(display, this);
		this.selection.drawToDisplayForCameraAndMesh(display, this.camera, this.mesh);
		this.cursor.drawToDisplayForCamera(display, this.camera);
	}
		
	Scene.prototype.update = function()
	{
		this.update_Input();
		this.camera.constraintsApply();
		this.drawToDisplay(Globals.Instance.display);
	}
	
	Scene.prototype.update_Input = function()
	{
		var session = Globals.Instance.session;
		var inputHelper = Globals.Instance.inputHelper;
		var keyPressed = inputHelper.keyPressed;
		if (keyPressed != null)
		{
			inputHelper.keyPressed = null;
					
			if (keyPressed.startsWith("Arrow") == true)
			{				
				if (keyPressed == "ArrowDown")
				{
					session.viewMoveDown();
				}
				else if (keyPressed == "ArrowLeft")
				{
					session.viewMoveLeft();
				}
				else if (keyPressed == "ArrowRight")
				{
					session.viewMoveRight();
				}				
				else if (keyPressed == "ArrowUp")
				{
					session.viewMoveUp();
				}				
			}
			else if (keyPressed == "=") // +
			{
				session.viewMoveIn();
			}
			else if (keyPressed == "-")
			{
				session.viewMoveOut();	
			}

		}	
		
		if (inputHelper.isMouseButtonPressed == true)
		{
			inputHelper.isMouseButtonPressed = false;
			var cursorPosApparent = this.camera.transformCoordsWorldToView(this.cursor.pos.clone());			
			var cursorPosNext = inputHelper.mouseClickPos.clone();
			cursorPosNext.z = cursorPosApparent.z;
			this.camera.transformCoordsViewToWorld(cursorPosNext);
			this.cursor.pos.overwriteWith(cursorPosNext);
		}
	}
}

function Selection()
{
	this.vertexIndices = [];
}
{	
	Selection.prototype.medianForMesh = function(mesh)
	{
		var returnValue = null;
		if (this.vertexIndicesSelected.length > 0)
		{
			var verticesSelected = this.verticesForMesh(mesh);
			var bounds = Bounds.ofPoints(verticesSelected);
			returnValue = bounds.center();
		}
		return returnValue;
	}
	
	Selection.prototype.verticesForMesh = function(mesh)
	{		
		var returnValues = [];
	
		for (var i = 0; i < this.vertexIndices.length; i++)
		{
			var vertexIndex = this.vertexIndices[i];
			var vertex = mesh.vertices[vertexIndex];
			returnValues.push(vertex);
		}
		
		return returnValues;
	}

	// drawable

	Selection.prototype.drawToDisplayForCameraAndMesh = function(display, camera, mesh)
	{
		var drawPos = display.drawPos;
		var vertexHandleRadiusActual = .25;
		
		for (var i = 0; i < this.vertexIndices.length; i++)
		{
			var vertexIndex = this.vertexIndices[i];
			var vertex = mesh.vertices[vertexIndex];
			drawPos.overwriteWith(vertex);
			camera.transformCoordsWorldToView(drawPos);
			var vertexHandleRadiusApparent = camera.transformDistanceWorldToView
			(
				vertexHandleRadiusActual, drawPos.z
			);
			display.drawCircle(drawPos, vertexHandleRadiusApparent, display.colorFore, null);
		}
	}	
}


function Serializer(knownTypes)
{
	this.knownTypes = knownTypes;

	for (var i = 0; i < this.knownTypes.length; i++)
	{
		var knownType = this.knownTypes[i];
		this.knownTypes[knownType.name] = knownType;
	}
}

{
	// internal classes
	
	function _ArrayWrapper(arrayToWrap)
	{
		this.arrayWrapped = arrayToWrap;
	}
	
	function _FunctionWrapper(functionBody)
	{
		this.functionBody = functionBody;
	}
	
	// methods
	
	Serializer.prototype.deserialize = function(stringToDeserialize)
	{
		var objectDeserialized = JSON.parse(stringToDeserialize);
		this.unwrapArraysRecursively(objectDeserialized);
		this.unwrapFunctionsRecursively(objectDeserialized);
		this.setPrototypeRecursively(objectDeserialized);
		this.deleteClassNameRecursively(objectDeserialized);

		return objectDeserialized;
	}

	Serializer.prototype.serialize = function(objectToSerialize)
	{
		this.wrapFunctionsRecursively(objectToSerialize);
		this.wrapArraysRecursively(objectToSerialize);		
		this.setClassNameRecursively(objectToSerialize);
		var returnValue = JSON.stringify(objectToSerialize);
		this.unwrapArraysRecursively(objectToSerialize);		
		this.unwrapFunctionsRecursively(objectToSerialize);
		this.deleteClassNameRecursively(objectToSerialize);

		return returnValue;
	}
	
	// class names
	
	Serializer.prototype.deleteClassNameRecursively = function(objectToDeleteClassNameOn)
	{
		if (objectToDeleteClassNameOn == null)
		{
			return; //throw "Unrecognized type!"
		}
		
		var className = objectToDeleteClassNameOn.constructor.name;
		if (this.knownTypes[className] != null)
		{
			delete objectToDeleteClassNameOn.className;

			for (var childPropertyName in objectToDeleteClassNameOn)
			{
				var childProperty = objectToDeleteClassNameOn[childPropertyName];
				this.deleteClassNameRecursively(childProperty);
			}
		}
		else if (className == "Array")
		{
			delete objectToDeleteClassNameOn.className;
			for (var i = 0; i < objectToDeleteClassNameOn.length; i++)
			{
				var element = objectToDeleteClassNameOn[i];
				this.deleteClassNameRecursively(element);
			}
		}
	}
	
	Serializer.prototype.setClassNameRecursively = function(objectToSetClassNameOn)
	{
		if (objectToSetClassNameOn == null)
		{
			return; // throw "Unrecognized type!"
		}
		var className = objectToSetClassNameOn.constructor.name;
		
		if (this.knownTypes[className] != null)
		{
			objectToSetClassNameOn.className = className;

			for (var childPropertyName in objectToSetClassNameOn)
			{
				var childProperty = objectToSetClassNameOn[childPropertyName];
				this.setClassNameRecursively(childProperty);
			}
		}
		else if (className == "Array")
		{
			for (var i = 0; i < objectToSetClassNameOn.length; i++)
			{
				var element = objectToSetClassNameOn[i];
				this.setClassNameRecursively(element);
			}
		}
		else if (className == "_ArrayWrapper")
		{
			objectToSetClassNameOn.className = className;

			var arrayWrapped = objectToSetClassNameOn.arrayWrapped;
			this.setClassNameRecursively(arrayWrapped);
		}
	}
	
	// prototypes
	
	Serializer.prototype.setPrototypeRecursively = function(objectToSetPrototypeOn)
	{
		if (objectToSetPrototypeOn == null)
		{
			return; // throw "Unrecognized type!"
		}
	
		var className = objectToSetPrototypeOn.className;
		var typeOfObjectToSetPrototypeOn = this.knownTypes[className];

		if (typeOfObjectToSetPrototypeOn != null)
		{
			objectToSetPrototypeOn.__proto__ = typeOfObjectToSetPrototypeOn.prototype;
	
			for (var childPropertyName in objectToSetPrototypeOn)
			{
				var childProperty = objectToSetPrototypeOn[childPropertyName];
				this.setPrototypeRecursively(childProperty);
			}
		}
		else if (objectToSetPrototypeOn.constructor.name == "Array")
		{
			for (var i = 0; i < objectToSetPrototypeOn.length; i++)
			{
				var element = objectToSetPrototypeOn[i];
				this.setPrototypeRecursively(element);
			}
		}
	}

	// functions
	
	Serializer.prototype.unwrapFunctionsRecursively = function(objectToUnwrapFunctionsOn)
	{
		if (objectToUnwrapFunctionsOn == null)
		{
			return;
		}
				
		var className = objectToUnwrapFunctionsOn.className;
				
		if (this.knownTypes[className] != null)
		{
			for (var childPropertyName in objectToUnwrapFunctionsOn)
			{
				var childProperty = objectToUnwrapFunctionsOn[childPropertyName];
				if (childProperty != null)
				{
					var childPropertyIsFunctionWrapped = 
						(childProperty.className == "_FunctionWrapper");
						
					if (childPropertyIsFunctionWrapped == true)
					{
						var functionBodyAsString = "(" + childProperty.functionBody + ")";
						delete childProperty.functionBody;
						var functionBodyAsFunction = eval(functionBodyAsString);
						functionBodyAsFunction.__proto__ = Function.prototype;

						objectToUnwrapFunctionsOn[childPropertyName] = functionBodyAsFunction;
					}
					else
					{
						this.unwrapFunctionsRecursively(childProperty);
					}
				}
			}
		}
		else if (objectToUnwrapFunctionsOn.constructor.name == "Array")
		{
			for (var i = 0; i < objectToUnwrapFunctionsOn.length; i++)
			{
				var element = objectToUnwrapFunctionsOn[i];
				this.unwrapFunctionsRecursively(element);
			}
		}
	}
	
	Serializer.prototype.wrapFunctionsRecursively = function(objectToWrapFunctionsOn)
	{
		var className = objectToWrapFunctionsOn.constructor.name;
		if (this.knownTypes[className] != null)
		{
			for (var childPropertyName in objectToWrapFunctionsOn)
			{
				var childProperty = objectToWrapFunctionsOn[childPropertyName];
				if (childProperty != null)
				{
					if (childProperty.constructor.name == "Function")
					{
						if (objectToWrapFunctionsOn.__proto__[childPropertyName] == null)
						{
							childProperty = new _FunctionWrapper
							(
								childProperty.toString()
							);
							objectToWrapFunctionsOn[childPropertyName] = childProperty;
						}
					}
					else
					{
						this.wrapFunctionsRecursively(childProperty);
					}
				}
			}
		}
		else if (className == "Array")
		{
			for (var i = 0; i < objectToWrapFunctionsOn.length; i++)
			{
				var element = objectToWrapFunctionsOn[i];
				this.wrapFunctionsRecursively(element);
			}
		}
	}
	
	// arrays
	
	Serializer.prototype.unwrapArraysRecursively = function(objectToUnwrapArraysOn)
	{
		if (objectToUnwrapArraysOn == null)
		{
			return;
		}
				
		var className = objectToUnwrapArraysOn.className;
				
		if (this.knownTypes[className] != null)
		{
			for (var childPropertyName in objectToUnwrapArraysOn)
			{
				var childProperty = objectToUnwrapArraysOn[childPropertyName];
				if (childProperty != null)
				{
					var childPropertyIsArrayWrapped = 
						(childProperty.className == "_ArrayWrapper");
						
					if (childPropertyIsArrayWrapped == true)
					{
						var wrapper = childProperty;
						var arrayWrapped = wrapper.arrayWrapped;
						delete wrapper.arrayWrapped;
						for (var grandchildPropertyName in wrapper)
						{
							var indexOfPropertyWithinArray = wrapper[grandchildPropertyName];
							var grandchildProperty = arrayWrapped[indexOfPropertyWithinArray];
							arrayWrapped[grandchildPropertyName] = grandchildProperty; 		
						}
						
						childProperty = arrayWrapped;
						objectToUnwrapArraysOn[childPropertyName] = childProperty;
					}

					this.unwrapArraysRecursively(childProperty);
				}
			}
		}
		else if (objectToUnwrapArraysOn.constructor.name == "Array")
		{
			for (var i = 0; i < objectToUnwrapArraysOn.length; i++)
			{
				var element = objectToUnwrapArraysOn[i];
				this.unwrapArraysRecursively(element);
			}
		}
	}
		
	Serializer.prototype.wrapArraysRecursively = function(objectToWrapArraysOn)
	{
		var className = objectToWrapArraysOn.constructor.name;
		if (this.knownTypes[className] != null)
		{
			for (var childPropertyName in objectToWrapArraysOn)
			{
				var childProperty = objectToWrapArraysOn[childPropertyName];
				if (childProperty != null)
				{
					if (childProperty.constructor.name == "Array")
					{
						var arrayWrapped = childProperty;

						var wrapper = new _ArrayWrapper(arrayWrapped);
						
						objectToWrapArraysOn[childPropertyName] = wrapper;						
						
						for (var grandchildPropertyName in arrayWrapped)
						{
							if (arrayWrapped.__proto__[grandchildPropertyName] == null)
							{
								var grandchildProperty = arrayWrapped[grandchildPropertyName];
								var indexOfPropertyWithinArray = arrayWrapped.indexOf(grandchildProperty);
								if (indexOfPropertyWithinArray >= 0)
								{
									wrapper[grandchildPropertyName] = indexOfPropertyWithinArray;
								}
							}
						}
						
						this.wrapArraysRecursively(arrayWrapped);
					}
					else
					{
						this.wrapArraysRecursively(childProperty);
					}
				}
			}
		}
		else if (className == "Array")
		{
			for (var i = 0; i < objectToWrapArraysOn.length; i++)
			{
				var element = objectToWrapArraysOn[i];
				this.wrapArraysRecursively(element);
			}
		}
	}
} 

function Session(scene)
{
	this.scene = scene;
}
{
	Session.prototype.initialize = function()
	{
		// todo
	}
		
	Session.prototype.update = function()
	{
		var display = Globals.Instance.display;
		display.clear();
		this.scene.update();
	}
	
	// tools
	
	Session.prototype.deselectAtCursor = function()
	{
		this.selectOrDeselectAtCursor(true);
	}
	
	Session.prototype.sceneLoadFromFile = function(fileToLoad)
	{
		var fileHelper = new FileHelper();
		fileHelper.loadFileAsText
		(
			fileToLoad,
			this.sceneLoadFromFile_2, // callback
			this // contextForCallback
		);
	}
	
	Session.prototype.sceneLoadFromFile_2 = function(fileAsText)
	{
		var sceneSerialized = fileAsText;
		var serializer = this.serializerBuild();
		var sceneDeserialized = serializer.deserialize(sceneSerialized);

		// hack - Serializer currently breaks referential integrity.		
		sceneDeserialized.camera.reinitialize();
		
		this.scene = sceneDeserialized;
		this.update();
	}
		
	Session.prototype.sceneSaveToFile = function()
	{
		var serializer = this.serializerBuild();
		var sceneSerialized = serializer.serialize(this.scene);
		var fileHelper = new FileHelper();
		fileHelper.saveTextStringToFileWithName(sceneSerialized, "Scene.json");
	}

	Session.prototype.selectAll = function()
	{
		var vertexIndicesSelected = this.scene.selection.vertexIndices;
		vertexIndicesSelected.length = 0;
		
		var numberOfVertices = this.scene.mesh.vertices.length;
				
		for (var i = 0; i < numberOfVertices; i++)
		{
			vertexIndicesSelected.push(i);
		}
		this.update();
	}
	
	Session.prototype.selectAtCursor = function()
	{
		this.selectOrDeselectAtCursor(false);
	}
	
	Session.prototype.selectOrDeselectAtCursor = function(deselectRatherThanSelect)
	{
		var inputHelper = Globals.Instance.inputHelper;
		var camera = this.scene.camera;
		var mesh = this.scene.mesh;
		var selection = this.scene.selection;
		var vertices = (deselectRatherThanSelect == true ? selection.verticesForMesh(mesh) : mesh.vertices);
		
		var vertexPosApparent = new Coords();
		var displacement = vertexPosApparent;
		var vertexHandleRadius = 5;
		
		var vertexZClosestSoFar = Number.POSITIVE_INFINITY;
		var vertexIndexClosestSoFar = null;

		for (var i = 0; i < vertices.length; i++)
		{
			var vertex = vertices[i];
			displacement = camera.transformCoordsWorldToView
			(
				vertexPosApparent.overwriteWith
				(
					vertex
				)
			).subtract
			(
				inputHelper.mouseClickPos
			);
			
			var vertexZ = displacement.z;
			displacement.z = 0;
			
			var distanceOfVertexFromClick = displacement.magnitude();
			
			if (distanceOfVertexFromClick <= vertexHandleRadius)
			{
				if (vertexZ < vertexZClosestSoFar)
				{
					vertexZClosestSoFar = vertexZ;
					vertexIndexClosestSoFar = i;
				}
			}
			
		} // end for each vertex
		
		if (vertexIndexClosestSoFar != null)
		{
			if (deselectRatherThanSelect == true)
			{
				selection.vertexIndices.remove(vertexIndexClosestSoFar);
			}
			else
			{
				if (selection.vertexIndices.contains(vertexIndexClosestSoFar) == false)
				{
					selection.vertexIndices.push(vertexIndexClosestSoFar);
				}
			}
		}
		this.update();
	}
	
	Session.prototype.selectNone = function()
	{
		var vertexIndicesSelected = this.scene.selection.vertexIndices;
		vertexIndicesSelected.length = 0;
		this.update();
	}
	
	Session.prototype.transformWithCenterApplyToSelected = function(transform, center)
	{
		// todo - Use center.
	
		var scene = this.scene;
		var selection = scene.selection;
		var selectionVertices = selection.verticesForMesh(scene.mesh);
		for (var i = 0; i < selectionVertices.length; i++)
		{
			var vertex = selectionVertices[i];
			transform.transformCoords(vertex);
		}
	}		
	
	Session.prototype.viewMove = function(direction)
	{
		var distanceToMove = .05;
		var camera = this.scene.camera;
		var cameraPos = camera.loc.pos;
		var offset = direction.clone().multiplyScalar(distanceToMove);
		cameraPos.add(offset);
	}
	
	Session.prototype.viewMoveDown = function() 
	{	
		this.viewMove(this.scene.camera.loc.orientation.down); 
	}
	
	Session.prototype.viewMoveIn = function() 
	{
		var distanceToMove = .1;
		var camera = this.scene.camera;
		camera.constraints[2].distanceToMaintain -= distanceToMove; // hack
	}	
	
	Session.prototype.viewMoveLeft = function() 
	{	
		this.viewMove(this.scene.camera.loc.orientation.right.clone().invert()); 
	}
	
	Session.prototype.viewMoveOut = function() 
	{	
		var distanceToMove = .1;
		var camera = this.scene.camera;
		camera.constraints[2].distanceToMaintain += distanceToMove; // hack
	}		

	Session.prototype.viewMoveRight = function() 
	{	
		this.viewMove(this.scene.camera.loc.orientation.right); 
	}

	Session.prototype.viewMoveUp = function() 
	{	
		this.viewMove(this.scene.camera.loc.orientation.down.clone().invert()); 
	}
	
	Session.prototype.viewSet = function(cameraOrientationNew)
	{
		var scene = this.scene;
		var camera = scene.camera;
		var cameraLoc = camera.loc;
		
		var distanceOfCameraFromOrigin = cameraLoc.pos.magnitude();

		cameraLoc.pos.overwriteWith
		(
			cameraOrientationNew.forward
		).invert().multiplyScalar
		(
			distanceOfCameraFromOrigin
		);
		
		cameraLoc.orientation.overwriteWith(cameraOrientationNew);
	}
	
	Session.prototype.viewSetFront = function()
	{
		this.viewSet
		(
			new Orientation
			(
				new Coords(0, 1, 0), // forward
				new Coords(1, 0, 0), // right
				new Coords(0, 0, 1) // down
			)
		);
	}
	
	Session.prototype.viewSetSelected = function()
	{
		var scene = this.scene;
		var camera = scene.camera;
		var mesh = scene.mesh;
		var selection = scene.selection;
		
		var selectionMedian = selection.medianForMesh(mesh);
		if (selectionMedian != null)
		{
			var constraintLookAt = this.scene.camera.constraints[1]; // hack
			constraintLookAt.targetPos.overwriteWith(selectionMedian);
		}
	}
		
	Session.prototype.viewSetSide = function()
	{
		this.viewSet
		(
			new Orientation
			(
				new Coords(-1, 0, 0), // forward
				new Coords(0, 1, 0), // right
				new Coords(0, 0, 1) // down
			)
		);
	}
			
	Session.prototype.viewSetTop = function()
	{
		this.viewSet
		(
			new Orientation
			(
				new Coords(0, 0, 1), // forward
				new Coords(1, 0, 0), // right
				new Coords(0, -1, 0) // down
			)
		);
	}
	
	// serializer
	
	Session.prototype.serializerBuild = function()
	{
		return new Serializer
		([
			Camera,
			Constraint_KeepDistance,
			Constraint_LookAt,
			Constraint_Upright,
			Coords,
			Cursor,
			Edge,
			Face,
			Location,
			Mesh,
			Orientation,
			Scene,
			Selection,
			Transform_Multiple,
			Transform_Orient,
			Transform_OrientInverse,
			Transform_Perspective,
			Transform_PerspectiveInverse,
			Transform_Rotate,
			Transform_Scale,
			Transform_Translate,
			Transform_TranslateInverse,
		]);
	}
}

function Transform_Multiple(children)
{
	this.children = children;
}
{
	Transform_Multiple.prototype.transformCoords = function(coordsToTransform)
	{
		for (var i = 0; i < this.children.length; i++)
		{
			var child = this.children[i];
			child.transformCoords(coordsToTransform);
		}
		
		return coordsToTransform;
	}
}

function Transform_Orient(orientation)
{
	this.orientation = orientation;
}
{
	Transform_Orient.prototype.transformCoords = function(coordsToTransform)
	{
		coordsToTransform.overwriteWithXYZ
		(
			this.orientation.right.dotProduct(coordsToTransform),
			this.orientation.down.dotProduct(coordsToTransform),
			this.orientation.forward.dotProduct(coordsToTransform)
		);
	
		return coordsToTransform;
	}
}

function Transform_OrientInverse(orientation)
{
	this.orientation = orientation;
}
{
	Transform_OrientInverse.prototype.transformCoords = function(coordsToTransform)
	{
		var original = coordsToTransform.clone();
		
		coordsToTransform.overwriteWith
		(
			this.orientation.right.clone().multiplyScalar(original.x)
		).add
		(
			this.orientation.down.clone().multiplyScalar(original.y)
		).add
		(
			this.orientation.forward.clone().multiplyScalar(original.z)
		);
	
		return coordsToTransform;
	}
}

function Transform_Perspective(focalLength)
{
	this.focalLength = focalLength;
}
{
	Transform_Perspective.prototype.transformCoords = function(coordsToTransform)
	{
		if (coordsToTransform.z != 0)
		{
			var multiplier = this.focalLength / coordsToTransform.z;
			coordsToTransform.x *= multiplier;
			coordsToTransform.y *= multiplier;
			coordsToTransform.z *= this.focalLength;
		}
		return coordsToTransform;
	}
}

function Transform_PerspectiveInverse(focalLength)
{
	this.focalLength = focalLength;
}
{
	Transform_PerspectiveInverse.prototype.transformCoords = function(coordsToTransform)
	{
		if (coordsToTransform.z != 0)
		{
			var multiplier = coordsToTransform.z / (this.focalLength * this.focalLength); // hack
			coordsToTransform.x *= multiplier;
			coordsToTransform.y *= multiplier;
			coordsToTransform.z /= this.focalLength; // fix
		}
		return coordsToTransform;
	}
}

function Transform_Rotate(axis, angleInCycles)
{
	this.axis = axis;
	this.angleInCycles = angleInCycles;
	
	var orientation = Orientation.fromForwardAndDown
	(
		this.axis.clone().right(), 
		this.axis
	);
	this.transformOrient = new Transform_Orient(orientation);
}
{
	Transform_Rotate.prototype.transformCoords = function(coordsToTransform)
	{
		this.transformOrient.transformCoords
		(
			coordsToTransform
		);
		
		// todo
		var one = 1;
		
		return coordsToTransform;
	}
}

function Transform_Scale(scaleFactors)
{
	this.scaleFactors = scaleFactors;
}
{
	Transform_Scale.prototype.transformCoords = function(coordsToTransform)
	{
		return coordsToTransform.multiply(this.scaleFactors);
	}
}

function Transform_Translate(offset)
{
	this.offset = offset;
}
{
	Transform_Translate.prototype.transformCoords = function(coordsToTransform)
	{
		return coordsToTransform.add(this.offset);
	}
}

function Transform_TranslateInverse(offset)
{
	this.offset = offset;
}
{
	Transform_TranslateInverse.prototype.transformCoords = function(coordsToTransform)
	{
		return coordsToTransform.subtract(this.offset);
	}
}

// run

main();

</script>

</body>
</html>

Advertisements
This entry was posted in Uncategorized and tagged , , , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s