export interface Point { x: number; y: number; } export type Polygon = Point[]; export const signedDistanceToEdge = (edgeStart: Point, edgeEnd: Point, point: Point): number => (edgeEnd.x - edgeStart.x) * (point.y - edgeStart.y) - (edgeEnd.y - edgeStart.y) * (point.x - edgeStart.x); export const iterateEdges = ( polygon: Polygon, callback: (current: Point, next: Point, index: number) => boolean | void, ): void => { if (!Array.isArray(polygon) || polygon.length === 0) { return; } for (let index = 0; index < polygon.length; index += 1) { const current = polygon[index]; const next = polygon[(index + 1) % polygon.length]; if (callback(current, next, index) === false) { break; } } }; export const forEachVertex = ( polygon: Polygon, callback: (current: Point, previous: Point, index: number) => boolean | void, ): void => { if (!Array.isArray(polygon) || polygon.length === 0) { return; } for (let index = 0; index < polygon.length; index += 1) { const current = polygon[index]; const prev = polygon[(index - 1 + polygon.length) % polygon.length]; if (callback(current, prev, index) === false) { break; } } }; export const lineIntersection = (p1: Point, p2: Point, cp1: Point, cp2: Point): Point => { const A1 = p2.y - p1.y; const B1 = p1.x - p2.x; const C1 = A1 * p1.x + B1 * p1.y; const A2 = cp2.y - cp1.y; const B2 = cp1.x - cp2.x; const C2 = A2 * cp1.x + B2 * cp1.y; const det = A1 * B2 - A2 * B1; if (Math.abs(det) < 1e-6) { return { x: cp1.x, y: cp1.y }; } return { x: (B2 * C1 - B1 * C2) / det, y: (A1 * C2 - A2 * C1) / det, }; }; export const clipPolygon = (subject: Polygon, clipper: Polygon): Polygon => { if (!Array.isArray(subject) || !subject.length) { return []; } let output = subject; iterateEdges(clipper, (cp1, cp2) => { const input = output; output = []; if (!Array.isArray(input) || !input.length) { return false; } forEachVertex(input, (current, prev) => { const currentInside = signedDistanceToEdge(cp1, cp2, current) >= 0; const prevInside = signedDistanceToEdge(cp1, cp2, prev) >= 0; if (currentInside) { if (!prevInside) { output.push(lineIntersection(prev, current, cp1, cp2)); } output.push(current); } else if (prevInside) { output.push(lineIntersection(prev, current, cp1, cp2)); } return true; }); return output.length > 0; }); return output; }; export const isPointInsideConvex = (point: Point, polygon: Polygon): boolean => { if (!polygon?.length) { return false; } let sign = 0; let inside = true; iterateEdges(polygon, (a, b) => { const cross = signedDistanceToEdge(a, b, point); if (cross === 0) { return true; } const currentSign = cross > 0 ? 1 : -1; if (sign === 0) { sign = currentSign; return true; } if (sign !== currentSign) { inside = false; return false; } return true; }); return inside; }; export const polygonCentroid = (polygon: Polygon): Point => { if (!polygon?.length) { return { x: 0, y: 0 }; } let area = 0; let cx = 0; let cy = 0; iterateEdges(polygon, (current, next) => { const cross = current.x * next.y - next.x * current.y; area += cross; cx += (current.x + next.x) * cross; cy += (current.y + next.y) * cross; }); if (Math.abs(area) < 1e-6) { let sumX = 0; let sumY = 0; forEachVertex(polygon, (point) => { sumX += point.x; sumY += point.y; }); return { x: sumX / polygon.length, y: sumY / polygon.length, }; } const areaFactor = 1 / (3 * area); return { x: cx * areaFactor, y: cy * areaFactor, }; };