function partition(nodes, edges) {
// Create an adjacency list
let adj = [];
for (let i = 0; i < nodes.length; i++) {
adj[i] = []; // initialise the list for each node as an empty one
}
for (let i = 0; i < edges.length; i++) {
let a = edges[i][0]; // Get the two nodes (a, b) that this edge connects
let b = edges[i][1];
adj[a].push(b); // Add as directed edge in both directions
adj[b].push(a);
}
// Traverse the graph to identify polygons, until none are to be found
let polygons = [[], []]; // two lists of polygons, one per "winding" (clockwise or ccw)
let more = true;
while (more) {
more = false;
for (let i = 0; i < nodes.length; i++) {
if (adj[i].length) { // we have unvisited directed edge(s) here
let start = i;
let polygon = [i]; // collect the vertices on a new polygon
let sumAngle = 0;
// Take one neighbor out of this node's neighbor list and follow a path
for (let j = adj[i].pop(), next; j !== start; i = j, j = next) {
polygon.push(j);
// Get coordinates of the current edge's end-points
let ix = nodes[i][0];
let iy = nodes[i][1];
let jx = nodes[j][0];
let jy = nodes[j][1];
let startAngle = Math.atan2(jy-iy, jx-ix);
// In the adjacency list of node j, find the next neighboring vertex in counterclockwise order
// relative to node i where we came from.
let minAngle = 10; // Larger than any normalised angle
for (let neighborIndex = 0; neighborIndex < adj[j].length; neighborIndex++) {
let k = adj[j][neighborIndex];
if (k === i) continue; // ignore the reverse of the edge we came from
let kx = nodes[k][0];
let ky = nodes[k][1];
let relAngle = Math.atan2(ky-jy, kx-jx) - startAngle; // The "magic"
// Normalise the relative angle to the range [-PI, +PI)
if (relAngle < -Math.PI) relAngle += 2*Math.PI;
if (relAngle >= Math.PI) relAngle -= 2*Math.PI;
if (relAngle < minAngle) { // this one comes earlier in counterclockwise order
minAngle = relAngle;
nextNeighborIndex = neighborIndex;
}
}
sumAngle += minAngle; // track the sum of all the angles in the polygon
next = adj[j][nextNeighborIndex];
// delete the chosen directed edge (so it cannot be visited again)
adj[j].splice(nextNeighborIndex, 1);
}
let winding = sumAngle > 0 ? 1 : 0; // sumAngle will be 2*PI or -2*PI. Clockwise or ccw.
polygons[winding].push(polygon);
more = true;
}
}
}
// return the largest list of polygons, so to exclude the whole polygon,
// which will be the only one with a winding that's different from all the others.
return polygons[0].length > polygons[1].length ? polygons[0] : polygons[1];
}
// Sample input:
let nodes = [[59,25],[26,27],[9,59],[3,99],[30,114],[77,116],[89,102],[102,136],[105,154],[146,157],[181,151],[201,125],[194,83],[155,72],[174,47],[182,24],[153,6],[117,2],[89,9],[97,45]];
let internalEdges = [[6, 13], [13, 19], [19, 6]];
// Join outer edges with inner edges to an overall list of edges:
let edges = nodes.map((a, i) => [i, (i+1)%nodes.length]).concat(internalEdges);
// Apply algorithm
let polygons = partition(nodes, edges);
// Report on results
document.querySelector("div").innerHTML =
"input polygon has these points, numbered 0..n<br>" +
JSON.stringify(nodes) + "<br>" +
"resulting polygons, by vertex numbers<br>" +
JSON.stringify(polygons)
// Graphics handling
let io = {
ctx: document.querySelector("canvas").getContext("2d"),
drawEdges(edges) {
for (let [a, b] of edges) {
this.ctx.moveTo(...a);
this.ctx.lineTo(...b);
this.ctx.stroke();
}
},
colorPolygon(polygon, color) {
this.ctx.beginPath();
this.ctx.moveTo(...polygon[0]);
for (let p of polygon.slice(1)) {
this.ctx.lineTo(...p);
}
this.ctx.closePath();
this.ctx.fillStyle = color;
this.ctx.fill();
}
};
// Display original graph
io.drawEdges(edges.map(([a,b]) => [nodes[a], nodes[b]]));
// Color the polygons that the algorithm identified
let colors = ["red", "blue", "silver", "purple", "green", "brown", "orange", "cyan"];
for (let polygon of polygons) {
io.colorPolygon(polygon.map(i => nodes[i]), colors.pop());
}
<canvas width="400" height="180"></canvas>
<div></div>