UNPKG

poly-math-2d

Version:

2D Polygon math: boolean operations, triangulation, graphs, support for holes and non-convex shapes.

286 lines (285 loc) 11.3 kB
"use strict"; // polygon_graph.ts // Classes for working with triangulated polygon graph var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Polygon = exports.TPolygonConnection = exports.TPolygon = exports.Point = void 0; const poly2d_js_1 = require("./poly2d.js"); Object.defineProperty(exports, "Point", { enumerable: true, get: function () { return poly2d_js_1.Point; } }); const polygon_clipping_1 = __importDefault(require("polygon-clipping")); const earcut_1 = __importDefault(require("earcut")); const polygon_map_js_1 = require("./polygon-map.js"); // Triangular polygon class TPolygon { constructor(triangle) { this.connections = []; this.mainTriangle = triangle; this.centerPoint = new poly2d_js_1.Point((triangle[0].x + triangle[1].x + triangle[2].x) / 3, (triangle[0].y + triangle[1].y + triangle[2].y) / 3); } } exports.TPolygon = TPolygon; // Connection between triangular polygons class TPolygonConnection { constructor(neighbor, distance) { this.neighbor = neighbor; this.distance = distance; } } exports.TPolygonConnection = TPolygonConnection; // Triangulate polygon with holes function triangulateWithHoles(outer, holes) { // Form a flat array of coordinates and hole indices const vertices = []; const holeIndices = []; let idx = 0; // Outer contour for (const p of outer) { vertices.push(p.x, p.y); } idx += outer.length; // Holes for (const hole of holes) { holeIndices.push(idx); for (const p of hole.points) { vertices.push(p.x, p.y); } idx += hole.points.length; } // Triangulation const triangles = []; const indices = (0, earcut_1.default)(vertices, holeIndices); for (let i = 0; i < indices.length; i += 3) { const ia = indices[i] * 2, ib = indices[i + 1] * 2, ic = indices[i + 2] * 2; const a = new poly2d_js_1.Point(vertices[ia], vertices[ia + 1]); const b = new poly2d_js_1.Point(vertices[ib], vertices[ib + 1]); const c = new poly2d_js_1.Point(vertices[ic], vertices[ic + 1]); triangles.push([a, b, c]); } return triangles; } // Check CCW order of points function isCCW(points) { let sum = 0; for (let i = 0; i < points.length; i++) { const p1 = points[i]; const p2 = points[(i + 1) % points.length]; sum += (p2.x - p1.x) * (p2.y + p1.y); } return sum < 0; } function reverseIfNotCCW(points) { return isCCW(points) ? points : [...points].reverse(); } // --- Ear Clipping Triangulation --- function earClippingTriangulation(points) { const triangles = []; if (points.length < 3) return triangles; const verts = points.map((p, i) => i); function isConvex(i0, i1, i2) { const a = points[i0], b = points[i1], c = points[i2]; // For CCW: cross < 0 — concave, cross > 0 — convex return cross(a, b, c) > 0; } function cross(a, b, c) { return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x); } function pointInTriangle(p, a, b, c) { const area = (a, b, c) => (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x); const s1 = area(p, a, b); const s2 = area(p, b, c); const s3 = area(p, c, a); return (s1 >= 0 && s2 >= 0 && s3 >= 0) || (s1 <= 0 && s2 <= 0 && s3 <= 0); } let guard = 0; while (verts.length > 3 && guard < 1000) { let earFound = false; for (let i = 0; i < verts.length; i++) { const i0 = verts[(i + verts.length - 1) % verts.length]; const i1 = verts[i]; const i2 = verts[(i + 1) % verts.length]; if (!isConvex(i0, i1, i2)) continue; // Check if no other point lies inside the ear let hasPointInside = false; for (let j = 0; j < verts.length; j++) { if (j === (i + verts.length - 1) % verts.length || j === i || j === (i + 1) % verts.length) continue; if (pointInTriangle(points[verts[j]], points[i0], points[i1], points[i2])) { hasPointInside = true; break; } } if (hasPointInside) continue; // Add triangle triangles.push([points[i0], points[i1], points[i2]]); verts.splice(i, 1); earFound = true; break; } if (!earFound) break; guard++; } if (verts.length === 3) { triangles.push([points[verts[0]], points[verts[1]], points[verts[2]]]); } return triangles; } // --- Building connections between TPolygon --- function buildTPolygonConnections(tpolys) { // Assume two TPolygons are neighbors if they share two vertices for (let i = 0; i < tpolys.length; i++) { for (let j = i + 1; j < tpolys.length; j++) { const shared = countSharedVertices(tpolys[i].mainTriangle, tpolys[j].mainTriangle); if (shared === 2) { const dist = distance(tpolys[i].centerPoint, tpolys[j].centerPoint); tpolys[i].connections.push(new TPolygonConnection(tpolys[j], dist)); tpolys[j].connections.push(new TPolygonConnection(tpolys[i], dist)); } } } } function countSharedVertices(tri1, tri2) { let count = 0; for (const v1 of tri1) { for (const v2 of tri2) { if (v1.x === v2.x && v1.y === v2.y) count++; } } return count; } function distance(a, b) { return poly2d_js_1.Point.getDistance(a, b); } // Check for convexity of polygon function isConvexPolygon(points) { if (points.length < 4) return true; let sign = 0; for (let i = 0; i < points.length; i++) { const dx1 = points[(i + 2) % points.length].x - points[(i + 1) % points.length].x; const dy1 = points[(i + 2) % points.length].y - points[(i + 1) % points.length].y; const dx2 = points[i].x - points[(i + 1) % points.length].x; const dy2 = points[i].y - points[(i + 1) % points.length].y; const zcross = dx1 * dy2 - dy1 * dx2; if (zcross !== 0) { if (sign === 0) sign = Math.sign(zcross); else if (sign !== Math.sign(zcross)) return false; } } return true; } // Main polygon class Polygon { constructor(points, holes = []) { this.tpolygons = []; this.holes = []; this.points = reverseIfNotCCW(points); this.holes = holes; this._rebuildTriangulation(); } _rebuildTriangulation() { // If there are holes - use earcut if (this.holes.length > 0) { const triangles = triangulateWithHoles(this.points, this.holes); this.tpolygons = triangles.map(tri => new TPolygon(tri)); buildTPolygonConnections(this.tpolygons); } else { const triangles = earClippingTriangulation(this.points); this.tpolygons = triangles.map(tri => new TPolygon(tri)); buildTPolygonConnections(this.tpolygons); } // Rebuild tpolygons and connections for all holes (their own triangulation) for (const hole of this.holes) { if (hole.holes.length > 0) { const trianglesH = triangulateWithHoles(hole.points, hole.holes); hole.tpolygons = trianglesH.map(tri => new TPolygon(tri)); buildTPolygonConnections(hole.tpolygons); } else { const trianglesH = earClippingTriangulation(hole.points); hole.tpolygons = trianglesH.map(tri => new TPolygon(tri)); buildTPolygonConnections(hole.tpolygons); } } } // Check for convexity isConvex() { return isConvexPolygon(this.points); } // Check if point is inside this polygon (using ray-casting algorithm) isPointInPolygon(point) { return Polygon.isPointInPolygon(point, this); } // Check if point is inside this polygon using triangulation isPointInPolygonTriangulated(point) { return Polygon.isPointInPolygonTriangulated(point, this); } // Static method to check if point is inside a polygon (using ray-casting algorithm) static isPointInPolygon(point, polygon) { // First check if point is in main polygon using ray-casting let inside = false; const points = polygon.points; for (let i = 0, j = points.length - 1; i < points.length; j = i++) { const xi = points[i].x, yi = points[i].y; const xj = points[j].x, yj = points[j].y; const intersect = ((yi > point.y) !== (yj > point.y)) && (point.x < (xj - xi) * (point.y - yi) / (yj - yi + 1e-12) + xi); if (intersect) inside = !inside; } // If point is inside main polygon, check if it's not in any hole if (inside && polygon.holes.length > 0) { for (const hole of polygon.holes) { if (Polygon.isPointInPolygon(point, hole)) { return false; // Point is in a hole } } } return inside; } // Static method to check if point is inside a polygon using triangulation static isPointInPolygonTriangulated(point, polygon) { // Check if point is inside any of the triangulated polygons for (const tpoly of polygon.tpolygons) { if ((0, poly2d_js_1.pointInTriangle)(point, tpoly.mainTriangle)) { return true; // Point is in polygon and not in any hole } } return false; // Point is not in any triangle } // Union unionPolygon(other) { const pcA = (0, polygon_map_js_1.toPolygonClippingFormat)(this.points); const pcB = (0, polygon_map_js_1.toPolygonClippingFormat)(other.points); const result = polygon_clipping_1.default.union(pcA, pcB); return new polygon_map_js_1.PolygonMap(Polygon.fromClippingResult(result)); } // Difference differencePolygon(other) { const pcA = (0, polygon_map_js_1.toPolygonClippingFormat)(this.points); const pcB = (0, polygon_map_js_1.toPolygonClippingFormat)(other.points); const result = polygon_clipping_1.default.difference(pcA, pcB); return new polygon_map_js_1.PolygonMap(Polygon.fromClippingResult(result)); } // Get all outer contours as an array of Polygon static fromClippingResult(result) { if (!result || result.length === 0) return []; // result: MultiPolygon (array of polygons), polygon: array of rings return result.map((poly) => { const outer = poly[0].map((pt) => new poly2d_js_1.Point(pt[0], pt[1])); const holes = poly.slice(1).map((hole) => new Polygon(hole.map((pt) => new poly2d_js_1.Point(pt[0], pt[1])))); return new Polygon(outer, holes); }); } } exports.Polygon = Polygon;