poly-math-2d
Version:
2D Polygon math: boolean operations, triangulation, graphs, support for holes and non-convex shapes.
204 lines (203 loc) • 8.08 kB
JavaScript
;
// poly2d.ts
// Optimized functions for working with triangles (3 vertices, always convex)
Object.defineProperty(exports, "__esModule", { value: true });
exports.Point = void 0;
exports.pointInTriangle = pointInTriangle;
exports.segmentsIntersect = segmentsIntersect;
exports.trianglesIntersect = trianglesIntersect;
exports.trianglesConvexUnion = trianglesConvexUnion;
exports.cross = cross;
exports.trianglesDifference = trianglesDifference;
exports.polygonIntersectsPolygon = polygonIntersectsPolygon;
exports.convexUnion = convexUnion;
exports.convexDifference = convexDifference;
const point_js_1 = require("./point.js");
Object.defineProperty(exports, "Point", { enumerable: true, get: function () { return point_js_1.Point; } });
// 1. Check if a point lies inside a triangle (barycentric method)
function pointInTriangle(p, tri) {
const [a, b, c] = tri;
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);
}
// 2. Check if two segments intersect (leaving as is)
function segmentsIntersect(a1, a2, b1, b2) {
function ccw(p1, p2, p3) {
return (p3.y - p1.y) * (p2.x - p1.x) > (p2.y - p1.y) * (p3.x - p1.x);
}
return (ccw(a1, b1, b2) !== ccw(a2, b1, b2)) && (ccw(a1, a2, b1) !== ccw(a1, a2, b2));
}
// 3. Check if two triangles intersect
function trianglesIntersect(t1, t2) {
// Check for edge intersections
for (let i = 0; i < 3; i++) {
const a1 = t1[i], a2 = t1[(i + 1) % 3];
for (let j = 0; j < 3; j++) {
const b1 = t2[j], b2 = t2[(j + 1) % 3];
if (segmentsIntersect(a1, a2, b1, b2))
return true;
}
}
// One triangle inside another
if (pointInTriangle(t1[0], t2) || pointInTriangle(t2[0], t1))
return true;
return false;
}
// 4. Union of two triangles (convex hull for 6 points)
function trianglesConvexUnion(t1, t2) {
const points = [...t1, ...t2];
// Convex hull (Graham's algorithm for <= 6 points)
points.sort((a, b) => a.x === b.x ? a.y - b.y : a.x - b.x);
const lower = [];
for (const p of points) {
while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], p) <= 0)
lower.pop();
lower.push(p);
}
const upper = [];
for (let i = points.length - 1; i >= 0; i--) {
const p = points[i];
while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], p) <= 0)
upper.pop();
upper.push(p);
}
upper.pop();
lower.pop();
return lower.concat(upper);
}
// Universal cross
function cross(o, a, b) {
return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
}
// 5. Simple triangle subtraction from triangle (stub)
// Plan:
// 1. Check if triangles intersect. If not - return subject.
// 2. Implement Sutherland-Hodgman algorithm for clipping subject by clip.
// 3. Return array of triangles (or polygons) obtained after clipping.
function trianglesDifference(subject, clip) {
// If they don't intersect - return the original triangle
if (!trianglesIntersect(subject, clip))
return [subject];
// Convert triangles to point arrays
let output = [...subject];
// Sutherland–Hodgman: clip by each edge of clip
for (let i = 0; i < 3; i++) {
const cp1 = clip[i];
const cp2 = clip[(i + 1) % 3];
const input = output;
output = [];
for (let j = 0; j < input.length; j++) {
const s = input[j];
const e = input[(j + 1) % input.length];
// Check if point is on the left side of the clip edge
const inside = (p) => {
// Left side of clip edge — inside
return ((cp2.x - cp1.x) * (p.y - cp1.y) - (cp2.y - cp1.y) * (p.x - cp1.x)) < 0;
};
const s_in = inside(s);
const e_in = inside(e);
if (s_in && e_in) {
// Both inside — add the end point
output.push(e);
}
else if (s_in && !e_in) {
// s inside, e outside — add intersection point
const inter = segmentIntersection(s, e, cp1, cp2);
if (inter)
output.push(inter);
}
else if (!s_in && e_in) {
// s outside, e inside — add intersection and end point
const inter = segmentIntersection(s, e, cp1, cp2);
if (inter)
output.push(inter);
output.push(e);
}
// both outside — add nothing
}
// If nothing remains after clipping — return []
if (output.length === 0)
return [];
}
// If after clipping less than 3 points remain — invalid polygon
if (output.length < 3)
return [];
// Triangulate the result (for a triangle, it's either the triangle itself or a convex polygon)
// For simplicity: return as an array of triangles (fan triangulation)
const triangles = [];
for (let i = 1; i < output.length - 1; i++) {
triangles.push([output[0], output[i], output[i + 1]]);
}
return triangles;
}
// Helper function: intersection of two segments, returns point or null
function segmentIntersection(a1, a2, b1, b2) {
const d = (a2.x - a1.x) * (b2.y - b1.y) - (a2.y - a1.y) * (b2.x - b1.x);
if (d === 0)
return null; // parallel
const ua = ((b2.x - b1.x) * (a1.y - b1.y) - (b2.y - b1.y) * (a1.x - b1.x)) / d;
if (ua < 0 || ua > 1)
return null;
const ub = ((a2.x - a1.x) * (a1.y - b1.y) - (a2.y - a1.y) * (a1.x - b1.x)) / d;
if (ub < 0 || ub > 1)
return null;
return new point_js_1.Point(a1.x + ua * (a2.x - a1.x), a1.y + ua * (a2.y - a1.y));
}
function polygonIntersectsPolygon(poly1, poly2) {
// Helper function for point in polygon check (ray algorithm)
const pointInPolygonLocal = (point, polygon) => {
let inside = false;
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
const xi = polygon[i].x, yi = polygon[i].y;
const xj = polygon[j].x, yj = polygon[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;
}
return inside;
};
for (let i = 0; i < poly1.length - 1; i++) {
for (let j = 0; j < poly2.length - 1; j++) {
if (segmentsIntersect(poly1[i], poly1[i + 1], poly2[j], poly2[j + 1])) {
return true;
}
}
}
// One inside another
if (pointInPolygonLocal(poly1[0], poly2) || pointInPolygonLocal(poly2[0], poly1))
return true;
return false;
}
// Convex hull (Convex Hull, Graham's algorithm)
function convexUnion(poly1, poly2) {
const points = [...poly1, ...poly2];
points.sort((a, b) => a.x === b.x ? a.y - b.y : a.x - b.x);
const lower = [];
for (const p of points) {
while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], p) <= 0)
lower.pop();
lower.push(p);
}
const upper = [];
for (let i = points.length - 1; i >= 0; i--) {
const p = points[i];
while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], p) <= 0)
upper.pop();
upper.push(p);
}
upper.pop();
lower.pop();
return lower.concat(upper);
}
// Simple difference of convex: if they don't intersect - return subject, otherwise empty
function convexDifference(subject, clip) {
// Use polygonIntersectsPolygon from poly2d
if (!polygonIntersectsPolygon(subject, clip))
return [subject];
// For complex cases, Sutherland–Hodgman implementation is needed
return [];
}