"use strict" //High level idea: // 1. Use Clarkson's incremental construction to find convex hull // 2. Point location in triangulation by jump and walk module.exports = incrementalConvexHull var orient = require("robust-orientation") var compareCell = require("simplicial-complex").compareCells function compareInt(a, b) { return a - b } function Simplex(vertices, adjacent, boundary) { this.vertices = vertices this.adjacent = adjacent this.boundary = boundary this.lastVisited = -1 } Simplex.prototype.flip = function() { var t = this.vertices[0] this.vertices[0] = this.vertices[1] this.vertices[1] = t var u = this.adjacent[0] this.adjacent[0] = this.adjacent[1] this.adjacent[1] = u } function GlueFacet(vertices, cell, index) { this.vertices = vertices this.cell = cell this.index = index } function compareGlue(a, b) { return compareCell(a.vertices, b.vertices) } function bakeOrient(d) { var code = ["function orient(){var tuple=this.tuple;return test("] for(var i=0; i<=d; ++i) { if(i > 0) { code.push(",") } code.push("tuple[", i, "]") } code.push(")}return orient") var proc = new Function("test", code.join("")) var test = orient[d+1] if(!test) { test = orient } return proc(test) } var BAKED = [] function Triangulation(dimension, vertices, simplices) { this.dimension = dimension this.vertices = vertices this.simplices = simplices this.interior = simplices.filter(function(c) { return !c.boundary }) this.tuple = new Array(dimension+1) for(var i=0; i<=dimension; ++i) { this.tuple[i] = this.vertices[i] } var o = BAKED[dimension] if(!o) { o = BAKED[dimension] = bakeOrient(dimension) } this.orient = o } var proto = Triangulation.prototype //Degenerate situation where we are on boundary, but coplanar to face proto.handleBoundaryDegeneracy = function(cell, point) { var d = this.dimension var n = this.vertices.length - 1 var tuple = this.tuple var verts = this.vertices //Dumb solution: Just do dfs from boundary cell until we find any peak, or terminate var toVisit = [ cell ] cell.lastVisited = -n while(toVisit.length > 0) { cell = toVisit.pop() var cellVerts = cell.vertices var cellAdj = cell.adjacent for(var i=0; i<=d; ++i) { var neighbor = cellAdj[i] if(!neighbor.boundary || neighbor.lastVisited <= -n) { continue } var nv = neighbor.vertices for(var j=0; j<=d; ++j) { var vv = nv[j] if(vv < 0) { tuple[j] = point } else { tuple[j] = verts[vv] } } var o = this.orient() if(o > 0) { return neighbor } neighbor.lastVisited = -n if(o === 0) { toVisit.push(neighbor) } } } return null } proto.walk = function(point, random) { //Alias local properties var n = this.vertices.length - 1 var d = this.dimension var verts = this.vertices var tuple = this.tuple //Compute initial jump cell var initIndex = random ? (this.interior.length * Math.random())|0 : (this.interior.length-1) var cell = this.interior[ initIndex ] //Start walking outerLoop: while(!cell.boundary) { var cellVerts = cell.vertices var cellAdj = cell.adjacent for(var i=0; i<=d; ++i) { tuple[i] = verts[cellVerts[i]] } cell.lastVisited = n //Find farthest adjacent cell for(var i=0; i<=d; ++i) { var neighbor = cellAdj[i] if(neighbor.lastVisited >= n) { continue } var prev = tuple[i] tuple[i] = point var o = this.orient() tuple[i] = prev if(o < 0) { cell = neighbor continue outerLoop } else { if(!neighbor.boundary) { neighbor.lastVisited = n } else { neighbor.lastVisited = -n } } } return } return cell } proto.addPeaks = function(point, cell) { var n = this.vertices.length - 1 var d = this.dimension var verts = this.vertices var tuple = this.tuple var interior = this.interior var simplices = this.simplices //Walking finished at boundary, time to add peaks var tovisit = [ cell ] //Stretch initial boundary cell into a peak cell.lastVisited = n cell.vertices[cell.vertices.indexOf(-1)] = n cell.boundary = false interior.push(cell) //Record a list of all new boundaries created by added peaks so we can glue them together when we are all done var glueFacets = [] //Do a traversal of the boundary walking outward from starting peak while(tovisit.length > 0) { //Pop off peak and walk over adjacent cells var cell = tovisit.pop() var cellVerts = cell.vertices var cellAdj = cell.adjacent var indexOfN = cellVerts.indexOf(n) if(indexOfN < 0) { continue } for(var i=0; i<=d; ++i) { if(i === indexOfN) { continue } //For each boundary neighbor of the cell var neighbor = cellAdj[i] if(!neighbor.boundary || neighbor.lastVisited >= n) { continue } var nv = neighbor.vertices //Test if neighbor is a peak if(neighbor.lastVisited !== -n) { //Compute orientation of p relative to each boundary peak var indexOfNeg1 = 0 for(var j=0; j<=d; ++j) { if(nv[j] < 0) { indexOfNeg1 = j tuple[j] = point } else { tuple[j] = verts[nv[j]] } } var o = this.orient() //Test if neighbor cell is also a peak if(o > 0) { nv[indexOfNeg1] = n neighbor.boundary = false interior.push(neighbor) tovisit.push(neighbor) neighbor.lastVisited = n continue } else { neighbor.lastVisited = -n } } var na = neighbor.adjacent //Otherwise, replace neighbor with new face var vverts = cellVerts.slice() var vadj = cellAdj.slice() var ncell = new Simplex(vverts, vadj, true) simplices.push(ncell) //Connect to neighbor var opposite = na.indexOf(cell) if(opposite < 0) { continue } na[opposite] = ncell vadj[indexOfN] = neighbor //Connect to cell vverts[i] = -1 vadj[i] = cell cellAdj[i] = ncell //Flip facet ncell.flip() //Add to glue list for(var j=0; j<=d; ++j) { var uu = vverts[j] if(uu < 0 || uu === n) { continue } var nface = new Array(d-1) var nptr = 0 for(var k=0; k<=d; ++k) { var vv = vverts[k] if(vv < 0 || k === j) { continue } nface[nptr++] = vv } glueFacets.push(new GlueFacet(nface, ncell, j)) } } } //Glue boundary facets together glueFacets.sort(compareGlue) for(var i=0; i+1= 0) { bcell[ptr++] = cv[j] } else { parity = j&1 } } if(parity === (d&1)) { var t = bcell[0] bcell[0] = bcell[1] bcell[1] = t } boundary.push(bcell) } } return boundary } function incrementalConvexHull(points, randomSearch) { var n = points.length if(n === 0) { throw new Error("Must have at least d+1 points") } var d = points[0].length if(n <= d) { throw new Error("Must input at least d+1 points") } //FIXME: This could be degenerate, but need to select d+1 non-coplanar points to bootstrap process var initialSimplex = points.slice(0, d+1) //Make sure initial simplex is positively oriented var o = orient.apply(void 0, initialSimplex) if(o === 0) { throw new Error("Input not in general position") } var initialCoords = new Array(d+1) for(var i=0; i<=d; ++i) { initialCoords[i] = i } if(o < 0) { initialCoords[0] = 1 initialCoords[1] = 0 } //Create initial topological index, glue pointers together (kind of messy) var initialCell = new Simplex(initialCoords, new Array(d+1), false) var boundary = initialCell.adjacent var list = new Array(d+2) for(var i=0; i<=d; ++i) { var verts = initialCoords.slice() for(var j=0; j<=d; ++j) { if(j === i) { verts[j] = -1 } } var t = verts[0] verts[0] = verts[1] verts[1] = t var cell = new Simplex(verts, new Array(d+1), true) boundary[i] = cell list[i] = cell } list[d+1] = initialCell for(var i=0; i<=d; ++i) { var verts = boundary[i].vertices var adj = boundary[i].adjacent for(var j=0; j<=d; ++j) { var v = verts[j] if(v < 0) { adj[j] = initialCell continue } for(var k=0; k<=d; ++k) { if(boundary[k].vertices.indexOf(v) < 0) { adj[j] = boundary[k] } } } } //Initialize triangles var triangles = new Triangulation(d, initialSimplex, list) //Insert remaining points var useRandom = !!randomSearch for(var i=d+1; i