├── .gitignore ├── .idea └── codeStyleSettings.xml ├── README.md ├── Skeleton.pdf ├── index.html ├── jquery-1.10.2.js ├── kdtree.js ├── medial_axis.js ├── medial_axis_test.js ├── numeric-1.2.6.js ├── polygons.js ├── qunit-1.12.0.css ├── qunit-1.12.0.js ├── rhill-voronoi-core.js ├── simplify.js ├── solver.js ├── test.js ├── testImage.png ├── test_contour.html ├── test_display.js ├── test_dynamic_programing.html ├── test_graph.html ├── test_intro_algorithms.html ├── test_kdtree.html ├── test_medial_axis.html └── testscanImage2.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | lerna-debug.log* 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | *.lcov 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # Bower dependency directory (https://bower.io/) 34 | bower_components 35 | 36 | # node-waf configuration 37 | .lock-wscript 38 | 39 | # Compiled binary addons (https://nodejs.org/api/addons.html) 40 | build/Release 41 | 42 | # Dependency directories 43 | node_modules/ 44 | jspm_packages/ 45 | 46 | # TypeScript v1 declaration files 47 | typings/ 48 | 49 | # TypeScript cache 50 | *.tsbuildinfo 51 | 52 | # Optional npm cache directory 53 | .npm 54 | 55 | # Optional eslint cache 56 | .eslintcache 57 | 58 | # Microbundle cache 59 | .rpt2_cache/ 60 | .rts2_cache_cjs/ 61 | .rts2_cache_es/ 62 | .rts2_cache_umd/ 63 | 64 | # Optional REPL history 65 | .node_repl_history 66 | 67 | # Output of 'npm pack' 68 | *.tgz 69 | 70 | # Yarn Integrity file 71 | .yarn-integrity 72 | 73 | # dotenv environment variables file 74 | .env 75 | .env.test 76 | 77 | # parcel-bundler cache (https://parceljs.org/) 78 | .cache 79 | 80 | # Next.js build output 81 | .next 82 | 83 | # Nuxt.js build / generate output 84 | .nuxt 85 | dist 86 | 87 | # Gatsby files 88 | .cache/ 89 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 90 | # https://nextjs.org/blog/next-9-1#public-directory-support 91 | # public 92 | 93 | # vuepress build output 94 | .vuepress/dist 95 | 96 | # Serverless directories 97 | .serverless/ 98 | 99 | # FuseBox cache 100 | .fusebox/ 101 | 102 | # DynamoDB Local files 103 | .dynamodb/ 104 | 105 | # TernJS port file 106 | .tern-port 107 | 108 | -------------------------------------------------------------------------------- /.idea/codeStyleSettings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 36 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | polygonsJS 2 | ========== 3 | 4 | 5 | A mock Bentley-Ottmann for training: http://nraynaud.github.io/polygonsJS/index.html 6 | 7 | 8 | 9 | Beginning of a Medial Axis Transform for simple polygons here: http://nraynaud.github.io/polygonsJS/test_medial_axis.html 10 | 11 | There are still 2 O(n^2) parts: point in polygon test and closest distance test filter, both to be solved with a KD tree -------------------------------------------------------------------------------- /Skeleton.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nraynaud/polygonsJS/379e73d9b421b7454e548f32e1d3ee14a127cae4/Skeleton.pdf -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Simple Bentley Ottmann with mock underlying structures 6 | 7 | 22 | 23 | 24 | The source code is there: https://github.com/nraynaud/polygonsJS/ 25 | 26 |
27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /kdtree.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | class KDtree { 4 | constructor(points) { 5 | this.tree = this.buildTree(points, true) 6 | } 7 | 8 | xComparison = (p1, p2) => p1.x - p2.x 9 | yComparison = (p1, p2) => p1.y - p2.y 10 | 11 | buildTree(points, useXAxis) { 12 | if (points.length === 0) 13 | return null; 14 | const sorted = [...points]; 15 | sorted.sort(useXAxis ? this.xComparison : this.yComparison) 16 | let pivotIndex = Math.floor(sorted.length / 2); 17 | const point = sorted[pivotIndex] 18 | return { 19 | point, 20 | left: this.buildTree(sorted.slice(0, pivotIndex), !useXAxis), 21 | right: this.buildTree(sorted.slice(pivotIndex + 1, sorted.length), !useXAxis), 22 | axis: useXAxis ? 'x' : 'y' 23 | } 24 | } 25 | 26 | findMinimum(axis) { 27 | return this.findMinimumNodeInTree(this.tree, axis).point 28 | } 29 | 30 | findMinimumNodeInTree(node, axis) { 31 | if (node.axis === axis) { 32 | return node.left ? this.findMinimumNodeInTree(node.left, axis) : node; 33 | } 34 | let left = node.left ? this.findMinimumNodeInTree(node.left, axis) : null 35 | let right = node.right ? this.findMinimumNodeInTree(node.right, axis) : null 36 | const nodes = [left, right, node] 37 | nodes.sort((p1, p2) => { 38 | if (p1 == null) 39 | return 1 40 | if (p2 == null) 41 | return -1 42 | return p1.point[axis] - p2.point[axis] 43 | }) 44 | return nodes[0] 45 | } 46 | 47 | findNearestNeighbor(point) { 48 | return this.findNNInTree(point, this.tree) 49 | } 50 | 51 | findNNInTree(point, node) { 52 | if (node == null) 53 | return null 54 | const closestChoice = (p1, p2) => { 55 | if (p1 == null) 56 | return p2 57 | if (p2 == null) 58 | return p1 59 | return sqSegLength([point, p1]) < sqSegLength([point, p2]) ? p1 : p2 60 | } 61 | const [pointValue, refValue] = [point[node.axis], node.point[node.axis]] 62 | const children = [node.left, node.right] 63 | if (pointValue >= refValue) 64 | children.reverse(); 65 | let best = closestChoice(this.findNNInTree(point, children[0]), node.point) 66 | if (sqSegLength([point, best]) > (point[node.axis] - node.point[node.axis]) ** 2) { 67 | best = closestChoice(this.findNNInTree(point, children[1]), best) 68 | } 69 | return best 70 | } 71 | 72 | deletePoint(point) { 73 | this.tree = this.deleteNode(point, this.tree) 74 | } 75 | 76 | deleteNode(point, node) { 77 | // https://www.cs.cmu.edu/~ckingsf/bioinfo-lectures/kdtrees.pdf 78 | if (node == null) 79 | throw new Error('oops, not found' + point) 80 | if (point === node.point) { 81 | if (node.right) { 82 | node.point = this.findMinimumNodeInTree(node.right, node.right.axis).point 83 | node.right = this.deleteNode(node.point, node.right) 84 | return node 85 | } else if (node.left) { 86 | // remove the minimum from the left tree, make what's remaining the new right 87 | // the minimum becomes the new self 88 | node.point = this.findMinimumNodeInTree(node.left, node.left.axis).point 89 | node.right = this.deleteNode(node.point, node.left) 90 | node.left = null 91 | } else 92 | // delete me, I was a leaf 93 | return null 94 | } else { 95 | if (point[node.axis] < node.point[node.axis]) 96 | node.left = this.deleteNode(point, node.left) 97 | else 98 | node.right = this.deleteNode(point, node.right) 99 | } 100 | return node 101 | } 102 | } -------------------------------------------------------------------------------- /medial_axis.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // source for the algorithm is here: http://maptools.home.comcast.net/~maptools/Skeleton.pdf 3 | var medialAxis = (function () { 4 | var ID_COUNTER = 0; 5 | 6 | function getId() { 7 | ID_COUNTER++; 8 | return ID_COUNTER; 9 | } 10 | 11 | function createLinkedList(content) { 12 | var val = null; 13 | for (var i = 0; i < content.length; i++) { 14 | var bucket = {val: content[i], next: null, prev: null}; 15 | if (val == null) { 16 | val = bucket; 17 | bucket.next = bucket; 18 | bucket.prev = bucket; 19 | } else { 20 | bucket.next = val.next; 21 | val.next = bucket; 22 | bucket.next.prev = bucket; 23 | bucket.prev = val; 24 | val = bucket; 25 | } 26 | } 27 | 28 | return { 29 | //if handler returns true value, we break the loop 30 | iterate: function (handler) { 31 | if (val == null) 32 | return; 33 | var currentBucket = val; 34 | do { 35 | if (handler(currentBucket)) 36 | break; 37 | currentBucket = currentBucket.next; 38 | } while (currentBucket !== val); 39 | }, 40 | remove: function (bucket) { 41 | if (val === bucket) { 42 | if (val.next === bucket) { 43 | val = null; 44 | return; 45 | } 46 | val = val.next; 47 | } 48 | bucket.prev.next = bucket.next; 49 | bucket.next.prev = bucket.prev; 50 | }, 51 | isEmpty: function () { 52 | return val == null; 53 | } 54 | } 55 | } 56 | 57 | function pointProjectsOnSegments(segments, point) { 58 | for (var j = 0; j < segments.length; j++) 59 | if (!pointProjectedOnSegment(point, segments[j]) || pointEquals(point, segments[j][0]) || pointEquals(point, segments[j][1])) 60 | return false; 61 | return true; 62 | } 63 | 64 | function reversedSegment(segment) { 65 | return [segment[1], segment[0]]; 66 | } 67 | 68 | /** 69 | * @param origin 70 | * @param direction 71 | * @param firstSite 72 | * @param secondSite 73 | * @param [candidateFilter] 74 | * @constructor 75 | */ 76 | function LinearRay(origin, direction, firstSite, secondSite, candidateFilter) { 77 | this.origin = origin; 78 | this.segment = [origin, direction]; 79 | this.firstSite = firstSite; 80 | firstSite.rays.push(this); 81 | this.secondSite = secondSite; 82 | secondSite.backRays.push(this); 83 | this.filterPointCandidate = candidateFilter ? candidateFilter : function () { 84 | return true; 85 | }; 86 | } 87 | 88 | function ParabolicRay(vertexSite, edgeSite, origin, firstSite, secondSite) { 89 | this.origin = origin; 90 | this.vertex = vertexSite; 91 | this.edge = edgeSite; 92 | this.firstSite = firstSite; 93 | firstSite.rays.push(this); 94 | this.secondSite = secondSite; 95 | secondSite.backRays.push(this); 96 | } 97 | 98 | ParabolicRay.prototype = { 99 | filterPointCandidate: function (point) { 100 | return pointProjectsOnSegments([this.edge.segment], point); 101 | } 102 | }; 103 | 104 | function LineSite(segment) { 105 | this.segment = segment; 106 | this.rays = []; 107 | this.backRays = []; 108 | } 109 | 110 | LineSite.prototype = { 111 | dropEquation: function (equationCreator) { 112 | return equationCreator.addSegment(this.segment); 113 | }, 114 | igniteRayWithPreviousSite: function (previousSite, origin) { 115 | return previousSite.igniteRayWithLineSite(this, origin); 116 | }, 117 | igniteRayWithLineSite: function (followingLineSite, origin, edgesIntersection) { 118 | if (edgesIntersection == null) { 119 | edgesIntersection = intersectionSegments(this.segment, followingLineSite.segment, true); 120 | if (edgesIntersection == null || !isFinite(edgesIntersection.x) || !isFinite(edgesIntersection.y)) 121 | edgesIntersection = origin; 122 | } 123 | var bVector = bisectorVectorFromSegments(reversedSegment(this.segment), followingLineSite.segment); 124 | var bPoint = {x: edgesIntersection.x + bVector.x, y: edgesIntersection.y + bVector.y}; 125 | var segment1 = this.segment; 126 | return new LinearRay(origin, bPoint, this, followingLineSite, function (point) { 127 | return pointProjectsOnSegments([segment1], point); 128 | }); 129 | }, 130 | igniteRayWithReflexVertexSite: function (followingVertexSite, origin) { 131 | return igniteVertexSegment(followingVertexSite, this, origin, this, followingVertexSite); 132 | }, 133 | sqDistanceFromPoint: function (point) { 134 | return distToSegmentSquared(point, this.segment); 135 | }, 136 | coveredEdges: function () { 137 | return [this]; 138 | } 139 | }; 140 | 141 | function ReflexVertexSite(vertex, previousSite, nextSite) { 142 | this.vertex = vertex; 143 | this.previousSite = previousSite; 144 | this.nextSite = nextSite; 145 | this.rays = []; 146 | this.backRays = []; 147 | } 148 | 149 | ReflexVertexSite.prototype = { 150 | dropEquation: function (equationCreator) { 151 | return equationCreator.addVertex(this.vertex); 152 | }, 153 | igniteRayWithPreviousSite: function (previousSite, origin) { 154 | return previousSite.igniteRayWithReflexVertexSite(this, origin); 155 | }, 156 | igniteRayWithLineSite: function (followingLineSite, origin) { 157 | return igniteVertexSegment(this, followingLineSite, origin, this, followingLineSite); 158 | }, 159 | igniteRayWithReflexVertexSite: function (nextSite, origin) { 160 | return new LinearRay(origin, perpendicularPoint(origin, [this.vertex, nextSite.vertex]), this, nextSite); 161 | }, 162 | ignitePerpendicularRays: function () { 163 | var ray1 = this.previousSite.igniteRayWithReflexVertexSite(this); 164 | var ray2 = this.igniteRayWithLineSite(this.nextSite, this.vertex); 165 | ray1.neverIntersects = ray2; 166 | ray2.neverIntersects = ray1; 167 | return [ray1, ray2]; 168 | }, 169 | coveredEdges: function () { 170 | return [this.previousSite, this.nextSite]; 171 | } 172 | }; 173 | 174 | function igniteVertexSegment(vertexSite, lineSite, origin, firstSite, secondSite) { 175 | var segment = lineSite.segment; 176 | if (segment[1] === vertexSite.vertex || segment[0] === vertexSite.vertex) 177 | return new LinearRay(vertexSite.vertex, perpendicularPoint(vertexSite.vertex, segment), firstSite, secondSite); 178 | return new ParabolicRay(vertexSite.vertex, lineSite, origin, firstSite, secondSite); 179 | } 180 | 181 | function createSkeleton(polygon, observers, afterProcess) { 182 | observers = observers ? observers : {}; 183 | var area = signedArea(polygon); 184 | 185 | function setIntersection(targetRay, intersectionPoint, nextRay) { 186 | function intersectionDistance(intersection, vertex) { 187 | if (intersection) { 188 | var len = segLength([intersection, vertex]); 189 | return len === 0 ? Infinity : len; 190 | } else 191 | return Infinity; 192 | } 193 | 194 | targetRay.ahead = intersectionDistance(intersectionPoint, targetRay.origin); 195 | nextRay.behind = intersectionDistance(intersectionPoint, nextRay.origin); 196 | nextRay.behindPoint = intersectionPoint; 197 | targetRay.aheadPoint = intersectionPoint; 198 | targetRay.nextRay = nextRay; 199 | } 200 | 201 | function fuseRay(ray) { 202 | var newRay = ray.nextRay.secondSite.igniteRayWithPreviousSite(ray.firstSite, ray.aheadPoint); 203 | newRay.children = [ray, ray.nextRay]; 204 | ray.parent = newRay; 205 | ray.nextRay.parent = newRay; 206 | ray.destination = ray.aheadPoint; 207 | ray.nextRay.destination = ray.aheadPoint; 208 | return newRay; 209 | } 210 | 211 | function intersectNextRay(current, next) { 212 | if (current.nextRay === next) 213 | return; 214 | var result; 215 | var rejectedPointsProjection = []; 216 | var rejectedPoint2 = []; 217 | if (current.neverIntersects === next || next.neverIntersects === current) 218 | result = []; 219 | else { 220 | var eq = new solver.EquationSystemCreator(); 221 | current.firstSite.dropEquation(eq); 222 | current.secondSite.dropEquation(eq); 223 | next.secondSite.dropEquation(eq); 224 | result = solver.solveEquations(eq, function (point) { 225 | point.id = getId(); 226 | var result1 = current.filterPointCandidate(point) && next.filterPointCandidate(point); 227 | var result2 = pointInPolygon(point, polygon); 228 | var result = result1 && result2; 229 | if (!result1) 230 | rejectedPointsProjection.push(point); 231 | if (!result2) 232 | rejectedPoint2.push(point); 233 | return result; 234 | }); 235 | 236 | } 237 | setIntersection(current, result[0], next); 238 | } 239 | 240 | function getCoveredSites(ray) { 241 | return ray.firstSite.coveredEdges().concat(ray.secondSite.coveredEdges()); 242 | } 243 | 244 | 245 | function isReflexVertex(vertex, previousVertex, nextVertex, polygonArea) { 246 | return signedArea([previousVertex, vertex, nextVertex]) * polygonArea < 0; 247 | } 248 | 249 | var lineSites = []; 250 | 251 | function createInitialRays(polygon) { 252 | var rays = []; 253 | var reflexPoints = []; 254 | var sites = []; 255 | for (var i = 0; i < polygon.length; i++) { 256 | var previousPoint = polygon[(i + polygon.length - 1) % polygon.length]; 257 | var vertex = polygon[i]; 258 | if (!previousPoint.nextEdge) 259 | previousPoint.nextEdge = new LineSite([previousPoint, vertex]); 260 | if (sites.length === 0) 261 | sites.push(previousPoint.nextEdge) 262 | var nextPoint = polygon[(i + 1) % polygon.length]; 263 | if (!vertex.nextEdge) 264 | vertex.nextEdge = new LineSite([vertex, nextPoint]); 265 | previousPoint.nextEdge.nextSite = vertex.nextEdge; 266 | vertex.nextEdge.previousSite = previousPoint.nextEdge; 267 | lineSites.push(vertex.nextEdge); 268 | if (isReflexVertex(vertex, previousPoint, nextPoint, area)) { 269 | vertex.reflex = true; 270 | reflexPoints.push(vertex); 271 | var reflexVertexSite = new ReflexVertexSite(vertex, previousPoint.nextEdge, vertex.nextEdge); 272 | reflexVertexSite.previousSite.nextSite = reflexVertexSite; 273 | reflexVertexSite.nextSite.previousSite = reflexVertexSite; 274 | Array.prototype.push.apply(rays, reflexVertexSite.ignitePerpendicularRays()); 275 | sites.push(reflexVertexSite); 276 | } else { 277 | var ray = previousPoint.nextEdge.igniteRayWithLineSite(vertex.nextEdge, vertex, vertex); 278 | rays.push(ray); 279 | ray.medialRay = {type: 'limb', vertex: vertex, edge1: previousPoint.nextEdge, edge2: vertex.nextEdge, 280 | origin: vertex, children: []}; 281 | } 282 | sites.push(vertex.nextEdge); 283 | } 284 | if (observers['initialized']) 285 | observers['initialized'](rays, reflexPoints); 286 | return {rays: rays, sites: sites}; 287 | } 288 | 289 | var initialRays = createInitialRays(polygon); 290 | var rayList = createLinkedList(initialRays.rays); 291 | var siteList = createLinkedList(initialRays.sites); 292 | var root; 293 | 294 | function isBestIntersection(currentRay, nextRay, lineSites, intersectionPoint, sqRadius) { 295 | var coveredSites = getCoveredSites(currentRay).concat(getCoveredSites(nextRay)); 296 | for (var i = 0; i < lineSites.length; i++) { 297 | var otherSqrDist = lineSites[i].sqDistanceFromPoint(intersectionPoint); 298 | if (otherSqrDist < sqRadius && coveredSites.indexOf(lineSites[i]) === -1) { 299 | if (observers['eliminatedRadius']) 300 | observers['eliminatedRadius'](currentRay, nextRay, intersectionPoint, sqRadius, lineSites[i], otherSqrDist); 301 | return false; 302 | } 303 | } 304 | return true; 305 | } 306 | 307 | function run(rayList, lineSites) { 308 | var stop = false; 309 | var hasMoved = false; 310 | rayList.iterate(function (currentBucket) { 311 | intersectNextRay(currentBucket.val, currentBucket.next.val); 312 | }); 313 | if (observers['raysIntersectionsComputed']) 314 | observers['raysIntersectionsComputed'](rayList); 315 | var deleteList = []; 316 | rayList.iterate(function (currentBucket) { 317 | var previousRay = currentBucket.prev.val; 318 | var currentRay = currentBucket.val; 319 | var nextRay = currentBucket.next.val; 320 | if (previousRay === nextRay) { 321 | if (observers['last2raysEncountered']) 322 | observers['last2raysEncountered'](currentRay, nextRay); 323 | root = {type: 'root', children: [ 324 | {type: 'spine', origin: previousRay.origin, children: [previousRay.medialRay], internalRay: previousRay}, 325 | currentRay.medialRay 326 | ], origin: currentRay.origin}; 327 | currentRay.destination = previousRay.origin; 328 | previousRay.destination = currentRay.origin; 329 | stop = true; 330 | return true; 331 | } else if (currentRay.aheadPoint && currentRay.behind >= currentRay.ahead && nextRay.ahead >= nextRay.behind) { 332 | var intersectionPoint = currentRay.aheadPoint; 333 | var sqRadius = currentRay.aheadPoint.r * currentRay.aheadPoint.r; 334 | if (!isBestIntersection(currentRay, nextRay, lineSites, intersectionPoint, sqRadius)) 335 | return false; 336 | var newRay = fuseRay(currentRay); 337 | newRay.medialRay = {type: 'spine', children: [currentRay.medialRay, nextRay.medialRay], internalRay: newRay, origin: newRay.origin}; 338 | if (observers['rayFused']) 339 | observers['rayFused'](previousRay, nextRay, currentRay, intersectionPoint, sqRadius, newRay); 340 | deleteList.push([currentBucket, newRay]); 341 | } 342 | return false; 343 | }); 344 | 345 | var rootMedialRays = []; 346 | var rootOrigin; 347 | for (var i = 0; i < deleteList.length; i++) { 348 | hasMoved = true; 349 | var bucket = deleteList[i][0]; 350 | rootMedialRays.push(bucket.val.medialRay); 351 | rootOrigin = bucket.val.aheadPoint; 352 | bucket.val = deleteList[i][1]; 353 | rayList.remove(bucket.next); 354 | } 355 | 356 | if (rayList.isEmpty()) 357 | root = {type: 'root', children: rootMedialRays, origin: rootOrigin}; 358 | 359 | if (observers['stepFinished']) 360 | observers['stepFinished'](); 361 | if (!hasMoved && !stop) 362 | throw new Error('skeleton has not moved'); 363 | return rayList.isEmpty() || stop; 364 | } 365 | 366 | while (!run(rayList, lineSites)) { 367 | } 368 | if (observers['afterProcess']) 369 | observers['afterProcess'](root, polygon, createLinkedList, run, siteList); 370 | return root; 371 | } 372 | 373 | return { 374 | createSkeleton: createSkeleton, 375 | LinearRay: LinearRay, 376 | ParabolicRay: ParabolicRay, 377 | LineSite: LineSite, 378 | ReflexVertexSite: ReflexVertexSite 379 | } 380 | })(); 381 | -------------------------------------------------------------------------------- /medial_axis_test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function pointOnParabola(t, directrix, focus) { 4 | // http://alecmce.com/category/math 5 | const directrixVector = {x: directrix[0].x - directrix[1].x, y: directrix[0].y - directrix[1].y}; 6 | const pt = {x: directrix[0].x * (1 - t) + directrix[1].x * t, y: directrix[0].y * (1 - t) + directrix[1].y * t}; 7 | const ptToFocus = {x: focus.x - pt.x, y: focus.y - pt.y}; 8 | const dist = sqVectorLength(ptToFocus.x, ptToFocus.y) / (2 * (directrixVector.x * ptToFocus.y - directrixVector.y * ptToFocus.x)); 9 | if (isFinite(dist)) 10 | return {x: pt.x - directrixVector.y * dist, y: pt.y + directrixVector.x * dist}; 11 | return null; 12 | } 13 | 14 | function segmentParabola(directrix, focus) { 15 | const pts = []; 16 | for (let i = 0; i <= 1; i += 0.1) { 17 | const point = pointOnParabola(i, directrix, focus); 18 | if (point) 19 | pts.push(point); 20 | } 21 | return pts; 22 | } 23 | 24 | function createSkeletonWithDisplay(polygon, observers) { 25 | if (!observers) 26 | observers = {}; 27 | 28 | function raySitesRepresentation(ray) { 29 | return ray.firstSite.representation() + ray.secondSite.representation(); 30 | } 31 | 32 | medialAxis.ReflexVertexSite.prototype.representation = function () { 33 | return pointArray2path([this.vertex], 2); 34 | }; 35 | 36 | medialAxis.LineSite.prototype.representation = function () { 37 | return polylines2path([this.segment]) + pointArray2path([this.segment[0]], 1); 38 | }; 39 | medialAxis.LinearRay.prototype.representation = function () { 40 | return polylines2path([(this.aheadPoint ? [this.origin, this.aheadPoint] : this.segment)]); 41 | }; 42 | medialAxis.LinearRay.prototype.behindRepresentation = function () { 43 | return polylines2path([ 44 | [this.behindPoint, this.origin] 45 | ]); 46 | }; 47 | medialAxis.ParabolicRay.prototype.representation = function () { 48 | if (this.aheadPoint) { 49 | const p1 = pointProjectedOnSegment(this.aheadPoint, this.edge.segment); 50 | const p2 = pointProjectedOnSegment(this.origin, this.edge.segment); 51 | return polylines2path([segmentParabola([p1, p2], this.vertex)]); 52 | } 53 | return polylines2path([segmentParabola(this.edge.segment, this.vertex)]); 54 | }; 55 | medialAxis.ParabolicRay.prototype.behindRepresentation = function () { 56 | const p1 = pointProjectedOnSegment(this.origin, this.edge.segment); 57 | const p2 = pointProjectedOnSegment(this.behindPoint, this.edge.segment); 58 | return p1 && p2 ? polylines2path([segmentParabola([p1, p2], this.vertex)]) : ''; 59 | }; 60 | 61 | let newSkelRepresentation = ''; 62 | let currentRays = ''; 63 | let skelPoints = []; 64 | let skelRepr = ''; 65 | const root = medialAxis.createSkeleton(polygon, { 66 | initialized: function (rays, reflexPoints) { 67 | if (reflexPoints.length) 68 | svgDisplayTable([ 69 | { 70 | label: 'reflex points', content: pathList2svg([ 71 | {d: polygon2path(polygon)}, 72 | {cssClass: 'red', d: pointArray2path(reflexPoints)} 73 | ]) 74 | } 75 | ]); 76 | let rayRepresentation = ''; 77 | for (let i = 0; i < rays.length; i++) 78 | rayRepresentation += rays[i].representation(); 79 | svgDisplayTable([ 80 | { 81 | label: 'initial rays', content: pathList2svg([ 82 | {d: polygon2path(polygon)}, 83 | {cssClass: 'red', d: rayRepresentation} 84 | ]) 85 | } 86 | ]); 87 | }, 88 | eliminatedRadius: function (currentRay, nextRay, intersectionPoint, sqRadius, nextEdge, otherSqrDist) { 89 | svgDisplayTable([ 90 | { 91 | label: 'eliminated because of radius', content: pathList2svg([ 92 | {cssClass: 'gray', d: polygon2path(polygon)}, 93 | { 94 | cssClass: 'blue', d: currentRay.representation() + raySitesRepresentation(currentRay) 95 | + nextRay.representation() + pointArray2path([intersectionPoint], Math.sqrt(sqRadius)) 96 | }, 97 | { 98 | cssClass: 'red', d: nextEdge.representation() 99 | + pointArray2path([intersectionPoint], Math.sqrt(otherSqrDist)) 100 | } 101 | ]) 102 | } 103 | ]); 104 | }, 105 | rayFused: function (previousRay, nextRay, currentRay, intersectionPoint, sqRadius, newRay) { 106 | newSkelRepresentation += currentRay.representation() + nextRay.behindRepresentation(); 107 | skelPoints.push(intersectionPoint); 108 | svgDisplayTable([ 109 | { 110 | label: 'selected intersection', content: pathList2svg([ 111 | {cssClass: 'gray', d: polygon2path(polygon) + previousRay.representation()}, 112 | {d: previousRay.representation()}, 113 | {cssClass: 'blue', d: nextRay.representation()}, 114 | {cssClass: 'red', d: currentRay.representation() + pointArray2path([intersectionPoint])} 115 | ]) 116 | }, 117 | { 118 | label: 'new ray and corresponding sites', content: pathList2svg([ 119 | {cssClass: 'gray', d: polygon2path(polygon)}, 120 | {cssClass: 'blue', d: currentRay.representation() + nextRay.behindRepresentation()}, 121 | { 122 | cssClass: 'red', d: pointArray2path([intersectionPoint], Math.sqrt(sqRadius)) 123 | + raySitesRepresentation(newRay) + newRay.representation() 124 | } 125 | ]) 126 | }, 127 | { 128 | label: 'intersection', content: pathList2svg([ 129 | {cssClass: 'gray', d: polygon2path(polygon)}, 130 | {cssClass: 'blue', d: currentRay.representation() + nextRay.behindRepresentation()} 131 | ]) 132 | } 133 | ]); 134 | if (observers['rayFused']) 135 | observers['rayFused'](previousRay, nextRay, currentRay, intersectionPoint, sqRadius, newRay); 136 | }, 137 | last2raysEncountered: function (currentRay, nextRay) { 138 | console.log('stop 2'); 139 | newSkelRepresentation += polylines2path([ 140 | [currentRay.origin, nextRay.origin] 141 | ]); 142 | }, 143 | raysIntersectionsComputed: function (rayList) { 144 | currentRays = ''; 145 | skelPoints = []; 146 | rayList.iterate(function (currentBucket) { 147 | const current = currentBucket.val; 148 | currentRays += current.representation(); 149 | }); 150 | }, 151 | stepFinished: function () { 152 | skelRepr += newSkelRepresentation; 153 | svgDisplayTable([ 154 | { 155 | label: 'input step rays, selected intersections in red', content: pathList2svg([ 156 | {cssClass: 'gray', d: polygon2path(polygon)}, 157 | {cssClass: 'blue', d: currentRays}, 158 | {cssClass: 'red', d: pointArray2path(skelPoints, 2)} 159 | ]) 160 | }, 161 | { 162 | label: 'added skeleton parts', content: pathList2svg([ 163 | {cssClass: 'gray', d: polygon2path(polygon)}, 164 | {cssClass: 'blue', d: newSkelRepresentation} 165 | ]) 166 | }, 167 | { 168 | label: 'skeleton after step', content: pathList2svg([ 169 | {cssClass: 'gray', d: polygon2path(polygon)}, 170 | {cssClass: 'blue', d: skelRepr} 171 | ]) 172 | } 173 | ]); 174 | newSkelRepresentation = ''; 175 | }, 176 | afterProcess: observers['afterProcess'] 177 | }); 178 | const medialAxisRepr = displayMedialAxis(root.origin, root); 179 | 180 | function displayMedialAxis(origin, branch) { 181 | if (branch.type === 'limb') 182 | return polylines2path([ 183 | [branch.vertex, origin] 184 | ]); 185 | const newOrigin = branch.origin; 186 | let res = polylines2path([ 187 | [newOrigin, origin] 188 | ]); 189 | for (let i = 0; i < branch.children.length; i++) 190 | if (branch.children[i] != null) 191 | res += displayMedialAxis(newOrigin, branch.children[i]); 192 | return res; 193 | } 194 | 195 | svgDisplayTable([ 196 | { 197 | label: 'medial axis', content: pathList2svg([ 198 | {cssClass: 'gray', d: polygon2path(polygon)}, 199 | {cssClass: 'blue', d: medialAxisRepr} 200 | ]) 201 | } 202 | ]); 203 | } 204 | 205 | function extractDCELAfterProcess(root, polygon, createLinkedList, run, siteList) { 206 | function findRayForPoint(point, site, forbidden) { 207 | for (var i = 0; i < site.rays.length; i++) { 208 | var ray = site.rays[i]; 209 | if (forbidden.indexOf(ray) !== -1) 210 | continue; 211 | if (ray.origin === point) 212 | return {ray: ray, direction: "forwards", nextPoint: ray.destination}; 213 | if (ray.destination === point) 214 | return {ray: ray, direction: "backwards", nextPoint: ray.origin}; 215 | } 216 | for (i = 0; i < site.backRays.length; i++) { 217 | ray = site.backRays[i]; 218 | if (forbidden.indexOf(ray) !== -1) 219 | continue; 220 | if (ray.origin === point) 221 | return {ray: ray, direction: "forwards", nextPoint: ray.destination}; 222 | if (ray.destination === point) 223 | return {ray: ray, direction: "backwards", nextPoint: ray.origin}; 224 | } 225 | return null; 226 | } 227 | 228 | const vVerticesMap = {}; 229 | 230 | function vVertexForPoint(point) { 231 | const key = 'p' + point.x + '|' + point.y; 232 | let cached = vVerticesMap[key]; 233 | if (cached == null) { 234 | cached = {point: point, outEdges: []}; 235 | vVerticesMap[key] = cached; 236 | } 237 | return cached; 238 | } 239 | 240 | function createFace(site, polygon) { 241 | function createVEdge(vvertex1, vvertex2, ray, face) { 242 | const vEdge = { 243 | v1: vvertex1, 244 | v2: vvertex2, 245 | ray: ray, 246 | face: face 247 | }; 248 | for (let i = 0; i < vvertex2.outEdges.length; i++) 249 | if (vvertex2.outEdges[i].v1 === vvertex2 && vvertex2.outEdges[i].v2 === vvertex1) { 250 | vEdge.twin = vvertex2.outEdges[i]; 251 | vvertex2.outEdges[i].twin = vEdge; 252 | } 253 | vvertex1.outEdges.push(vEdge); 254 | return vEdge; 255 | } 256 | 257 | const finalPoint = site instanceof medialAxis.LineSite ? site.segment[0] : site.vertex; 258 | let point = site instanceof medialAxis.LineSite ? site.segment[1] : site.vertex; 259 | const forbidden = []; 260 | const points = [point]; 261 | const vertices = [vVertexForPoint(point)]; 262 | const edges = []; 263 | var face = {site: site, edges: edges}; 264 | do { 265 | const result = findRayForPoint(point, site, forbidden); 266 | forbidden.push(result.ray); 267 | point = result.nextPoint; 268 | if (result.nextPoint == null) 269 | console.log(result); 270 | points.push(point); 271 | vertices.push(vVertexForPoint(point)); 272 | const edge = createVEdge(vertices[vertices.length - 2], vertices[vertices.length - 1], result.ray, face); 273 | if (edges.length) 274 | edges[edges.length - 1].next = edge; 275 | edges.push(edge); 276 | } while (point !== finalPoint); 277 | console.log(points); 278 | svgDisplayTable([ 279 | { 280 | label: 'face ' + name, content: pathList2svg([ 281 | {cssClass: 'gray', d: polygon2path(polygon)}, 282 | {cssClass: 'red', d: site.representation()}, 283 | {cssClass: 'green', d: polylines2path([points])} 284 | ]) 285 | } 286 | ]); 287 | console.log(face); 288 | return face; 289 | } 290 | 291 | const faces = []; 292 | siteList.iterate(function (bucket) { 293 | faces.push(createFace(bucket.val, polygon)); 294 | }); 295 | for (var i = 0; i < faces.length; i++) { 296 | var face = faces[i]; 297 | for (let j = 0; j < face.edges.length; j++) { 298 | var edge = face.edges[j]; 299 | if (edge.twin == null) 300 | svgDisplayTable([ 301 | { 302 | label: 'no twin', content: pathList2svg([ 303 | {cssClass: 'gray', d: polygon2path(polygon)}, 304 | {cssClass: 'green', d: face.site.representation()}, 305 | { 306 | cssClass: 'red', d: polylines2path([ 307 | [edge.v1.point, edge.v2.point] 308 | ]) 309 | } 310 | ]) 311 | } 312 | ]); 313 | } 314 | } 315 | 316 | } 317 | 318 | test('parabola', function () { 319 | const focus = p(50, 50); 320 | const directrix = [p(5, 60), p(60, 5)]; 321 | 322 | svgDisplayTable([ 323 | { 324 | label: 'focus directrix', content: pathList2svg([ 325 | {d: polylines2path([directrix]) + pointArray2path([focus]) + polylines2path([segmentParabola(directrix, focus)])} 326 | ]) 327 | } 328 | ]); 329 | }); 330 | 331 | test('PLL solver non-parallel', function () { 332 | const s1 = [p(10, 10), p(100, 10)]; 333 | const s2 = [p(100, 10), p(100, 140)]; 334 | const vertex = p(40, 100); 335 | 336 | const result = solver.solveEquations(new solver.EquationSystemCreator().addSegment(s1).addSegment(s2).addVertex(vertex), function () { 337 | return true; 338 | }); 339 | let resultsDisplay = ''; 340 | for (let i = 0; i < result.length; i++) { 341 | const obj = result[i]; 342 | resultsDisplay += pointArray2path([obj], obj.r); 343 | } 344 | svgDisplayTable([ 345 | { 346 | label: 'focus directrix', content: pathList2svg([ 347 | {cssClass: 'blue', d: polylines2path([s1, s2]) + pointArray2path([vertex])}, 348 | { 349 | cssClass: 'red', d: pointArray2path(result, 2) + resultsDisplay 350 | + polylines2path([segmentParabola(s2, vertex)]) 351 | + polylines2path([segmentParabola(s1, vertex)]) 352 | } 353 | ]) 354 | } 355 | ]); 356 | }); 357 | 358 | test('PLL solver perpendicular', function () { 359 | let s1 = [p(10, 10), p(100, 10)]; 360 | let s2 = [p(100, 10), p(100, 140)]; 361 | let vertex = p(10, 10); 362 | let result = solver.solveEquations(new solver.EquationSystemCreator().addSegment(s1).addSegment(s2).addVertex(vertex)); 363 | let resultsDisplay = ''; 364 | for (var i = 0; i < result.length; i++) 365 | resultsDisplay += pointArray2path([result[i]], result[i].r); 366 | svgDisplayTable([ 367 | { 368 | label: 'focus directrix', content: pathList2svg([ 369 | {cssClass: 'blue', d: polylines2path([s1, s2]) + pointArray2path([vertex])}, 370 | { 371 | cssClass: 'red', d: pointArray2path(result, 2) + resultsDisplay 372 | + polylines2path([segmentParabola(s2, vertex)]) 373 | + polylines2path([segmentParabola(s1, vertex)]) 374 | } 375 | ]) 376 | } 377 | ]); 378 | 379 | s1 = [p(20, 150), p(10, 140)]; 380 | s2 = [p(10, 140), p(10, 95)]; 381 | vertex = p(10, 95); 382 | 383 | result = solver.solveEquations(new solver.EquationSystemCreator().addSegment(s1).addSegment(s2).addVertex(vertex)); 384 | resultsDisplay = ''; 385 | for (i = 0; i < result.length; i++) 386 | resultsDisplay += pointArray2path([result[i]], result[i].r); 387 | svgDisplayTable([ 388 | { 389 | label: 'focus directrix', content: pathList2svg([ 390 | {cssClass: 'blue', d: polylines2path([s1, s2]) + pointArray2path([vertex])}, 391 | { 392 | cssClass: 'red', d: pointArray2path(result, 2) + resultsDisplay 393 | + polylines2path([segmentParabola(s2, vertex)]) 394 | + polylines2path([segmentParabola(s1, vertex)]) 395 | } 396 | ]) 397 | } 398 | ]); 399 | }); 400 | 401 | test('PLL solver parallel', function () { 402 | const s1 = [p(10, 10), p(10, 140)]; 403 | const s2 = [p(100, 140), p(100, 10)]; 404 | const vertex = p(25, 100); 405 | const result = solver.solveEquations(new solver.EquationSystemCreator().addSegment(s1).addSegment(s2).addVertex(vertex)); 406 | let resultsDisplay = ''; 407 | for (let i = 0; i < result.length; i++) 408 | resultsDisplay += pointArray2path([result[i]], result[i].r); 409 | svgDisplayTable([ 410 | { 411 | label: 'edges and circle', content: pathList2svg([ 412 | {cssClass: 'blue', d: polylines2path([s1, s2]) + pointArray2path([vertex])}, 413 | { 414 | cssClass: 'red', 415 | d: pointArray2path(result) + resultsDisplay + polylines2path([segmentParabola(s2, vertex)]) + polylines2path([segmentParabola(s1, vertex)]) 416 | } 417 | ]) 418 | } 419 | ]); 420 | deepEqual(result, [ 421 | {x: 55, y: 133.54101966249684, r: -45}, 422 | {x: 55, y: 66.45898033750316, r: -45} 423 | ]); 424 | }); 425 | 426 | test('PLL solver point on side', function () { 427 | const s1 = [p(10, 10), p(10, 140)]; 428 | const s2 = [p(100, 140), p(100, 10)]; 429 | const vertex = p(10, 50); 430 | const result = solver.solveEquations(new solver.EquationSystemCreator().addSegment(s1).addSegment(s2).addVertex(vertex)); 431 | let resultsDisplay = ''; 432 | for (let i = 0; i < result.length; i++) { 433 | const obj = result[i]; 434 | resultsDisplay += pointArray2path([obj], obj.r); 435 | } 436 | svgDisplayTable([ 437 | { 438 | label: 'edges and circle', content: pathList2svg([ 439 | {cssClass: 'blue', d: polylines2path([s1, s2]) + pointArray2path([vertex])}, 440 | { 441 | cssClass: 'red', 442 | d: pointArray2path(result) + resultsDisplay + polylines2path([segmentParabola(s2, vertex)]) + polylines2path([segmentParabola(s1, vertex)]) 443 | } 444 | ]) 445 | } 446 | ]); 447 | deepEqual(result, [ 448 | {x: 55, y: 50, r: -45} 449 | ]) 450 | }); 451 | 452 | test('PLL solver unstable', function () { 453 | const factor = 1; 454 | const s1 = [p(30 / factor, 100 / factor), p(10 / factor, 10 / factor)]; 455 | const s2 = [p(100 / factor, 10 / factor), p(50 / factor, 65 / factor)]; 456 | const vertex = p(50 / factor, 65 / factor); 457 | const result = solver.solveEquations(new solver.EquationSystemCreator().addSegment(s1).addSegment(s2).addVertex(vertex)); 458 | let resultsDisplay = ''; 459 | for (let i = 0; i < result.length; i++) { 460 | const obj = result[i]; 461 | resultsDisplay += pointArray2path([obj], obj.r); 462 | } 463 | deepEqual(result.length, 1); 464 | svgDisplayTable([ 465 | { 466 | label: 'edges and circle', content: pathList2svg([ 467 | {cssClass: 'blue', d: polylines2path([s1, s2]) + pointArray2path([vertex])}, 468 | { 469 | cssClass: 'red', 470 | d: pointArray2path(result) + resultsDisplay + polylines2path([segmentParabola(s1, vertex)]) + polylines2path([segmentParabola(s1, vertex)]) 471 | } 472 | ]) 473 | } 474 | ]); 475 | }); 476 | 477 | test('LLL solver', function () { 478 | const s1 = [p(10, 10), p(100, 10)]; 479 | const s2 = [p(100, 10), p(100, 140)]; 480 | const s3 = [p(100, 140), p(10, 140)]; 481 | 482 | const result = solver.solveEquations(new solver.EquationSystemCreator().addSegment(s1).addSegment(s2).addSegment(s3)); 483 | svgDisplayTable([ 484 | { 485 | label: 'edges and circle', content: pathList2svg([ 486 | {cssClass: 'blue', d: polylines2path([s1, s2, s3])}, 487 | {cssClass: 'red', d: pointArray2path(result, result[0].r)} 488 | ]) 489 | } 490 | ]); 491 | deepEqual(result, [ 492 | {x: 35, y: 75, r: 65} 493 | ]); 494 | }); 495 | 496 | test('LLL solver with flat vertex', function () { 497 | const flatVertex = p(50, 10); 498 | const v1 = p(10, 10); 499 | const v2 = p(100, 10); 500 | const v3 = p(100, 140); 501 | const s1 = [v1, flatVertex]; 502 | const s2 = [flatVertex, v2]; 503 | const s3 = [v2, v3]; 504 | const result = solver.solveEquations(new solver.EquationSystemCreator().addSegment(s1).addSegment(s2).addSegment(s3)); 505 | svgDisplayTable([ 506 | { 507 | label: 'edges and circle', content: pathList2svg([ 508 | {cssClass: 'blue', d: polylines2path([s1, s2, s3]) + pointArray2path([v1, flatVertex, v2, v3])}, 509 | {cssClass: 'red', d: pointArray2path(result, result[0].r)} 510 | ]) 511 | } 512 | ]); 513 | deepEqual(result, [ 514 | {x: 50, y: 60, r: 50} 515 | ]); 516 | }); 517 | test('medial axis1, 3 reflex points', function () { 518 | createSkeletonWithDisplay([ 519 | p(10, 10), 520 | p(100, 10), 521 | p(50, 65), 522 | p(100, 140), 523 | p(40, 100), 524 | p(10, 140), 525 | p(20, 100) 526 | ]); 527 | }); 528 | test('medial axis2, 1 reflex point', function () { 529 | createSkeletonWithDisplay([ 530 | p(10, 10), 531 | p(100, 10), 532 | p(100, 140), 533 | p(40, 100), 534 | p(10, 140) 535 | ]); 536 | }); 537 | 538 | 539 | test('medial axis3, convex polygon', function () { 540 | createSkeletonWithDisplay([ 541 | p(10, 10), 542 | p(100, 10), 543 | p(150, 60), 544 | p(150, 100), 545 | p(100, 140), 546 | p(20, 150), 547 | p(10, 140) 548 | ]); 549 | }); 550 | 551 | test('medial axis4, rectangle', function () { 552 | createSkeletonWithDisplay([ 553 | p(10, 10), 554 | p(100, 10), 555 | p(100, 140), 556 | p(10, 140) 557 | ]); 558 | }); 559 | 560 | test('medial axis5, convex polygon', function () { 561 | 562 | const p2 = [ 563 | [326, 361], 564 | [361, 300], 565 | [397, 258], 566 | [457, 225], 567 | [490, 235], 568 | [522, 255], 569 | [564, 308], 570 | [606, 373], 571 | [575, 426], 572 | [540, 464], 573 | [465, 503], 574 | [439, 510], 575 | [367, 475], 576 | [348, 438] 577 | ]; 578 | const polygon2 = []; 579 | for (let i = 0; i < p2.length; i++) { 580 | polygon2.push(p((p2[i][0] - 326) / 2, (p2[i][1] - 220) / 2)); 581 | } 582 | createSkeletonWithDisplay(polygon2); 583 | }); 584 | 585 | test('medial axis6, rectangle with flat vertices', function () { 586 | createSkeletonWithDisplay([ 587 | p(10, 10), 588 | p(100, 10), 589 | p(100, 20), 590 | p(100, 30), 591 | p(100, 40), 592 | p(100, 50), 593 | p(100, 60), 594 | p(100, 100), 595 | p(100, 140), 596 | p(10, 140), 597 | p(10, 60), 598 | p(10, 50), 599 | p(10, 40), 600 | p(10, 30), 601 | p(10, 20) 602 | ]); 603 | }); 604 | 605 | test('snake', function () { 606 | const poly = [p(458, 39), 607 | p(458, 39), 608 | p(395, 46), 609 | p(308, 76), 610 | p(141, 64), 611 | p(100, 80), 612 | p(234, 89), 613 | p(343, 99), 614 | p(400, 115), 615 | p(405, 66), 616 | p(423, 50), 617 | p(419, 135), 618 | p(378, 205), 619 | p(337, 201), 620 | p(72, 185), 621 | p(73, 205), 622 | p(306, 213), 623 | p(375, 226), 624 | p(412, 261), 625 | p(343, 326), 626 | p(233, 330), 627 | p(74, 344), 628 | p(57, 316), 629 | p(133, 290), 630 | p(290, 291), 631 | p(366, 232), 632 | p(296, 222), 633 | p(172, 246), 634 | p(41, 214), 635 | p(35, 178), 636 | p(197, 171), 637 | p(350, 194), 638 | p(398, 140), 639 | p(326, 117), 640 | p(155, 92), 641 | p(28, 138), 642 | p(22, 71), 643 | p(113, 52), 644 | p(277, 64), 645 | p(326, 35), 646 | p(391, 25), 647 | p(456, 23), 648 | p(498, 15) 649 | ]; 650 | for (let i = 0; i < poly.length; i++) { 651 | const point = poly[i]; 652 | point.x /= 2.5; 653 | point.y /= 2.5; 654 | } 655 | 656 | createSkeletonWithDisplay(poly, { 657 | afterProcess: extractDCELAfterProcess 658 | }); 659 | }); 660 | 661 | test('cut square with hole', function () { 662 | 663 | const centerX = 75; 664 | const centerY = 75; 665 | 666 | function cp(xdiff, ydiff) { 667 | return p(centerX + xdiff, centerY + ydiff); 668 | } 669 | 670 | const outerSide = 130; 671 | const halfOuterSide = outerSide / 2; 672 | const wallThickness = 50; 673 | 674 | const inserted1 = cp(halfOuterSide, 0); 675 | const inserted2 = cp(halfOuterSide - wallThickness, 0); 676 | const inserted3 = cp(halfOuterSide - wallThickness, 0); 677 | const inserted4 = cp(halfOuterSide, 0); 678 | 679 | const polygon = [ 680 | cp(-halfOuterSide, -halfOuterSide), 681 | cp(-halfOuterSide, halfOuterSide), 682 | cp(halfOuterSide, halfOuterSide), 683 | inserted1, 684 | inserted2, 685 | cp(halfOuterSide - wallThickness, halfOuterSide - wallThickness), 686 | cp(-halfOuterSide + wallThickness, halfOuterSide - wallThickness), 687 | cp(-halfOuterSide + wallThickness, -halfOuterSide + wallThickness), 688 | cp(halfOuterSide - wallThickness, -halfOuterSide + wallThickness), 689 | inserted3, 690 | inserted4, 691 | cp(halfOuterSide, -halfOuterSide) 692 | ]; 693 | const root = createSkeletonWithDisplay(polygon, {afterProcess: extractDCELAfterProcess}); 694 | 695 | }); 696 | -------------------------------------------------------------------------------- /polygons.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | //https://github.com/substack/point-in-polygon/blob/master/index.js 4 | // http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html 5 | //to be replaced by a vertex radius filter when there is a kd map. 6 | function pointInPolygon(point, polygon) { 7 | var x = point.x, y = point.y; 8 | var inside = false; 9 | for (var i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { 10 | var xi = polygon[i].x, yi = polygon[i].y; 11 | var xj = polygon[j].x, yj = polygon[j].y; 12 | 13 | var intersect = ((yi > y) != (yj > y)) 14 | && (x < (xj - xi) * (y - yi) / (yj - yi) + xi); 15 | if (intersect) inside = !inside; 16 | } 17 | return inside; 18 | } 19 | 20 | function commonVertex(segment1, segment2) { 21 | if(pointEquals(segment1[0], segment2[0]) || pointEquals(segment1[0], segment2[1])) 22 | return segment1[0]; 23 | if (pointEquals(segment1[1], segment2[0]) || pointEquals(segment1[1], segment2[1])) 24 | return segment1[1]; 25 | return null 26 | } 27 | 28 | function vectLength(x, y) { 29 | return Math.sqrt(sqVectorLength(x, y)); 30 | } 31 | 32 | function sqr(val) { 33 | return val * val; 34 | } 35 | 36 | function sqVectorLength(x, y) { 37 | return sqr(x) + sqr(y); 38 | } 39 | 40 | function segLength(segment) { 41 | return vectLength(segment[0].x - segment[1].x, segment[0].y - segment[1].y); 42 | } 43 | 44 | function sqSegLength(segment) { 45 | return sqVectorLength(segment[0].x - segment[1].x, segment[0].y - segment[1].y); 46 | } 47 | 48 | function unitVector(inVector) { 49 | var len = vectLength(inVector.x, inVector.y); 50 | return {x: inVector.x / len, y: inVector.y / len}; 51 | } 52 | 53 | function bisectorVector(v1, v2) { 54 | var l1 = vectLength(v1.x, v1.y); 55 | var l2 = vectLength(v2.x, v2.y); 56 | var x = l2 * v1.x + l1 * v2.x; 57 | var y = l2 * v1.y + l1 * v2.y; 58 | if (x == 0 && y == 0) 59 | return {x: v1.y, y: -v1.x}; 60 | return {x: x, y: y}; 61 | } 62 | 63 | function segmentToVector(segment) { 64 | return {x: segment[1].x - segment[0].x, y: segment[1].y - segment[0].y}; 65 | } 66 | 67 | function perpendicularPoint(vertex, segment) { 68 | var vector = unitVector(segmentToVector(segment)); 69 | //noinspection JSSuspiciousNameCombination 70 | var v = {x: -vector.y, y: vector.x}; 71 | return {x: vertex.x + v.x * 100, y: vertex.y + v.y * 100}; 72 | } 73 | 74 | function bisectorVectorFromSegments(s1, s2) { 75 | return bisectorVector({x: s1[1].x - s1[0].x, y: s1[1].y - s1[0].y}, 76 | {x: s2[1].x - s2[0].x, y: s2[1].y - s2[0].y}); 77 | } 78 | 79 | function pointEquals(p1, p2) { 80 | return p1.x == p2.x && p1.y == p2.y; 81 | } 82 | 83 | function pointProjectedOnSegment(point, segment) { 84 | function dist2(v, w) { 85 | return sqr(v.x - w.x) + sqr(v.y - w.y) 86 | } 87 | 88 | var v = segment[0]; 89 | var w = segment[1]; 90 | var l2 = dist2(v, w); 91 | if (l2 == 0) return dist2(point, v); 92 | var t = ((point.x - v.x) * (w.x - v.x) + (point.y - v.y) * (w.y - v.y)) / l2; 93 | if (t >= 0 && t <= 1) 94 | return { x: v.x + t * (w.x - v.x), y: v.y + t * (w.y - v.y) }; 95 | return null; 96 | } 97 | 98 | function distToSegmentSquared(point, segment) { 99 | function dist2(v, w) { 100 | return sqr(v.x - w.x) + sqr(v.y - w.y) 101 | } 102 | 103 | var v = segment[0]; 104 | var w = segment[1]; 105 | var l2 = dist2(v, w); 106 | if (l2 == 0) return dist2(point, v); 107 | var t = ((point.x - v.x) * (w.x - v.x) + (point.y - v.y) * (w.y - v.y)) / l2; 108 | if (t < 0) return dist2(point, v); 109 | if (t > 1) return dist2(point, w); 110 | return dist2(point, { x: v.x + t * (w.x - v.x), 111 | y: v.y + t * (w.y - v.y) }); 112 | } 113 | 114 | function signedArea(polygon) { 115 | var area = 0; 116 | for (var i = 0; i < polygon.length; i++) { 117 | var next = polygon[(i + 1) % polygon.length]; 118 | area += polygon[i].x * next.y - polygon[i].y * next.x; 119 | } 120 | return area / 2; 121 | } 122 | 123 | //http://www.kevlindev.com/gui/math/intersection/Intersection.js 124 | function intersectionAbscissa(seg1, seg2) { 125 | var a1 = seg1[0], a2 = seg1[1], b1 = seg2[0], b2 = seg2[1]; 126 | var divisor = (b2.y - b1.y) * (a2.x - a1.x) - (b2.x - b1.x) * (a2.y - a1.y); 127 | var ua_t = (b2.x - b1.x) * (a1.y - b1.y) - (b2.y - b1.y) * (a1.x - b1.x); 128 | var ub_t = (a2.x - a1.x) * (a1.y - b1.y) - (a2.y - a1.y) * (a1.x - b1.x); 129 | var ua = ua_t / divisor; 130 | var ub = ub_t / divisor; 131 | return [ua, ub]; 132 | } 133 | 134 | //http://www.kevlindev.com/gui/math/intersection/Intersection.js 135 | function intersectionSegments(seg1, seg2, allowOutside) { 136 | //order the segments so that the results are always the same 137 | if (comparePoint(seg1[0], seg2[0]) > 0) { 138 | var tmp = seg1; 139 | seg1 = seg2; 140 | seg2 = tmp; 141 | } 142 | var a1 = seg1[0], a2 = seg1[1]; 143 | var u = intersectionAbscissa(seg1, seg2); 144 | var ua = u[0]; 145 | var ub = u[1]; 146 | if (allowOutside && isFinite(ua) && isFinite(ub) || (0 <= ua && ua <= 1 && 0 <= ub && ub <= 1)) 147 | return {x: a1.x + ua * (a2.x - a1.x), y: a1.y + ua * (a2.y - a1.y)}; 148 | return null; 149 | } 150 | 151 | function createIntersections(segments) { 152 | var intersections = []; 153 | for (var i = 0; i < segments.length; i++) 154 | if (comparePoint(segments[i][0], segments[i][1]) > 0) 155 | segments[i].reverse(); 156 | for (i = 0; i < segments.length; i++) { 157 | for (var j = i + 1; j < segments.length; j++) { 158 | var intersection = intersectionSegments(segments[i], segments[j]); 159 | if (intersection) 160 | intersections.push([intersection, [segments[i], segments[j]]]); 161 | } 162 | } 163 | return intersections; 164 | } 165 | 166 | function comparePoint(p1, p2) { 167 | if (p1.x < p2.x) 168 | return -1; 169 | if (p1.x == p2.x) 170 | return p1.y - p2.y; 171 | return 1; 172 | } 173 | 174 | function eventsForSegment(segment) { 175 | var s = [segment[0], segment[1]]; 176 | if (comparePoint(segment[0], segment[1]) > 0) 177 | s.reverse(); 178 | return [ 179 | {type: 'left', point: s[0], segment: s, action: function (scanBeam, eventQueue) { 180 | scanBeam.leftPoint(s[0], s, eventQueue); 181 | }}, 182 | {type: 'right', point: s[1], segment: s, action: function (scanBeam, eventQueue) { 183 | scanBeam.rightPoint(s[1], s, eventQueue); 184 | }} 185 | ]; 186 | } 187 | 188 | function initialPopulationOfQueue(segments) { 189 | var initialEvents = []; 190 | for (var i = 0; i < segments.length; i++) { 191 | var events = eventsForSegment(segments[i]); 192 | initialEvents.push(events[0]); 193 | initialEvents.push(events[1]); 194 | } 195 | return createMockEventQueue(initialEvents); 196 | } 197 | 198 | function bentleyOttmann(segments) { 199 | var queue = initialPopulationOfQueue(segments); 200 | var scanBeam = createMockScanBeam(); 201 | while (!queue.isEmpty()) 202 | queue.fetchFirst().action(scanBeam, queue); 203 | return scanBeam.getResult(); 204 | } 205 | 206 | function createMockEventQueue(initialEvents) { 207 | var queue = []; 208 | 209 | function segmentEqual(s1, s2) { 210 | return s1 == s2 211 | || pointEquals(s1[0], s2[0]) && pointEquals(s1[1], s2[1]) 212 | || pointEquals(s1[1], s2[0]) && pointEquals(s1[0], s2[1]); 213 | } 214 | 215 | function eventExists(event) { 216 | for (var i = 0; i < queue.length; i++) 217 | if (queue[i].type == 'intersection' 218 | && (segmentEqual(event.segments[0], queue[i].segments[0]) && segmentEqual(event.segments[1], queue[i].segments[1]) 219 | || segmentEqual(event.segments[1], queue[i].segments[0]) && segmentEqual(event.segments[0], queue[i].segments[1]))) 220 | return true; 221 | return false 222 | } 223 | 224 | function compareEvents(e1, e2) { 225 | return comparePoint(e1.point, e2.point); 226 | } 227 | 228 | function sort() { 229 | queue.sort(compareEvents); 230 | } 231 | 232 | Array.prototype.push.apply(queue, initialEvents); 233 | sort(); 234 | return { 235 | pushIntersectionEvent: function (event) { 236 | if (eventExists(event)) 237 | return; 238 | queue.push(event); 239 | sort(); 240 | }, 241 | fetchFirst: function () { 242 | return queue.shift(); 243 | }, 244 | isEmpty: function () { 245 | return queue.length == 0; 246 | }, 247 | dumpQueue: function () { 248 | var res = []; 249 | for (var i = 0; i < queue.length; i++) 250 | res.push(queue[i]); 251 | return res; 252 | } 253 | } 254 | } 255 | 256 | function createMockScanBeam() { 257 | var beam = []; 258 | var result = []; 259 | 260 | function findIndex(point) { 261 | function segmentHeight(segment, x) { 262 | if (segment[0].x > segment[1].x) 263 | throw 'backwards segment'; 264 | var t = (x - segment[0].x) / (segment[1].x - segment[0].x); 265 | return segment[0].y + t * (segment[1].y - segment[0].y); 266 | } 267 | 268 | for (var i = 0; i < beam.length; i++) { 269 | var segmentH = segmentHeight(beam[i], point.x); 270 | if (segmentH > point.y) 271 | return i; 272 | } 273 | return beam.length; 274 | } 275 | 276 | function findSegmentIndex(segment) { 277 | for (var i = 0; i < beam.length; i++) 278 | if (beam[i] == segment) 279 | return i; 280 | throw 'oops segment not in beam???'; 281 | } 282 | 283 | function swap(i1, i2) { 284 | var tmp = beam[i1]; 285 | beam[i1] = beam[i2]; 286 | beam[i2] = tmp; 287 | } 288 | 289 | function pushIfIntersectsOnRight(currentPoint, previousSegment, nextSegment, eventQueue) { 290 | var intersection = intersectionSegments(nextSegment, previousSegment); 291 | if (intersection != null && comparePoint(currentPoint, intersection) < 0) { 292 | var segments = [previousSegment, nextSegment]; 293 | eventQueue.pushIntersectionEvent({type: 'intersection', point: intersection, segments: segments, 294 | action: function (scanBeam, eventQueue) { 295 | scanBeam.intersectionPoint(intersection, segments, eventQueue); 296 | }}); 297 | } 298 | } 299 | 300 | return { 301 | leftPoint: function (point, segment, eventQueue) { 302 | var insertionIndex = findIndex(point); 303 | if (insertionIndex > 0) 304 | pushIfIntersectsOnRight(point, beam[insertionIndex - 1], segment, eventQueue); 305 | if (insertionIndex < beam.length) 306 | pushIfIntersectsOnRight(point, segment, beam[insertionIndex], eventQueue); 307 | beam.splice(insertionIndex, 0, segment); 308 | }, 309 | rightPoint: function (point, segment, eventQueue) { 310 | var segmentIndex = findSegmentIndex(segment); 311 | if (segmentIndex > 0 && segmentIndex < beam.length - 1) 312 | pushIfIntersectsOnRight(point, beam[segmentIndex - 1], beam[segmentIndex + 1], eventQueue); 313 | beam.splice(segmentIndex, 1); 314 | }, 315 | intersectionPoint: function (point, segments, eventQueue) { 316 | result.push([point, segments]); 317 | var before = findSegmentIndex(segments[0]); 318 | var after = findSegmentIndex(segments[1]); 319 | swap(before, after); 320 | if (before > 0) 321 | pushIfIntersectsOnRight(point, beam[before - 1], segments[1], eventQueue); 322 | if (after < beam.length - 1) 323 | pushIfIntersectsOnRight(point, segments[0], beam[after + 1], eventQueue); 324 | }, 325 | getResult: function () { 326 | return result; 327 | }, 328 | dumpBeam: function () { 329 | var res = []; 330 | for (var i = 0; i < beam.length; i++) 331 | res.push(beam[i]); 332 | return res; 333 | } 334 | }; 335 | } 336 | -------------------------------------------------------------------------------- /qunit-1.12.0.css: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit v1.12.0 - A JavaScript Unit Testing Framework 3 | * 4 | * http://qunitjs.com 5 | * 6 | * Copyright 2012 jQuery Foundation and other contributors 7 | * Released under the MIT license. 8 | * http://jquery.org/license 9 | */ 10 | 11 | /** Font Family and Sizes */ 12 | 13 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { 14 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; 15 | } 16 | 17 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } 18 | #qunit-tests { font-size: smaller; } 19 | 20 | 21 | /** Resets */ 22 | 23 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter { 24 | margin: 0; 25 | padding: 0; 26 | } 27 | 28 | 29 | /** Header */ 30 | 31 | #qunit-header { 32 | padding: 0.5em 0 0.5em 1em; 33 | 34 | color: #8699a4; 35 | background-color: #0d3349; 36 | 37 | font-size: 1.5em; 38 | line-height: 1em; 39 | font-weight: normal; 40 | 41 | border-radius: 5px 5px 0 0; 42 | -moz-border-radius: 5px 5px 0 0; 43 | -webkit-border-top-right-radius: 5px; 44 | -webkit-border-top-left-radius: 5px; 45 | } 46 | 47 | #qunit-header a { 48 | text-decoration: none; 49 | color: #c2ccd1; 50 | } 51 | 52 | #qunit-header a:hover, 53 | #qunit-header a:focus { 54 | color: #fff; 55 | } 56 | 57 | #qunit-testrunner-toolbar label { 58 | display: inline-block; 59 | padding: 0 .5em 0 .1em; 60 | } 61 | 62 | #qunit-banner { 63 | height: 5px; 64 | } 65 | 66 | #qunit-testrunner-toolbar { 67 | padding: 0.5em 0 0.5em 2em; 68 | color: #5E740B; 69 | background-color: #eee; 70 | overflow: hidden; 71 | } 72 | 73 | #qunit-userAgent { 74 | padding: 0.5em 0 0.5em 2.5em; 75 | background-color: #2b81af; 76 | color: #fff; 77 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 78 | } 79 | 80 | #qunit-modulefilter-container { 81 | float: right; 82 | } 83 | 84 | /** Tests: Pass/Fail */ 85 | 86 | #qunit-tests { 87 | list-style-position: inside; 88 | } 89 | 90 | #qunit-tests li { 91 | padding: 0.4em 0.5em 0.4em 2.5em; 92 | border-bottom: 1px solid #fff; 93 | list-style-position: inside; 94 | } 95 | 96 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { 97 | display: none; 98 | } 99 | 100 | #qunit-tests li strong { 101 | cursor: pointer; 102 | } 103 | 104 | #qunit-tests li a { 105 | padding: 0.5em; 106 | color: #c2ccd1; 107 | text-decoration: none; 108 | } 109 | #qunit-tests li a:hover, 110 | #qunit-tests li a:focus { 111 | color: #000; 112 | } 113 | 114 | #qunit-tests li .runtime { 115 | float: right; 116 | font-size: smaller; 117 | } 118 | 119 | .qunit-assert-list { 120 | margin-top: 0.5em; 121 | padding: 0.5em; 122 | 123 | background-color: #fff; 124 | 125 | border-radius: 5px; 126 | -moz-border-radius: 5px; 127 | -webkit-border-radius: 5px; 128 | } 129 | 130 | .qunit-collapsed { 131 | display: none; 132 | } 133 | 134 | #qunit-tests table { 135 | border-collapse: collapse; 136 | margin-top: .2em; 137 | } 138 | 139 | #qunit-tests th { 140 | text-align: right; 141 | vertical-align: top; 142 | padding: 0 .5em 0 0; 143 | } 144 | 145 | #qunit-tests td { 146 | vertical-align: top; 147 | } 148 | 149 | #qunit-tests pre { 150 | margin: 0; 151 | white-space: pre-wrap; 152 | word-wrap: break-word; 153 | } 154 | 155 | #qunit-tests del { 156 | background-color: #e0f2be; 157 | color: #374e0c; 158 | text-decoration: none; 159 | } 160 | 161 | #qunit-tests ins { 162 | background-color: #ffcaca; 163 | color: #500; 164 | text-decoration: none; 165 | } 166 | 167 | /*** Test Counts */ 168 | 169 | #qunit-tests b.counts { color: black; } 170 | #qunit-tests b.passed { color: #5E740B; } 171 | #qunit-tests b.failed { color: #710909; } 172 | 173 | #qunit-tests li li { 174 | padding: 5px; 175 | background-color: #fff; 176 | border-bottom: none; 177 | list-style-position: inside; 178 | } 179 | 180 | /*** Passing Styles */ 181 | 182 | #qunit-tests li li.pass { 183 | color: #3c510c; 184 | background-color: #fff; 185 | border-left: 10px solid #C6E746; 186 | } 187 | 188 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } 189 | #qunit-tests .pass .test-name { color: #366097; } 190 | 191 | #qunit-tests .pass .test-actual, 192 | #qunit-tests .pass .test-expected { color: #999999; } 193 | 194 | #qunit-banner.qunit-pass { background-color: #C6E746; } 195 | 196 | /*** Failing Styles */ 197 | 198 | #qunit-tests li li.fail { 199 | color: #710909; 200 | background-color: #fff; 201 | border-left: 10px solid #EE5757; 202 | white-space: pre; 203 | } 204 | 205 | #qunit-tests > li:last-child { 206 | border-radius: 0 0 5px 5px; 207 | -moz-border-radius: 0 0 5px 5px; 208 | -webkit-border-bottom-right-radius: 5px; 209 | -webkit-border-bottom-left-radius: 5px; 210 | } 211 | 212 | #qunit-tests .fail { color: #000000; background-color: #EE5757; } 213 | #qunit-tests .fail .test-name, 214 | #qunit-tests .fail .module-name { color: #000000; } 215 | 216 | #qunit-tests .fail .test-actual { color: #EE5757; } 217 | #qunit-tests .fail .test-expected { color: green; } 218 | 219 | #qunit-banner.qunit-fail { background-color: #EE5757; } 220 | 221 | 222 | /** Result */ 223 | 224 | #qunit-testresult { 225 | padding: 0.5em 0.5em 0.5em 2.5em; 226 | 227 | color: #2b81af; 228 | background-color: #D2E0E6; 229 | 230 | border-bottom: 1px solid white; 231 | } 232 | #qunit-testresult .module-name { 233 | font-weight: bold; 234 | } 235 | 236 | /** Fixture */ 237 | 238 | #qunit-fixture { 239 | position: absolute; 240 | top: -10000px; 241 | left: -10000px; 242 | width: 1000px; 243 | height: 1000px; 244 | } 245 | -------------------------------------------------------------------------------- /rhill-voronoi-core.js: -------------------------------------------------------------------------------- 1 | /*! 2 | Copyright (C) 2010-2013 Raymond Hill: https://github.com/gorhill/Javascript-Voronoi 3 | MIT License: See https://github.com/gorhill/Javascript-Voronoi/LICENSE.md 4 | */ 5 | /* 6 | Author: Raymond Hill (rhill@raymondhill.net) 7 | Contributor: Jesse Morgan (morgajel@gmail.com) 8 | File: rhill-voronoi-core.js 9 | Version: 0.98 10 | Date: January 21, 2013 11 | Description: This is my personal Javascript implementation of 12 | Steven Fortune's algorithm to compute Voronoi diagrams. 13 | 14 | License: See https://github.com/gorhill/Javascript-Voronoi/LICENSE.md 15 | Credits: See https://github.com/gorhill/Javascript-Voronoi/CREDITS.md 16 | History: See https://github.com/gorhill/Javascript-Voronoi/CHANGELOG.md 17 | 18 | ## Usage: 19 | 20 | var sites = [{x:300,y:300}, {x:100,y:100}, {x:200,y:500}, {x:250,y:450}, {x:600,y:150}]; 21 | // xl, xr means x left, x right 22 | // yt, yb means y top, y bottom 23 | var bbox = {xl:0, xr:800, yt:0, yb:600}; 24 | var voronoi = new Voronoi(); 25 | // pass an object which exhibits xl, xr, yt, yb properties. The bounding 26 | // box will be used to connect unbound edges, and to close open cells 27 | result = voronoi.compute(sites, bbox); 28 | // render, further analyze, etc. 29 | 30 | Return value: 31 | An object with the following properties: 32 | 33 | result.vertices = an array of unordered, unique Voronoi.Vertex objects making 34 | up the Voronoi diagram. 35 | result.edges = an array of unordered, unique Voronoi.Edge objects making up 36 | the Voronoi diagram. 37 | result.cells = an array of Voronoi.Cell object making up the Voronoi diagram. 38 | A Cell object might have an empty array of halfedges, meaning no Voronoi 39 | cell could be computed for a particular cell. 40 | result.execTime = the time it took to compute the Voronoi diagram, in 41 | milliseconds. 42 | 43 | Voronoi.Vertex object: 44 | x: The x position of the vertex. 45 | y: The y position of the vertex. 46 | 47 | Voronoi.Edge object: 48 | lSite: the Voronoi site object at the left of this Voronoi.Edge object. 49 | rSite: the Voronoi site object at the right of this Voronoi.Edge object (can 50 | be null). 51 | va: an object with an 'x' and a 'y' property defining the start point 52 | (relative to the Voronoi site on the left) of this Voronoi.Edge object. 53 | vb: an object with an 'x' and a 'y' property defining the end point 54 | (relative to Voronoi site on the left) of this Voronoi.Edge object. 55 | 56 | For edges which are used to close open cells (using the supplied bounding 57 | box), the rSite property will be null. 58 | 59 | Voronoi.Cell object: 60 | site: the Voronoi site object associated with the Voronoi cell. 61 | halfedges: an array of Voronoi.Halfedge objects, ordered counterclockwise, 62 | defining the polygon for this Voronoi cell. 63 | 64 | Voronoi.Halfedge object: 65 | site: the Voronoi site object owning this Voronoi.Halfedge object. 66 | edge: a reference to the unique Voronoi.Edge object underlying this 67 | Voronoi.Halfedge object. 68 | getStartpoint(): a method returning an object with an 'x' and a 'y' property 69 | for the start point of this halfedge. Keep in mind halfedges are always 70 | countercockwise. 71 | getEndpoint(): a method returning an object with an 'x' and a 'y' property 72 | for the end point of this halfedge. Keep in mind halfedges are always 73 | countercockwise. 74 | 75 | TODO: Identify opportunities for performance improvement. 76 | 77 | TODO: Let the user close the Voronoi cells, do not do it automatically. Not only let 78 | him close the cells, but also allow him to close more than once using a different 79 | bounding box for the same Voronoi diagram. 80 | */ 81 | 82 | /*global Math */ 83 | 84 | // --------------------------------------------------------------------------- 85 | 86 | function Voronoi() { 87 | this.vertices = null; 88 | this.edges = null; 89 | this.cells = null; 90 | this.toRecycle = null; 91 | this.beachsectionJunkyard = []; 92 | this.circleEventJunkyard = []; 93 | this.vertexJunkyard = []; 94 | this.edgeJunkyard = []; 95 | this.cellJunkyard = []; 96 | } 97 | 98 | // --------------------------------------------------------------------------- 99 | 100 | Voronoi.prototype.reset = function () { 101 | if (!this.beachline) { 102 | this.beachline = new this.RBTree(); 103 | } 104 | // Move leftover beachsections to the beachsection junkyard. 105 | if (this.beachline.root) { 106 | var beachsection = this.beachline.getFirst(this.beachline.root); 107 | while (beachsection) { 108 | this.beachsectionJunkyard.push(beachsection); // mark for reuse 109 | beachsection = beachsection.rbNext; 110 | } 111 | } 112 | this.beachline.root = null; 113 | if (!this.circleEvents) { 114 | this.circleEvents = new this.RBTree(); 115 | } 116 | this.circleEvents.root = this.firstCircleEvent = null; 117 | this.vertices = []; 118 | this.edges = []; 119 | this.cells = []; 120 | }; 121 | 122 | Voronoi.prototype.sqrt = Math.sqrt; 123 | Voronoi.prototype.abs = Math.abs; 124 | Voronoi.prototype.ε = Voronoi.ε = 1e-9; 125 | Voronoi.prototype.invε = Voronoi.invε = 1.0 / Voronoi.ε; 126 | Voronoi.prototype.equalWithEpsilon = function (a, b) { 127 | return this.abs(a - b) < 1e-9; 128 | }; 129 | Voronoi.prototype.greaterThanWithEpsilon = function (a, b) { 130 | return a - b > 1e-9; 131 | }; 132 | Voronoi.prototype.greaterThanOrEqualWithEpsilon = function (a, b) { 133 | return b - a < 1e-9; 134 | }; 135 | Voronoi.prototype.lessThanWithEpsilon = function (a, b) { 136 | return b - a > 1e-9; 137 | }; 138 | Voronoi.prototype.lessThanOrEqualWithEpsilon = function (a, b) { 139 | return a - b < 1e-9; 140 | }; 141 | 142 | // --------------------------------------------------------------------------- 143 | // Red-Black tree code (based on C version of "rbtree" by Franck Bui-Huu 144 | // https://github.com/fbuihuu/libtree/blob/master/rb.c 145 | 146 | Voronoi.prototype.RBTree = function () { 147 | this.root = null; 148 | }; 149 | 150 | Voronoi.prototype.RBTree.prototype.rbInsertSuccessor = function (node, successor) { 151 | var parent; 152 | if (node) { 153 | // >>> rhill 2011-05-27: Performance: cache previous/next nodes 154 | successor.rbPrevious = node; 155 | successor.rbNext = node.rbNext; 156 | if (node.rbNext) { 157 | node.rbNext.rbPrevious = successor; 158 | } 159 | node.rbNext = successor; 160 | // <<< 161 | if (node.rbRight) { 162 | // in-place expansion of node.rbRight.getFirst(); 163 | node = node.rbRight; 164 | while (node.rbLeft) { 165 | node = node.rbLeft; 166 | } 167 | node.rbLeft = successor; 168 | } 169 | else { 170 | node.rbRight = successor; 171 | } 172 | parent = node; 173 | } 174 | // rhill 2011-06-07: if node is null, successor must be inserted 175 | // to the left-most part of the tree 176 | else if (this.root) { 177 | node = this.getFirst(this.root); 178 | // >>> Performance: cache previous/next nodes 179 | successor.rbPrevious = null; 180 | successor.rbNext = node; 181 | node.rbPrevious = successor; 182 | // <<< 183 | node.rbLeft = successor; 184 | parent = node; 185 | } 186 | else { 187 | // >>> Performance: cache previous/next nodes 188 | successor.rbPrevious = successor.rbNext = null; 189 | // <<< 190 | this.root = successor; 191 | parent = null; 192 | } 193 | successor.rbLeft = successor.rbRight = null; 194 | successor.rbParent = parent; 195 | successor.rbRed = true; 196 | // Fixup the modified tree by recoloring nodes and performing 197 | // rotations (2 at most) hence the red-black tree properties are 198 | // preserved. 199 | var grandpa, uncle; 200 | node = successor; 201 | while (parent && parent.rbRed) { 202 | grandpa = parent.rbParent; 203 | if (parent === grandpa.rbLeft) { 204 | uncle = grandpa.rbRight; 205 | if (uncle && uncle.rbRed) { 206 | parent.rbRed = uncle.rbRed = false; 207 | grandpa.rbRed = true; 208 | node = grandpa; 209 | } 210 | else { 211 | if (node === parent.rbRight) { 212 | this.rbRotateLeft(parent); 213 | node = parent; 214 | parent = node.rbParent; 215 | } 216 | parent.rbRed = false; 217 | grandpa.rbRed = true; 218 | this.rbRotateRight(grandpa); 219 | } 220 | } 221 | else { 222 | uncle = grandpa.rbLeft; 223 | if (uncle && uncle.rbRed) { 224 | parent.rbRed = uncle.rbRed = false; 225 | grandpa.rbRed = true; 226 | node = grandpa; 227 | } 228 | else { 229 | if (node === parent.rbLeft) { 230 | this.rbRotateRight(parent); 231 | node = parent; 232 | parent = node.rbParent; 233 | } 234 | parent.rbRed = false; 235 | grandpa.rbRed = true; 236 | this.rbRotateLeft(grandpa); 237 | } 238 | } 239 | parent = node.rbParent; 240 | } 241 | this.root.rbRed = false; 242 | }; 243 | 244 | Voronoi.prototype.RBTree.prototype.rbRemoveNode = function (node) { 245 | // >>> rhill 2011-05-27: Performance: cache previous/next nodes 246 | if (node.rbNext) { 247 | node.rbNext.rbPrevious = node.rbPrevious; 248 | } 249 | if (node.rbPrevious) { 250 | node.rbPrevious.rbNext = node.rbNext; 251 | } 252 | node.rbNext = node.rbPrevious = null; 253 | // <<< 254 | var parent = node.rbParent, 255 | left = node.rbLeft, 256 | right = node.rbRight, 257 | next; 258 | if (!left) { 259 | next = right; 260 | } 261 | else if (!right) { 262 | next = left; 263 | } 264 | else { 265 | next = this.getFirst(right); 266 | } 267 | if (parent) { 268 | if (parent.rbLeft === node) { 269 | parent.rbLeft = next; 270 | } 271 | else { 272 | parent.rbRight = next; 273 | } 274 | } 275 | else { 276 | this.root = next; 277 | } 278 | // enforce red-black rules 279 | var isRed; 280 | if (left && right) { 281 | isRed = next.rbRed; 282 | next.rbRed = node.rbRed; 283 | next.rbLeft = left; 284 | left.rbParent = next; 285 | if (next !== right) { 286 | parent = next.rbParent; 287 | next.rbParent = node.rbParent; 288 | node = next.rbRight; 289 | parent.rbLeft = node; 290 | next.rbRight = right; 291 | right.rbParent = next; 292 | } 293 | else { 294 | next.rbParent = parent; 295 | parent = next; 296 | node = next.rbRight; 297 | } 298 | } 299 | else { 300 | isRed = node.rbRed; 301 | node = next; 302 | } 303 | // 'node' is now the sole successor's child and 'parent' its 304 | // new parent (since the successor can have been moved) 305 | if (node) { 306 | node.rbParent = parent; 307 | } 308 | // the 'easy' cases 309 | if (isRed) { 310 | return; 311 | } 312 | if (node && node.rbRed) { 313 | node.rbRed = false; 314 | return; 315 | } 316 | // the other cases 317 | var sibling; 318 | do { 319 | if (node === this.root) { 320 | break; 321 | } 322 | if (node === parent.rbLeft) { 323 | sibling = parent.rbRight; 324 | if (sibling.rbRed) { 325 | sibling.rbRed = false; 326 | parent.rbRed = true; 327 | this.rbRotateLeft(parent); 328 | sibling = parent.rbRight; 329 | } 330 | if ((sibling.rbLeft && sibling.rbLeft.rbRed) || (sibling.rbRight && sibling.rbRight.rbRed)) { 331 | if (!sibling.rbRight || !sibling.rbRight.rbRed) { 332 | sibling.rbLeft.rbRed = false; 333 | sibling.rbRed = true; 334 | this.rbRotateRight(sibling); 335 | sibling = parent.rbRight; 336 | } 337 | sibling.rbRed = parent.rbRed; 338 | parent.rbRed = sibling.rbRight.rbRed = false; 339 | this.rbRotateLeft(parent); 340 | node = this.root; 341 | break; 342 | } 343 | } 344 | else { 345 | sibling = parent.rbLeft; 346 | if (sibling.rbRed) { 347 | sibling.rbRed = false; 348 | parent.rbRed = true; 349 | this.rbRotateRight(parent); 350 | sibling = parent.rbLeft; 351 | } 352 | if ((sibling.rbLeft && sibling.rbLeft.rbRed) || (sibling.rbRight && sibling.rbRight.rbRed)) { 353 | if (!sibling.rbLeft || !sibling.rbLeft.rbRed) { 354 | sibling.rbRight.rbRed = false; 355 | sibling.rbRed = true; 356 | this.rbRotateLeft(sibling); 357 | sibling = parent.rbLeft; 358 | } 359 | sibling.rbRed = parent.rbRed; 360 | parent.rbRed = sibling.rbLeft.rbRed = false; 361 | this.rbRotateRight(parent); 362 | node = this.root; 363 | break; 364 | } 365 | } 366 | sibling.rbRed = true; 367 | node = parent; 368 | parent = parent.rbParent; 369 | } while (!node.rbRed); 370 | if (node) { 371 | node.rbRed = false; 372 | } 373 | }; 374 | 375 | Voronoi.prototype.RBTree.prototype.rbRotateLeft = function (node) { 376 | var p = node, 377 | q = node.rbRight, // can't be null 378 | parent = p.rbParent; 379 | if (parent) { 380 | if (parent.rbLeft === p) { 381 | parent.rbLeft = q; 382 | } 383 | else { 384 | parent.rbRight = q; 385 | } 386 | } 387 | else { 388 | this.root = q; 389 | } 390 | q.rbParent = parent; 391 | p.rbParent = q; 392 | p.rbRight = q.rbLeft; 393 | if (p.rbRight) { 394 | p.rbRight.rbParent = p; 395 | } 396 | q.rbLeft = p; 397 | }; 398 | 399 | Voronoi.prototype.RBTree.prototype.rbRotateRight = function (node) { 400 | var p = node, 401 | q = node.rbLeft, // can't be null 402 | parent = p.rbParent; 403 | if (parent) { 404 | if (parent.rbLeft === p) { 405 | parent.rbLeft = q; 406 | } 407 | else { 408 | parent.rbRight = q; 409 | } 410 | } 411 | else { 412 | this.root = q; 413 | } 414 | q.rbParent = parent; 415 | p.rbParent = q; 416 | p.rbLeft = q.rbRight; 417 | if (p.rbLeft) { 418 | p.rbLeft.rbParent = p; 419 | } 420 | q.rbRight = p; 421 | }; 422 | 423 | Voronoi.prototype.RBTree.prototype.getFirst = function (node) { 424 | while (node.rbLeft) { 425 | node = node.rbLeft; 426 | } 427 | return node; 428 | }; 429 | 430 | Voronoi.prototype.RBTree.prototype.getLast = function (node) { 431 | while (node.rbRight) { 432 | node = node.rbRight; 433 | } 434 | return node; 435 | }; 436 | 437 | // --------------------------------------------------------------------------- 438 | // Diagram methods 439 | 440 | Voronoi.prototype.Diagram = function (site) { 441 | this.site = site; 442 | }; 443 | 444 | // --------------------------------------------------------------------------- 445 | // Cell methods 446 | 447 | Voronoi.prototype.Cell = function (site) { 448 | this.site = site; 449 | this.halfedges = []; 450 | this.closeMe = false; 451 | }; 452 | 453 | Voronoi.prototype.Cell.prototype.init = function (site) { 454 | this.site = site; 455 | this.halfedges = []; 456 | this.closeMe = false; 457 | return this; 458 | }; 459 | 460 | Voronoi.prototype.createCell = function (site) { 461 | var cell = this.cellJunkyard.pop(); 462 | if (cell) { 463 | return cell.init(site); 464 | } 465 | return new this.Cell(site); 466 | }; 467 | 468 | Voronoi.prototype.Cell.prototype.prepareHalfedges = function () { 469 | var halfedges = this.halfedges, 470 | iHalfedge = halfedges.length, 471 | edge; 472 | // get rid of unused halfedges 473 | // rhill 2011-05-27: Keep it simple, no point here in trying 474 | // to be fancy: dangling edges are a typically a minority. 475 | while (iHalfedge--) { 476 | edge = halfedges[iHalfedge].edge; 477 | if (!edge.vb || !edge.va) { 478 | halfedges.splice(iHalfedge, 1); 479 | } 480 | } 481 | 482 | // rhill 2011-05-26: I tried to use a binary search at insertion 483 | // time to keep the array sorted on-the-fly (in Cell.addHalfedge()). 484 | // There was no real benefits in doing so, performance on 485 | // Firefox 3.6 was improved marginally, while performance on 486 | // Opera 11 was penalized marginally. 487 | halfedges.sort(function (a, b) { 488 | return b.angle - a.angle; 489 | }); 490 | return halfedges.length; 491 | }; 492 | 493 | // Return a list of the neighbor Ids 494 | Voronoi.prototype.Cell.prototype.getNeighborIds = function () { 495 | var neighbors = [], 496 | iHalfedge = this.halfedges.length, 497 | edge; 498 | while (iHalfedge--) { 499 | edge = this.halfedges[iHalfedge].edge; 500 | if (edge.lSite !== null && edge.lSite.voronoiId != this.site.voronoiId) { 501 | neighbors.push(edge.lSite.voronoiId); 502 | } 503 | else if (edge.rSite !== null && edge.rSite.voronoiId != this.site.voronoiId) { 504 | neighbors.push(edge.rSite.voronoiId); 505 | } 506 | } 507 | return neighbors; 508 | }; 509 | 510 | // Compute bounding box 511 | // 512 | Voronoi.prototype.Cell.prototype.getBbox = function () { 513 | var halfedges = this.halfedges, 514 | iHalfedge = halfedges.length, 515 | xmin = Infinity, 516 | ymin = Infinity, 517 | xmax = -Infinity, 518 | ymax = -Infinity, 519 | v, vx, vy; 520 | while (iHalfedge--) { 521 | v = halfedges[iHalfedge].getStartpoint(); 522 | vx = v.x; 523 | vy = v.y; 524 | if (vx < xmin) { 525 | xmin = vx; 526 | } 527 | if (vy < ymin) { 528 | ymin = vy; 529 | } 530 | if (vx > xmax) { 531 | xmax = vx; 532 | } 533 | if (vy > ymax) { 534 | ymax = vy; 535 | } 536 | // we dont need to take into account end point, 537 | // since each end point matches a start point 538 | } 539 | return { 540 | x: xmin, 541 | y: ymin, 542 | width: xmax - xmin, 543 | height: ymax - ymin 544 | }; 545 | }; 546 | 547 | // Return whether a point is inside, on, or outside the cell: 548 | // -1: point is outside the perimeter of the cell 549 | // 0: point is on the perimeter of the cell 550 | // 1: point is inside the perimeter of the cell 551 | // 552 | Voronoi.prototype.Cell.prototype.pointIntersection = function (x, y) { 553 | // Check if point in polygon. Since all polygons of a Voronoi 554 | // diagram are convex, then: 555 | // http://paulbourke.net/geometry/polygonmesh/ 556 | // Solution 3 (2D): 557 | // "If the polygon is convex then one can consider the polygon 558 | // "as a 'path' from the first vertex. A point is on the interior 559 | // "of this polygons if it is always on the same side of all the 560 | // "line segments making up the path. ... 561 | // "(y - y0) (x1 - x0) - (x - x0) (y1 - y0) 562 | // "if it is less than 0 then P is to the right of the line segment, 563 | // "if greater than 0 it is to the left, if equal to 0 then it lies 564 | // "on the line segment" 565 | var halfedges = this.halfedges, 566 | iHalfedge = halfedges.length, 567 | halfedge, 568 | p0, p1, r; 569 | while (iHalfedge--) { 570 | halfedge = halfedges[iHalfedge]; 571 | p0 = halfedge.getStartpoint(); 572 | p1 = halfedge.getEndpoint(); 573 | r = (y - p0.y) * (p1.x - p0.x) - (x - p0.x) * (p1.y - p0.y); 574 | if (!r) { 575 | return 0; 576 | } 577 | if (r > 0) { 578 | return -1; 579 | } 580 | } 581 | return 1; 582 | }; 583 | 584 | // --------------------------------------------------------------------------- 585 | // Edge methods 586 | // 587 | 588 | Voronoi.prototype.Vertex = function (x, y) { 589 | this.x = x; 590 | this.y = y; 591 | }; 592 | 593 | Voronoi.prototype.Edge = function (lSite, rSite) { 594 | this.lSite = lSite; 595 | this.rSite = rSite; 596 | this.va = this.vb = null; 597 | }; 598 | 599 | Voronoi.prototype.Halfedge = function (edge, lSite, rSite) { 600 | this.site = lSite; 601 | this.edge = edge; 602 | // 'angle' is a value to be used for properly sorting the 603 | // halfsegments counterclockwise. By convention, we will 604 | // use the angle of the line defined by the 'site to the left' 605 | // to the 'site to the right'. 606 | // However, border edges have no 'site to the right': thus we 607 | // use the angle of line perpendicular to the halfsegment (the 608 | // edge should have both end points defined in such case.) 609 | if (rSite) { 610 | this.angle = Math.atan2(rSite.y - lSite.y, rSite.x - lSite.x); 611 | } 612 | else { 613 | var va = edge.va, 614 | vb = edge.vb; 615 | // rhill 2011-05-31: used to call getStartpoint()/getEndpoint(), 616 | // but for performance purpose, these are expanded in place here. 617 | this.angle = edge.lSite === lSite ? 618 | Math.atan2(vb.x - va.x, va.y - vb.y) : 619 | Math.atan2(va.x - vb.x, vb.y - va.y); 620 | } 621 | }; 622 | 623 | Voronoi.prototype.createHalfedge = function (edge, lSite, rSite) { 624 | return new this.Halfedge(edge, lSite, rSite); 625 | }; 626 | 627 | Voronoi.prototype.Halfedge.prototype.getStartpoint = function () { 628 | return this.edge.lSite === this.site ? this.edge.va : this.edge.vb; 629 | }; 630 | 631 | Voronoi.prototype.Halfedge.prototype.getEndpoint = function () { 632 | return this.edge.lSite === this.site ? this.edge.vb : this.edge.va; 633 | }; 634 | 635 | 636 | // this create and add a vertex to the internal collection 637 | 638 | Voronoi.prototype.createVertex = function (x, y) { 639 | var v = this.vertexJunkyard.pop(); 640 | if (!v) { 641 | v = new this.Vertex(x, y); 642 | } 643 | else { 644 | v.x = x; 645 | v.y = y; 646 | } 647 | this.vertices.push(v); 648 | return v; 649 | }; 650 | 651 | // this create and add an edge to internal collection, and also create 652 | // two halfedges which are added to each site's counterclockwise array 653 | // of halfedges. 654 | 655 | Voronoi.prototype.createEdge = function (lSite, rSite, va, vb) { 656 | var edge = this.edgeJunkyard.pop(); 657 | if (!edge) { 658 | edge = new this.Edge(lSite, rSite); 659 | } 660 | else { 661 | edge.lSite = lSite; 662 | edge.rSite = rSite; 663 | edge.va = edge.vb = null; 664 | } 665 | 666 | this.edges.push(edge); 667 | if (va) { 668 | this.setEdgeStartpoint(edge, lSite, rSite, va); 669 | } 670 | if (vb) { 671 | this.setEdgeEndpoint(edge, lSite, rSite, vb); 672 | } 673 | this.cells[lSite.voronoiId].halfedges.push(this.createHalfedge(edge, lSite, rSite)); 674 | this.cells[rSite.voronoiId].halfedges.push(this.createHalfedge(edge, rSite, lSite)); 675 | return edge; 676 | }; 677 | 678 | Voronoi.prototype.createBorderEdge = function (lSite, va, vb) { 679 | var edge = this.edgeJunkyard.pop(); 680 | if (!edge) { 681 | edge = new this.Edge(lSite, null); 682 | } 683 | else { 684 | edge.lSite = lSite; 685 | edge.rSite = null; 686 | } 687 | edge.va = va; 688 | edge.vb = vb; 689 | this.edges.push(edge); 690 | return edge; 691 | }; 692 | 693 | Voronoi.prototype.setEdgeStartpoint = function (edge, lSite, rSite, vertex) { 694 | if (!edge.va && !edge.vb) { 695 | edge.va = vertex; 696 | edge.lSite = lSite; 697 | edge.rSite = rSite; 698 | } 699 | else if (edge.lSite === rSite) { 700 | edge.vb = vertex; 701 | } 702 | else { 703 | edge.va = vertex; 704 | } 705 | }; 706 | 707 | Voronoi.prototype.setEdgeEndpoint = function (edge, lSite, rSite, vertex) { 708 | this.setEdgeStartpoint(edge, rSite, lSite, vertex); 709 | }; 710 | 711 | // --------------------------------------------------------------------------- 712 | // Beachline methods 713 | 714 | // rhill 2011-06-07: For some reasons, performance suffers significantly 715 | // when instanciating a literal object instead of an empty ctor 716 | Voronoi.prototype.Beachsection = function () { 717 | }; 718 | 719 | // rhill 2011-06-02: A lot of Beachsection instanciations 720 | // occur during the computation of the Voronoi diagram, 721 | // somewhere between the number of sites and twice the 722 | // number of sites, while the number of Beachsections on the 723 | // beachline at any given time is comparatively low. For this 724 | // reason, we reuse already created Beachsections, in order 725 | // to avoid new memory allocation. This resulted in a measurable 726 | // performance gain. 727 | 728 | Voronoi.prototype.createBeachsection = function (site) { 729 | var beachsection = this.beachsectionJunkyard.pop(); 730 | if (!beachsection) { 731 | beachsection = new this.Beachsection(); 732 | } 733 | beachsection.site = site; 734 | return beachsection; 735 | }; 736 | 737 | // calculate the left break point of a particular beach section, 738 | // given a particular sweep line 739 | Voronoi.prototype.leftBreakPoint = function (arc, directrix) { 740 | // http://en.wikipedia.org/wiki/Parabola 741 | // http://en.wikipedia.org/wiki/Quadratic_equation 742 | // h1 = x1, 743 | // k1 = (y1+directrix)/2, 744 | // h2 = x2, 745 | // k2 = (y2+directrix)/2, 746 | // p1 = k1-directrix, 747 | // a1 = 1/(4*p1), 748 | // b1 = -h1/(2*p1), 749 | // c1 = h1*h1/(4*p1)+k1, 750 | // p2 = k2-directrix, 751 | // a2 = 1/(4*p2), 752 | // b2 = -h2/(2*p2), 753 | // c2 = h2*h2/(4*p2)+k2, 754 | // x = (-(b2-b1) + Math.sqrt((b2-b1)*(b2-b1) - 4*(a2-a1)*(c2-c1))) / (2*(a2-a1)) 755 | // When x1 become the x-origin: 756 | // h1 = 0, 757 | // k1 = (y1+directrix)/2, 758 | // h2 = x2-x1, 759 | // k2 = (y2+directrix)/2, 760 | // p1 = k1-directrix, 761 | // a1 = 1/(4*p1), 762 | // b1 = 0, 763 | // c1 = k1, 764 | // p2 = k2-directrix, 765 | // a2 = 1/(4*p2), 766 | // b2 = -h2/(2*p2), 767 | // c2 = h2*h2/(4*p2)+k2, 768 | // x = (-b2 + Math.sqrt(b2*b2 - 4*(a2-a1)*(c2-k1))) / (2*(a2-a1)) + x1 769 | 770 | // change code below at your own risk: care has been taken to 771 | // reduce errors due to computers' finite arithmetic precision. 772 | // Maybe can still be improved, will see if any more of this 773 | // kind of errors pop up again. 774 | var site = arc.site, 775 | rfocx = site.x, 776 | rfocy = site.y, 777 | pby2 = rfocy - directrix; 778 | // parabola in degenerate case where focus is on directrix 779 | if (!pby2) { 780 | return rfocx; 781 | } 782 | var lArc = arc.rbPrevious; 783 | if (!lArc) { 784 | return -Infinity; 785 | } 786 | site = lArc.site; 787 | var lfocx = site.x, 788 | lfocy = site.y, 789 | plby2 = lfocy - directrix; 790 | // parabola in degenerate case where focus is on directrix 791 | if (!plby2) { 792 | return lfocx; 793 | } 794 | var hl = lfocx - rfocx, 795 | aby2 = 1 / pby2 - 1 / plby2, 796 | b = hl / plby2; 797 | if (aby2) { 798 | return (-b + this.sqrt(b * b - 2 * aby2 * (hl * hl / (-2 * plby2) - lfocy + plby2 / 2 + rfocy - pby2 / 2))) / aby2 + rfocx; 799 | } 800 | // both parabolas have same distance to directrix, thus break point is midway 801 | return (rfocx + lfocx) / 2; 802 | }; 803 | 804 | // calculate the right break point of a particular beach section, 805 | // given a particular directrix 806 | Voronoi.prototype.rightBreakPoint = function (arc, directrix) { 807 | var rArc = arc.rbNext; 808 | if (rArc) { 809 | return this.leftBreakPoint(rArc, directrix); 810 | } 811 | var site = arc.site; 812 | return site.y === directrix ? site.x : Infinity; 813 | }; 814 | 815 | Voronoi.prototype.detachBeachsection = function (beachsection) { 816 | this.detachCircleEvent(beachsection); // detach potentially attached circle event 817 | this.beachline.rbRemoveNode(beachsection); // remove from RB-tree 818 | this.beachsectionJunkyard.push(beachsection); // mark for reuse 819 | }; 820 | 821 | Voronoi.prototype.removeBeachsection = function (beachsection) { 822 | var circle = beachsection.circleEvent, 823 | x = circle.x, 824 | y = circle.ycenter, 825 | vertex = this.createVertex(x, y), 826 | previous = beachsection.rbPrevious, 827 | next = beachsection.rbNext, 828 | disappearingTransitions = [beachsection], 829 | abs_fn = Math.abs; 830 | 831 | // remove collapsed beachsection from beachline 832 | this.detachBeachsection(beachsection); 833 | 834 | // there could be more than one empty arc at the deletion point, this 835 | // happens when more than two edges are linked by the same vertex, 836 | // so we will collect all those edges by looking up both sides of 837 | // the deletion point. 838 | // by the way, there is *always* a predecessor/successor to any collapsed 839 | // beach section, it's just impossible to have a collapsing first/last 840 | // beach sections on the beachline, since they obviously are unconstrained 841 | // on their left/right side. 842 | 843 | // look left 844 | var lArc = previous; 845 | while (lArc.circleEvent && abs_fn(x - lArc.circleEvent.x) < 1e-9 && abs_fn(y - lArc.circleEvent.ycenter) < 1e-9) { 846 | previous = lArc.rbPrevious; 847 | disappearingTransitions.unshift(lArc); 848 | this.detachBeachsection(lArc); // mark for reuse 849 | lArc = previous; 850 | } 851 | // even though it is not disappearing, I will also add the beach section 852 | // immediately to the left of the left-most collapsed beach section, for 853 | // convenience, since we need to refer to it later as this beach section 854 | // is the 'left' site of an edge for which a start point is set. 855 | disappearingTransitions.unshift(lArc); 856 | this.detachCircleEvent(lArc); 857 | 858 | // look right 859 | var rArc = next; 860 | while (rArc.circleEvent && abs_fn(x - rArc.circleEvent.x) < 1e-9 && abs_fn(y - rArc.circleEvent.ycenter) < 1e-9) { 861 | next = rArc.rbNext; 862 | disappearingTransitions.push(rArc); 863 | this.detachBeachsection(rArc); // mark for reuse 864 | rArc = next; 865 | } 866 | // we also have to add the beach section immediately to the right of the 867 | // right-most collapsed beach section, since there is also a disappearing 868 | // transition representing an edge's start point on its left. 869 | disappearingTransitions.push(rArc); 870 | this.detachCircleEvent(rArc); 871 | 872 | // walk through all the disappearing transitions between beach sections and 873 | // set the start point of their (implied) edge. 874 | var nArcs = disappearingTransitions.length, 875 | iArc; 876 | for (iArc = 1; iArc < nArcs; iArc++) { 877 | rArc = disappearingTransitions[iArc]; 878 | lArc = disappearingTransitions[iArc - 1]; 879 | this.setEdgeStartpoint(rArc.edge, lArc.site, rArc.site, vertex); 880 | } 881 | 882 | // create a new edge as we have now a new transition between 883 | // two beach sections which were previously not adjacent. 884 | // since this edge appears as a new vertex is defined, the vertex 885 | // actually define an end point of the edge (relative to the site 886 | // on the left) 887 | lArc = disappearingTransitions[0]; 888 | rArc = disappearingTransitions[nArcs - 1]; 889 | rArc.edge = this.createEdge(lArc.site, rArc.site, undefined, vertex); 890 | 891 | // create circle events if any for beach sections left in the beachline 892 | // adjacent to collapsed sections 893 | this.attachCircleEvent(lArc); 894 | this.attachCircleEvent(rArc); 895 | }; 896 | 897 | Voronoi.prototype.addBeachsection = function (site) { 898 | var x = site.x, 899 | directrix = site.y; 900 | 901 | // find the left and right beach sections which will surround the newly 902 | // created beach section. 903 | // rhill 2011-06-01: This loop is one of the most often executed, 904 | // hence we expand in-place the comparison-against-epsilon calls. 905 | var lArc, rArc, 906 | dxl, dxr, 907 | node = this.beachline.root; 908 | 909 | while (node) { 910 | dxl = this.leftBreakPoint(node, directrix) - x; 911 | // x lessThanWithEpsilon xl => falls somewhere before the left edge of the beachsection 912 | if (dxl > 1e-9) { 913 | // this case should never happen 914 | // if (!node.rbLeft) { 915 | // rArc = node.rbLeft; 916 | // break; 917 | // } 918 | node = node.rbLeft; 919 | } 920 | else { 921 | dxr = x - this.rightBreakPoint(node, directrix); 922 | // x greaterThanWithEpsilon xr => falls somewhere after the right edge of the beachsection 923 | if (dxr > 1e-9) { 924 | if (!node.rbRight) { 925 | lArc = node; 926 | break; 927 | } 928 | node = node.rbRight; 929 | } 930 | else { 931 | // x equalWithEpsilon xl => falls exactly on the left edge of the beachsection 932 | if (dxl > -1e-9) { 933 | lArc = node.rbPrevious; 934 | rArc = node; 935 | } 936 | // x equalWithEpsilon xr => falls exactly on the right edge of the beachsection 937 | else if (dxr > -1e-9) { 938 | lArc = node; 939 | rArc = node.rbNext; 940 | } 941 | // falls exactly somewhere in the middle of the beachsection 942 | else { 943 | lArc = rArc = node; 944 | } 945 | break; 946 | } 947 | } 948 | } 949 | // at this point, keep in mind that lArc and/or rArc could be 950 | // undefined or null. 951 | 952 | // create a new beach section object for the site and add it to RB-tree 953 | var newArc = this.createBeachsection(site); 954 | this.beachline.rbInsertSuccessor(lArc, newArc); 955 | 956 | // cases: 957 | // 958 | 959 | // [null,null] 960 | // least likely case: new beach section is the first beach section on the 961 | // beachline. 962 | // This case means: 963 | // no new transition appears 964 | // no collapsing beach section 965 | // new beachsection become root of the RB-tree 966 | if (!lArc && !rArc) { 967 | return; 968 | } 969 | 970 | // [lArc,rArc] where lArc == rArc 971 | // most likely case: new beach section split an existing beach 972 | // section. 973 | // This case means: 974 | // one new transition appears 975 | // the left and right beach section might be collapsing as a result 976 | // two new nodes added to the RB-tree 977 | if (lArc === rArc) { 978 | // invalidate circle event of split beach section 979 | this.detachCircleEvent(lArc); 980 | 981 | // split the beach section into two separate beach sections 982 | rArc = this.createBeachsection(lArc.site); 983 | this.beachline.rbInsertSuccessor(newArc, rArc); 984 | 985 | // since we have a new transition between two beach sections, 986 | // a new edge is born 987 | newArc.edge = rArc.edge = this.createEdge(lArc.site, newArc.site); 988 | 989 | // check whether the left and right beach sections are collapsing 990 | // and if so create circle events, to be notified when the point of 991 | // collapse is reached. 992 | this.attachCircleEvent(lArc); 993 | this.attachCircleEvent(rArc); 994 | return; 995 | } 996 | 997 | // [lArc,null] 998 | // even less likely case: new beach section is the *last* beach section 999 | // on the beachline -- this can happen *only* if *all* the previous beach 1000 | // sections currently on the beachline share the same y value as 1001 | // the new beach section. 1002 | // This case means: 1003 | // one new transition appears 1004 | // no collapsing beach section as a result 1005 | // new beach section become right-most node of the RB-tree 1006 | if (lArc && !rArc) { 1007 | newArc.edge = this.createEdge(lArc.site, newArc.site); 1008 | return; 1009 | } 1010 | 1011 | // [null,rArc] 1012 | // impossible case: because sites are strictly processed from top to bottom, 1013 | // and left to right, which guarantees that there will always be a beach section 1014 | // on the left -- except of course when there are no beach section at all on 1015 | // the beach line, which case was handled above. 1016 | // rhill 2011-06-02: No point testing in non-debug version 1017 | //if (!lArc && rArc) { 1018 | // throw "Voronoi.addBeachsection(): What is this I don't even"; 1019 | // } 1020 | 1021 | // [lArc,rArc] where lArc != rArc 1022 | // somewhat less likely case: new beach section falls *exactly* in between two 1023 | // existing beach sections 1024 | // This case means: 1025 | // one transition disappears 1026 | // two new transitions appear 1027 | // the left and right beach section might be collapsing as a result 1028 | // only one new node added to the RB-tree 1029 | if (lArc !== rArc) { 1030 | // invalidate circle events of left and right sites 1031 | this.detachCircleEvent(lArc); 1032 | this.detachCircleEvent(rArc); 1033 | 1034 | // an existing transition disappears, meaning a vertex is defined at 1035 | // the disappearance point. 1036 | // since the disappearance is caused by the new beachsection, the 1037 | // vertex is at the center of the circumscribed circle of the left, 1038 | // new and right beachsections. 1039 | // http://mathforum.org/library/drmath/view/55002.html 1040 | // Except that I bring the origin at A to simplify 1041 | // calculation 1042 | var lSite = lArc.site, 1043 | ax = lSite.x, 1044 | ay = lSite.y, 1045 | bx = site.x - ax, 1046 | by = site.y - ay, 1047 | rSite = rArc.site, 1048 | cx = rSite.x - ax, 1049 | cy = rSite.y - ay, 1050 | d = 2 * (bx * cy - by * cx), 1051 | hb = bx * bx + by * by, 1052 | hc = cx * cx + cy * cy, 1053 | vertex = this.createVertex((cy * hb - by * hc) / d + ax, (bx * hc - cx * hb) / d + ay); 1054 | 1055 | // one transition disappear 1056 | this.setEdgeStartpoint(rArc.edge, lSite, rSite, vertex); 1057 | 1058 | // two new transitions appear at the new vertex location 1059 | newArc.edge = this.createEdge(lSite, site, undefined, vertex); 1060 | rArc.edge = this.createEdge(site, rSite, undefined, vertex); 1061 | 1062 | // check whether the left and right beach sections are collapsing 1063 | // and if so create circle events, to handle the point of collapse. 1064 | this.attachCircleEvent(lArc); 1065 | this.attachCircleEvent(rArc); 1066 | return; 1067 | } 1068 | }; 1069 | 1070 | // --------------------------------------------------------------------------- 1071 | // Circle event methods 1072 | 1073 | // rhill 2011-06-07: For some reasons, performance suffers significantly 1074 | // when instanciating a literal object instead of an empty ctor 1075 | Voronoi.prototype.CircleEvent = function () { 1076 | // rhill 2013-10-12: it helps to state exactly what we are at ctor time. 1077 | this.arc = null; 1078 | this.rbLeft = null; 1079 | this.rbNext = null; 1080 | this.rbParent = null; 1081 | this.rbPrevious = null; 1082 | this.rbRed = false; 1083 | this.rbRight = null; 1084 | this.site = null; 1085 | this.x = this.y = this.ycenter = 0; 1086 | }; 1087 | 1088 | Voronoi.prototype.attachCircleEvent = function (arc) { 1089 | var lArc = arc.rbPrevious, 1090 | rArc = arc.rbNext; 1091 | if (!lArc || !rArc) { 1092 | return; 1093 | } // does that ever happen? 1094 | var lSite = lArc.site, 1095 | cSite = arc.site, 1096 | rSite = rArc.site; 1097 | 1098 | // If site of left beachsection is same as site of 1099 | // right beachsection, there can't be convergence 1100 | if (lSite === rSite) { 1101 | return; 1102 | } 1103 | 1104 | // Find the circumscribed circle for the three sites associated 1105 | // with the beachsection triplet. 1106 | // rhill 2011-05-26: It is more efficient to calculate in-place 1107 | // rather than getting the resulting circumscribed circle from an 1108 | // object returned by calling Voronoi.circumcircle() 1109 | // http://mathforum.org/library/drmath/view/55002.html 1110 | // Except that I bring the origin at cSite to simplify calculations. 1111 | // The bottom-most part of the circumcircle is our Fortune 'circle 1112 | // event', and its center is a vertex potentially part of the final 1113 | // Voronoi diagram. 1114 | var bx = cSite.x, 1115 | by = cSite.y, 1116 | ax = lSite.x - bx, 1117 | ay = lSite.y - by, 1118 | cx = rSite.x - bx, 1119 | cy = rSite.y - by; 1120 | 1121 | // If points l->c->r are clockwise, then center beach section does not 1122 | // collapse, hence it can't end up as a vertex (we reuse 'd' here, which 1123 | // sign is reverse of the orientation, hence we reverse the test. 1124 | // http://en.wikipedia.org/wiki/Curve_orientation#Orientation_of_a_simple_polygon 1125 | // rhill 2011-05-21: Nasty finite precision error which caused circumcircle() to 1126 | // return infinites: 1e-12 seems to fix the problem. 1127 | var d = 2 * (ax * cy - ay * cx); 1128 | if (d >= -2e-12) { 1129 | return; 1130 | } 1131 | 1132 | var ha = ax * ax + ay * ay, 1133 | hc = cx * cx + cy * cy, 1134 | x = (cy * ha - ay * hc) / d, 1135 | y = (ax * hc - cx * ha) / d, 1136 | ycenter = y + by; 1137 | 1138 | // Important: ybottom should always be under or at sweep, so no need 1139 | // to waste CPU cycles by checking 1140 | 1141 | // recycle circle event object if possible 1142 | var circleEvent = this.circleEventJunkyard.pop(); 1143 | if (!circleEvent) { 1144 | circleEvent = new this.CircleEvent(); 1145 | } 1146 | circleEvent.arc = arc; 1147 | circleEvent.site = cSite; 1148 | circleEvent.x = x + bx; 1149 | circleEvent.y = ycenter + this.sqrt(x * x + y * y); // y bottom 1150 | circleEvent.ycenter = ycenter; 1151 | arc.circleEvent = circleEvent; 1152 | 1153 | // find insertion point in RB-tree: circle events are ordered from 1154 | // smallest to largest 1155 | var predecessor = null, 1156 | node = this.circleEvents.root; 1157 | while (node) { 1158 | if (circleEvent.y < node.y || (circleEvent.y === node.y && circleEvent.x <= node.x)) { 1159 | if (node.rbLeft) { 1160 | node = node.rbLeft; 1161 | } 1162 | else { 1163 | predecessor = node.rbPrevious; 1164 | break; 1165 | } 1166 | } 1167 | else { 1168 | if (node.rbRight) { 1169 | node = node.rbRight; 1170 | } 1171 | else { 1172 | predecessor = node; 1173 | break; 1174 | } 1175 | } 1176 | } 1177 | this.circleEvents.rbInsertSuccessor(predecessor, circleEvent); 1178 | if (!predecessor) { 1179 | this.firstCircleEvent = circleEvent; 1180 | } 1181 | }; 1182 | 1183 | Voronoi.prototype.detachCircleEvent = function (arc) { 1184 | var circleEvent = arc.circleEvent; 1185 | if (circleEvent) { 1186 | if (!circleEvent.rbPrevious) { 1187 | this.firstCircleEvent = circleEvent.rbNext; 1188 | } 1189 | this.circleEvents.rbRemoveNode(circleEvent); // remove from RB-tree 1190 | this.circleEventJunkyard.push(circleEvent); 1191 | arc.circleEvent = null; 1192 | } 1193 | }; 1194 | 1195 | // --------------------------------------------------------------------------- 1196 | // Diagram completion methods 1197 | 1198 | // connect dangling edges (not if a cursory test tells us 1199 | // it is not going to be visible. 1200 | // return value: 1201 | // false: the dangling endpoint couldn't be connected 1202 | // true: the dangling endpoint could be connected 1203 | Voronoi.prototype.connectEdge = function (edge, bbox) { 1204 | // skip if end point already connected 1205 | var vb = edge.vb; 1206 | if (!!vb) { 1207 | return true; 1208 | } 1209 | 1210 | // make local copy for performance purpose 1211 | var va = edge.va, 1212 | xl = bbox.xl, 1213 | xr = bbox.xr, 1214 | yt = bbox.yt, 1215 | yb = bbox.yb, 1216 | lSite = edge.lSite, 1217 | rSite = edge.rSite, 1218 | lx = lSite.x, 1219 | ly = lSite.y, 1220 | rx = rSite.x, 1221 | ry = rSite.y, 1222 | fx = (lx + rx) / 2, 1223 | fy = (ly + ry) / 2, 1224 | fm, fb; 1225 | 1226 | // if we reach here, this means cells which use this edge will need 1227 | // to be closed, whether because the edge was removed, or because it 1228 | // was connected to the bounding box. 1229 | this.cells[lSite.voronoiId].closeMe = true; 1230 | this.cells[rSite.voronoiId].closeMe = true; 1231 | 1232 | // get the line equation of the bisector if line is not vertical 1233 | if (ry !== ly) { 1234 | fm = (lx - rx) / (ry - ly); 1235 | fb = fy - fm * fx; 1236 | } 1237 | 1238 | // remember, direction of line (relative to left site): 1239 | // upward: left.x < right.x 1240 | // downward: left.x > right.x 1241 | // horizontal: left.x == right.x 1242 | // upward: left.x < right.x 1243 | // rightward: left.y < right.y 1244 | // leftward: left.y > right.y 1245 | // vertical: left.y == right.y 1246 | 1247 | // depending on the direction, find the best side of the 1248 | // bounding box to use to determine a reasonable start point 1249 | 1250 | // rhill 2013-12-02: 1251 | // While at it, since we have the values which define the line, 1252 | // clip the end of va if it is outside the bbox. 1253 | // https://github.com/gorhill/Javascript-Voronoi/issues/15 1254 | // TODO: Do all the clipping here rather than rely on Liang-Barsky 1255 | // which does not do well sometimes due to loss of arithmetic 1256 | // precision. The code here doesn't degrade if one of the vertex is 1257 | // at a huge distance. 1258 | 1259 | // special case: vertical line 1260 | if (fm === undefined) { 1261 | // doesn't intersect with viewport 1262 | if (fx < xl || fx >= xr) { 1263 | return false; 1264 | } 1265 | // downward 1266 | if (lx > rx) { 1267 | if (!va || va.y < yt) { 1268 | va = this.createVertex(fx, yt); 1269 | } 1270 | else if (va.y >= yb) { 1271 | return false; 1272 | } 1273 | vb = this.createVertex(fx, yb); 1274 | } 1275 | // upward 1276 | else { 1277 | if (!va || va.y > yb) { 1278 | va = this.createVertex(fx, yb); 1279 | } 1280 | else if (va.y < yt) { 1281 | return false; 1282 | } 1283 | vb = this.createVertex(fx, yt); 1284 | } 1285 | } 1286 | // closer to vertical than horizontal, connect start point to the 1287 | // top or bottom side of the bounding box 1288 | else if (fm < -1 || fm > 1) { 1289 | // downward 1290 | if (lx > rx) { 1291 | if (!va || va.y < yt) { 1292 | va = this.createVertex((yt - fb) / fm, yt); 1293 | } 1294 | else if (va.y >= yb) { 1295 | return false; 1296 | } 1297 | vb = this.createVertex((yb - fb) / fm, yb); 1298 | } 1299 | // upward 1300 | else { 1301 | if (!va || va.y > yb) { 1302 | va = this.createVertex((yb - fb) / fm, yb); 1303 | } 1304 | else if (va.y < yt) { 1305 | return false; 1306 | } 1307 | vb = this.createVertex((yt - fb) / fm, yt); 1308 | } 1309 | } 1310 | // closer to horizontal than vertical, connect start point to the 1311 | // left or right side of the bounding box 1312 | else { 1313 | // rightward 1314 | if (ly < ry) { 1315 | if (!va || va.x < xl) { 1316 | va = this.createVertex(xl, fm * xl + fb); 1317 | } 1318 | else if (va.x >= xr) { 1319 | return false; 1320 | } 1321 | vb = this.createVertex(xr, fm * xr + fb); 1322 | } 1323 | // leftward 1324 | else { 1325 | if (!va || va.x > xr) { 1326 | va = this.createVertex(xr, fm * xr + fb); 1327 | } 1328 | else if (va.x < xl) { 1329 | return false; 1330 | } 1331 | vb = this.createVertex(xl, fm * xl + fb); 1332 | } 1333 | } 1334 | edge.va = va; 1335 | edge.vb = vb; 1336 | 1337 | return true; 1338 | }; 1339 | 1340 | // line-clipping code taken from: 1341 | // Liang-Barsky function by Daniel White 1342 | // http://www.skytopia.com/project/articles/compsci/clipping.html 1343 | // Thanks! 1344 | // A bit modified to minimize code paths 1345 | Voronoi.prototype.clipEdge = function (edge, bbox) { 1346 | var ax = edge.va.x, 1347 | ay = edge.va.y, 1348 | bx = edge.vb.x, 1349 | by = edge.vb.y, 1350 | t0 = 0, 1351 | t1 = 1, 1352 | dx = bx - ax, 1353 | dy = by - ay; 1354 | // left 1355 | var q = ax - bbox.xl; 1356 | if (dx === 0 && q < 0) { 1357 | return false; 1358 | } 1359 | var r = -q / dx; 1360 | if (dx < 0) { 1361 | if (r < t0) { 1362 | return false; 1363 | } 1364 | if (r < t1) { 1365 | t1 = r; 1366 | } 1367 | } 1368 | else if (dx > 0) { 1369 | if (r > t1) { 1370 | return false; 1371 | } 1372 | if (r > t0) { 1373 | t0 = r; 1374 | } 1375 | } 1376 | // right 1377 | q = bbox.xr - ax; 1378 | if (dx === 0 && q < 0) { 1379 | return false; 1380 | } 1381 | r = q / dx; 1382 | if (dx < 0) { 1383 | if (r > t1) { 1384 | return false; 1385 | } 1386 | if (r > t0) { 1387 | t0 = r; 1388 | } 1389 | } 1390 | else if (dx > 0) { 1391 | if (r < t0) { 1392 | return false; 1393 | } 1394 | if (r < t1) { 1395 | t1 = r; 1396 | } 1397 | } 1398 | // top 1399 | q = ay - bbox.yt; 1400 | if (dy === 0 && q < 0) { 1401 | return false; 1402 | } 1403 | r = -q / dy; 1404 | if (dy < 0) { 1405 | if (r < t0) { 1406 | return false; 1407 | } 1408 | if (r < t1) { 1409 | t1 = r; 1410 | } 1411 | } 1412 | else if (dy > 0) { 1413 | if (r > t1) { 1414 | return false; 1415 | } 1416 | if (r > t0) { 1417 | t0 = r; 1418 | } 1419 | } 1420 | // bottom 1421 | q = bbox.yb - ay; 1422 | if (dy === 0 && q < 0) { 1423 | return false; 1424 | } 1425 | r = q / dy; 1426 | if (dy < 0) { 1427 | if (r > t1) { 1428 | return false; 1429 | } 1430 | if (r > t0) { 1431 | t0 = r; 1432 | } 1433 | } 1434 | else if (dy > 0) { 1435 | if (r < t0) { 1436 | return false; 1437 | } 1438 | if (r < t1) { 1439 | t1 = r; 1440 | } 1441 | } 1442 | 1443 | // if we reach this point, Voronoi edge is within bbox 1444 | 1445 | // if t0 > 0, va needs to change 1446 | // rhill 2011-06-03: we need to create a new vertex rather 1447 | // than modifying the existing one, since the existing 1448 | // one is likely shared with at least another edge 1449 | if (t0 > 0) { 1450 | edge.va = this.createVertex(ax + t0 * dx, ay + t0 * dy); 1451 | } 1452 | 1453 | // if t1 < 1, vb needs to change 1454 | // rhill 2011-06-03: we need to create a new vertex rather 1455 | // than modifying the existing one, since the existing 1456 | // one is likely shared with at least another edge 1457 | if (t1 < 1) { 1458 | edge.vb = this.createVertex(ax + t1 * dx, ay + t1 * dy); 1459 | } 1460 | 1461 | // va and/or vb were clipped, thus we will need to close 1462 | // cells which use this edge. 1463 | if (t0 > 0 || t1 < 1) { 1464 | this.cells[edge.lSite.voronoiId].closeMe = true; 1465 | this.cells[edge.rSite.voronoiId].closeMe = true; 1466 | } 1467 | 1468 | return true; 1469 | }; 1470 | 1471 | // Connect/cut edges at bounding box 1472 | Voronoi.prototype.clipEdges = function (bbox) { 1473 | // connect all dangling edges to bounding box 1474 | // or get rid of them if it can't be done 1475 | var edges = this.edges, 1476 | iEdge = edges.length, 1477 | edge, 1478 | abs_fn = Math.abs; 1479 | 1480 | // iterate backward so we can splice safely 1481 | while (iEdge--) { 1482 | edge = edges[iEdge]; 1483 | // edge is removed if: 1484 | // it is wholly outside the bounding box 1485 | // it is looking more like a point than a line 1486 | if (!this.connectEdge(edge, bbox) || !this.clipEdge(edge, bbox) || 1487 | (abs_fn(edge.va.x - edge.vb.x) < 1e-9 && abs_fn(edge.va.y - edge.vb.y) < 1e-9)) { 1488 | edge.va = edge.vb = null; 1489 | edges.splice(iEdge, 1); 1490 | } 1491 | } 1492 | }; 1493 | 1494 | // Close the cells. 1495 | // The cells are bound by the supplied bounding box. 1496 | // Each cell refers to its associated site, and a list 1497 | // of halfedges ordered counterclockwise. 1498 | Voronoi.prototype.closeCells = function (bbox) { 1499 | var xl = bbox.xl, 1500 | xr = bbox.xr, 1501 | yt = bbox.yt, 1502 | yb = bbox.yb, 1503 | cells = this.cells, 1504 | iCell = cells.length, 1505 | cell, 1506 | iLeft, 1507 | halfedges, nHalfedges, 1508 | edge, 1509 | va, vb, vz, 1510 | lastBorderSegment, 1511 | abs_fn = Math.abs; 1512 | 1513 | while (iCell--) { 1514 | cell = cells[iCell]; 1515 | // prune, order halfedges counterclockwise, then add missing ones 1516 | // required to close cells 1517 | if (!cell.prepareHalfedges()) { 1518 | continue; 1519 | } 1520 | if (!cell.closeMe) { 1521 | continue; 1522 | } 1523 | // find first 'unclosed' point. 1524 | // an 'unclosed' point will be the end point of a halfedge which 1525 | // does not match the start point of the following halfedge 1526 | halfedges = cell.halfedges; 1527 | nHalfedges = halfedges.length; 1528 | // special case: only one site, in which case, the viewport is the cell 1529 | // ... 1530 | 1531 | // all other cases 1532 | iLeft = 0; 1533 | while (iLeft < nHalfedges) { 1534 | va = halfedges[iLeft].getEndpoint(); 1535 | vz = halfedges[(iLeft + 1) % nHalfedges].getStartpoint(); 1536 | // if end point is not equal to start point, we need to add the missing 1537 | // halfedge(s) up to vz 1538 | if (abs_fn(va.x - vz.x) >= 1e-9 || abs_fn(va.y - vz.y) >= 1e-9) { 1539 | 1540 | // rhill 2013-12-02: 1541 | // "Holes" in the halfedges are not necessarily always adjacent. 1542 | // https://github.com/gorhill/Javascript-Voronoi/issues/16 1543 | 1544 | // find entry point: 1545 | switch (true) { 1546 | 1547 | // walk downward along left side 1548 | case this.equalWithEpsilon(va.x, xl) && this.lessThanWithEpsilon(va.y, yb): 1549 | lastBorderSegment = this.equalWithEpsilon(vz.x, xl); 1550 | vb = this.createVertex(xl, lastBorderSegment ? vz.y : yb); 1551 | edge = this.createBorderEdge(cell.site, va, vb); 1552 | iLeft++; 1553 | halfedges.splice(iLeft, 0, this.createHalfedge(edge, cell.site, null)); 1554 | nHalfedges++; 1555 | if (lastBorderSegment) { 1556 | break; 1557 | } 1558 | va = vb; 1559 | // fall through 1560 | 1561 | // walk rightward along bottom side 1562 | case this.equalWithEpsilon(va.y, yb) && this.lessThanWithEpsilon(va.x, xr): 1563 | lastBorderSegment = this.equalWithEpsilon(vz.y, yb); 1564 | vb = this.createVertex(lastBorderSegment ? vz.x : xr, yb); 1565 | edge = this.createBorderEdge(cell.site, va, vb); 1566 | iLeft++; 1567 | halfedges.splice(iLeft, 0, this.createHalfedge(edge, cell.site, null)); 1568 | nHalfedges++; 1569 | if (lastBorderSegment) { 1570 | break; 1571 | } 1572 | va = vb; 1573 | // fall through 1574 | 1575 | // walk upward along right side 1576 | case this.equalWithEpsilon(va.x, xr) && this.greaterThanWithEpsilon(va.y, yt): 1577 | lastBorderSegment = this.equalWithEpsilon(vz.x, xr); 1578 | vb = this.createVertex(xr, lastBorderSegment ? vz.y : yt); 1579 | edge = this.createBorderEdge(cell.site, va, vb); 1580 | iLeft++; 1581 | halfedges.splice(iLeft, 0, this.createHalfedge(edge, cell.site, null)); 1582 | nHalfedges++; 1583 | if (lastBorderSegment) { 1584 | break; 1585 | } 1586 | va = vb; 1587 | // fall through 1588 | 1589 | // walk leftward along top side 1590 | case this.equalWithEpsilon(va.y, yt) && this.greaterThanWithEpsilon(va.x, xl): 1591 | lastBorderSegment = this.equalWithEpsilon(vz.y, yt); 1592 | vb = this.createVertex(lastBorderSegment ? vz.x : xl, yt); 1593 | edge = this.createBorderEdge(cell.site, va, vb); 1594 | iLeft++; 1595 | halfedges.splice(iLeft, 0, this.createHalfedge(edge, cell.site, null)); 1596 | nHalfedges++; 1597 | if (lastBorderSegment) { 1598 | break; 1599 | } 1600 | va = vb; 1601 | // fall through 1602 | 1603 | // walk downward along left side 1604 | lastBorderSegment = this.equalWithEpsilon(vz.x, xl); 1605 | vb = this.createVertex(xl, lastBorderSegment ? vz.y : yb); 1606 | edge = this.createBorderEdge(cell.site, va, vb); 1607 | iLeft++; 1608 | halfedges.splice(iLeft, 0, this.createHalfedge(edge, cell.site, null)); 1609 | nHalfedges++; 1610 | if (lastBorderSegment) { 1611 | break; 1612 | } 1613 | va = vb; 1614 | // fall through 1615 | 1616 | // walk rightward along bottom side 1617 | lastBorderSegment = this.equalWithEpsilon(vz.y, yb); 1618 | vb = this.createVertex(lastBorderSegment ? vz.x : xr, yb); 1619 | edge = this.createBorderEdge(cell.site, va, vb); 1620 | iLeft++; 1621 | halfedges.splice(iLeft, 0, this.createHalfedge(edge, cell.site, null)); 1622 | nHalfedges++; 1623 | if (lastBorderSegment) { 1624 | break; 1625 | } 1626 | va = vb; 1627 | // fall through 1628 | 1629 | // walk upward along right side 1630 | lastBorderSegment = this.equalWithEpsilon(vz.x, xr); 1631 | vb = this.createVertex(xr, lastBorderSegment ? vz.y : yt); 1632 | edge = this.createBorderEdge(cell.site, va, vb); 1633 | iLeft++; 1634 | halfedges.splice(iLeft, 0, this.createHalfedge(edge, cell.site, null)); 1635 | nHalfedges++; 1636 | if (lastBorderSegment) { 1637 | break; 1638 | } 1639 | // fall through 1640 | 1641 | default: 1642 | throw "Voronoi.closeCells() > this makes no sense!"; 1643 | } 1644 | } 1645 | iLeft++; 1646 | } 1647 | cell.closeMe = false; 1648 | } 1649 | }; 1650 | 1651 | // --------------------------------------------------------------------------- 1652 | // Debugging helper 1653 | /* 1654 | Voronoi.prototype.dumpBeachline = function(y) { 1655 | console.log('Voronoi.dumpBeachline(%f) > Beachsections, from left to right:', y); 1656 | if ( !this.beachline ) { 1657 | console.log(' None'); 1658 | } 1659 | else { 1660 | var bs = this.beachline.getFirst(this.beachline.root); 1661 | while ( bs ) { 1662 | console.log(' site %d: xl: %f, xr: %f', bs.site.voronoiId, this.leftBreakPoint(bs, y), this.rightBreakPoint(bs, y)); 1663 | bs = bs.rbNext; 1664 | } 1665 | } 1666 | }; 1667 | */ 1668 | 1669 | // --------------------------------------------------------------------------- 1670 | // Helper: Quantize sites 1671 | 1672 | // rhill 2013-10-12: 1673 | // This is to solve https://github.com/gorhill/Javascript-Voronoi/issues/15 1674 | // Since not all users will end up using the kind of coord values which would 1675 | // cause the issue to arise, I chose to let the user decide whether or not 1676 | // he should sanitize his coord values through this helper. This way, for 1677 | // those users who uses coord values which are known to be fine, no overhead is 1678 | // added. 1679 | 1680 | Voronoi.prototype.quantizeSites = function (sites) { 1681 | var ε = this.ε, 1682 | n = sites.length, 1683 | site; 1684 | while (n--) { 1685 | site = sites[n]; 1686 | site.x = Math.floor(site.x / ε) * ε; 1687 | site.y = Math.floor(site.y / ε) * ε; 1688 | } 1689 | }; 1690 | 1691 | // --------------------------------------------------------------------------- 1692 | // Helper: Recycle diagram: all vertex, edge and cell objects are 1693 | // "surrendered" to the Voronoi object for reuse. 1694 | // TODO: rhill-voronoi-core v2: more performance to be gained 1695 | // when I change the semantic of what is returned. 1696 | 1697 | Voronoi.prototype.recycle = function (diagram) { 1698 | if (diagram) { 1699 | if (diagram instanceof this.Diagram) { 1700 | this.toRecycle = diagram; 1701 | } 1702 | else { 1703 | throw 'Voronoi.recycleDiagram() > Need a Diagram object.'; 1704 | } 1705 | } 1706 | }; 1707 | 1708 | // --------------------------------------------------------------------------- 1709 | // Top-level Fortune loop 1710 | 1711 | // rhill 2011-05-19: 1712 | // Voronoi sites are kept client-side now, to allow 1713 | // user to freely modify content. At compute time, 1714 | // *references* to sites are copied locally. 1715 | 1716 | Voronoi.prototype.compute = function (sites, bbox) { 1717 | // to measure execution time 1718 | var startTime = new Date(); 1719 | 1720 | // init internal state 1721 | this.reset(); 1722 | 1723 | // any diagram data available for recycling? 1724 | // I do that here so that this is included in execution time 1725 | if (this.toRecycle) { 1726 | this.vertexJunkyard = this.vertexJunkyard.concat(this.toRecycle.vertices); 1727 | this.edgeJunkyard = this.edgeJunkyard.concat(this.toRecycle.edges); 1728 | this.cellJunkyard = this.cellJunkyard.concat(this.toRecycle.cells); 1729 | this.toRecycle = null; 1730 | } 1731 | 1732 | // Initialize site event queue 1733 | var siteEvents = sites.slice(0); 1734 | siteEvents.sort(function (a, b) { 1735 | var r = b.y - a.y; 1736 | if (r) { 1737 | return r; 1738 | } 1739 | return b.x - a.x; 1740 | }); 1741 | 1742 | // process queue 1743 | var site = siteEvents.pop(), 1744 | siteid = 0, 1745 | xsitex, // to avoid duplicate sites 1746 | xsitey, 1747 | cells = this.cells, 1748 | circle; 1749 | 1750 | // main loop 1751 | for (; ;) { 1752 | // we need to figure whether we handle a site or circle event 1753 | // for this we find out if there is a site event and it is 1754 | // 'earlier' than the circle event 1755 | circle = this.firstCircleEvent; 1756 | 1757 | // add beach section 1758 | if (site && (!circle || site.y < circle.y || (site.y === circle.y && site.x < circle.x))) { 1759 | // only if site is not a duplicate 1760 | if (site.x !== xsitex || site.y !== xsitey) { 1761 | // first create cell for new site 1762 | cells[siteid] = this.createCell(site); 1763 | site.voronoiId = siteid++; 1764 | // then create a beachsection for that site 1765 | this.addBeachsection(site); 1766 | // remember last site coords to detect duplicate 1767 | xsitey = site.y; 1768 | xsitex = site.x; 1769 | } 1770 | site = siteEvents.pop(); 1771 | } 1772 | 1773 | // remove beach section 1774 | else if (circle) { 1775 | this.removeBeachsection(circle.arc); 1776 | } 1777 | 1778 | // all done, quit 1779 | else { 1780 | break; 1781 | } 1782 | } 1783 | 1784 | // wrapping-up: 1785 | // connect dangling edges to bounding box 1786 | // cut edges as per bounding box 1787 | // discard edges completely outside bounding box 1788 | // discard edges which are point-like 1789 | this.clipEdges(bbox); 1790 | 1791 | // add missing edges in order to close opened cells 1792 | this.closeCells(bbox); 1793 | 1794 | // to measure execution time 1795 | var stopTime = new Date(); 1796 | 1797 | // prepare return values 1798 | var diagram = new this.Diagram(); 1799 | diagram.cells = this.cells; 1800 | diagram.edges = this.edges; 1801 | diagram.vertices = this.vertices; 1802 | diagram.execTime = stopTime.getTime() - startTime.getTime(); 1803 | 1804 | // clean up 1805 | this.reset(); 1806 | 1807 | return diagram; 1808 | }; 1809 | -------------------------------------------------------------------------------- /simplify.js: -------------------------------------------------------------------------------- 1 | /* 2 | (c) 2013, Vladimir Agafonkin 3 | Simplify.js, a high-performance JS polyline simplification library 4 | mourner.github.io/simplify-js 5 | */ 6 | 7 | (function () { 8 | "use strict"; 9 | 10 | // to suit your point format, run search/replace for '.x' and '.y'; 11 | // for 3D version, see 3d branch 12 | 13 | function getX(p) { 14 | return p.x; 15 | } 16 | 17 | function getY(p) { 18 | return p.y; 19 | } 20 | 21 | // square distance between 2 points 22 | function getSqDist(p1, p2) { 23 | 24 | var dx = getX(p1) - getX(p2), 25 | dy = getY(p1) - getY(p2); 26 | 27 | return dx * dx + dy * dy; 28 | } 29 | 30 | // square distance from a point to a segment 31 | function getSqSegDist(p, p1, p2) { 32 | 33 | var x = getX(p1), 34 | y = getY(p1), 35 | dx = getX(p2) - x, 36 | dy = getY(p2) - y; 37 | 38 | if (dx !== 0 || dy !== 0) { 39 | 40 | var t = ((getX(p) - x) * dx + (getY(p) - y) * dy) / (dx * dx + dy * dy); 41 | 42 | if (t > 1) { 43 | x = getX(p2); 44 | y = getY(p2); 45 | 46 | } else if (t > 0) { 47 | x += dx * t; 48 | y += dy * t; 49 | } 50 | } 51 | 52 | dx = getX(p) - x; 53 | dy = getY(p) - y; 54 | 55 | return dx * dx + dy * dy; 56 | } 57 | 58 | // rest of the code doesn't care about point format 59 | 60 | // basic distance-based simplification 61 | function simplifyRadialDist(points, sqTolerance) { 62 | 63 | var prevPoint = points[0], 64 | newPoints = [prevPoint], 65 | point; 66 | 67 | for (var i = 1, len = points.length; i < len; i++) { 68 | point = points[i]; 69 | 70 | if (getSqDist(point, prevPoint) > sqTolerance) { 71 | newPoints.push(point); 72 | prevPoint = point; 73 | } 74 | } 75 | 76 | if (prevPoint !== point) { 77 | newPoints.push(point); 78 | } 79 | 80 | return newPoints; 81 | } 82 | 83 | // simplification using optimized Douglas-Peucker algorithm with recursion elimination 84 | function simplifyDouglasPeucker(points, sqTolerance) { 85 | 86 | var len = points.length, 87 | MarkerArray = typeof Uint8Array !== 'undefined' ? Uint8Array : Array, 88 | markers = new MarkerArray(len), 89 | first = 0, 90 | last = len - 1, 91 | stack = [], 92 | newPoints = [], 93 | i, maxSqDist, sqDist, index; 94 | 95 | markers[first] = markers[last] = 1; 96 | 97 | while (last) { 98 | 99 | maxSqDist = 0; 100 | 101 | for (i = first + 1; i < last; i++) { 102 | sqDist = getSqSegDist(points[i], points[first], points[last]); 103 | 104 | if (sqDist > maxSqDist) { 105 | index = i; 106 | maxSqDist = sqDist; 107 | } 108 | } 109 | 110 | if (maxSqDist > sqTolerance) { 111 | markers[index] = 1; 112 | stack.push(first, index, index, last); 113 | } 114 | 115 | last = stack.pop(); 116 | first = stack.pop(); 117 | } 118 | 119 | for (i = 0; i < len; i++) { 120 | if (markers[i]) { 121 | newPoints.push(points[i]); 122 | } 123 | } 124 | 125 | return newPoints; 126 | } 127 | 128 | // both algorithms combined for awesome performance 129 | function simplify(points, tolerance, highestQuality) { 130 | 131 | var sqTolerance = tolerance !== undefined ? tolerance * tolerance : 1; 132 | 133 | points = highestQuality ? points : simplifyRadialDist(points, sqTolerance); 134 | points = simplifyDouglasPeucker(points, sqTolerance); 135 | 136 | return points; 137 | } 138 | 139 | // export as AMD module / Node module / browser variable 140 | if (typeof define === 'function' && define.amd) { 141 | define(function () { 142 | return simplify; 143 | }); 144 | } else if (typeof module !== 'undefined') { 145 | module.exports = simplify; 146 | } else { 147 | window.simplify = simplify; 148 | } 149 | 150 | })(); -------------------------------------------------------------------------------- /solver.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var solver = (function () { 3 | function det2x2(a, b, c, d) { 4 | var C = Math.pow(2, 27) + 1; 5 | 6 | function veltkamp(val) { 7 | var p = val * C; 8 | var q = val - p; 9 | var val1 = p + q; 10 | return [val1, val - val1]; 11 | } 12 | 13 | function mult(a, b) { 14 | var va = veltkamp(a); 15 | var vb = veltkamp(b); 16 | var r1 = a * b; 17 | var t1 = -r1 + va[0] * vb[0]; 18 | var t2 = t1 + va[0] * vb[1]; 19 | var t3 = t2 + va[1] * vb[0]; 20 | var r2 = t3 + va[1] * vb[1]; 21 | return [r1, r2]; 22 | } 23 | 24 | function twoDiff(a, b) { 25 | var s = a - b; 26 | var bb = s - a; 27 | var err = (a - (s - bb)) - (b + bb); 28 | return [s, err]; 29 | } 30 | 31 | function quickTwoSum(a, b) { 32 | var res = a + b; 33 | var e = b - (res - a); 34 | return [res, e]; 35 | } 36 | 37 | function subtraction(a, b) { 38 | var s = twoDiff(a[0], b[0]); 39 | var t = twoDiff(a[1], b[1]); 40 | var s2 = s[1] + t[1]; 41 | var g = quickTwoSum(s[0], s2); 42 | s2 = g[1] + t[1]; 43 | return quickTwoSum(g[0], s2); 44 | } 45 | 46 | var v1 = mult(a, d); 47 | var v2 = mult(b, c); 48 | var res = subtraction(v1, v2); 49 | return res[0]; 50 | } 51 | 52 | function EquationSystemCreator() { 53 | this.quadraticEquations = []; 54 | this.linearEquations = []; 55 | this.segments = []; 56 | } 57 | 58 | EquationSystemCreator.prototype = { 59 | _addEquation: function (quadratic, a, b, k, c) { 60 | var equation = {a: a, b: b, k: k, c: c}; 61 | if (quadratic) 62 | this.quadraticEquations.push(equation); 63 | else 64 | this.linearEquations.push(equation); 65 | }, 66 | addVertex: function (vertex) { 67 | this._addEquation(1, -2 * vertex.x, -2 * vertex.y, 0, vertex.x * vertex.x + vertex.y * vertex.y); 68 | return this; 69 | }, 70 | addSegment: function (segment) { 71 | //here is some cheating. 72 | // if we receive 2 collinear segments having a common point, we substitute the second segment by the common point 73 | // this case arises with collinear edges. 74 | for (var i = 0; i < this.segments.length; i++) { 75 | var other = this.segments[i]; 76 | var commonPoint = commonVertex(segment, other); 77 | var v1 = segmentToVector(segment); 78 | var v2 = segmentToVector(other); 79 | if (commonPoint && det2x2(v1.x, v1.y, v2.x, v2.y) == 0) 80 | return this.addVertex(commonPoint); 81 | } 82 | this.segments.push(segment); 83 | var length = segLength(segment); 84 | var slope = det2x2(segment[1].x, segment[0].x, segment[1].y, segment[0].y) / length; 85 | var a = (segment[1].y - segment[0].y) / length; 86 | var b = -(segment[1].x - segment[0].x) / length; 87 | this._addEquation(0, a, b, 1, slope); 88 | return this; 89 | } 90 | }; 91 | 92 | // https://github.com/aewallin/openvoronoi/blob/master/src/solvers/solver_qll.hpp 93 | // http://www.payne.org/index.php/Calculating_Voronoi_Nodes 94 | function solveEquations(creator, solutionsFilter) { 95 | function subtractEquations(eLeft, eRight) { 96 | var newEquation = {}; 97 | for (var key in eLeft) 98 | if (eLeft.hasOwnProperty(key)) 99 | newEquation[key] = eLeft[key] - eRight[key]; 100 | return newEquation; 101 | } 102 | 103 | if (!solutionsFilter) 104 | solutionsFilter = function () { 105 | return true; 106 | }; 107 | function solve(linearEquations, xi, yi, ti, quadraticEquation) { 108 | var firstEq = linearEquations[0]; 109 | var secondEq = linearEquations[1]; 110 | var determinant = det2x2(firstEq[xi], secondEq[xi], firstEq[yi], secondEq[yi]); 111 | if (determinant == 0) 112 | return []; 113 | var a0 = det2x2(firstEq[yi], secondEq[yi], firstEq[ti], secondEq[ti]) / determinant; 114 | var a1 = -det2x2(firstEq[xi], secondEq[xi], firstEq[ti], secondEq[ti]) / determinant; 115 | var b0 = det2x2(firstEq[yi], secondEq[yi], firstEq.c, secondEq.c) / determinant; 116 | var b1 = -det2x2(firstEq[xi], secondEq[xi], firstEq.c, secondEq.c) / determinant; 117 | 118 | var aargs = { 119 | a: [1, quadraticEquation.a], 120 | b: [1, quadraticEquation.b], 121 | k: [-1, quadraticEquation.k] 122 | }; 123 | var solutions = qll_solve(aargs[xi][0], aargs[xi][1], 124 | aargs[yi][0], aargs[yi][1], 125 | aargs[ti][0], aargs[ti][1], 126 | quadraticEquation.c, // xk*xk + yk*yk - rk*rk, 127 | a0, b0, a1, b1); 128 | var realSolutions = []; 129 | for (var i = 0; i < solutions.length; i++) { 130 | var o = {}; 131 | o[xi] = solutions[i].a; 132 | o[yi] = solutions[i].b; 133 | o[ti] = solutions[i].k; 134 | var point = {x: o.a, y: o.b, r: o.k}; 135 | if (solutionsFilter(point)) 136 | realSolutions.push(point); 137 | } 138 | return realSolutions; 139 | } 140 | 141 | function qll_solve(a0, b0, c0, d0, e0, f0, g0, a1, b1, a2, b2) { 142 | var a = a0 * (a1 * a1) + c0 * (a2 * a2) + e0; 143 | var b = 2 * a0 * a1 * b1 + 2 * a2 * b2 * c0 + a1 * b0 + a2 * d0 + f0; 144 | var c = a0 * (b1 * b1) + c0 * (b2 * b2) + b0 * b1 + b2 * d0 + g0; 145 | var roots = quadratic_roots(a, b, c); 146 | 147 | if (roots.length == 0) 148 | return []; 149 | var solutions = []; 150 | for (var i = 0; i < roots.length; i++) { 151 | var w = roots[i]; 152 | solutions.push({a: a1 * w + b1, b: a2 * w + b2, k: w}); 153 | } 154 | return solutions; 155 | } 156 | 157 | function quadratic_roots(a, b, c) { 158 | if (!a && !b) 159 | return []; 160 | if (!a) 161 | return [-c / b]; 162 | if (!b) { 163 | var sqr = -c / a; 164 | if (sqr > 0) 165 | return [Math.sqrt(sqr), -Math.sqrt(sqr)]; 166 | else if (sqr == 0) 167 | return [0]; 168 | return []; 169 | } 170 | var discriminant = det2x2(b, 4 * a, c, b); // b * b - 4 * a * c; 171 | if (discriminant > 0) { 172 | var q = b > 0 ? (b + Math.sqrt(discriminant)) / -2 : (b - Math.sqrt(discriminant)) / -2; 173 | return [q / a, c / q]; 174 | //not really proud of that one, but I found cases where I couldn't get a good result 175 | } else if (discriminant == 0 || Math.abs(discriminant / (b * b)) < Math.pow(2, -50)) 176 | return [-b / (2 * a)]; 177 | return []; 178 | } 179 | 180 | function clone(array) { 181 | return array.slice(0); 182 | } 183 | 184 | function solveLinear(equations) { 185 | var matrix = []; 186 | var rhs = []; 187 | for (var i = 0; i < equations.length; i++) { 188 | var equation = equations[i]; 189 | matrix.push([equation.a, equation.b, equation.k]); 190 | rhs.push(-equation.c); 191 | } 192 | var result = numeric.solve(matrix, rhs); 193 | if (isFinite(result[0]) && isFinite(result[1]) && isFinite(result[2])) { 194 | var res = {x: result[0], y: result[1], r: result[2]}; 195 | if (solutionsFilter(res)) 196 | return [res]; 197 | } 198 | return []; 199 | } 200 | 201 | var quadraticEquations = clone(creator.quadraticEquations); 202 | var linearEquations = clone(creator.linearEquations); 203 | var quadLength = quadraticEquations.length; 204 | var linLength = linearEquations.length; 205 | if (quadLength + linLength != 3) 206 | throw new Error('should have 3 equations, had ' + quadLength + ' quadratic equations and ' + linLength + ' linear equations.'); 207 | 208 | if (quadraticEquations.length == 0) 209 | return solveLinear(linearEquations); 210 | 211 | var firstQuad = quadraticEquations[0]; 212 | if (quadraticEquations.length > 1) 213 | for (var i = 1; i < quadLength; i++) 214 | linearEquations.push(subtractEquations(quadraticEquations[i], firstQuad)); 215 | var solutions = []; 216 | solutions = solutions.concat(solve(linearEquations, 'a', 'b', 'k', firstQuad)); 217 | if (!solutions.length) 218 | solutions = solutions.concat(solve(linearEquations, 'k', 'a', 'b', firstQuad)); 219 | if (!solutions.length) 220 | solutions = solutions.concat(solve(linearEquations, 'b', 'k', 'a', firstQuad)); 221 | return solutions; 222 | } 223 | 224 | return { 225 | solveEquations: solveEquations, 226 | EquationSystemCreator: EquationSystemCreator 227 | }; 228 | })(); 229 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | 4 | function cleanupIntersectionsForDisplay(intersections) { 5 | var res = []; 6 | for (var i = 0; i < intersections.length; i++) 7 | res.push(intersections[i][0]); 8 | return res; 9 | } 10 | 11 | test('2 segments intersections', function () { 12 | var s1 = [p(0, 0), p(20, 20)]; 13 | var s2 = [p(20, 0), p(0, 20)]; 14 | var res = intersectionSegments(s1, s2); 15 | var expected = {x: 10, y: 10}; 16 | deepEqual(res, expected); 17 | displaySegmentsAndPoint([s1, s2], [expected], [res]); 18 | }); 19 | 20 | test('horizontal-vertical intersection', function () { 21 | var s1 = [p(0, 30), p(60, 30)]; 22 | var s2 = [p(30, 0), p(30, 60)]; 23 | var res = [intersectionSegments(s1, s2)]; 24 | var expected = [p(30, 30)]; 25 | deepEqual(res, expected, 'direct computation'); 26 | var segments = [s1, s2]; 27 | displaySegmentsAndPoint(segments, expected, res); 28 | res = cleanupIntersectionsForDisplay(bentleyOttmann(segments)); 29 | deepEqual(res, expected, 'in Bentley-Ottmann'); 30 | displaySegmentsAndPoint(segments, expected, res); 31 | }); 32 | 33 | test('2 almost horizontal segments intersections', function () { 34 | var epsilon = Math.pow(2, -50); 35 | var big = Math.pow(2, 52); 36 | var s1 = [p(1 - big, 1 - epsilon), p(1 + big, 1 + epsilon)]; 37 | var s2 = [p(1 - big, 1 + epsilon), p(1 + big, 1 - epsilon)]; 38 | var res = intersectionSegments(s1, s2); 39 | var expected = {x: 1, y: 1}; 40 | deepEqual(res, expected); 41 | }); 42 | 43 | test('2 almost vertical segments intersections', function () { 44 | var epsilon = Math.pow(2, -50); 45 | var big = Math.pow(2, 52); 46 | var s1 = [p(1 - epsilon, 1 - big), p(1 + epsilon, 1 + big)]; 47 | var s2 = [p(1 + epsilon, 1 - big), p(1 - epsilon, 1 + big)]; 48 | var res = intersectionSegments(s1, s2); 49 | var expected = {x: 1, y: 1}; 50 | deepEqual(res, expected); 51 | }); 52 | 53 | test('3 segments array intersections', function () { 54 | var s1 = [p(0, 0), p(20, 20)]; 55 | var s2 = [p(20, 0), p(0, 20)]; 56 | var s3 = [p(0, 10), p(10, 20)]; 57 | var segments = [s1, s2, s3]; 58 | var res = createIntersections(segments); 59 | var expected = [ 60 | [p(10, 10), [s1, s2]], 61 | [p(5, 15), [s2, s3]] 62 | ]; 63 | deepEqual(res, expected); 64 | expected = cleanupIntersectionsForDisplay(expected); 65 | res = cleanupIntersectionsForDisplay(res); 66 | displaySegmentsAndPoint(segments, res, expected); 67 | }); 68 | 69 | test('initialization of event queue', function () { 70 | var expected = [p(0, 0), p(0, 10), p(0, 20), p(10, 20), p(20, 0), p(20, 20)]; 71 | var s1 = [expected[0], expected[5]]; 72 | var s2 = [expected[4], expected[2]]; 73 | var s3 = [expected[1], expected[3]]; 74 | var segments = [s1, s2, s3]; 75 | var queue = initialPopulationOfQueue(segments); 76 | var res = []; 77 | ok(!queue.isEmpty(), 'queue is not empty'); 78 | while (!queue.isEmpty()) 79 | res.push(queue.fetchFirst().point); 80 | deepEqual(res, expected, 'events in correct order'); 81 | ok(queue.isEmpty(), 'queue is empty after dumping the events'); 82 | displaySegmentsAndPoint(segments, res, expected); 83 | }); 84 | 85 | test('adding event to queue', function () { 86 | var expected = [p(0, 0), p(0, 20), p(10, 20), p(20, 20)]; 87 | var s1 = [expected[0], expected[3]]; 88 | var segments = [s1]; 89 | var queue = initialPopulationOfQueue(segments); 90 | queue.pushIntersectionEvent({point: expected[2]}); 91 | queue.pushIntersectionEvent({point: expected[1]}); 92 | var res = []; 93 | ok(!queue.isEmpty(), 'queue is not empty'); 94 | while (!queue.isEmpty()) 95 | res.push(queue.fetchFirst().point); 96 | deepEqual(res, expected, 'events in correct order'); 97 | ok(queue.isEmpty(), 'queue is empty after dumping the events'); 98 | displaySegmentsAndPoint(segments, res, expected); 99 | }); 100 | 101 | test('adding left events to beam', function () { 102 | var s1 = [p(0, 0), p(20, 20)]; 103 | var s2 = [p(20, 0), p(0, 20)]; 104 | var s3 = [p(0, 10), p(5, 10)]; 105 | var segments = [s1, s2, s3]; 106 | var res = bentleyOttmann(segments); 107 | var expected = [p(10, 10)]; 108 | res = cleanupIntersectionsForDisplay(res); 109 | deepEqual(res, expected); 110 | displaySegmentsAndPoint(segments, expected, res); 111 | }); 112 | 113 | test('beam 2 segments step by step checking', function () { 114 | var s1 = [p(0, 0), p(20, 20)]; 115 | var s2 = [p(20, 0), p(0, 20)]; 116 | var queue = createMockEventQueue([]); 117 | var beam = createMockScanBeam(); 118 | beam.leftPoint(s1[0], s1, queue); 119 | beam.leftPoint(s2[1], s2, queue); 120 | var segments = [s1, s2]; 121 | deepEqual(beam.dumpBeam(), segments, 'beam contains [s1, s2]'); 122 | deepEqual(queue.dumpQueue()[0].point, p(10, 10), 'queue event point is correct'); 123 | deepEqual(queue.dumpQueue()[0].type, 'intersection', 'queue event type is correct'); 124 | deepEqual(queue.dumpQueue()[0].segments, segments, 'queue intersection event segments is correct'); 125 | beam.intersectionPoint(p(10, 10), segments, queue); 126 | deepEqual(beam.dumpBeam(), [s2, s1], 'beam contains [s1, s2]'); 127 | var expected = [ 128 | [ 129 | p(10, 10), 130 | segments 131 | ] 132 | ]; 133 | deepEqual(beam.getResult(), expected, 'correct beam reults'); 134 | beam.rightPoint(s2[0], s2, queue); 135 | deepEqual(beam.dumpBeam(), [s1], 'beam contains [s1]'); 136 | beam.rightPoint(s1[1], s1, queue); 137 | deepEqual(beam.dumpBeam(), [], 'beam is empty'); 138 | var result = beam.getResult(); 139 | deepEqual(result, expected, 'correct beam reults'); 140 | displaySegmentsAndPoint(segments, cleanupIntersectionsForDisplay(expected), cleanupIntersectionsForDisplay(result)); 141 | }); 142 | 143 | test('beam 4 segments; intersection masked by intersection', function () { 144 | var s1 = [p(0, 0), p(20, 20)]; 145 | var s2 = [p(20, 0), p(0, 20)]; 146 | var s3 = [p(7.5, 5), p(20, 5)]; 147 | var s4 = [p(7.5, 15), p(20, 15)]; 148 | var segments = [s1, s2, s3, s4]; 149 | var res = bentleyOttmann(segments); 150 | var expected = [p(10, 10), p(15, 5), p(15, 15)]; 151 | res = cleanupIntersectionsForDisplay(res); 152 | deepEqual(res, expected, 'same intersections as expected'); 153 | displaySegmentsAndPoint(segments, expected, res); 154 | }); 155 | 156 | test('beam 3 segments; intersection masked by right segment', function () { 157 | var s1 = [p(5, 0), p(20, 20)]; 158 | var s2 = [p(20, 0), p(5, 20)]; 159 | var s3 = [p(0, 10), p(10, 10)]; 160 | var segments = [s1, s2, s3]; 161 | var res = bentleyOttmann(segments); 162 | var expected = [p(12.5, 10)]; 163 | res = cleanupIntersectionsForDisplay(res); 164 | deepEqual(res, expected, 'same intersection as expected'); 165 | displaySegmentsAndPoint(segments, expected, res); 166 | }); 167 | 168 | test('beam 3 segments; intersecting beam comes late', function () { 169 | var s1 = [p(0, 0), p(200, 110)]; 170 | var s2 = [p(0, 150), p(150, 90)]; 171 | var s3 = [p(50, 100), p(200, 100)]; 172 | var segments = [s1, s2, s3]; 173 | var res = bentleyOttmann(segments); 174 | var expected = [p(125, 100), p(181.8181818181818, 100)]; 175 | res = cleanupIntersectionsForDisplay(res); 176 | deepEqual(res, expected, 'same intersections as expected'); 177 | displaySegmentsAndPoint(segments, expected, res); 178 | }); 179 | 180 | test('eventQueue prevents double insertion', function () { 181 | var s1 = [p(0, 0), p(20, 20)]; 182 | var s2 = [p(20, 0), p(0, 20)]; 183 | var queue = createMockEventQueue([]); 184 | var event = {type: 'intersection', point: p(10, 10), segments: [s1, s2]}; 185 | queue.pushIntersectionEvent(event); 186 | deepEqual(queue.dumpQueue(), [event]); 187 | queue.pushIntersectionEvent({type: 'intersection', point: p(10, 10), segments: [s1, s2]}); 188 | deepEqual(queue.dumpQueue(), [event]); 189 | }); 190 | 191 | test('small random segment batch O(n^2) intersections against mock Bentley Ottmann', function () { 192 | var segments = []; 193 | for (var i = 0; i < 30; i++) 194 | segments.push([randomPoint(), randomPoint()]); 195 | var expected = cleanupIntersectionsForDisplay(createIntersections(segments)); 196 | var res = cleanupIntersectionsForDisplay(bentleyOttmann(segments)); 197 | expected.sort(comparePoint); 198 | res.sort(comparePoint); 199 | displaySegmentsAndPoint(segments, expected, res); 200 | deepEqual(res.length, expected.length, 'same number of points as expected'); 201 | deepEqual(res, expected, 'intersections are equal'); 202 | }); 203 | 204 | test('big random segment batch O(n^2) intersections against mock Bentley Ottmann', function () { 205 | var segments = []; 206 | for (var i = 0; i < 300; i++) 207 | segments.push([randomPoint(), randomPoint()]); 208 | var expected = cleanupIntersectionsForDisplay(createIntersections(segments)); 209 | var res = cleanupIntersectionsForDisplay(bentleyOttmann(segments)); 210 | expected.sort(comparePoint); 211 | res.sort(comparePoint); 212 | deepEqual(res.length, expected.length, 'same number of points as expected'); 213 | deepEqual(res, expected, 'intersections are equal'); 214 | }); 215 | 216 | test('rectangle', function () { 217 | var polygon = [p(10, 10), p(100, 10), p(100, 150), p(10, 150), p(10, 10)]; 218 | displayMessage(svgAssertTable(polygon2path(polygon), '', '')); 219 | }); 220 | 221 | test('point In Polygon', function () { 222 | var poly = [p(458, 39), 223 | p(458, 39), 224 | p(395, 46), 225 | p(308, 76), 226 | p(141, 64), 227 | p(100, 80), 228 | p(234, 89), 229 | p(343, 99), 230 | p(400, 115), 231 | p(405, 66), 232 | p(423, 50), 233 | p(419, 135), 234 | p(378, 205), 235 | p(337, 201), 236 | p(72, 185), 237 | p(73, 205), 238 | p(306, 213), 239 | p(375, 226), 240 | p(412, 261), 241 | p(343, 326), 242 | p(233, 330), 243 | p(74, 344), 244 | p(57, 316), 245 | p(133, 290), 246 | p(290, 291), 247 | p(366, 232), 248 | p(296, 222), 249 | p(172, 246), 250 | p(41, 214), 251 | p(35, 178), 252 | p(197, 171), 253 | p(350, 194), 254 | p(398, 140), 255 | p(326, 117), 256 | p(155, 92), 257 | p(28, 138), 258 | p(22, 71), 259 | p(113, 52), 260 | p(277, 64), 261 | p(326, 35), 262 | p(391, 25), 263 | p(456, 23), 264 | p(498, 15) 265 | ]; 266 | for (var i = 0; i < poly.length; i++) { 267 | var point = poly[i]; 268 | point.x /= 2.5; 269 | point.y /= 2.5; 270 | } 271 | 272 | var testPoint = {"x": 114.01865740403706, "y": 160.8977133216299}; 273 | equal(pointInPolygon(testPoint, poly), false); 274 | }); -------------------------------------------------------------------------------- /testImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nraynaud/polygonsJS/379e73d9b421b7454e548f32e1d3ee14a127cae4/testImage.png -------------------------------------------------------------------------------- /test_contour.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Testing Contour Extraction 6 | 7 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | 31 | 32 | 258 | 259 | -------------------------------------------------------------------------------- /test_display.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function pathList2svg(paths, test_display_viewBox) { 4 | let content = ''; 5 | for (let i = 0; i < paths.length; i++) 6 | content += '\n'; 7 | const viewBox = test_display_viewBox ? ' viewBox="' + test_display_viewBox + '"' : ''; 8 | return '' + content + ''; 9 | } 10 | 11 | function path2svg(path, cssClass) { 12 | return pathList2svg([ 13 | {d: path, cssClass: cssClass} 14 | ]); 15 | } 16 | 17 | function inTd(element) { 18 | return '' + element + ''; 19 | } 20 | 21 | function svgDisplayTable(table) { 22 | let div = '
Geometry: '; 23 | for (let i = 0; i < table.length; i++) 24 | div += ''; 25 | div += ''; 26 | for (let i = 0; i < table.length; i++) 27 | div += ''; 28 | div += '
' + table[i].label + '
' + table[i].content + '
'; 29 | QUnit.config.current.assertions.push({ 30 | result: true, 31 | message: div 32 | }); 33 | } 34 | 35 | function svgAssertTable(inputPath, outputPath, expectedPath) { 36 | const row = inTd(path2svg(inputPath, 'input')) 37 | + inTd(path2svg(outputPath, 'output')) 38 | + inTd(path2svg(expectedPath, 'expected')) 39 | + inTd(pathList2svg([ 40 | {d: expectedPath, cssClass: 'expected'}, 41 | {d: outputPath, cssClass: 'output'} 42 | ])); 43 | return '
Geometry: ' 44 | + row + '
InputActual OutputExpected OutputSuperposed
'; 45 | } 46 | 47 | function svgTable(title, obj) { 48 | const cols = [...Object.keys(obj)]; 49 | const header = cols.map((k) => '' + k + ''); 50 | const row = cols.map((k) => inTd(path2svg(obj[k], 'input'))); 51 | return '
' + title + '' + header + '' 52 | + row + '
' 53 | } 54 | 55 | function randomPoint() { 56 | return p(Math.random() * 200, Math.random() * 150); 57 | } 58 | 59 | function p(x, y) { 60 | return {x: x, y: y}; 61 | } 62 | 63 | function pp(x, y) { 64 | return x + ',' + y; 65 | } 66 | 67 | function ppp(point) { 68 | return pp(point.x, point.y); 69 | } 70 | 71 | function point2circlePath(center, radius) { 72 | if (radius == null) 73 | radius = 4; 74 | 75 | return 'M' + pp((center.x - radius), center.y) 76 | + ' a' + pp(radius, radius) + ' 0 1,0 ' + pp(radius * 2, 0) 77 | + ' a' + pp(radius, radius) + ' 0 1,0 ' + pp(-radius * 2, 0); 78 | } 79 | 80 | function pointArray2path(points, radius) { 81 | let res = ''; 82 | for (let i = 0; i < points.length; i++) 83 | if (points[i]) 84 | res += point2circlePath(points[i], radius); 85 | return res; 86 | } 87 | 88 | function polylines2path(segments) { 89 | let p = ''; 90 | for (let i = 0; i < segments.length; i++) { 91 | if (segments[i] && segments[i].length) { 92 | p += 'M' + ppp(segments[i][0]); 93 | for (let j = 1; j < segments[i].length; j++) 94 | p += ' L' + ppp(segments[i][j]); 95 | } 96 | } 97 | return p; 98 | } 99 | 100 | function polygon2path(polygon) { 101 | let res = ''; 102 | for (let i = 0; i < polygon.length; i++) 103 | res += (i === 0 ? 'M' : 'L') + ppp(polygon[i]) + ' '; 104 | return res + ' Z'; 105 | } 106 | 107 | function displaySegmentsAndPoint(segments, expectedPoints, resultPoints) { 108 | displayMessage(svgAssertTable(polylines2path(segments), 109 | polylines2path(segments) + pointArray2path(resultPoints, 4), 110 | polylines2path(segments) + pointArray2path(expectedPoints, 2))); 111 | } 112 | 113 | function displayMessage(message) { 114 | QUnit.config.current.assertions.push({ 115 | result: true, 116 | message: message 117 | }); 118 | } -------------------------------------------------------------------------------- /test_dynamic_programing.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Simple Dynamic programming test 6 | 7 | 8 | 9 | The source code is there: https://github.com/nraynaud/polygonsJS/ 10 | 11 | 12 |
13 | Trying to justify this text: "AAA BB CC DDDDD" in 6 char lines with DP. 15 | 16 |

17 | 
18 | 
62 | 
63 | 


--------------------------------------------------------------------------------
/test_graph.html:
--------------------------------------------------------------------------------
  1 | 
  2 | 
  3 | 
  4 |     
  5 |     Simple Polygon Medial Axis Computation
  6 |     
  7 |     
 39 | 
 40 | 
 41 | The source code is there: https://github.com/nraynaud/polygonsJS/
 42 | 
 43 | 
44 |
45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 379 | 380 | -------------------------------------------------------------------------------- /test_intro_algorithms.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Testing Algorithms 6 | 7 | 23 | 24 | 25 |

some "Introduction to Algorithms" exercises.

26 |
27 |
28 | 29 | 30 | 326 | -------------------------------------------------------------------------------- /test_kdtree.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Simple KD-tree computation 6 | 7 | 39 | 40 | 41 | The source code is there: https://github.com/nraynaud/polygonsJS/ 42 | 43 |
44 |
45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 142 | 143 | -------------------------------------------------------------------------------- /test_medial_axis.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Simple Polygon Medial Axis Computation 6 | 7 | 39 | 40 | 41 | The source code is there: https://github.com/nraynaud/polygonsJS/ 42 | 43 |
44 |
45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /testscanImage2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nraynaud/polygonsJS/379e73d9b421b7454e548f32e1d3ee14a127cae4/testscanImage2.png --------------------------------------------------------------------------------