├── img ├── demo.png ├── logo.png └── example.png ├── .gitignore ├── test ├── stars.js ├── collinear.js ├── check-graph.js └── crash.js ├── example └── example.js ├── lib └── rat-seg-intersect.js ├── LICENSE ├── package.json ├── README.md ├── visual └── cleanup.js └── clean-pslg.js /img/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikolalysenko/clean-pslg/HEAD/img/demo.png -------------------------------------------------------------------------------- /img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikolalysenko/clean-pslg/HEAD/img/logo.png -------------------------------------------------------------------------------- /img/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikolalysenko/clean-pslg/HEAD/img/example.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules/* 16 | *.DS_Store -------------------------------------------------------------------------------- /test/stars.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var tape = require('tape') 4 | var cleanPSLG = require('../clean-pslg') 5 | var checkPSLG = require('./check-graph') 6 | 7 | tape('stars', function(t) { 8 | 9 | for(var n=0; n<10; ++n) { 10 | var points = [] 11 | var edges = [] 12 | for(var i=0; i<10; ++i) { 13 | var x = Math.random() * 0.8 - 0.4 14 | var y = Math.random() * 0.8 - 0.4 15 | points.push([0.5+x,0.5+y], [0.5-x,0.5-y]) 16 | edges.push([2*i, 2*i+1]) 17 | } 18 | cleanPSLG(points, edges) 19 | checkPSLG(t, points, edges) 20 | } 21 | 22 | t.end() 23 | }) 24 | -------------------------------------------------------------------------------- /test/collinear.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var tape = require('tape') 4 | var cleanPSLG = require('../clean-pslg') 5 | var checkPSLG = require('./check-graph') 6 | 7 | tape('collinear', function(t) { 8 | 9 | for(var n=0; n<10; ++n) { 10 | var points = [] 11 | var edges = [] 12 | for(var i=0; i<10; ++i) { 13 | var x0 = Math.random() 14 | var x1 = Math.random() 15 | points.push([x0, 0], [x1, 0]) 16 | edges.push([2*i, 2*i+1]) 17 | } 18 | cleanPSLG(points, edges) 19 | console.log(points.length, edges.length) 20 | console.log(points, edges) 21 | checkPSLG(t, points, edges) 22 | } 23 | 24 | t.end() 25 | }) 26 | -------------------------------------------------------------------------------- /example/example.js: -------------------------------------------------------------------------------- 1 | var cleanupPSLG = require('../clean-pslg') 2 | 3 | //Create a planar straight line graph with many degenerate crossings 4 | var points = [ 5 | [ 0.25, 0.5 ], 6 | [ 0.75, 0.5 ], 7 | [ 0.5, 0.25 ], 8 | [ 0.5, 0.75 ], 9 | [ 0.25, 0.25 ], 10 | [ 0.75, 0.75 ], 11 | [ 0.25, 0.75 ], 12 | [ 0.75, 0.25 ] 13 | ] 14 | 15 | //These are the edges of the graph 16 | var edges = [ 17 | [0, 1], 18 | [2, 3], 19 | [4, 5], 20 | [6, 7] 21 | ] 22 | 23 | //Run clean up on the graph 24 | if(cleanupPSLG(points, edges)) { 25 | console.log('removed degeneracies from graph') 26 | } 27 | 28 | console.log('points = \n', points) 29 | console.log('edges = \n', edges) 30 | -------------------------------------------------------------------------------- /lib/rat-seg-intersect.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = solveIntersection 4 | 5 | var ratMul = require('big-rat/mul') 6 | var ratDiv = require('big-rat/div') 7 | var ratSub = require('big-rat/sub') 8 | var ratSign = require('big-rat/sign') 9 | var rvSub = require('rat-vec/sub') 10 | var rvAdd = require('rat-vec/add') 11 | var rvMuls = require('rat-vec/muls') 12 | 13 | function ratPerp (a, b) { 14 | return ratSub(ratMul(a[0], b[1]), ratMul(a[1], b[0])) 15 | } 16 | 17 | // Solve for intersection 18 | // x = a + t (b-a) 19 | // (x - c) ^ (d-c) = 0 20 | // (t * (b-a) + (a-c) ) ^ (d-c) = 0 21 | // t * (b-a)^(d-c) = (d-c)^(a-c) 22 | // t = (d-c)^(a-c) / (b-a)^(d-c) 23 | 24 | function solveIntersection (a, b, c, d) { 25 | var ba = rvSub(b, a) 26 | var dc = rvSub(d, c) 27 | 28 | var baXdc = ratPerp(ba, dc) 29 | 30 | if (ratSign(baXdc) === 0) { 31 | return null 32 | } 33 | 34 | var ac = rvSub(a, c) 35 | var dcXac = ratPerp(dc, ac) 36 | 37 | var t = ratDiv(dcXac, baXdc) 38 | var s = rvMuls(ba, t) 39 | var r = rvAdd(a, s) 40 | 41 | return r 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2015 Mikola Lysenko 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clean-pslg", 3 | "version": "1.1.2", 4 | "description": "Remove self intersections, t-junctions and duplicate edges/vertices from a planar straight line graph", 5 | "main": "clean-pslg.js", 6 | "directories": { 7 | "example": "example", 8 | "test": "test" 9 | }, 10 | "dependencies": { 11 | "big-rat": "^1.0.3", 12 | "box-intersect": "^1.0.1", 13 | "nextafter": "^1.0.0", 14 | "rat-vec": "^1.1.1", 15 | "robust-segment-intersect": "^1.0.1", 16 | "union-find": "^1.0.2", 17 | "uniq": "^1.0.1" 18 | }, 19 | "devDependencies": { 20 | "mouse-change": "^1.2.1", 21 | "canvas-fit": "^1.4.0", 22 | "robust-orientation": "^1.1.3", 23 | "tape": "^4.0.0", 24 | "segment2": "^0.3.2", 25 | "vec2": "^1.6.0" 26 | }, 27 | "scripts": { 28 | "test": "tape test/*.js" 29 | }, 30 | "repository": { 31 | "type": "git", 32 | "url": "https://github.com/mikolalysenko/clean-pslg.git" 33 | }, 34 | "keywords": [ 35 | "planar", 36 | "straight", 37 | "line", 38 | "graph", 39 | "geometry", 40 | "topology", 41 | "polygon", 42 | "snap", 43 | "rounding", 44 | "tjunction", 45 | "crossing", 46 | "junction", 47 | "duplicate", 48 | "vertex", 49 | "edge", 50 | "point", 51 | "cell", 52 | "complex", 53 | "repair", 54 | "clean", 55 | "self", 56 | "intersection" 57 | ], 58 | "author": "Mikola Lysenko", 59 | "license": "MIT", 60 | "bugs": { 61 | "url": "https://github.com/mikolalysenko/clean-pslg/issues" 62 | }, 63 | "homepage": "https://github.com/mikolalysenko/clean-pslg" 64 | } 65 | -------------------------------------------------------------------------------- /test/check-graph.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var segseg = require('robust-segment-intersect') 4 | 5 | module.exports = checkGraph 6 | 7 | function checkUnique(tape, list, compare) { 8 | var sorted = list.slice().sort(compare) 9 | for(var i=1; i 4 | 5 | Resolves all self intersections, t-junctions, and removes duplicate vertices/edges from a [planar straight line graph](https://en.wikipedia.org/wiki/Planar_straight-line_graph) using [iterated snap rounding](http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.23.220). 6 | 7 | # [Demo](https://mikolalysenko.github.io/clean-pslg) 8 | 9 | Click on the following link to try out `clean-pslg` in your browser: 10 | 11 | [](https://mikolalysenko.github.io/clean-pslg) 12 | 13 | * [Demo link](https://mikolalysenko.github.io/clean-pslg) 14 | 15 | # Example 16 | 17 | This module really only does one thing, which is clean up planar straight line graphs. You invoke it by passing it an array of points and an array of edges like so: 18 | 19 | ```javascript 20 | var cleanPSLG = require('clean-pslg') 21 | 22 | //Create a planar straight line graph with many degenerate crossings 23 | var points = [ 24 | [ 0.25, 0.5 ], 25 | [ 0.75, 0.5 ], 26 | [ 0.5, 0.25 ], 27 | [ 0.5, 0.75 ], 28 | [ 0.25, 0.25 ], 29 | [ 0.75, 0.75 ], 30 | [ 0.25, 0.75 ], 31 | [ 0.75, 0.25 ] 32 | ] 33 | 34 | //These are the edges of the graph 35 | //They are defined by pairs of indices of vertices 36 | var edges = [ 37 | [0, 1], 38 | [2, 3], 39 | [4, 5], 40 | [6, 7] 41 | ] 42 | 43 | //Run clean up on the graph 44 | if(cleanPSLG(points, edges)) { 45 | console.log('removed degeneracies from graph') 46 | } 47 | 48 | //clean-pslg operates on the graph in place, so after running it the points/edges will be modified 49 | console.log('points = \n', points) 50 | console.log('edges = \n', edges) 51 | ``` 52 | 53 | #### Output 54 | 55 | The program will output the following text: 56 | 57 | ``` 58 | removed degeneracies from graph 59 | points = 60 | [ [ 0.25, 0.5 ], 61 | [ 0.75, 0.5 ], 62 | [ 0.5, 0.25 ], 63 | [ 0.5, 0.75 ], 64 | [ 0.25, 0.25 ], 65 | [ 0.75, 0.75 ], 66 | [ 0.25, 0.75 ], 67 | [ 0.75, 0.25 ], 68 | [ 0.5, 0.5 ] ] 69 | edges = 70 | [ [ 8, 0 ], 71 | [ 1, 8 ], 72 | [ 8, 2 ], 73 | [ 3, 8 ], 74 | [ 8, 4 ], 75 | [ 5, 8 ], 76 | [ 8, 6 ], 77 | [ 7, 8 ], 78 | [ 8, 8 ] ] 79 | ``` 80 | 81 | Visually, this corresponds to the following refinement of a planar graph: 82 | 83 | 84 | 85 | # Install 86 | 87 | ``` 88 | npm i clean-pslg 89 | ``` 90 | 91 | # API 92 | 93 | #### `require('clean-pslg')(points, edges[, colors])` 94 | Processes an unoriented planar straight line graph defined by `points` and `edges` in place. 95 | 96 | * `points` is an array encoding the vertices of the planar straight line graph as pairs of numbers 97 | * `edges` is an array encoding the edges of the planar straight line graph as pairs of indices 98 | * `colors` is an optional array of edge colors. If specified, only merge edges if they have the same color. This can be used to implement orientation preservation or handle solid geometry. 99 | 100 | The following degeneracies are handled: 101 | 102 | * Duplicate points are merged 103 | * Duplicate edges are merged 104 | * T-junctions are split 105 | * Edge crossings are split 106 | 107 | The resulting graph meets all invariants required by `cdt2d`, so it may be triangulated. Note that this procedure *does not preserve orientation*. 108 | 109 | **Returns** `true` if repairs were necessary, otherwise `false` 110 | 111 | **Note** This is a destructive procedure, which means that the contents of `points` and `edges` may change. If you don't want this to happen, you should make a deep copy of `points` and `edges` before calling `clean-pslg` 112 | 113 | # License 114 | (c) 2015 Mikola Lysenko. MIT License 115 | -------------------------------------------------------------------------------- /visual/cleanup.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var orient = require('robust-orientation') 4 | var vec2 = require('vec2') 5 | var segment2 = require('segment2') 6 | var mouseChange = require('mouse-change') 7 | var segCrosses = require('robust-segment-intersect') 8 | var fit = require('canvas-fit') 9 | var cleanupPSLG = require('../clean-pslg') 10 | 11 | //Create canvas and context 12 | var canvas = document.createElement('canvas') 13 | var context = canvas.getContext('2d') 14 | document.body.appendChild(canvas) 15 | window.addEventListener('resize', fit(canvas), false) 16 | 17 | var optionDiv = document.createElement('div') 18 | optionDiv.style.position = 'absolute' 19 | optionDiv.style.left = '5px' 20 | optionDiv.style.top = '5px' 21 | optionDiv.style['z-index'] = '10' 22 | document.body.appendChild(optionDiv) 23 | 24 | var points = [] 25 | var edges = [] 26 | var cleanPoints = [] 27 | var cleanEdges = [] 28 | 29 | function dataChanged() { 30 | cleanPoints = points.map(function(p) { return [p[0], p[1]] }) 31 | cleanEdges = edges.map(function(e) { return [e[0], e[1]] }) 32 | cleanupPSLG(cleanPoints, cleanEdges) 33 | } 34 | 35 | for(var i=0; i<10; ++i) { 36 | var x = Math.random() * 0.8 - 0.4 37 | var y = Math.random() * 0.8 - 0.4 38 | points.push([0.5+x,0.5+y], [0.5-x,0.5-y]) 39 | edges.push([2*i, 2*i+1]) 40 | } 41 | 42 | dataChanged() 43 | 44 | var resetButton = document.createElement('input') 45 | resetButton.type = 'button' 46 | resetButton.value = 'reset' 47 | resetButton.addEventListener('click', function() { 48 | points.length = edges.length = 0 49 | dataChanged() 50 | }) 51 | var resetP = document.createElement('p') 52 | resetP.appendChild(resetButton) 53 | optionDiv.appendChild(resetP) 54 | 55 | var description = document.createElement('p') 56 | description.innerHTML = 'click to add/remove points
drag to add edges
Project page' 57 | optionDiv.appendChild(description) 58 | 59 | function edgeDistance(a, b, c) { 60 | var p = vec2(c[0], c[1]) 61 | return segment2(vec2(a[0], a[1]), vec2(b[0], b[1])).closestPointTo(p).distance(p) 62 | } 63 | 64 | function isValidEdge(a, b) { 65 | return true 66 | } 67 | 68 | var lastButtons = 0, 69 | highlightPoint = -1, 70 | startPoint = -1, 71 | highlightEdge = -1, 72 | activeEdge = null 73 | mouseChange(canvas, function(buttons, x, y) { 74 | var s = Math.min(canvas.width, canvas.height) 75 | var lx = (x - canvas.width/2) / s + 0.5 76 | var ly = (y - canvas.height/2) / s + 0.5 77 | var closestDist = 0.0125 78 | highlightPoint = -1 79 | highlightEdge = -1 80 | for(var i=0; i= 0) { 105 | edges.splice(highlightEdge, 1) 106 | dataChanged() 107 | highlightEdge = -1 108 | } else if(highlightPoint < 0) { 109 | points.push([lx, ly]) 110 | dataChanged() 111 | } else { 112 | startPoint = highlightPoint 113 | activeEdge = [ points[highlightPoint], [lx, ly] ] 114 | } 115 | } else if(!!lastButtons && !buttons) { 116 | if(startPoint >= 0) { 117 | if(highlightPoint === startPoint) { 118 | points.splice(highlightPoint, 1) 119 | var nedges = [] 120 | discard_edge: 121 | for(var i=0; i highlightPoint) { 125 | e[j] -= 1 126 | } else if(e[j] === highlightPoint) { 127 | continue discard_edge 128 | } 129 | } 130 | nedges.push(e) 131 | } 132 | edges = nedges 133 | highlightPoint = -1 134 | dataChanged() 135 | } else if(highlightPoint >= 0) { 136 | if(isValidEdge(points[startPoint], points[highlightPoint])) { 137 | edges.push([startPoint, highlightPoint]) 138 | dataChanged() 139 | } 140 | } 141 | startPoint = -1 142 | activeEdge = null 143 | } 144 | } else if(!!buttons) { 145 | if(activeEdge) { 146 | activeEdge[1] = [lx, ly] 147 | } 148 | } 149 | lastButtons = buttons 150 | }) 151 | 152 | function line(a, b) { 153 | var x0 = a[0]-0.5 154 | var y0 = a[1]-0.5 155 | var x1 = b[0]-0.5 156 | var y1 = b[1]-0.5 157 | var w = canvas.width 158 | var h = canvas.height 159 | var s = Math.min(w, h) 160 | context.beginPath() 161 | context.moveTo(s*x0 + w/2, s*y0 + h/2) 162 | context.lineTo(s*x1 + w/2, s*y1 + h/2) 163 | context.stroke() 164 | } 165 | 166 | function circle(x, y, r) { 167 | var w = canvas.width 168 | var h = canvas.height 169 | var s = Math.min(w, h) 170 | context.beginPath() 171 | context.moveTo(s*x, s*y) 172 | context.arc(s*(x-0.5) + w/2, s*(y-0.5) + h/2, r, 0.0, 2.0*Math.PI) 173 | context.fill() 174 | } 175 | 176 | var EDGE_PALETTE = [ 177 | 'rgba(255,0,0,0.25)', 178 | 'rgba(0,255,0,0.25)', 179 | 'rgba(0,0,255,0.25)', 180 | 'rgba(255,255,0,0.25)', 181 | 'rgba(255,0,255,0.25)', 182 | 'rgba(0,255,255,0.25)' 183 | ] 184 | 185 | function draw() { 186 | requestAnimationFrame(draw) 187 | 188 | var w = canvas.width 189 | var h = canvas.height 190 | context.fillStyle = '#fff' 191 | context.fillRect(0, 0, w, h) 192 | 193 | for(var i=0; i= 0) { 231 | var e = edges[highlightEdge] 232 | context.strokeStyle = '#f00' 233 | line(points[e[0]], points[e[1]]) 234 | } 235 | 236 | 237 | for(var i=0; i= 0; --i) { 140 | var junction = junctions[i] 141 | e = junction[0] 142 | 143 | var edge = edges[e] 144 | var s = edge[0] 145 | var t = edge[1] 146 | 147 | // Check if edge is not lexicographically sorted 148 | var a = floatPoints[s] 149 | var b = floatPoints[t] 150 | if (((a[0] - b[0]) || (a[1] - b[1])) < 0) { 151 | var tmp = s 152 | s = t 153 | t = tmp 154 | } 155 | 156 | // Split leading edge 157 | edge[0] = s 158 | var last = edge[1] = junction[1] 159 | 160 | // If we are grouping edges by color, remember to track data 161 | var color 162 | if (useColor) { 163 | color = edge[2] 164 | } 165 | 166 | // Split other edges 167 | while (i > 0 && junctions[i - 1][0] === e) { 168 | var junction = junctions[--i] 169 | var next = junction[1] 170 | if (useColor) { 171 | edges.push([last, next, color]) 172 | } else { 173 | edges.push([last, next]) 174 | } 175 | last = next 176 | } 177 | 178 | // Add final edge 179 | if (useColor) { 180 | edges.push([last, t, color]) 181 | } else { 182 | edges.push([last, t]) 183 | } 184 | } 185 | 186 | // Return constructed rational points 187 | return ratPoints 188 | } 189 | 190 | // Merge overlapping points 191 | function dedupPoints (floatPoints, ratPoints, floatBounds) { 192 | var numPoints = ratPoints.length 193 | var uf = new UnionFind(numPoints) 194 | 195 | // Compute rational bounds 196 | var bounds = [] 197 | for (var i = 0; i < ratPoints.length; ++i) { 198 | var p = ratPoints[i] 199 | var xb = boundRat(p[0]) 200 | var yb = boundRat(p[1]) 201 | bounds.push([ 202 | nextafter(xb[0], -Infinity), 203 | nextafter(yb[0], -Infinity), 204 | nextafter(xb[1], Infinity), 205 | nextafter(yb[1], Infinity) 206 | ]) 207 | } 208 | 209 | // Link all points with over lapping boxes 210 | boxIntersect(bounds, function (i, j) { 211 | uf.link(i, j) 212 | }) 213 | 214 | // Do 1 pass over points to combine points in label sets 215 | var noDupes = true 216 | var labels = new Array(numPoints) 217 | for (var i = 0; i < numPoints; ++i) { 218 | var j = uf.find(i) 219 | if (j !== i) { 220 | // Clear no-dupes flag, zero out label 221 | noDupes = false 222 | // Make each point the top-left point from its cell 223 | floatPoints[j] = [ 224 | Math.min(floatPoints[i][0], floatPoints[j][0]), 225 | Math.min(floatPoints[i][1], floatPoints[j][1]) 226 | ] 227 | } 228 | } 229 | 230 | // If no duplicates, return null to signal termination 231 | if (noDupes) { 232 | return null 233 | } 234 | 235 | var ptr = 0 236 | for (var i = 0; i < numPoints; ++i) { 237 | var j = uf.find(i) 238 | if (j === i) { 239 | labels[i] = ptr 240 | floatPoints[ptr++] = floatPoints[i] 241 | } else { 242 | labels[i] = -1 243 | } 244 | } 245 | 246 | floatPoints.length = ptr 247 | 248 | // Do a second pass to fix up missing labels 249 | for (var i = 0; i < numPoints; ++i) { 250 | if (labels[i] < 0) { 251 | labels[i] = labels[uf.find(i)] 252 | } 253 | } 254 | 255 | // Return resulting union-find data structure 256 | return labels 257 | } 258 | 259 | function compareLex2 (a, b) { return (a[0] - b[0]) || (a[1] - b[1]) } 260 | function compareLex3 (a, b) { 261 | var d = (a[0] - b[0]) || (a[1] - b[1]) 262 | if (d) { 263 | return d 264 | } 265 | if (a[2] < b[2]) { 266 | return -1 267 | } else if (a[2] > b[2]) { 268 | return 1 269 | } 270 | return 0 271 | } 272 | 273 | // Remove duplicate edge labels 274 | function dedupEdges (edges, labels, useColor) { 275 | if (edges.length === 0) { 276 | return 277 | } 278 | if (labels) { 279 | for (var i = 0; i < edges.length; ++i) { 280 | var e = edges[i] 281 | var a = labels[e[0]] 282 | var b = labels[e[1]] 283 | e[0] = Math.min(a, b) 284 | e[1] = Math.max(a, b) 285 | } 286 | } else { 287 | for (var i = 0; i < edges.length; ++i) { 288 | var e = edges[i] 289 | var a = e[0] 290 | var b = e[1] 291 | e[0] = Math.min(a, b) 292 | e[1] = Math.max(a, b) 293 | } 294 | } 295 | if (useColor) { 296 | edges.sort(compareLex3) 297 | } else { 298 | edges.sort(compareLex2) 299 | } 300 | var ptr = 1 301 | for (var i = 1; i < edges.length; ++i) { 302 | var prev = edges[i - 1] 303 | var next = edges[i] 304 | if (next[0] === prev[0] && next[1] === prev[1] && 305 | (!useColor || next[2] === prev[2])) { 306 | continue 307 | } 308 | edges[ptr++] = next 309 | } 310 | edges.length = ptr 311 | } 312 | 313 | function preRound (points, edges, useColor) { 314 | var labels = dedupPoints(points, [], boundPoints(points)) 315 | dedupEdges(edges, labels, useColor) 316 | return !!labels 317 | } 318 | 319 | // Repeat until convergence 320 | function snapRound (points, edges, useColor) { 321 | // 1. find edge crossings 322 | var edgeBounds = boundEdges(points, edges) 323 | var crossings = getCrossings(points, edges, edgeBounds) 324 | 325 | // 2. find t-junctions 326 | var vertBounds = boundPoints(points) 327 | var tjunctions = getTJunctions(points, edges, edgeBounds, vertBounds) 328 | 329 | // 3. cut edges, construct rational points 330 | var ratPoints = cutEdges(points, edges, crossings, tjunctions, useColor) 331 | 332 | // 4. dedupe verts 333 | var labels = dedupPoints(points, ratPoints, vertBounds) 334 | 335 | // 5. dedupe edges 336 | dedupEdges(edges, labels, useColor) 337 | 338 | // 6. check termination 339 | if (!labels) { 340 | return (crossings.length > 0 || tjunctions.length > 0) 341 | } 342 | 343 | // More iterations necessary 344 | return true 345 | } 346 | 347 | // Main loop, runs PSLG clean up until completion 348 | function cleanPSLG (points, edges, colors) { 349 | // If using colors, augment edges with color data 350 | var prevEdges 351 | if (colors) { 352 | prevEdges = edges 353 | var augEdges = new Array(edges.length) 354 | for (var i = 0; i < edges.length; ++i) { 355 | var e = edges[i] 356 | augEdges[i] = [e[0], e[1], colors[i]] 357 | } 358 | edges = augEdges 359 | } 360 | 361 | // First round: remove duplicate edges and points 362 | var modified = preRound(points, edges, !!colors) 363 | 364 | // Run snap rounding until convergence 365 | while (snapRound(points, edges, !!colors)) { 366 | modified = true 367 | } 368 | 369 | // Strip color tags 370 | if (!!colors && modified) { 371 | prevEdges.length = 0 372 | colors.length = 0 373 | for (var i = 0; i < edges.length; ++i) { 374 | var e = edges[i] 375 | prevEdges.push([e[0], e[1]]) 376 | colors.push(e[2]) 377 | } 378 | } 379 | 380 | return modified 381 | } 382 | --------------------------------------------------------------------------------