docs: function (tiles) {
  		const maxDist = 5;
  		
  		let t, tile, tileBody, tileVertices, n, neighbour, neighbourBody, neighbourVertices, i, j, tv0, tv1, tv2, tv3, nv1, nv2, edge;
  		
  		for (t = 0; t < tiles.length; t++) {
  			tile = tiles[t];
  			
  			// Skip over irrelevant tiles
  			if (isIrrelevant(tile)) {
  				continue;
  			}
  			
  			// Grab the tile's body and vertices to build its edges and compare with neighbours
  			tileBody = tile.physics.matterBody.body;
  			tileVertices = tileBody.vertices;
  			
  			tileBody.edges = [];
  			tileBody.ignormals = [];
  			
  			for (i = 0; i < tileVertices.length; i++) {
  				tv1 = tileVertices[i];
  				tv2 = tileVertices[(i + 1) % tileVertices.length];
  				
  				// Find or build the edge for this tile vertex
  				edge = tileBody.edges[i];
  				
  				// Build the edge
  				edge = buildEdge(tv1, tv2, tileBody);
  				edge.ignore = tv1.isInternal;
  				
  				// Add it to the tile's edge list
  				tileBody.edges[i] = edge;
  			}
  			
  			for (n in tile.physics.slopes.neighbours) {
  				neighbour = tile.physics.slopes.neighbours[n];
  				
  				// Skip over irrelevant neighbouring tiles
  				if (isIrrelevant(neighbour)) {
  					continue;
  				}
  				
  				// Grab the neighbour's body and vertices
  				neighbourBody = neighbour.physics.matterBody.body;
  				neighbourVertices = neighbourBody.vertices;
  				
  				// Skip over tile-neighbour pairs that don't overlap
  				if (!Bounds.overlaps(tileBody.bounds, neighbourBody.bounds)) {
  					continue;
  				}
  				
  				// Iterate over the vertices of both the tile and its neighbour
  				for (i = 0; i < tileVertices.length; i++) {
  					tv1 = tileVertices[i];
  					tv2 = tileVertices[(i + 1) % tileVertices.length];
  					
  					// Find the edge for this tile vertex
  					edge = tileBody.edges[i];
  					
  					// Find neighbouring edges leading into this vertex
  					for (j = 0; j < neighbourVertices.length; j++) {
  						nv1 = neighbourVertices[j];
  						nv2 = neighbourVertices[(j + 1) % neighbourVertices.length];
  						
  						// Skip internal neighbouring edges
  						if (nv1.isInternal) {
  							continue;
  						}
  						
  						// Check for a leading or trailing ghost edges
  						let leadingEdge = distance(tv1, nv2) < maxDist;
  						let trailingEdge = distance(tv2, nv1) < maxDist;
  						
  						if (leadingEdge || trailingEdge) {
  							// Add leading/trailing edge vertices
  							if (leadingEdge && !edge.vertices[0]) {
  								edge.vertices[0] = buildEdgeVertex(0, nv1.x, nv1.y, tileBody, true);
  								
  								// Link edges to each other
  								if (neighbourBody.edges) {
  									edge.prev = neighbourBody.edges[j];
  									neighbourBody.edges[j].next = edge;
  								}
  							}
  							
  							if (trailingEdge && !edge.vertices[3]) {
  								edge.vertices[3] = buildEdgeVertex(3, nv2.x, nv2.y, tileBody, true);
  							}
  							
  							// Find the ignormals
  							let ignormalVertex = leadingEdge ? nv2 : nv1;
  							let tileEdgeNormal = tileBody.axes[i];
  							let neighbourEdgeNormal = neighbourBody.axes[j];
  							let ignormalCandidate = Vector.dot(tileEdgeNormal, neighbourEdgeNormal) > 0;// && !isAxisAligned(tileEdgeNormal) && !isAxisAligned(neighbourEdgeNormal);
  							
  							// If the tile edge normal and its neighbouring ghost edge normal are less than
  							// 90 degrees different, we can set ignormals based on which bounds the joining
  							// vertex lands on
  							if (ignormalCandidate) {
  								/**
  								 * If the joining vertex is on a bounds edge, and the normal of its edge and the
  								 * the neighbouring edge are not exactly the same as the corresponding direction
  								 * constant, we can add an axis aligned ignormal
  								 */
  								
  								// Leftwards ignormal
  								if (Math.abs(ignormalVertex.x - tileBody.bounds.min.x) < 1 && !containsNormal(Constants.Directions.LEFT, tileBody.axes) && Vector.dot(tileEdgeNormal, Constants.Directions.LEFT) < 1 && Vector.dot(neighbourEdgeNormal, Constants.Directions.LEFT) < 1) {
  									tileBody.ignormals.push(Constants.Directions.LEFT);
  								}
  								
  								// Upwards ignormal
  								if (Math.abs(ignormalVertex.y - tileBody.bounds.min.y) < 1 && !containsNormal(Constants.Directions.UP, tileBody.axes) && Vector.dot(tileEdgeNormal, Constants.Directions.UP) < 1 && Vector.dot(neighbourEdgeNormal, Constants.Directions.UP) < 1) {
  									tileBody.ignormals.push(Constants.Directions.UP);
  								}
  								
  								// Rightwards ignormal
  								if (Math.abs(ignormalVertex.x - tileBody.bounds.max.x) < 1 && !containsNormal(Constants.Directions.RIGHT, tileBody.axes) && Vector.dot(tileEdgeNormal, Constants.Directions.RIGHT) < 1 && Vector.dot(neighbourEdgeNormal, Constants.Directions.RIGHT) < 1) {
  									tileBody.ignormals.push(Constants.Directions.RIGHT);
  								}
  								
  								// Downwards ignormal
  								if (Math.abs(ignormalVertex.y - tileBody.bounds.max.y) < 1 && !containsNormal(Constants.Directions.DOWN, tileBody.axes) && Vector.dot(tileEdgeNormal, Constants.Directions.DOWN) < 1 && Vector.dot(neighbourEdgeNormal, Constants.Directions.DOWN) < 1) {
  									tileBody.ignormals.push(Constants.Directions.DOWN);
  								}
  							}
  						}
  					}
  				}
  			}
  			
  			// Resolve edge normal data, now that we've seen all the neighbours
  			// and ensure ghost vertices are set on each edge
  			for (i = 0; i < tileVertices.length; i++) {
  				edge = tileBody.edges[i];
  				
  				if (edge.ignore) {
  					continue;
  				}
  				
  				tv0 = tileVertices[((i - 1) % tileVertices.length + tileVertices.length) % tileVertices.length];
  				tv1 = tileVertices[i];
  				tv2 = tileVertices[(i + 1) % tileVertices.length];
  				tv3 = tileVertices[(i + 2) % tileVertices.length];
  				
  				// Use this body's vertices as ghost vertices if neighbours
  				// haven't provided them, and if they aren't internal edges
  				if (!edge.vertices[0] && !tv0.isInternal) {
  					edge.vertices[0] = buildEdgeVertex(0, tv0.x, tv0.y, tileBody, true);
  				}
  				
  				if (!edge.vertices[3] && !tv2.isInternal) {
  					edge.vertices[3] = buildEdgeVertex(3, tv3.x, tv3.y, tileBody, true);
  				}
  				
  				// Calculate edge normals
  				edge.normals = Axes.fromVertices(edge.vertices);
  				
  				// Calculate edge normal ranges
  				buildEdgeNormalRanges(edge);
  			}
  		}
  	}