fernandez-polygon-decomposition
Version:
An algorithm to decompose polygons with holes from "A practical algorithm for decomposing polygonal domains into convex polygons by diagonals" by J Fernández
263 lines (229 loc) • 10 kB
JavaScript
import { getEdges, containsPolygon, containsEntirePolygon, isClockwiseOrdered, vertexEquality, vertexEqualityAfterAbsorption, orientation, substractPolygons, inConvexPolygon, squaredDistance, lineIntersection, isFlat } from './utils.js';
import { MP5Procedure } from './mp5.js';
import { mergingAlgorithm, mergePolygons } from './merge.js';
import { preprocessPolygon } from './common.js';
/**
* Checks if the given segment instersects the polygon
*
* @param {{ a: { x: number, y: number }, b: { x: number, y: number }}} segment
* @param {{ x: number, y: number }[]} polygon
* @returns {boolean}
*/
const segmentIntersectsPolygon = (segment, polygon) => {
const polygonLength = polygon.length;
for (let i = 0; i < polygonLength; i++) {
const edge = {
a: polygon[(i - 1 + polygonLength) % polygonLength],
b: polygon[i],
};
const intersection = lineIntersection(segment, edge);
if (intersection === null) {
continue;
}
const { insideSegment1, insideSegment2, onEdgeSegment1, onEdgeSegment2 } = intersection;
if ((insideSegment1 || onEdgeSegment1) && (insideSegment2 || onEdgeSegment2)) {
return true;
}
}
return false;
};
/**
* Returns all the edges of the given hole that intersects the given segment.
*
* @param {{ a: { x: number, y: number }, b: { x: number, y: number }}} segment
* @param {{ x: number, y: number, id: number }[]} hole
* @returns {{ x: number, y: number, edge: { a: { x: number, y: number }, b: { x: number, y: number }}, hole: { x: number, y: number, id: number }[] }[]}
*/
const getSegmentHoleIntersectionEdges = (segment, hole) => {
const edges = [];
const holeLength = hole.length;
for (let i = 0; i < holeLength; i++) {
const edge = {
a: hole[(i - 1 + holeLength) % holeLength],
b: hole[i],
};
const intersection = lineIntersection(segment, edge);
if (intersection === null) {
continue;
}
const { x, y, insideSegment1, insideSegment2, onEdgeSegment1, onEdgeSegment2 } = intersection;
if ((insideSegment1 || onEdgeSegment1) && (insideSegment2 || onEdgeSegment2)) {
edges.push({
x,
y,
edge,
hole,
});
}
}
return edges;
};
const rotateLeft = (a, i = 1) => {
return [...a.slice(i), ...a.slice(0, i)];
};
/**
* This is the DrawTrueDiagonal procedure taken from "A practical algorithm for decomposing polygonal domains into convex polygons by diagonals"
*
* @param {{ a: { x: number, y: number }, b: { x: number, y: number }}} diagonal
* @param {{ x: number, y: number, id: number }[]} C
* @param {{ x: number, y: number, id: number }[][]} holesInC
* @returns {{ a: { x: number, y: number }, b: { x: number, y: number }, hole: { x: number, y: number, id: number }[]}}
*/
const drawTrueDiagonal = (diagonal, C, holesInC) => {
const comparator = (a, b) => squaredDistance(diagonal.a, a) - squaredDistance(diagonal.a, b);
let edges = [];
const holesInCLength = holesInC.length;
for (let i = 0; i < holesInCLength; i++) {
edges.push(...getSegmentHoleIntersectionEdges(diagonal, holesInC[i]));
}
let previousClosestVertexId = diagonal.b.id;
while (edges.length > 0) {
/* const closestEdge = edges.sort(comparator).find(({ edge }) => {
return inConvexPolygon(edge.a, C) || inConvexPolygon(edge.b, C);
}); */
const closestEdge = edges.sort(comparator)[0];
const closestVertex = Object.values(closestEdge.edge).filter(v => inConvexPolygon(v, C)).sort(comparator)[0];
if (closestVertex.id === previousClosestVertexId) {
return diagonal;
}
diagonal = { a: diagonal.a, b: closestVertex, hole: closestEdge.hole };
previousClosestVertexId = closestVertex.id;
edges = [];
for (let i = 0; i < holesInCLength; i++) {
edges.push(...getSegmentHoleIntersectionEdges(diagonal, holesInC[i]));
}
}
return diagonal;
};
/**
* This is the AbsHol algorithm taken from "A practical algorithm for decomposing polygonal domains into convex polygons by diagonals"
*
* @param {{ x: number, y: number, id: number }[]} P
* @param {{ x: number, y: number, id: number }[][]} holes
* @param {number} idOffset
* @returns {{ LPCP: { x: number, y: number, id: number }[][], trueDiagonals: { a: { x: number, y: number, id: number }, b: { x: number, y: number, id: number } }[] , LLE: { i2: { x: number, y: number, id: number }, j2: { x: number, y: number, id: number }, rightPolygon: { x: number, y: number, id: number }[][], leftPolygon: { x: number, y: number, id: number }[][] }[] }} The partition of convex polygons
*/
function absHolProcedure (P, holes, idOffset) {
const LLE = [];
const trueDiagonals = [];
const LPCP = [];
let Q = [...P];
while (true) {
const { convexPolygon: C, end } = MP5Procedure(Q);
let diagonal = { a: C[0], b: C[C.length - 1], hole: null };
const holesLength = holes.length;
let diagonalIsCutByAHole = false;
const holesInC = [];
for (let i = 0; i < holesLength; i++) {
const hole = holes[i];
if (!diagonalIsCutByAHole && segmentIntersectsPolygon(diagonal, hole)) {
diagonalIsCutByAHole = true;
diagonal.hole = hole;
}
if (containsPolygon(C, hole)) {
holesInC.push(hole);
}
}
if (diagonalIsCutByAHole || holesInC.length > 0) {
if (!diagonalIsCutByAHole) {
diagonal = { a: C[0], b: holesInC[0][0], hole: holesInC[0] };
}
const { hole: HPrime, ...dPrime } = drawTrueDiagonal(diagonal, C, holesInC);
trueDiagonals.push(dPrime);
// Absorption of H'
holes = holes.filter(hole => hole !== HPrime);
const vi = C[0];
const id1 = ++idOffset;
const id2 = ++idOffset;
const rotatedHPrime = rotateLeft(HPrime, HPrime.findIndex(v => vertexEquality(v, dPrime.b)) + 1).reverse();
const viIndexInQ = Q.findIndex(v => vertexEquality(v, vi));
Q = [...Q.slice(0, viIndexInQ + 1), ...rotatedHPrime, { ...rotatedHPrime[0], id: id1, originalId: rotatedHPrime[0].id.originalId || rotatedHPrime[0].id }, { ...vi, id: id2, originalId: vi.originalId || vi.id }, ...Q.slice(viIndexInQ + 1)];
} else {
LPCP.push(C);
getEdges(C).forEach((edge) => {
for (let i = 0; i < LLE.length; i++) {
const { i2: diagonalA, j2: diagonalB } = LLE[i];
if (vertexEqualityAfterAbsorption(diagonalA, edge.b) && vertexEqualityAfterAbsorption(diagonalB, edge.a)) {
LLE[i].leftPolygon = C;
break;
}
}
});
if (end) {
break;
}
LLE.push({
i2: diagonal.b,
j2: diagonal.a,
rightPolygon: C,
});
Q = substractPolygons(Q, C);
}
}
return { LPCP, trueDiagonals, LLE };
}
/**
* An implementation of the algorithm presented in "A practical algorithm for decomposing polygonal domains into convex polygons by diagonals"
* It will decompose a polygon (with or without holes) in a partition of convex polygons.
*
* @param {{ x: number, y: number }[]} polygon
* @param {{ x: number, y: number }[][]} holes
* @returns {{ x: number, y: number }[][]} The partition of convex polygons
*/
export function absHol (polygon, holes = []) {
if (!Array.isArray(polygon)) {
throw new Error('absHol can only take an array of points {x, y} as input');
}
if (polygon.length <= 2) {
return [polygon];
}
if (!isClockwiseOrdered(polygon)) {
throw new Error('absHol can only work with clockwise ordered vertices');
}
// Assert every holes are in the polygon. TODO : disable in prod ??
if (!holes.every(hole => containsEntirePolygon(polygon, hole))) {
throw new Error('One or more holes are not totally inside the polygon !');
}
// Starting the ids at 1 to ensure there are no problem when computing the diagonals (originalId || id)
const preprocessedPolygon = preprocessPolygon(polygon, 1);
let offset = preprocessedPolygon.length + 1;
const preprocessedHoles = holes.map((hole) => {
const preprocessedHole = preprocessPolygon(hole, offset);
offset += preprocessedHole.length;
return preprocessedHole;
});
const { LPCP, trueDiagonals, LLE } = absHolProcedure(preprocessedPolygon, preprocessedHoles, offset);
// Removing "flat" polygons
let mergedPoly = mergingAlgorithm(LPCP, LLE).filter((poly) => !isFlat(poly));
// Merging the inessentials true diagonals
trueDiagonals.forEach(({ a: i2, b: j2 }) => {
let Pj, Pu, i1, i3, j1, j3;
for (let i = 0; i < mergedPoly.length; i++) {
const poly = mergedPoly[i];
const polyLength = poly.length;
const edges = getEdges(poly);
for (let j = 0; j < edges.length; j++) {
const { a: edgeA, b: edgeB } = edges[j];
// TODO : ici je pense qu'on peut passer previous/nextVertex, a verifier
if (vertexEqualityAfterAbsorption(i2, edgeA) && vertexEqualityAfterAbsorption(j2, edgeB)) {
i1 = poly[(poly.findIndex(v => vertexEqualityAfterAbsorption(v, edgeA)) + polyLength - 1) % polyLength]; // previousVertex(edgeA, poly);
j3 = poly[(poly.findIndex(v => vertexEqualityAfterAbsorption(v, edgeB)) + 1) % polyLength]; // nextVertex(edgeB, poly);
Pu = poly;
break;
} else if (vertexEqualityAfterAbsorption(i2, edgeB) && vertexEqualityAfterAbsorption(j2, edgeA)) {
i3 = poly[(poly.findIndex(v => vertexEqualityAfterAbsorption(v, edgeB)) + 1) % polyLength]; // nextVertex(edgeB, poly);
j1 = poly[(poly.findIndex(v => vertexEqualityAfterAbsorption(v, edgeA)) + polyLength - 1) % polyLength]; // previousVertex(edgeA, poly);
Pj = poly;
break;
}
}
if (Pu && Pj) {
if (orientation(i1, i2, i3) >= 0 && orientation(j1, j2, j3) >= 0) {
mergedPoly = mergedPoly.filter(poly => (poly !== Pu && poly !== Pj)).concat([mergePolygons(Pj, Pu)]);
}
break;
}
}
});
return mergedPoly.map((poly) => poly.map(({ x, y }) => ({ x, y })));
}