diff options
author | sanine <sanine.not@pm.me> | 2022-05-29 14:50:55 -0500 |
---|---|---|
committer | sanine <sanine.not@pm.me> | 2022-05-29 14:50:55 -0500 |
commit | 3bc2838360627f78a91a7cad81f3bc97711410c2 (patch) | |
tree | 54d260b45895c1923a785efce817b94836660f35 /modules | |
parent | d35e02ecdcef6d4f120bd572f2aae36b467b7761 (diff) |
refactor: move everything into src/ subdirectory
Diffstat (limited to 'modules')
-rw-r--r-- | modules/3rdparty/rhill-voronoi-core.js | 1723 | ||||
-rw-r--r-- | modules/Geometry.js | 229 | ||||
-rw-r--r-- | modules/Geometry.test.js | 259 | ||||
-rw-r--r-- | modules/Terrain.js | 82 | ||||
-rw-r--r-- | modules/Util.js | 13 | ||||
-rw-r--r-- | modules/Util.test.js | 23 | ||||
-rw-r--r-- | modules/test-assert.js | 21 |
7 files changed, 0 insertions, 2350 deletions
diff --git a/modules/3rdparty/rhill-voronoi-core.js b/modules/3rdparty/rhill-voronoi-core.js deleted file mode 100644 index 26dac8f..0000000 --- a/modules/3rdparty/rhill-voronoi-core.js +++ /dev/null @@ -1,1723 +0,0 @@ -/*! -Copyright (C) 2010-2013 Raymond Hill: https://github.com/gorhill/Javascript-Voronoi -MIT License: See https://github.com/gorhill/Javascript-Voronoi/LICENSE.md -*/ -/* -Author: Raymond Hill (rhill@raymondhill.net) -Contributor: Jesse Morgan (morgajel@gmail.com) -File: rhill-voronoi-core.js -Version: 0.98 -Date: January 21, 2013 -Description: This is my personal Javascript implementation of -Steven Fortune's algorithm to compute Voronoi diagrams. - -License: See https://github.com/gorhill/Javascript-Voronoi/LICENSE.md -Credits: See https://github.com/gorhill/Javascript-Voronoi/CREDITS.md -History: See https://github.com/gorhill/Javascript-Voronoi/CHANGELOG.md - -## Usage: - - var sites = [{x:300,y:300}, {x:100,y:100}, {x:200,y:500}, {x:250,y:450}, {x:600,y:150}]; - // xl, xr means x left, x right - // yt, yb means y top, y bottom - var bbox = {xl:0, xr:800, yt:0, yb:600}; - var voronoi = new Voronoi(); - // pass an object which exhibits xl, xr, yt, yb properties. The bounding - // box will be used to connect unbound edges, and to close open cells - result = voronoi.compute(sites, bbox); - // render, further analyze, etc. - -Return value: - An object with the following properties: - - result.vertices = an array of unordered, unique Voronoi.Vertex objects making - up the Voronoi diagram. - result.edges = an array of unordered, unique Voronoi.Edge objects making up - the Voronoi diagram. - result.cells = an array of Voronoi.Cell object making up the Voronoi diagram. - A Cell object might have an empty array of halfedges, meaning no Voronoi - cell could be computed for a particular cell. - result.execTime = the time it took to compute the Voronoi diagram, in - milliseconds. - -Voronoi.Vertex object: - x: The x position of the vertex. - y: The y position of the vertex. - -Voronoi.Edge object: - lSite: the Voronoi site object at the left of this Voronoi.Edge object. - rSite: the Voronoi site object at the right of this Voronoi.Edge object (can - be null). - va: an object with an 'x' and a 'y' property defining the start point - (relative to the Voronoi site on the left) of this Voronoi.Edge object. - vb: an object with an 'x' and a 'y' property defining the end point - (relative to Voronoi site on the left) of this Voronoi.Edge object. - - For edges which are used to close open cells (using the supplied bounding - box), the rSite property will be null. - -Voronoi.Cell object: - site: the Voronoi site object associated with the Voronoi cell. - halfedges: an array of Voronoi.Halfedge objects, ordered counterclockwise, - defining the polygon for this Voronoi cell. - -Voronoi.Halfedge object: - site: the Voronoi site object owning this Voronoi.Halfedge object. - edge: a reference to the unique Voronoi.Edge object underlying this - Voronoi.Halfedge object. - getStartpoint(): a method returning an object with an 'x' and a 'y' property - for the start point of this halfedge. Keep in mind halfedges are always - countercockwise. - getEndpoint(): a method returning an object with an 'x' and a 'y' property - for the end point of this halfedge. Keep in mind halfedges are always - countercockwise. - -TODO: Identify opportunities for performance improvement. - -TODO: Let the user close the Voronoi cells, do not do it automatically. Not only let - him close the cells, but also allow him to close more than once using a different - bounding box for the same Voronoi diagram. -*/ - -/*global Math */ - -// --------------------------------------------------------------------------- - -function Voronoi() { - this.vertices = null; - this.edges = null; - this.cells = null; - this.toRecycle = null; - this.beachsectionJunkyard = []; - this.circleEventJunkyard = []; - this.vertexJunkyard = []; - this.edgeJunkyard = []; - this.cellJunkyard = []; - } - -// --------------------------------------------------------------------------- - -Voronoi.prototype.reset = function() { - if (!this.beachline) { - this.beachline = new this.RBTree(); - } - // Move leftover beachsections to the beachsection junkyard. - if (this.beachline.root) { - var beachsection = this.beachline.getFirst(this.beachline.root); - while (beachsection) { - this.beachsectionJunkyard.push(beachsection); // mark for reuse - beachsection = beachsection.rbNext; - } - } - this.beachline.root = null; - if (!this.circleEvents) { - this.circleEvents = new this.RBTree(); - } - this.circleEvents.root = this.firstCircleEvent = null; - this.vertices = []; - this.edges = []; - this.cells = []; - }; - -Voronoi.prototype.sqrt = Math.sqrt; -Voronoi.prototype.abs = Math.abs; -Voronoi.prototype.ε = Voronoi.ε = 1e-9; -Voronoi.prototype.invε = Voronoi.invε = 1.0 / Voronoi.ε; -Voronoi.prototype.equalWithEpsilon = function(a,b){return this.abs(a-b)<1e-9;}; -Voronoi.prototype.greaterThanWithEpsilon = function(a,b){return a-b>1e-9;}; -Voronoi.prototype.greaterThanOrEqualWithEpsilon = function(a,b){return b-a<1e-9;}; -Voronoi.prototype.lessThanWithEpsilon = function(a,b){return b-a>1e-9;}; -Voronoi.prototype.lessThanOrEqualWithEpsilon = function(a,b){return a-b<1e-9;}; - -// --------------------------------------------------------------------------- -// Red-Black tree code (based on C version of "rbtree" by Franck Bui-Huu -// https://github.com/fbuihuu/libtree/blob/master/rb.c - -Voronoi.prototype.RBTree = function() { - this.root = null; - }; - -Voronoi.prototype.RBTree.prototype.rbInsertSuccessor = function(node, successor) { - var parent; - if (node) { - // >>> rhill 2011-05-27: Performance: cache previous/next nodes - successor.rbPrevious = node; - successor.rbNext = node.rbNext; - if (node.rbNext) { - node.rbNext.rbPrevious = successor; - } - node.rbNext = successor; - // <<< - if (node.rbRight) { - // in-place expansion of node.rbRight.getFirst(); - node = node.rbRight; - while (node.rbLeft) {node = node.rbLeft;} - node.rbLeft = successor; - } - else { - node.rbRight = successor; - } - parent = node; - } - // rhill 2011-06-07: if node is null, successor must be inserted - // to the left-most part of the tree - else if (this.root) { - node = this.getFirst(this.root); - // >>> Performance: cache previous/next nodes - successor.rbPrevious = null; - successor.rbNext = node; - node.rbPrevious = successor; - // <<< - node.rbLeft = successor; - parent = node; - } - else { - // >>> Performance: cache previous/next nodes - successor.rbPrevious = successor.rbNext = null; - // <<< - this.root = successor; - parent = null; - } - successor.rbLeft = successor.rbRight = null; - successor.rbParent = parent; - successor.rbRed = true; - // Fixup the modified tree by recoloring nodes and performing - // rotations (2 at most) hence the red-black tree properties are - // preserved. - var grandpa, uncle; - node = successor; - while (parent && parent.rbRed) { - grandpa = parent.rbParent; - if (parent === grandpa.rbLeft) { - uncle = grandpa.rbRight; - if (uncle && uncle.rbRed) { - parent.rbRed = uncle.rbRed = false; - grandpa.rbRed = true; - node = grandpa; - } - else { - if (node === parent.rbRight) { - this.rbRotateLeft(parent); - node = parent; - parent = node.rbParent; - } - parent.rbRed = false; - grandpa.rbRed = true; - this.rbRotateRight(grandpa); - } - } - else { - uncle = grandpa.rbLeft; - if (uncle && uncle.rbRed) { - parent.rbRed = uncle.rbRed = false; - grandpa.rbRed = true; - node = grandpa; - } - else { - if (node === parent.rbLeft) { - this.rbRotateRight(parent); - node = parent; - parent = node.rbParent; - } - parent.rbRed = false; - grandpa.rbRed = true; - this.rbRotateLeft(grandpa); - } - } - parent = node.rbParent; - } - this.root.rbRed = false; - }; - -Voronoi.prototype.RBTree.prototype.rbRemoveNode = function(node) { - // >>> rhill 2011-05-27: Performance: cache previous/next nodes - if (node.rbNext) { - node.rbNext.rbPrevious = node.rbPrevious; - } - if (node.rbPrevious) { - node.rbPrevious.rbNext = node.rbNext; - } - node.rbNext = node.rbPrevious = null; - // <<< - var parent = node.rbParent, - left = node.rbLeft, - right = node.rbRight, - next; - if (!left) { - next = right; - } - else if (!right) { - next = left; - } - else { - next = this.getFirst(right); - } - if (parent) { - if (parent.rbLeft === node) { - parent.rbLeft = next; - } - else { - parent.rbRight = next; - } - } - else { - this.root = next; - } - // enforce red-black rules - var isRed; - if (left && right) { - isRed = next.rbRed; - next.rbRed = node.rbRed; - next.rbLeft = left; - left.rbParent = next; - if (next !== right) { - parent = next.rbParent; - next.rbParent = node.rbParent; - node = next.rbRight; - parent.rbLeft = node; - next.rbRight = right; - right.rbParent = next; - } - else { - next.rbParent = parent; - parent = next; - node = next.rbRight; - } - } - else { - isRed = node.rbRed; - node = next; - } - // 'node' is now the sole successor's child and 'parent' its - // new parent (since the successor can have been moved) - if (node) { - node.rbParent = parent; - } - // the 'easy' cases - if (isRed) {return;} - if (node && node.rbRed) { - node.rbRed = false; - return; - } - // the other cases - var sibling; - do { - if (node === this.root) { - break; - } - if (node === parent.rbLeft) { - sibling = parent.rbRight; - if (sibling.rbRed) { - sibling.rbRed = false; - parent.rbRed = true; - this.rbRotateLeft(parent); - sibling = parent.rbRight; - } - if ((sibling.rbLeft && sibling.rbLeft.rbRed) || (sibling.rbRight && sibling.rbRight.rbRed)) { - if (!sibling.rbRight || !sibling.rbRight.rbRed) { - sibling.rbLeft.rbRed = false; - sibling.rbRed = true; - this.rbRotateRight(sibling); - sibling = parent.rbRight; - } - sibling.rbRed = parent.rbRed; - parent.rbRed = sibling.rbRight.rbRed = false; - this.rbRotateLeft(parent); - node = this.root; - break; - } - } - else { - sibling = parent.rbLeft; - if (sibling.rbRed) { - sibling.rbRed = false; - parent.rbRed = true; - this.rbRotateRight(parent); - sibling = parent.rbLeft; - } - if ((sibling.rbLeft && sibling.rbLeft.rbRed) || (sibling.rbRight && sibling.rbRight.rbRed)) { - if (!sibling.rbLeft || !sibling.rbLeft.rbRed) { - sibling.rbRight.rbRed = false; - sibling.rbRed = true; - this.rbRotateLeft(sibling); - sibling = parent.rbLeft; - } - sibling.rbRed = parent.rbRed; - parent.rbRed = sibling.rbLeft.rbRed = false; - this.rbRotateRight(parent); - node = this.root; - break; - } - } - sibling.rbRed = true; - node = parent; - parent = parent.rbParent; - } while (!node.rbRed); - if (node) {node.rbRed = false;} - }; - -Voronoi.prototype.RBTree.prototype.rbRotateLeft = function(node) { - var p = node, - q = node.rbRight, // can't be null - parent = p.rbParent; - if (parent) { - if (parent.rbLeft === p) { - parent.rbLeft = q; - } - else { - parent.rbRight = q; - } - } - else { - this.root = q; - } - q.rbParent = parent; - p.rbParent = q; - p.rbRight = q.rbLeft; - if (p.rbRight) { - p.rbRight.rbParent = p; - } - q.rbLeft = p; - }; - -Voronoi.prototype.RBTree.prototype.rbRotateRight = function(node) { - var p = node, - q = node.rbLeft, // can't be null - parent = p.rbParent; - if (parent) { - if (parent.rbLeft === p) { - parent.rbLeft = q; - } - else { - parent.rbRight = q; - } - } - else { - this.root = q; - } - q.rbParent = parent; - p.rbParent = q; - p.rbLeft = q.rbRight; - if (p.rbLeft) { - p.rbLeft.rbParent = p; - } - q.rbRight = p; - }; - -Voronoi.prototype.RBTree.prototype.getFirst = function(node) { - while (node.rbLeft) { - node = node.rbLeft; - } - return node; - }; - -Voronoi.prototype.RBTree.prototype.getLast = function(node) { - while (node.rbRight) { - node = node.rbRight; - } - return node; - }; - -// --------------------------------------------------------------------------- -// Diagram methods - -Voronoi.prototype.Diagram = function(site) { - this.site = site; - }; - -// --------------------------------------------------------------------------- -// Cell methods - -Voronoi.prototype.Cell = function(site) { - this.site = site; - this.halfedges = []; - this.closeMe = false; - }; - -Voronoi.prototype.Cell.prototype.init = function(site) { - this.site = site; - this.halfedges = []; - this.closeMe = false; - return this; - }; - -Voronoi.prototype.createCell = function(site) { - var cell = this.cellJunkyard.pop(); - if ( cell ) { - return cell.init(site); - } - return new this.Cell(site); - }; - -Voronoi.prototype.Cell.prototype.prepareHalfedges = function() { - var halfedges = this.halfedges, - iHalfedge = halfedges.length, - edge; - // get rid of unused halfedges - // rhill 2011-05-27: Keep it simple, no point here in trying - // to be fancy: dangling edges are a typically a minority. - while (iHalfedge--) { - edge = halfedges[iHalfedge].edge; - if (!edge.vb || !edge.va) { - halfedges.splice(iHalfedge,1); - } - } - - // rhill 2011-05-26: I tried to use a binary search at insertion - // time to keep the array sorted on-the-fly (in Cell.addHalfedge()). - // There was no real benefits in doing so, performance on - // Firefox 3.6 was improved marginally, while performance on - // Opera 11 was penalized marginally. - halfedges.sort(function(a,b){return b.angle-a.angle;}); - return halfedges.length; - }; - -// Return a list of the neighbor Ids -Voronoi.prototype.Cell.prototype.getNeighborIds = function() { - var neighbors = [], - iHalfedge = this.halfedges.length, - edge; - while (iHalfedge--){ - edge = this.halfedges[iHalfedge].edge; - if (edge.lSite !== null && edge.lSite.voronoiId != this.site.voronoiId) { - neighbors.push(edge.lSite.voronoiId); - } - else if (edge.rSite !== null && edge.rSite.voronoiId != this.site.voronoiId){ - neighbors.push(edge.rSite.voronoiId); - } - } - return neighbors; - }; - -// Compute bounding box -// -Voronoi.prototype.Cell.prototype.getBbox = function() { - var halfedges = this.halfedges, - iHalfedge = halfedges.length, - xmin = Infinity, - ymin = Infinity, - xmax = -Infinity, - ymax = -Infinity, - v, vx, vy; - while (iHalfedge--) { - v = halfedges[iHalfedge].getStartpoint(); - vx = v.x; - vy = v.y; - if (vx < xmin) {xmin = vx;} - if (vy < ymin) {ymin = vy;} - if (vx > xmax) {xmax = vx;} - if (vy > ymax) {ymax = vy;} - // we dont need to take into account end point, - // since each end point matches a start point - } - return { - x: xmin, - y: ymin, - width: xmax-xmin, - height: ymax-ymin - }; - }; - -// Return whether a point is inside, on, or outside the cell: -// -1: point is outside the perimeter of the cell -// 0: point is on the perimeter of the cell -// 1: point is inside the perimeter of the cell -// -Voronoi.prototype.Cell.prototype.pointIntersection = function(x, y) { - // Check if point in polygon. Since all polygons of a Voronoi - // diagram are convex, then: - // http://paulbourke.net/geometry/polygonmesh/ - // Solution 3 (2D): - // "If the polygon is convex then one can consider the polygon - // "as a 'path' from the first vertex. A point is on the interior - // "of this polygons if it is always on the same side of all the - // "line segments making up the path. ... - // "(y - y0) (x1 - x0) - (x - x0) (y1 - y0) - // "if it is less than 0 then P is to the right of the line segment, - // "if greater than 0 it is to the left, if equal to 0 then it lies - // "on the line segment" - var halfedges = this.halfedges, - iHalfedge = halfedges.length, - halfedge, - p0, p1, r; - while (iHalfedge--) { - halfedge = halfedges[iHalfedge]; - p0 = halfedge.getStartpoint(); - p1 = halfedge.getEndpoint(); - r = (y-p0.y)*(p1.x-p0.x)-(x-p0.x)*(p1.y-p0.y); - if (!r) { - return 0; - } - if (r > 0) { - return -1; - } - } - return 1; - }; - -// --------------------------------------------------------------------------- -// Edge methods -// - -Voronoi.prototype.Vertex = function(x, y) { - this.x = x; - this.y = y; - }; - -Voronoi.prototype.Edge = function(lSite, rSite) { - this.lSite = lSite; - this.rSite = rSite; - this.va = this.vb = null; - }; - -Voronoi.prototype.Halfedge = function(edge, lSite, rSite) { - this.site = lSite; - this.edge = edge; - // 'angle' is a value to be used for properly sorting the - // halfsegments counterclockwise. By convention, we will - // use the angle of the line defined by the 'site to the left' - // to the 'site to the right'. - // However, border edges have no 'site to the right': thus we - // use the angle of line perpendicular to the halfsegment (the - // edge should have both end points defined in such case.) - if (rSite) { - this.angle = Math.atan2(rSite.y-lSite.y, rSite.x-lSite.x); - } - else { - var va = edge.va, - vb = edge.vb; - // rhill 2011-05-31: used to call getStartpoint()/getEndpoint(), - // but for performance purpose, these are expanded in place here. - this.angle = edge.lSite === lSite ? - Math.atan2(vb.x-va.x, va.y-vb.y) : - Math.atan2(va.x-vb.x, vb.y-va.y); - } - }; - -Voronoi.prototype.createHalfedge = function(edge, lSite, rSite) { - return new this.Halfedge(edge, lSite, rSite); - }; - -Voronoi.prototype.Halfedge.prototype.getStartpoint = function() { - return this.edge.lSite === this.site ? this.edge.va : this.edge.vb; - }; - -Voronoi.prototype.Halfedge.prototype.getEndpoint = function() { - return this.edge.lSite === this.site ? this.edge.vb : this.edge.va; - }; - - - -// this create and add a vertex to the internal collection - -Voronoi.prototype.createVertex = function(x, y) { - var v = this.vertexJunkyard.pop(); - if ( !v ) { - v = new this.Vertex(x, y); - } - else { - v.x = x; - v.y = y; - } - this.vertices.push(v); - return v; - }; - -// this create and add an edge to internal collection, and also create -// two halfedges which are added to each site's counterclockwise array -// of halfedges. - -Voronoi.prototype.createEdge = function(lSite, rSite, va, vb) { - var edge = this.edgeJunkyard.pop(); - if ( !edge ) { - edge = new this.Edge(lSite, rSite); - } - else { - edge.lSite = lSite; - edge.rSite = rSite; - edge.va = edge.vb = null; - } - - this.edges.push(edge); - if (va) { - this.setEdgeStartpoint(edge, lSite, rSite, va); - } - if (vb) { - this.setEdgeEndpoint(edge, lSite, rSite, vb); - } - this.cells[lSite.voronoiId].halfedges.push(this.createHalfedge(edge, lSite, rSite)); - this.cells[rSite.voronoiId].halfedges.push(this.createHalfedge(edge, rSite, lSite)); - return edge; - }; - -Voronoi.prototype.createBorderEdge = function(lSite, va, vb) { - var edge = this.edgeJunkyard.pop(); - if ( !edge ) { - edge = new this.Edge(lSite, null); - } - else { - edge.lSite = lSite; - edge.rSite = null; - } - edge.va = va; - edge.vb = vb; - this.edges.push(edge); - return edge; - }; - -Voronoi.prototype.setEdgeStartpoint = function(edge, lSite, rSite, vertex) { - if (!edge.va && !edge.vb) { - edge.va = vertex; - edge.lSite = lSite; - edge.rSite = rSite; - } - else if (edge.lSite === rSite) { - edge.vb = vertex; - } - else { - edge.va = vertex; - } - }; - -Voronoi.prototype.setEdgeEndpoint = function(edge, lSite, rSite, vertex) { - this.setEdgeStartpoint(edge, rSite, lSite, vertex); - }; - -// --------------------------------------------------------------------------- -// Beachline methods - -// rhill 2011-06-07: For some reasons, performance suffers significantly -// when instanciating a literal object instead of an empty ctor -Voronoi.prototype.Beachsection = function() { - }; - -// rhill 2011-06-02: A lot of Beachsection instanciations -// occur during the computation of the Voronoi diagram, -// somewhere between the number of sites and twice the -// number of sites, while the number of Beachsections on the -// beachline at any given time is comparatively low. For this -// reason, we reuse already created Beachsections, in order -// to avoid new memory allocation. This resulted in a measurable -// performance gain. - -Voronoi.prototype.createBeachsection = function(site) { - var beachsection = this.beachsectionJunkyard.pop(); - if (!beachsection) { - beachsection = new this.Beachsection(); - } - beachsection.site = site; - return beachsection; - }; - -// calculate the left break point of a particular beach section, -// given a particular sweep line -Voronoi.prototype.leftBreakPoint = function(arc, directrix) { - // http://en.wikipedia.org/wiki/Parabola - // http://en.wikipedia.org/wiki/Quadratic_equation - // h1 = x1, - // k1 = (y1+directrix)/2, - // h2 = x2, - // k2 = (y2+directrix)/2, - // p1 = k1-directrix, - // a1 = 1/(4*p1), - // b1 = -h1/(2*p1), - // c1 = h1*h1/(4*p1)+k1, - // p2 = k2-directrix, - // a2 = 1/(4*p2), - // b2 = -h2/(2*p2), - // c2 = h2*h2/(4*p2)+k2, - // x = (-(b2-b1) + Math.sqrt((b2-b1)*(b2-b1) - 4*(a2-a1)*(c2-c1))) / (2*(a2-a1)) - // When x1 become the x-origin: - // h1 = 0, - // k1 = (y1+directrix)/2, - // h2 = x2-x1, - // k2 = (y2+directrix)/2, - // p1 = k1-directrix, - // a1 = 1/(4*p1), - // b1 = 0, - // c1 = k1, - // p2 = k2-directrix, - // a2 = 1/(4*p2), - // b2 = -h2/(2*p2), - // c2 = h2*h2/(4*p2)+k2, - // x = (-b2 + Math.sqrt(b2*b2 - 4*(a2-a1)*(c2-k1))) / (2*(a2-a1)) + x1 - - // change code below at your own risk: care has been taken to - // reduce errors due to computers' finite arithmetic precision. - // Maybe can still be improved, will see if any more of this - // kind of errors pop up again. - var site = arc.site, - rfocx = site.x, - rfocy = site.y, - pby2 = rfocy-directrix; - // parabola in degenerate case where focus is on directrix - if (!pby2) { - return rfocx; - } - var lArc = arc.rbPrevious; - if (!lArc) { - return -Infinity; - } - site = lArc.site; - var lfocx = site.x, - lfocy = site.y, - plby2 = lfocy-directrix; - // parabola in degenerate case where focus is on directrix - if (!plby2) { - return lfocx; - } - var hl = lfocx-rfocx, - aby2 = 1/pby2-1/plby2, - b = hl/plby2; - if (aby2) { - return (-b+this.sqrt(b*b-2*aby2*(hl*hl/(-2*plby2)-lfocy+plby2/2+rfocy-pby2/2)))/aby2+rfocx; - } - // both parabolas have same distance to directrix, thus break point is midway - return (rfocx+lfocx)/2; - }; - -// calculate the right break point of a particular beach section, -// given a particular directrix -Voronoi.prototype.rightBreakPoint = function(arc, directrix) { - var rArc = arc.rbNext; - if (rArc) { - return this.leftBreakPoint(rArc, directrix); - } - var site = arc.site; - return site.y === directrix ? site.x : Infinity; - }; - -Voronoi.prototype.detachBeachsection = function(beachsection) { - this.detachCircleEvent(beachsection); // detach potentially attached circle event - this.beachline.rbRemoveNode(beachsection); // remove from RB-tree - this.beachsectionJunkyard.push(beachsection); // mark for reuse - }; - -Voronoi.prototype.removeBeachsection = function(beachsection) { - var circle = beachsection.circleEvent, - x = circle.x, - y = circle.ycenter, - vertex = this.createVertex(x, y), - previous = beachsection.rbPrevious, - next = beachsection.rbNext, - disappearingTransitions = [beachsection], - abs_fn = Math.abs; - - // remove collapsed beachsection from beachline - this.detachBeachsection(beachsection); - - // there could be more than one empty arc at the deletion point, this - // happens when more than two edges are linked by the same vertex, - // so we will collect all those edges by looking up both sides of - // the deletion point. - // by the way, there is *always* a predecessor/successor to any collapsed - // beach section, it's just impossible to have a collapsing first/last - // beach sections on the beachline, since they obviously are unconstrained - // on their left/right side. - - // look left - var lArc = previous; - while (lArc.circleEvent && abs_fn(x-lArc.circleEvent.x)<1e-9 && abs_fn(y-lArc.circleEvent.ycenter)<1e-9) { - previous = lArc.rbPrevious; - disappearingTransitions.unshift(lArc); - this.detachBeachsection(lArc); // mark for reuse - lArc = previous; - } - // even though it is not disappearing, I will also add the beach section - // immediately to the left of the left-most collapsed beach section, for - // convenience, since we need to refer to it later as this beach section - // is the 'left' site of an edge for which a start point is set. - disappearingTransitions.unshift(lArc); - this.detachCircleEvent(lArc); - - // look right - var rArc = next; - while (rArc.circleEvent && abs_fn(x-rArc.circleEvent.x)<1e-9 && abs_fn(y-rArc.circleEvent.ycenter)<1e-9) { - next = rArc.rbNext; - disappearingTransitions.push(rArc); - this.detachBeachsection(rArc); // mark for reuse - rArc = next; - } - // we also have to add the beach section immediately to the right of the - // right-most collapsed beach section, since there is also a disappearing - // transition representing an edge's start point on its left. - disappearingTransitions.push(rArc); - this.detachCircleEvent(rArc); - - // walk through all the disappearing transitions between beach sections and - // set the start point of their (implied) edge. - var nArcs = disappearingTransitions.length, - iArc; - for (iArc=1; iArc<nArcs; iArc++) { - rArc = disappearingTransitions[iArc]; - lArc = disappearingTransitions[iArc-1]; - this.setEdgeStartpoint(rArc.edge, lArc.site, rArc.site, vertex); - } - - // create a new edge as we have now a new transition between - // two beach sections which were previously not adjacent. - // since this edge appears as a new vertex is defined, the vertex - // actually define an end point of the edge (relative to the site - // on the left) - lArc = disappearingTransitions[0]; - rArc = disappearingTransitions[nArcs-1]; - rArc.edge = this.createEdge(lArc.site, rArc.site, undefined, vertex); - - // create circle events if any for beach sections left in the beachline - // adjacent to collapsed sections - this.attachCircleEvent(lArc); - this.attachCircleEvent(rArc); - }; - -Voronoi.prototype.addBeachsection = function(site) { - var x = site.x, - directrix = site.y; - - // find the left and right beach sections which will surround the newly - // created beach section. - // rhill 2011-06-01: This loop is one of the most often executed, - // hence we expand in-place the comparison-against-epsilon calls. - var lArc, rArc, - dxl, dxr, - node = this.beachline.root; - - while (node) { - dxl = this.leftBreakPoint(node,directrix)-x; - // x lessThanWithEpsilon xl => falls somewhere before the left edge of the beachsection - if (dxl > 1e-9) { - // this case should never happen - // if (!node.rbLeft) { - // rArc = node.rbLeft; - // break; - // } - node = node.rbLeft; - } - else { - dxr = x-this.rightBreakPoint(node,directrix); - // x greaterThanWithEpsilon xr => falls somewhere after the right edge of the beachsection - if (dxr > 1e-9) { - if (!node.rbRight) { - lArc = node; - break; - } - node = node.rbRight; - } - else { - // x equalWithEpsilon xl => falls exactly on the left edge of the beachsection - if (dxl > -1e-9) { - lArc = node.rbPrevious; - rArc = node; - } - // x equalWithEpsilon xr => falls exactly on the right edge of the beachsection - else if (dxr > -1e-9) { - lArc = node; - rArc = node.rbNext; - } - // falls exactly somewhere in the middle of the beachsection - else { - lArc = rArc = node; - } - break; - } - } - } - // at this point, keep in mind that lArc and/or rArc could be - // undefined or null. - - // create a new beach section object for the site and add it to RB-tree - var newArc = this.createBeachsection(site); - this.beachline.rbInsertSuccessor(lArc, newArc); - - // cases: - // - - // [null,null] - // least likely case: new beach section is the first beach section on the - // beachline. - // This case means: - // no new transition appears - // no collapsing beach section - // new beachsection become root of the RB-tree - if (!lArc && !rArc) { - return; - } - - // [lArc,rArc] where lArc == rArc - // most likely case: new beach section split an existing beach - // section. - // This case means: - // one new transition appears - // the left and right beach section might be collapsing as a result - // two new nodes added to the RB-tree - if (lArc === rArc) { - // invalidate circle event of split beach section - this.detachCircleEvent(lArc); - - // split the beach section into two separate beach sections - rArc = this.createBeachsection(lArc.site); - this.beachline.rbInsertSuccessor(newArc, rArc); - - // since we have a new transition between two beach sections, - // a new edge is born - newArc.edge = rArc.edge = this.createEdge(lArc.site, newArc.site); - - // check whether the left and right beach sections are collapsing - // and if so create circle events, to be notified when the point of - // collapse is reached. - this.attachCircleEvent(lArc); - this.attachCircleEvent(rArc); - return; - } - - // [lArc,null] - // even less likely case: new beach section is the *last* beach section - // on the beachline -- this can happen *only* if *all* the previous beach - // sections currently on the beachline share the same y value as - // the new beach section. - // This case means: - // one new transition appears - // no collapsing beach section as a result - // new beach section become right-most node of the RB-tree - if (lArc && !rArc) { - newArc.edge = this.createEdge(lArc.site,newArc.site); - return; - } - - // [null,rArc] - // impossible case: because sites are strictly processed from top to bottom, - // and left to right, which guarantees that there will always be a beach section - // on the left -- except of course when there are no beach section at all on - // the beach line, which case was handled above. - // rhill 2011-06-02: No point testing in non-debug version - //if (!lArc && rArc) { - // throw "Voronoi.addBeachsection(): What is this I don't even"; - // } - - // [lArc,rArc] where lArc != rArc - // somewhat less likely case: new beach section falls *exactly* in between two - // existing beach sections - // This case means: - // one transition disappears - // two new transitions appear - // the left and right beach section might be collapsing as a result - // only one new node added to the RB-tree - if (lArc !== rArc) { - // invalidate circle events of left and right sites - this.detachCircleEvent(lArc); - this.detachCircleEvent(rArc); - - // an existing transition disappears, meaning a vertex is defined at - // the disappearance point. - // since the disappearance is caused by the new beachsection, the - // vertex is at the center of the circumscribed circle of the left, - // new and right beachsections. - // http://mathforum.org/library/drmath/view/55002.html - // Except that I bring the origin at A to simplify - // calculation - var lSite = lArc.site, - ax = lSite.x, - ay = lSite.y, - bx=site.x-ax, - by=site.y-ay, - rSite = rArc.site, - cx=rSite.x-ax, - cy=rSite.y-ay, - d=2*(bx*cy-by*cx), - hb=bx*bx+by*by, - hc=cx*cx+cy*cy, - vertex = this.createVertex((cy*hb-by*hc)/d+ax, (bx*hc-cx*hb)/d+ay); - - // one transition disappear - this.setEdgeStartpoint(rArc.edge, lSite, rSite, vertex); - - // two new transitions appear at the new vertex location - newArc.edge = this.createEdge(lSite, site, undefined, vertex); - rArc.edge = this.createEdge(site, rSite, undefined, vertex); - - // check whether the left and right beach sections are collapsing - // and if so create circle events, to handle the point of collapse. - this.attachCircleEvent(lArc); - this.attachCircleEvent(rArc); - return; - } - }; - -// --------------------------------------------------------------------------- -// Circle event methods - -// rhill 2011-06-07: For some reasons, performance suffers significantly -// when instanciating a literal object instead of an empty ctor -Voronoi.prototype.CircleEvent = function() { - // rhill 2013-10-12: it helps to state exactly what we are at ctor time. - this.arc = null; - this.rbLeft = null; - this.rbNext = null; - this.rbParent = null; - this.rbPrevious = null; - this.rbRed = false; - this.rbRight = null; - this.site = null; - this.x = this.y = this.ycenter = 0; - }; - -Voronoi.prototype.attachCircleEvent = function(arc) { - var lArc = arc.rbPrevious, - rArc = arc.rbNext; - if (!lArc || !rArc) {return;} // does that ever happen? - var lSite = lArc.site, - cSite = arc.site, - rSite = rArc.site; - - // If site of left beachsection is same as site of - // right beachsection, there can't be convergence - if (lSite===rSite) {return;} - - // Find the circumscribed circle for the three sites associated - // with the beachsection triplet. - // rhill 2011-05-26: It is more efficient to calculate in-place - // rather than getting the resulting circumscribed circle from an - // object returned by calling Voronoi.circumcircle() - // http://mathforum.org/library/drmath/view/55002.html - // Except that I bring the origin at cSite to simplify calculations. - // The bottom-most part of the circumcircle is our Fortune 'circle - // event', and its center is a vertex potentially part of the final - // Voronoi diagram. - var bx = cSite.x, - by = cSite.y, - ax = lSite.x-bx, - ay = lSite.y-by, - cx = rSite.x-bx, - cy = rSite.y-by; - - // If points l->c->r are clockwise, then center beach section does not - // collapse, hence it can't end up as a vertex (we reuse 'd' here, which - // sign is reverse of the orientation, hence we reverse the test. - // http://en.wikipedia.org/wiki/Curve_orientation#Orientation_of_a_simple_polygon - // rhill 2011-05-21: Nasty finite precision error which caused circumcircle() to - // return infinites: 1e-12 seems to fix the problem. - var d = 2*(ax*cy-ay*cx); - if (d >= -2e-12){return;} - - var ha = ax*ax+ay*ay, - hc = cx*cx+cy*cy, - x = (cy*ha-ay*hc)/d, - y = (ax*hc-cx*ha)/d, - ycenter = y+by; - - // Important: ybottom should always be under or at sweep, so no need - // to waste CPU cycles by checking - - // recycle circle event object if possible - var circleEvent = this.circleEventJunkyard.pop(); - if (!circleEvent) { - circleEvent = new this.CircleEvent(); - } - circleEvent.arc = arc; - circleEvent.site = cSite; - circleEvent.x = x+bx; - circleEvent.y = ycenter+this.sqrt(x*x+y*y); // y bottom - circleEvent.ycenter = ycenter; - arc.circleEvent = circleEvent; - - // find insertion point in RB-tree: circle events are ordered from - // smallest to largest - var predecessor = null, - node = this.circleEvents.root; - while (node) { - if (circleEvent.y < node.y || (circleEvent.y === node.y && circleEvent.x <= node.x)) { - if (node.rbLeft) { - node = node.rbLeft; - } - else { - predecessor = node.rbPrevious; - break; - } - } - else { - if (node.rbRight) { - node = node.rbRight; - } - else { - predecessor = node; - break; - } - } - } - this.circleEvents.rbInsertSuccessor(predecessor, circleEvent); - if (!predecessor) { - this.firstCircleEvent = circleEvent; - } - }; - -Voronoi.prototype.detachCircleEvent = function(arc) { - var circleEvent = arc.circleEvent; - if (circleEvent) { - if (!circleEvent.rbPrevious) { - this.firstCircleEvent = circleEvent.rbNext; - } - this.circleEvents.rbRemoveNode(circleEvent); // remove from RB-tree - this.circleEventJunkyard.push(circleEvent); - arc.circleEvent = null; - } - }; - -// --------------------------------------------------------------------------- -// Diagram completion methods - -// connect dangling edges (not if a cursory test tells us -// it is not going to be visible. -// return value: -// false: the dangling endpoint couldn't be connected -// true: the dangling endpoint could be connected -Voronoi.prototype.connectEdge = function(edge, bbox) { - // skip if end point already connected - var vb = edge.vb; - if (!!vb) {return true;} - - // make local copy for performance purpose - var va = edge.va, - xl = bbox.xl, - xr = bbox.xr, - yt = bbox.yt, - yb = bbox.yb, - lSite = edge.lSite, - rSite = edge.rSite, - lx = lSite.x, - ly = lSite.y, - rx = rSite.x, - ry = rSite.y, - fx = (lx+rx)/2, - fy = (ly+ry)/2, - fm, fb; - - // if we reach here, this means cells which use this edge will need - // to be closed, whether because the edge was removed, or because it - // was connected to the bounding box. - this.cells[lSite.voronoiId].closeMe = true; - this.cells[rSite.voronoiId].closeMe = true; - - // get the line equation of the bisector if line is not vertical - if (ry !== ly) { - fm = (lx-rx)/(ry-ly); - fb = fy-fm*fx; - } - - // remember, direction of line (relative to left site): - // upward: left.x < right.x - // downward: left.x > right.x - // horizontal: left.x == right.x - // upward: left.x < right.x - // rightward: left.y < right.y - // leftward: left.y > right.y - // vertical: left.y == right.y - - // depending on the direction, find the best side of the - // bounding box to use to determine a reasonable start point - - // rhill 2013-12-02: - // While at it, since we have the values which define the line, - // clip the end of va if it is outside the bbox. - // https://github.com/gorhill/Javascript-Voronoi/issues/15 - // TODO: Do all the clipping here rather than rely on Liang-Barsky - // which does not do well sometimes due to loss of arithmetic - // precision. The code here doesn't degrade if one of the vertex is - // at a huge distance. - - // special case: vertical line - if (fm === undefined) { - // doesn't intersect with viewport - if (fx < xl || fx >= xr) {return false;} - // downward - if (lx > rx) { - if (!va || va.y < yt) { - va = this.createVertex(fx, yt); - } - else if (va.y >= yb) { - return false; - } - vb = this.createVertex(fx, yb); - } - // upward - else { - if (!va || va.y > yb) { - va = this.createVertex(fx, yb); - } - else if (va.y < yt) { - return false; - } - vb = this.createVertex(fx, yt); - } - } - // closer to vertical than horizontal, connect start point to the - // top or bottom side of the bounding box - else if (fm < -1 || fm > 1) { - // downward - if (lx > rx) { - if (!va || va.y < yt) { - va = this.createVertex((yt-fb)/fm, yt); - } - else if (va.y >= yb) { - return false; - } - vb = this.createVertex((yb-fb)/fm, yb); - } - // upward - else { - if (!va || va.y > yb) { - va = this.createVertex((yb-fb)/fm, yb); - } - else if (va.y < yt) { - return false; - } - vb = this.createVertex((yt-fb)/fm, yt); - } - } - // closer to horizontal than vertical, connect start point to the - // left or right side of the bounding box - else { - // rightward - if (ly < ry) { - if (!va || va.x < xl) { - va = this.createVertex(xl, fm*xl+fb); - } - else if (va.x >= xr) { - return false; - } - vb = this.createVertex(xr, fm*xr+fb); - } - // leftward - else { - if (!va || va.x > xr) { - va = this.createVertex(xr, fm*xr+fb); - } - else if (va.x < xl) { - return false; - } - vb = this.createVertex(xl, fm*xl+fb); - } - } - edge.va = va; - edge.vb = vb; - - return true; - }; - -// line-clipping code taken from: -// Liang-Barsky function by Daniel White -// http://www.skytopia.com/project/articles/compsci/clipping.html -// Thanks! -// A bit modified to minimize code paths -Voronoi.prototype.clipEdge = function(edge, bbox) { - var ax = edge.va.x, - ay = edge.va.y, - bx = edge.vb.x, - by = edge.vb.y, - t0 = 0, - t1 = 1, - dx = bx-ax, - dy = by-ay; - // left - var q = ax-bbox.xl; - if (dx===0 && q<0) {return false;} - var r = -q/dx; - if (dx<0) { - if (r<t0) {return false;} - if (r<t1) {t1=r;} - } - else if (dx>0) { - if (r>t1) {return false;} - if (r>t0) {t0=r;} - } - // right - q = bbox.xr-ax; - if (dx===0 && q<0) {return false;} - r = q/dx; - if (dx<0) { - if (r>t1) {return false;} - if (r>t0) {t0=r;} - } - else if (dx>0) { - if (r<t0) {return false;} - if (r<t1) {t1=r;} - } - // top - q = ay-bbox.yt; - if (dy===0 && q<0) {return false;} - r = -q/dy; - if (dy<0) { - if (r<t0) {return false;} - if (r<t1) {t1=r;} - } - else if (dy>0) { - if (r>t1) {return false;} - if (r>t0) {t0=r;} - } - // bottom - q = bbox.yb-ay; - if (dy===0 && q<0) {return false;} - r = q/dy; - if (dy<0) { - if (r>t1) {return false;} - if (r>t0) {t0=r;} - } - else if (dy>0) { - if (r<t0) {return false;} - if (r<t1) {t1=r;} - } - - // if we reach this point, Voronoi edge is within bbox - - // if t0 > 0, va needs to change - // rhill 2011-06-03: we need to create a new vertex rather - // than modifying the existing one, since the existing - // one is likely shared with at least another edge - if (t0 > 0) { - edge.va = this.createVertex(ax+t0*dx, ay+t0*dy); - } - - // if t1 < 1, vb needs to change - // rhill 2011-06-03: we need to create a new vertex rather - // than modifying the existing one, since the existing - // one is likely shared with at least another edge - if (t1 < 1) { - edge.vb = this.createVertex(ax+t1*dx, ay+t1*dy); - } - - // va and/or vb were clipped, thus we will need to close - // cells which use this edge. - if ( t0 > 0 || t1 < 1 ) { - this.cells[edge.lSite.voronoiId].closeMe = true; - this.cells[edge.rSite.voronoiId].closeMe = true; - } - - return true; - }; - -// Connect/cut edges at bounding box -Voronoi.prototype.clipEdges = function(bbox) { - // connect all dangling edges to bounding box - // or get rid of them if it can't be done - var edges = this.edges, - iEdge = edges.length, - edge, - abs_fn = Math.abs; - - // iterate backward so we can splice safely - while (iEdge--) { - edge = edges[iEdge]; - // edge is removed if: - // it is wholly outside the bounding box - // it is looking more like a point than a line - if (!this.connectEdge(edge, bbox) || - !this.clipEdge(edge, bbox) || - (abs_fn(edge.va.x-edge.vb.x)<1e-9 && abs_fn(edge.va.y-edge.vb.y)<1e-9)) { - edge.va = edge.vb = null; - edges.splice(iEdge,1); - } - } - }; - -// Close the cells. -// The cells are bound by the supplied bounding box. -// Each cell refers to its associated site, and a list -// of halfedges ordered counterclockwise. -Voronoi.prototype.closeCells = function(bbox) { - var xl = bbox.xl, - xr = bbox.xr, - yt = bbox.yt, - yb = bbox.yb, - cells = this.cells, - iCell = cells.length, - cell, - iLeft, - halfedges, nHalfedges, - edge, - va, vb, vz, - lastBorderSegment, - abs_fn = Math.abs; - - while (iCell--) { - cell = cells[iCell]; - // prune, order halfedges counterclockwise, then add missing ones - // required to close cells - if (!cell.prepareHalfedges()) { - continue; - } - if (!cell.closeMe) { - continue; - } - // find first 'unclosed' point. - // an 'unclosed' point will be the end point of a halfedge which - // does not match the start point of the following halfedge - halfedges = cell.halfedges; - nHalfedges = halfedges.length; - // special case: only one site, in which case, the viewport is the cell - // ... - - // all other cases - iLeft = 0; - while (iLeft < nHalfedges) { - va = halfedges[iLeft].getEndpoint(); - vz = halfedges[(iLeft+1) % nHalfedges].getStartpoint(); - // if end point is not equal to start point, we need to add the missing - // halfedge(s) up to vz - if (abs_fn(va.x-vz.x)>=1e-9 || abs_fn(va.y-vz.y)>=1e-9) { - - // rhill 2013-12-02: - // "Holes" in the halfedges are not necessarily always adjacent. - // https://github.com/gorhill/Javascript-Voronoi/issues/16 - - // find entry point: - switch (true) { - - // walk downward along left side - case this.equalWithEpsilon(va.x,xl) && this.lessThanWithEpsilon(va.y,yb): - lastBorderSegment = this.equalWithEpsilon(vz.x,xl); - vb = this.createVertex(xl, lastBorderSegment ? vz.y : yb); - edge = this.createBorderEdge(cell.site, va, vb); - iLeft++; - halfedges.splice(iLeft, 0, this.createHalfedge(edge, cell.site, null)); - nHalfedges++; - if ( lastBorderSegment ) { break; } - va = vb; - // fall through - - // walk rightward along bottom side - case this.equalWithEpsilon(va.y,yb) && this.lessThanWithEpsilon(va.x,xr): - lastBorderSegment = this.equalWithEpsilon(vz.y,yb); - vb = this.createVertex(lastBorderSegment ? vz.x : xr, yb); - edge = this.createBorderEdge(cell.site, va, vb); - iLeft++; - halfedges.splice(iLeft, 0, this.createHalfedge(edge, cell.site, null)); - nHalfedges++; - if ( lastBorderSegment ) { break; } - va = vb; - // fall through - - // walk upward along right side - case this.equalWithEpsilon(va.x,xr) && this.greaterThanWithEpsilon(va.y,yt): - lastBorderSegment = this.equalWithEpsilon(vz.x,xr); - vb = this.createVertex(xr, lastBorderSegment ? vz.y : yt); - edge = this.createBorderEdge(cell.site, va, vb); - iLeft++; - halfedges.splice(iLeft, 0, this.createHalfedge(edge, cell.site, null)); - nHalfedges++; - if ( lastBorderSegment ) { break; } - va = vb; - // fall through - - // walk leftward along top side - case this.equalWithEpsilon(va.y,yt) && this.greaterThanWithEpsilon(va.x,xl): - lastBorderSegment = this.equalWithEpsilon(vz.y,yt); - vb = this.createVertex(lastBorderSegment ? vz.x : xl, yt); - edge = this.createBorderEdge(cell.site, va, vb); - iLeft++; - halfedges.splice(iLeft, 0, this.createHalfedge(edge, cell.site, null)); - nHalfedges++; - if ( lastBorderSegment ) { break; } - va = vb; - // fall through - - // walk downward along left side - lastBorderSegment = this.equalWithEpsilon(vz.x,xl); - vb = this.createVertex(xl, lastBorderSegment ? vz.y : yb); - edge = this.createBorderEdge(cell.site, va, vb); - iLeft++; - halfedges.splice(iLeft, 0, this.createHalfedge(edge, cell.site, null)); - nHalfedges++; - if ( lastBorderSegment ) { break; } - va = vb; - // fall through - - // walk rightward along bottom side - lastBorderSegment = this.equalWithEpsilon(vz.y,yb); - vb = this.createVertex(lastBorderSegment ? vz.x : xr, yb); - edge = this.createBorderEdge(cell.site, va, vb); - iLeft++; - halfedges.splice(iLeft, 0, this.createHalfedge(edge, cell.site, null)); - nHalfedges++; - if ( lastBorderSegment ) { break; } - va = vb; - // fall through - - // walk upward along right side - lastBorderSegment = this.equalWithEpsilon(vz.x,xr); - vb = this.createVertex(xr, lastBorderSegment ? vz.y : yt); - edge = this.createBorderEdge(cell.site, va, vb); - iLeft++; - halfedges.splice(iLeft, 0, this.createHalfedge(edge, cell.site, null)); - nHalfedges++; - if ( lastBorderSegment ) { break; } - // fall through - - default: - throw "Voronoi.closeCells() > this makes no sense!"; - } - } - iLeft++; - } - cell.closeMe = false; - } - }; - -// --------------------------------------------------------------------------- -// Debugging helper -/* -Voronoi.prototype.dumpBeachline = function(y) { - console.log('Voronoi.dumpBeachline(%f) > Beachsections, from left to right:', y); - if ( !this.beachline ) { - console.log(' None'); - } - else { - var bs = this.beachline.getFirst(this.beachline.root); - while ( bs ) { - console.log(' site %d: xl: %f, xr: %f', bs.site.voronoiId, this.leftBreakPoint(bs, y), this.rightBreakPoint(bs, y)); - bs = bs.rbNext; - } - } - }; -*/ - -// --------------------------------------------------------------------------- -// Helper: Quantize sites - -// rhill 2013-10-12: -// This is to solve https://github.com/gorhill/Javascript-Voronoi/issues/15 -// Since not all users will end up using the kind of coord values which would -// cause the issue to arise, I chose to let the user decide whether or not -// he should sanitize his coord values through this helper. This way, for -// those users who uses coord values which are known to be fine, no overhead is -// added. - -Voronoi.prototype.quantizeSites = function(sites) { - var ε = this.ε, - n = sites.length, - site; - while ( n-- ) { - site = sites[n]; - site.x = Math.floor(site.x / ε) * ε; - site.y = Math.floor(site.y / ε) * ε; - } - }; - -// --------------------------------------------------------------------------- -// Helper: Recycle diagram: all vertex, edge and cell objects are -// "surrendered" to the Voronoi object for reuse. -// TODO: rhill-voronoi-core v2: more performance to be gained -// when I change the semantic of what is returned. - -Voronoi.prototype.recycle = function(diagram) { - if ( diagram ) { - if ( diagram instanceof this.Diagram ) { - this.toRecycle = diagram; - } - else { - throw 'Voronoi.recycleDiagram() > Need a Diagram object.'; - } - } - }; - -// --------------------------------------------------------------------------- -// Top-level Fortune loop - -// rhill 2011-05-19: -// Voronoi sites are kept client-side now, to allow -// user to freely modify content. At compute time, -// *references* to sites are copied locally. - -Voronoi.prototype.compute = function(sites, bbox) { - // to measure execution time - var startTime = new Date(); - - // init internal state - this.reset(); - - // any diagram data available for recycling? - // I do that here so that this is included in execution time - if ( this.toRecycle ) { - this.vertexJunkyard = this.vertexJunkyard.concat(this.toRecycle.vertices); - this.edgeJunkyard = this.edgeJunkyard.concat(this.toRecycle.edges); - this.cellJunkyard = this.cellJunkyard.concat(this.toRecycle.cells); - this.toRecycle = null; - } - - // Initialize site event queue - var siteEvents = sites.slice(0); - siteEvents.sort(function(a,b){ - var r = b.y - a.y; - if (r) {return r;} - return b.x - a.x; - }); - - // process queue - var site = siteEvents.pop(), - siteid = 0, - xsitex, // to avoid duplicate sites - xsitey, - cells = this.cells, - circle; - - // main loop - for (;;) { - // we need to figure whether we handle a site or circle event - // for this we find out if there is a site event and it is - // 'earlier' than the circle event - circle = this.firstCircleEvent; - - // add beach section - if (site && (!circle || site.y < circle.y || (site.y === circle.y && site.x < circle.x))) { - // only if site is not a duplicate - if (site.x !== xsitex || site.y !== xsitey) { - // first create cell for new site - cells[siteid] = this.createCell(site); - site.voronoiId = siteid++; - // then create a beachsection for that site - this.addBeachsection(site); - // remember last site coords to detect duplicate - xsitey = site.y; - xsitex = site.x; - } - site = siteEvents.pop(); - } - - // remove beach section - else if (circle) { - this.removeBeachsection(circle.arc); - } - - // all done, quit - else { - break; - } - } - - // wrapping-up: - // connect dangling edges to bounding box - // cut edges as per bounding box - // discard edges completely outside bounding box - // discard edges which are point-like - this.clipEdges(bbox); - - // add missing edges in order to close opened cells - this.closeCells(bbox); - - // to measure execution time - var stopTime = new Date(); - - // prepare return values - var diagram = new this.Diagram(); - diagram.cells = this.cells; - diagram.edges = this.edges; - diagram.vertices = this.vertices; - diagram.execTime = stopTime.getTime()-startTime.getTime(); - - // clean up - this.reset(); - - return diagram; - }; - -/******************************************************************************/ - -// stuff below here was modified by sanine -export default Voronoi; diff --git a/modules/Geometry.js b/modules/Geometry.js deleted file mode 100644 index 628512b..0000000 --- a/modules/Geometry.js +++ /dev/null @@ -1,229 +0,0 @@ -'use strict'; - -/* AABB - axis-aligned bounding box */ -class AABB { - /* create a new AABB */ - constructor(x, y, width, height) { - this.x = x; this.y = y; - this.width = width; this.height = height; - } - - /* check if a point is inside the box - * - * a "point" is any object with an x- and y-coordinate. - * any other data in the object is ignored. - * - * returns true if the point is inside, and false otherwise. - */ - contains(point) { - if (point.x >= this.x && point.y >= this.y - && point.x < this.x + this.width - && point.y < this.y + this.height) - return true; - return false; - } - - /* check if two AABBs overlap */ - intersects(aabb) { - const overlap = (ax, arange, bx, brange) => { - const range = ax < bx ? arange : brange; - if (Math.abs(bx - ax) < range) - return true; - return false; - }; - - if (overlap(this.x, this.width, aabb.x, aabb.width) && - overlap(this.y, this.height, aabb.y, aabb.height)) - return true; - return false; - } -} - - -/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - * - * QUADTREE - * - * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - */ - -/* lil enum class for QTNode types */ -class QTNodeType { - constructor(name) { this.name = name; } - toString() { return `QTNode.${this.name}`; } -} - - -/* nodes in a quadtree - * - * they can be either "Empty" (no point), - * "Leaf" (they contain a point and no sub-nodes), - * or "Branch" (they contain no point and have sub-nodes). - * There are no QTNodes with children and point data. - */ -class QTNode { - static Empty = new QTNodeType('Empty'); - static Leaf = new QTNodeType('Leaf'); - static Branch = new QTNodeType('Branch'); - - constructor(x, y, width, height) { - this.aabb = new AABB(x, y, width, height); - /* all QTNodes start empty */ - this.type = QTNode.Empty; - } - - /* insert a point into the tree, starting with this node - * - * as above, points are just objects with 'x' and 'y' values. - * other data in the point is not examined and is incorporated into the - * tree. - * - * this function returns false if the point was not contained in the bounding box, - * and true otherwise. inserting a point may cause the node's type to change - * (e.g. a Leaf node would become a Branch node and its point data would be - * moved lower in the tree). - */ - insert(point) { - if (this.aabb.contains(point)) { - if (this.type == QTNode.Empty) { - /* empty nodes are easy -- just switch to Leaf and add the point */ - this.type = QTNode.Leaf; - this.point = point; - return true; - } - else if (this.type == QTNode.Leaf) { - /* Leaf must become a Branch */ - this.type = QTNode.Branch; - this.subdivide(); - - /* move current point into a child */ - let result = this.insert(this.point); - if (!result) return false; - this.point = undefined; - - /* add new point */ - return this.insert(point); - } - else { - /* This is a Branch, and we should insert into a child node */ - /* just try them all until we find one where the point can go */ - if (this.subnode[0].insert(point)) return true; - if (this.subnode[1].insert(point)) return true; - if (this.subnode[2].insert(point)) return true; - if (this.subnode[3].insert(point)) return true; - return false; // something strange has happened!! - } - } - /* point not in this node, return false */ - return false; - } - - /* subdivide the node into four child nodes - * - * under normal circumstances, the user should not call this function -- - * it's just meant to be used during insertion. - */ - subdivide() { - /* some useful constants */ - const x = this.aabb.x; const y = this.aabb.y; - const w = this.aabb.width/2; const h = this.aabb.height/2; - - /* generate sub-nodes */ - this.subnode = [ - new QTNode(x, y, w, h), - new QTNode(x+w, y, w, h), - new QTNode(x, y+h, w, h), - new QTNode(x+w, y+h, w, h), - ]; - } - - /* query the tree for all points contained within a region recursively - * - * returns an array containing any points in the tree beneath this node - * that are inside the supplied AABB. - */ - getPointsInRegion(aabb) { - if (this.type == QTNode.Empty) return []; - if (!this.aabb.intersects(aabb)) return []; - if (this.type == QTNode.Leaf) return [this.point]; - - /* Branch - recusively query children */ - return [ - ...this.subnode[0].getPointsInRegion(aabb), - ...this.subnode[1].getPointsInRegion(aabb), - ...this.subnode[2].getPointsInRegion(aabb), - ...this.subnode[3].getPointsInRegion(aabb), - ]; - } -} - - -/* full-blown quadtree! */ -class QuadTree { - /* create a new quadtree */ - constructor(width, height) { - this.root = new QTNode(0, 0, width, height); - } - - /* insert a point into the tree */ - insert(point) { - return this.root.insert(point); - } - - /* get the closest tree point to the given point */ - closest(point) { - /* the algorithm goes like this: - * 1. find the smallest node containing the point - * 2. the closest point is within a box at most 4x the - * size of that lowest node, so query all points within - * such a region. - * 3. brute-force within the list of points. - */ - - /* recursively find the node containing a point */ - const getLeaf = (point, node) => { - /* is point in bounding box? */ - if (!node.aabb.contains(point)) return null; - /* is this a Leaf or Empty node? */ - if (node.type == QTNode.Empty || - node.type == QTNode.Leaf) return node; - - /* this is a branch node, so recurse */ - for (let subnode of node.subnode) { - const leaf = getLeaf(point, subnode); - if (leaf !== null) return leaf; - } - - /* should never get here */ - return null; - } - - /* find the node, starting at the tree root */ - const leaf = getLeaf(point, this.root); - if (leaf === null) return leaf; - - /* construct the 4x AABB, centered on the query point */ - const dim = 2*Math.max(leaf.aabb.width, leaf.aabb.height); - const region = new AABB(point.x - dim, point.y - dim, 2*dim, 2*dim); - - /* get all points */ - const points = this.root.getPointsInRegion(region); - - /* brute force search within the small list */ - let closest = null; - let min_dist = 100 * this.root.aabb.width * this.root.aabb.height; - const dist = (a, b) => (a.x - b.x)**2 + (a.y - b.y)**2; - for (let pt of points) { - const d = dist(point, pt); - if (d < min_dist) { - closest = pt; - min_dist = d; - } - } - - /* done! */ - return closest; - } -} - -export { AABB, QTNode, QuadTree }; diff --git a/modules/Geometry.test.js b/modules/Geometry.test.js deleted file mode 100644 index 925a53d..0000000 --- a/modules/Geometry.test.js +++ /dev/null @@ -1,259 +0,0 @@ -import { test, assert } from './test-assert.js'; -import { AABB, QTNode, QuadTree } from './Geometry.js'; - -const RUN_BENCHMARKS = false; - - -test('AABB correctly contains/excludes points', () => { - const box = new AABB(0, 0, 1, 1); - - // interior - assert.ok(box.contains({ x: 0.5, y: 0.5 })); - - // upper left - assert.ok(!box.contains({ x: -1, y: -1 })); - - // above - assert.ok(!box.contains({ x: 0.5, y: -1})); - - // upper right - assert.ok(!box.contains({ x: 2, y: -1 })); - - // left - assert.ok(!box.contains({ x: -1, y: 0.5 })); - - // right - assert.ok(!box.contains({ x: 2, y: 0.5})); - - // lower left - assert.ok(!box.contains({ x: -1, y: 2 })); - - // below - assert.ok(!box.contains({ x: 0.5, y: 2})); - - // lower right - assert.ok(!box.contains({ x: 2, y: 2 })); -}); - - -test('AABB correctly intersects other AABBs', () => { - const box = new AABB(1, 1, 4, 4); - - // interior - assert.ok(box.intersects(new AABB(2, 2, 2, 2,))); - - // upper left - assert.ok(box.intersects(new AABB(0, 0, 4, 4))); - assert.ok(!box.intersects(new AABB(0, 0, 0.5, 0.5))); - - // above - assert.ok(box.intersects(new AABB(2, 0, 2, 2))); - assert.ok(!box.intersects(new AABB(2, 0, 2, 0.5))); - - // upper right - assert.ok(box.intersects(new AABB(2, 0, 4, 2))); - assert.ok(!box.intersects(new AABB(6, 0, 4, 2))); - - // left - assert.ok(box.intersects(new AABB(0, 2, 2, 2))); - assert.ok(!box.intersects(new AABB(0, 2, 0.5, 2))); - - // right - assert.ok(box.intersects(new AABB(4, 2, 2, 2))); - assert.ok(!box.intersects(new AABB(6, 2, 2, 2))); - - // lower left - assert.ok(box.intersects(new AABB(0, 4, 4, 4))); - assert.ok(!box.intersects(new AABB(0, 6, 0.5, 0.5))); - - // below - assert.ok(box.intersects(new AABB(2, 4, 2, 2))); - assert.ok(!box.intersects(new AABB(2, 6, 2, 0.5))); - - // lower right - assert.ok(box.intersects(new AABB(2, 4, 4, 2))); - assert.ok(!box.intersects(new AABB(6, 6, 4, 2))); -}); - - -test('AABB correctly handles points at the edges', () => { - const box = new AABB(0, 0, 1, 1); - - assert.ok(box.contains({ x: 0, y: 0 })); - assert.ok(box.contains({ x: 0.5, y: 0 })); - assert.ok(box.contains({ x: 0, y: 0.5 })); - - // bad corners - assert.ok(!box.contains({ x: 1, y: 0 })); - assert.ok(!box.contains({ x: 0, y: 1 })); - assert.ok(!box.contains({ x: 1, y: 1 })); - - // bad edges - assert.ok(!box.contains({ x: 1, y: 0.5 })); - assert.ok(!box.contains({ x: 0.5, y: 1 })); -}); - - -test('QTNode correctly inserts points', () => { - const node = new QTNode(0, 0, 1, 1); - assert.equal(node.type.toString(), 'QTNode.Empty'); - - let result = node.insert({ x: -1, y: -1 }); // out of range, should not insert - assert.ok(!result); - assert.equal(node.type.toString(), 'QTNode.Empty'); - - result = node.insert({ x: 0.5, y: 0.5 }); // in range - assert.ok(result); - assert.equal(node.type.toString(), 'QTNode.Leaf'); - assert.deepEqual(node.point, { x: 0.5, y: 0.5 }); -}); - - -test('QTNode correctly subdivides', () => { - const node = new QTNode(0, 0, 2, 2); - assert.ok(!node.subnode); - node.subdivide(); - assert.equal(node.subnode.length, 4); - - assert.deepEqual(node.subnode[0], new QTNode(0, 0, 1, 1)); - assert.deepEqual(node.subnode[1], new QTNode(1, 0, 1, 1)); - assert.deepEqual(node.subnode[2], new QTNode(0, 1, 1, 1)); - assert.deepEqual(node.subnode[3], new QTNode(1, 1, 1, 1)); -}); - - -test('QTNode correctly inserts multiple points', () => { - const node = new QTNode(0, 0, 2, 2); - - const p0 = { x: 1, y: 1 }; - const p1 = { x: 0.5, y: 0.5 }; - const oob = { x: 10, y: 15 }; - - assert.ok(node.insert(p0)); - assert.ok(node.insert(p1)); - assert.ok(!node.insert(oob)); - - assert.equal(node.type.toString(), 'QTNode.Branch'); - - assert.equal(node.subnode[0].type.toString(), 'QTNode.Leaf'); - assert.deepEqual(node.subnode[0].point, p1); - - assert.equal(node.subnode[1].type.toString(), 'QTNode.Empty'); - assert.equal(node.subnode[2].type.toString(), 'QTNode.Empty'); - - assert.equal(node.subnode[3].type.toString(), 'QTNode.Leaf'); -assert.deepEqual(node.subnode[3].point, p0); -}); - - -const randomPoint = () => ({ x: Math.random(), y: Math.random() }); - - -test('QTNode correctly returns points in region', () => { - const node = new QTNode(0, 0, 1, 1); - let points = []; - for (let i=0; i<100; i++) { - const pt = randomPoint(); - node.insert(pt); - points.push(pt); - } - - const pointsInRegion = aabb => { - let pts = []; - for (let pt of points) { - if (aabb.contains(pt)) pts.push(pt); - } - return pts; - }; - - const region = new AABB(0.25, 0.25, 0.5, 0.5); - let bf_points = pointsInRegion(region); - let qt_points = node.getPointsInRegion(region); - const compare = (a, b) => { - if (a.x == b.x) - return b.y - a.y; - return b.x - a.x; - }; - bf_points.sort(compare); - qt_points.sort(compare); - assert.deepEqual(bf_points, qt_points);}); - - -test('QuadTree correctly finds closest point for 10,000 random points', () => { - const tree = new QuadTree(1, 1); - - let points = []; - for (let i=0; i<10000; i++) { - const pt = randomPoint(); - tree.insert(pt); - points.push(pt); - } - - const distance = (a, b) => Math.sqrt((a.x - b.x)**2 + (a.y - b.y)**2); - const bf_closest = a => { - let closest = null; - let min_dist = 1000; - for (let b of points) { - let d = distance(a, b); - if (d < min_dist) { - min_dist = d; - closest = b; - } - } - return closest; - } - - for (let i=0; i<10000; i++) { - const pt = randomPoint(); - assert.deepEqual(bf_closest(pt), tree.closest(pt)); - } -}); - - -function benchmark(desc, f, iterations) { - process.stdout.write(`${desc}: `); - const start = process.uptime(); - for (let i=0; i<iterations; i++) f(); - const elapsed = process.uptime() - start; - console.log(`${elapsed} seconds`); -} - -const closest_benchmark = () => { - const n_points = 1e5; - const iterations = 1e4; - console.log(`> benchmark get closest of ${n_points} random points ${iterations} times`); - - const tree = new QuadTree(1, 1); - let points = []; - for (let i=0; i<n_points; i++) { - const pt = randomPoint(); - tree.insert(pt); - points.push(pt); - } - - const distance = (a, b) => Math.sqrt((a.x - b.x)**2 + (a.y - b.y)**2); - const bf_closest = a => { - let closest = null; - let min_dist = 1000; - for (let b of points) { - let d = distance(a, b); - if (d < min_dist) { - min_dist = d; - closest = b; - } - } - return closest; - } - - benchmark('\tbrute force', () => { - const pt = randomPoint(); - bf_closest(pt); - }, iterations); - - benchmark('\tquadtree', () => { - const pt = randomPoint(); - tree.closest(pt); - }, iterations); -} -if (RUN_BENCHMARKS) - closest_benchmark(); diff --git a/modules/Terrain.js b/modules/Terrain.js deleted file mode 100644 index ca0ab77..0000000 --- a/modules/Terrain.js +++ /dev/null @@ -1,82 +0,0 @@ -'use strict'; - -import Voronoi from './3rdparty/rhill-voronoi-core.js'; - -import { useAverage } from './Util.js'; -import { QuadTree } from './Geometry.js'; - - -/* from here on up, we always assume that points live in the range [(0,0), (1,1)) */ - -function lloydRelax(point_set, density) { - /* setup quadtree and averages */ - let tree = new QuadTree(1,1); - let averages = {}; - for (let i=0; i<point_set.length; i++) { - const point = point_set[i]; - point.index = i; - tree.insert(point); - - let [avg, append] = useAverage(); - const cent_x = { avg, append }; - [avg, append] = useAverage(); - const cent_y = { avg, append }; - averages[i] = { cent_x, cent_y }; - } - - /* compute average centroids */ - for (let x=0; x<1; x += 1/density) { - for (let y=0; y<1; y += 1/density) { - const point = { x, y }; - const closest = tree.closest(point); - const { cent_x, cent_y } = averages[closest.index]; - cent_x.append(point.x); - cent_y.append(point.y); - } - } - - /* return centroid points */ - const result = []; - for (let i=0; i<point_set.length; i++) { - const point = { x: averages[i].cent_x.avg(), y: averages[i].cent_y.avg() }; - result.push(point); - } - return result; -} - - -class Terrain { - constructor() { - const N_SEED_POINTS = 2**14; - const N_RELAX_ITERATIONS = 1; - const RELAX_DENSITY = 400; - const randomPoint = () => ({x: Math.random(), y: Math.random()}); - - let seed_points = []; - for (let i=0; i<N_SEED_POINTS; i++) seed_points.push(randomPoint()); - - for (let i=0; i<N_RELAX_ITERATIONS; i++) - lloydRelax(seed_points, RELAX_DENSITY); - - const v = new Voronoi(); - this.graph = v.compute(seed_points, {xl: 0, xr: 1, yt: 0, yb: 1}); - - this.tree = new QuadTree(1,1); - for (let v of this.graph.vertices) this.tree.insert(v); - } - - - renderGrid(ct) { - ct.lineWidth = 0.001; - for (let edge of this.graph.edges) { - ct.beginPath(); - ct.moveTo(edge.va.x, edge.va.y); - ct.lineTo(edge.vb.x, edge.vb.y); - ct.closePath(); - ct.stroke(); - } - } -} - -export { lloydRelax }; -export default Terrain; diff --git a/modules/Util.js b/modules/Util.js deleted file mode 100644 index cbe466d..0000000 --- a/modules/Util.js +++ /dev/null @@ -1,13 +0,0 @@ -function useAverage() { - var avg = 0; - let weight = 0; - const append = value => { - avg = (weight * avg) + value; - weight += 1; - avg = avg/weight; - } - - return [() => avg, append]; -} - -export { useAverage }; diff --git a/modules/Util.test.js b/modules/Util.test.js deleted file mode 100644 index 000d54e..0000000 --- a/modules/Util.test.js +++ /dev/null @@ -1,23 +0,0 @@ -import { test, assert} from './test-assert.js'; -import { useAverage } from './Util.js'; - - -test('Average correctly accumulates an average', () => { - let [avg, avg_append] = useAverage(); - let data = []; - for (let i=0; i<5000; i++) { - let d = Math.random(); - data.push(d); - avg_append(d); - } - - let manual_average = 0; - for (let d of data) manual_average += d; - manual_average /= data.length; - - const precision = (decimalPlaces, num) => { - const theta = 10**decimalPlaces; - return Math.floor(num * theta) / theta; - }; - assert.equal(precision(5, avg()), precision(5, manual_average)); -}); diff --git a/modules/test-assert.js b/modules/test-assert.js deleted file mode 100644 index 7be2989..0000000 --- a/modules/test-assert.js +++ /dev/null @@ -1,21 +0,0 @@ -import { strict as assert } from 'node:assert'; - -function test(description, func) -{ - process.stdout.write(description); - let error = null; - try { - func(); - } - catch(err) { - error = err; - } - - if (error) { - console.log(' - FAIL'); - console.log(error); - } - else console.log(' - OK'); -} - -export { test, assert }; |