import StraightSkeleton from './skeleton/StraightSkeleton'; import { clone, Point, sub, normalize, equal, area, scale } from './vector'; export function normalizePolygon(vertices: Point[], isHole?: boolean) { const polyArea = area(vertices); if ((!isHole && polyArea < 0) || (isHole && polyArea > 0)) { vertices = vertices.slice(); vertices.reverse(); } const len = vertices.length; let prev = vertices[len - 1]; const newContour = []; const v0: Point = [0, 0]; const v1: Point = [0, 0]; for (let i = 0; i < len; i++) { const point = vertices[i]; const next = vertices[(i + 1) % len]; if (equal(point, next)) { // Point same continue; } sub(v0, point, prev); sub(v1, next, point); normalize(v0, v0); normalize(v1, v1); if (equal(v0, v1)) { // Parallel continue; } newContour.push(point); prev = point; } return newContour; } function bboxFromPoints(pts: Point[]) { let minX = Infinity; let minY = Infinity; let maxX = -Infinity; let maxY = -Infinity; for (let i = 0; i < pts.length; i++) { const pt = pts[i]; minX = Math.min(minX, pt[0]); minY = Math.min(minY, pt[1]); maxX = Math.max(maxX, pt[0]); maxY = Math.max(maxY, pt[1]); } return [minX, minY, maxX, maxY]; } function resizePoints(pts: Point[], scale: number) { const newPts: Point[] = []; for (let i = 0; i < pts.length; i++) { const pt = pts[i]; newPts.push([pt[0] * scale, pt[1] * scale]); } return newPts; } export function offsetPolygon( polygon: Point[], holes: Point[][], distance: number, opts?: { epsilon?: number; infiniteLoopGuard?: number; } ): Point[][] { const [minX, minY, maxX, maxY] = bboxFromPoints(polygon); const width = maxX - minX; const height = maxY - minY; // Avoid too small polygons cause precision issue. // TODO better way to check the threshold? const scale = (width + height) / 2 < 10 ? 100 : 1; distance *= scale; if (scale > 1) { polygon = resizePoints(polygon, scale); holes = holes.map((hole) => resizePoints(hole, scale)); } holes = holes || []; const ss = new StraightSkeleton(); if (opts?.epsilon) { ss.setEpsilon(opts.epsilon); } ss.getCtx().infiniteLoopGuard = opts?.infiniteLoopGuard || 1e4; ss.setDistance(distance); ss.execute(polygon, holes); return ss.getNodeLoops().map((loop) => loop.map((node) => // Scale back if necessary scale > 1 ? [node.p[0] / scale, node.p[1] / scale] : clone(node.p) ) ); }