export type Point2D = [number, number] const EPSILON = 1e-7 const KEY_SCALE = 1e6 type Edge = { start: Point2D end: Point2D polygonIndex: number splits: number[] } type Segment = { start: Point2D end: Point2D used: boolean } function pointsEqual(a: Point2D, b: Point2D, tolerance = EPSILON) { return Math.hypot(a[0] - b[0], a[1] - b[1]) <= tolerance } function pointKey(point: Point2D) { return `${Math.round(point[0] * KEY_SCALE)}:${Math.round(point[1] * KEY_SCALE)}` } function interpolate(a: Point2D, b: Point2D, t: number): Point2D { return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t] } function cross(ax: number, ay: number, bx: number, by: number) { return ax * by - ay * bx } function polygonArea(points: Point2D[]) { let area = 0 for (let i = 0; i < points.length; i++) { const current = points[i]! const next = points[(i + 1) % points.length]! area += current[0] * next[1] - next[0] * current[1] } return area / 2 } function pointOnSegment(point: Point2D, start: Point2D, end: Point2D) { const dx = end[0] - start[0] const dz = end[1] - start[1] const crossValue = cross(point[0] - start[0], point[1] - start[1], dx, dz) if (Math.abs(crossValue) > EPSILON) return false const dot = (point[0] - start[0]) * (point[0] - end[0]) + (point[1] - start[1]) * (point[1] - end[1]) return dot <= EPSILON } function pointInPolygonOrOnBoundary(point: Point2D, polygon: Point2D[]) { if ( polygon.some((start, index) => pointOnSegment(point, start, polygon[(index + 1) % polygon.length]!), ) ) { return true } return pointInPolygon(point, polygon) } function pointInPolygon(point: Point2D, polygon: Point2D[]) { let inside = false for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { const pi = polygon[i]! const pj = polygon[j]! if (pointOnSegment(point, pj, pi)) return false const intersects = pi[1] > point[1] !== pj[1] > point[1] && point[0] < ((pj[0] - pi[0]) * (point[1] - pi[1])) / (pj[1] - pi[1]) + pi[0] if (intersects) inside = !inside } return inside } function normalizeRing(ring: Point2D[]) { const normalized: Point2D[] = [] for (const [x, z] of ring) { if (!Number.isFinite(x) || !Number.isFinite(z)) continue const point: Point2D = [x, z] const previous = normalized[normalized.length - 1] if (!previous || !pointsEqual(previous, point)) { normalized.push(point) } } const first = normalized[0] const last = normalized[normalized.length - 1] if (first && last && pointsEqual(first, last)) { normalized.pop() } if (normalized.length < 3 || Math.abs(polygonArea(normalized)) <= EPSILON) return [] return polygonArea(normalized) < 0 ? [...normalized].reverse() : normalized } function addSplit(edge: Edge, t: number) { if (t < -EPSILON || t > 1 + EPSILON) return const clamped = Math.max(0, Math.min(1, t)) if (edge.splits.some((split) => Math.abs(split - clamped) <= EPSILON)) return edge.splits.push(clamped) } function parameterOnEdge(point: Point2D, edge: Edge) { const dx = edge.end[0] - edge.start[0] const dz = edge.end[1] - edge.start[1] const lengthSquared = dx * dx + dz * dz if (lengthSquared <= EPSILON) return 0 return ((point[0] - edge.start[0]) * dx + (point[1] - edge.start[1]) * dz) / lengthSquared } function addIntersectionSplits(left: Edge, right: Edge) { const rx = left.end[0] - left.start[0] const rz = left.end[1] - left.start[1] const sx = right.end[0] - right.start[0] const sz = right.end[1] - right.start[1] const qpx = right.start[0] - left.start[0] const qpz = right.start[1] - left.start[1] const denominator = cross(rx, rz, sx, sz) const numerator = cross(qpx, qpz, rx, rz) if (Math.abs(denominator) <= EPSILON) { if (Math.abs(numerator) > EPSILON) return for (const point of [left.start, left.end, right.start, right.end]) { if ( pointOnSegment(point, left.start, left.end) && pointOnSegment(point, right.start, right.end) ) { addSplit(left, parameterOnEdge(point, left)) addSplit(right, parameterOnEdge(point, right)) } } return } const t = cross(qpx, qpz, sx, sz) / denominator const u = cross(qpx, qpz, rx, rz) / denominator if (t < -EPSILON || t > 1 + EPSILON || u < -EPSILON || u > 1 + EPSILON) return addSplit(left, t) addSplit(right, u) } function buildEdges(polygons: Point2D[][]) { const edges: Edge[] = [] polygons.forEach((polygon, polygonIndex) => { for (let i = 0; i < polygon.length; i++) { edges.push({ start: polygon[i]!, end: polygon[(i + 1) % polygon.length]!, polygonIndex, splits: [0, 1], }) } }) for (let i = 0; i < edges.length; i++) { for (let j = i + 1; j < edges.length; j++) { const left = edges[i]! const right = edges[j]! if (left.polygonIndex === right.polygonIndex) continue addIntersectionSplits(left, right) } } return edges } function buildBoundarySegments(edges: Edge[], polygons: Point2D[][]) { const segments: Segment[] = [] for (const edge of edges) { const splits = [...edge.splits].sort((a, b) => a - b) for (let i = 0; i < splits.length - 1; i++) { const startT = splits[i]! const endT = splits[i + 1]! if (endT - startT <= EPSILON) continue const start = interpolate(edge.start, edge.end, startT) const end = interpolate(edge.start, edge.end, endT) const mid = interpolate(edge.start, edge.end, (startT + endT) / 2) const insideAnother = polygons.some( (polygon, index) => index !== edge.polygonIndex && pointInPolygon(mid, polygon), ) if (!insideAnother) { segments.push({ start, end, used: false }) } } } return removeDuplicateInteriorSegments(segments) } function segmentKey(segment: Segment) { const start = pointKey(segment.start) const end = pointKey(segment.end) return start < end ? `${start}|${end}` : `${end}|${start}` } function removeDuplicateInteriorSegments(segments: Segment[]) { const groups = new Map() for (const segment of segments) { const key = segmentKey(segment) const group = groups.get(key) if (group) { group.push(segment) } else { groups.set(key, [segment]) } } const result: Segment[] = [] for (const group of groups.values()) { if (group.length === 1) { result.push(group[0]!) continue } const firstStart = pointKey(group[0]!.start) const firstEnd = pointKey(group[0]!.end) const hasOppositeDirection = group.some( (segment) => pointKey(segment.start) === firstEnd && pointKey(segment.end) === firstStart, ) if (!hasOppositeDirection) { result.push(group[0]!) } } return result } function assembleRings(segments: Segment[]) { const byStart = new Map() for (const segment of segments) { const key = pointKey(segment.start) const group = byStart.get(key) if (group) { group.push(segment) } else { byStart.set(key, [segment]) } } const rings: Point2D[][] = [] for (const firstSegment of segments) { if (firstSegment.used) continue firstSegment.used = true const ring: Point2D[] = [firstSegment.start, firstSegment.end] const startKey = pointKey(firstSegment.start) let currentKey = pointKey(firstSegment.end) while (currentKey !== startKey) { const next = byStart.get(currentKey)?.find((segment) => !segment.used) if (!next) break next.used = true ring.push(next.end) currentKey = pointKey(next.end) } if (currentKey !== startKey) continue const last = ring[ring.length - 1] if (last && pointsEqual(ring[0]!, last)) { ring.pop() } const normalized = normalizeRing(ring) if (normalized.length >= 3) { rings.push(normalized) } } return rings } export function unionPolygons(polygons: Point2D[][]): Point2D[][] { const validPolygons = polygons.map(normalizeRing).filter((polygon) => polygon.length >= 3) if (validPolygons.length <= 1) return validPolygons const edges = buildEdges(validPolygons) const segments = buildBoundarySegments(edges, validPolygons) const rings = assembleRings(segments) return rings.length > 0 ? rings : validPolygons } function buildDifferenceBoundarySegments(edges: Edge[], polygons: Point2D[][]) { const subject = polygons[0] const cutters = polygons.slice(1) if (!subject) return [] const segments: Segment[] = [] for (const edge of edges) { const splits = [...edge.splits].sort((a, b) => a - b) for (let i = 0; i < splits.length - 1; i++) { const startT = splits[i]! const endT = splits[i + 1]! if (endT - startT <= EPSILON) continue const start = interpolate(edge.start, edge.end, startT) const end = interpolate(edge.start, edge.end, endT) const mid = interpolate(edge.start, edge.end, (startT + endT) / 2) if (edge.polygonIndex === 0) { const insideCutter = cutters.some((cutter) => pointInPolygonOrOnBoundary(mid, cutter)) if (!insideCutter) { segments.push({ start, end, used: false }) } continue } const insideSubject = pointInPolygon(mid, subject) const insideAnotherCutter = cutters.some( (cutter, cutterIndex) => cutterIndex !== edge.polygonIndex - 1 && pointInPolygonOrOnBoundary(mid, cutter), ) if (insideSubject && !insideAnotherCutter) { segments.push({ start: end, end: start, used: false }) } } } return removeDuplicateInteriorSegments(segments) } export function subtractPolygonsFromPolygon(subject: Point2D[], cutters: Point2D[][]): Point2D[][] { const validSubject = normalizeRing(subject) if (validSubject.length < 3) return [] const validCutters = cutters.map(normalizeRing).filter((polygon) => polygon.length >= 3) if (validCutters.length === 0) return [validSubject] const polygons = [validSubject, ...validCutters] const edges = buildEdges(polygons) const segments = buildDifferenceBoundarySegments(edges, polygons) const rings = assembleRings(segments) if (rings.length > 0) return rings const fullyCovered = validSubject.every((point) => validCutters.some((cutter) => pointInPolygonOrOnBoundary(point, cutter)), ) return fullyCovered ? [] : [validSubject] }