import Errors from '../utils/errors' import * as Utils from '../utils/utils' import * as Constants from '../utils/constants' import LinkedList from '../data_structures/linked_list' import { addToIntPoints, sortIntersections, filterDuplicatedIntersections, initializeInclusionFlags, calculateInclusionFlags, setOverlappingFlags, intPointsPoolCount, splitByIntersections, Intersections, Intersection, } from '../data_structures/smart_intersections' import type { Point } from '../classes/Point' import type { Polygon } from '../classes/Polygon' import type { Segment } from '../classes/Segment' import type { Shape } from '../classes/Shape' import { Face } from '../classes' const { Inclusion, Overlap } = Constants const { START_VERTEX, END_VERTEX } = Constants export enum BooleanOp { UNION = 1, INTERSECT = 2, SUBTRACT = 3, } /** * Unify two polygons polygons and returns new polygon.
* Point belongs to the resulted polygon if it belongs to the first OR to the second polygon */ export function unify(polygon1: Polygon, polygon2: Polygon): Polygon { return booleanOpBinary(polygon1, polygon2, BooleanOp.UNION, true)[0] } /** * Subtract second polygon from the first and returns new polygon * Point belongs to the resulted polygon if it belongs to the first polygon AND NOT to the second polygon */ export function subtract(polygon1: Polygon, polygon2: Polygon): Polygon { const polygon2_reversed = polygon2.clone().reverse() return booleanOpBinary(polygon1, polygon2_reversed, BooleanOp.SUBTRACT, true)[0] } /** * Intersect two polygons and returns new polygon * Point belongs to the resulted polygon is it belongs to the first AND to the second polygon */ export function intersect(polygon1: Polygon, polygon2: Polygon): Polygon { return booleanOpBinary(polygon1, polygon2, BooleanOp.INTERSECT, true)[0] } /** * Returns boundary of intersection between two polygons as two arrays of shapes (Segments/Arcs)
* The first array are shapes from the first polygon, the second array are shapes from the second */ export function innerClip(polygon1: Polygon, polygon2: Polygon): Shape[][] { let [res_poly, wrk_poly] = booleanOpBinary(polygon1, polygon2, BooleanOp.INTERSECT, false) let clip_shapes1 = [] for (let face of res_poly.faces) { clip_shapes1 = [...clip_shapes1, ...[...face.edges].map((edge) => edge.shape)] } let clip_shapes2 = [] for (let face of wrk_poly.faces) { clip_shapes2 = [...clip_shapes2, ...[...face.edges].map((edge) => edge.shape)] } return [clip_shapes1, clip_shapes2] } /** * Returns boundary of subtraction of the second polygon from first polygon as array of shapes */ export function outerClip(polygon1: Polygon, polygon2: Polygon): Shape[] { const [res_poly] = booleanOpBinary(polygon1, polygon2, BooleanOp.SUBTRACT, false) let clip_shapes1 = [] for (let face of res_poly.faces) { clip_shapes1 = [...clip_shapes1, ...[...face.edges].map((edge) => edge.shape)] } return clip_shapes1 } /** * Returns intersection points between boundaries of two polygons as two array of points
* Points in the first array belong to first polygon, points from the second - to the second. * Points in each array are ordered according to the direction of the correspondent polygon */ export function calculateIntersections(polygon1: Polygon, polygon2: Polygon): Point[][] { let res_poly = polygon1.clone() let wrk_poly = polygon2.clone() // get intersection points let intersections = getIntersections(res_poly, wrk_poly) // sort intersection points sortIntersections(intersections) // split by intersection points splitByIntersections(res_poly, intersections.int_points1_sorted) splitByIntersections(wrk_poly, intersections.int_points2_sorted) // filter duplicated intersection points filterDuplicatedIntersections(intersections) // sort intersection points again after filtering sortIntersections(intersections) let ip_sorted1 = intersections.int_points1_sorted.map((int_point) => int_point.pt) let ip_sorted2 = intersections.int_points2_sorted.map((int_point) => int_point.pt) return [ip_sorted1, ip_sorted2] } function filterNotRelevantEdges(res_poly: Polygon, wrk_poly: Polygon, intersections: Intersections, op: BooleanOp) { // keep not intersected faces for further remove and merge let notIntersectedFacesRes = getNotIntersectedFaces(res_poly, intersections.int_points1) let notIntersectedFacesWrk = getNotIntersectedFaces(wrk_poly, intersections.int_points2) // calculate inclusion flag for not intersected faces calcInclusionForNotIntersectedFaces(notIntersectedFacesRes, wrk_poly) calcInclusionForNotIntersectedFaces(notIntersectedFacesWrk, res_poly) // initialize inclusion flags for edges incident to intersections initializeInclusionFlags(intersections.int_points1) initializeInclusionFlags(intersections.int_points2) // calculate inclusion flags only for edges incident to intersections calculateInclusionFlags(intersections.int_points1, wrk_poly) calculateInclusionFlags(intersections.int_points2, res_poly) // fix boundary conflicts while ( fixBoundaryConflicts( res_poly, wrk_poly, intersections.int_points1, intersections.int_points1_sorted, intersections.int_points2, intersections, ) ); // while (fixBoundaryConflicts(wrk_poly, res_poly, intersections.int_points2, intersections.int_points2_sorted, intersections.int_points1, intersections)); // Set overlapping flags for boundary chains: SAME or OPPOSITE setOverlappingFlags(intersections) // remove not relevant chains between intersection points removeNotRelevantChains(res_poly, op, intersections.int_points1_sorted, true) removeNotRelevantChains(wrk_poly, op, intersections.int_points2_sorted, false) // remove not relevant not intersected faces from res_polygon and wrk_polygon // if op == UNION, remove faces that are included in wrk_polygon without intersection // if op == INTERSECT, remove faces that are not included into wrk_polygon removeNotRelevantNotIntersectedFaces(res_poly, notIntersectedFacesRes, op, true) removeNotRelevantNotIntersectedFaces(wrk_poly, notIntersectedFacesWrk, op, false) } function swapLinksAndRestore(res_poly: Polygon, wrk_poly: Polygon, intersections: Intersections, op: BooleanOp) { // add edges of wrk_poly into the edge container of res_poly copyWrkToRes(res_poly, wrk_poly, op, intersections.int_points2) // swap links from res_poly to wrk_poly and vice versa swapLinks(res_poly, wrk_poly, intersections) // remove old faces removeOldFaces(res_poly, intersections.int_points1) removeOldFaces(wrk_poly, intersections.int_points2) // restore faces restoreFaces(res_poly, intersections.int_points1, intersections.int_points2) restoreFaces(res_poly, intersections.int_points2, intersections.int_points1) // merge relevant not intersected faces from wrk_polygon to res_polygon // mergeRelevantNotIntersectedFaces(res_poly, wrk_poly); } function booleanOpBinary(polygon1: Polygon, polygon2: Polygon, op: BooleanOp, restore: boolean) { let res_poly = polygon1.clone() let wrk_poly = polygon2.clone() // get intersection points let intersections = getIntersections(res_poly, wrk_poly) // sort intersection points sortIntersections(intersections) // split by intersection points splitByIntersections(res_poly, intersections.int_points1_sorted) splitByIntersections(wrk_poly, intersections.int_points2_sorted) // filter duplicated intersection points filterDuplicatedIntersections(intersections) // sort intersection points again after filtering sortIntersections(intersections) // calculate inclusion and remove not relevant edges filterNotRelevantEdges(res_poly, wrk_poly, intersections, op) if (restore) { swapLinksAndRestore(res_poly, wrk_poly, intersections, op) } return [res_poly, wrk_poly] } function getIntersections(polygon1: Polygon, polygon2: Polygon) { let intersections = { int_points1: [], int_points2: [], int_points1_sorted: [], int_points2_sorted: [], } // calculate intersections for (let edge1 of polygon1.edges) { // request edges of polygon2 in the box of edge1 let resp = polygon2.edges.search(edge1.box) // for each edge2 in response for (let edge2 of resp) { // calculate intersections between edge1 and edge2 let ip = edge1.shape.intersect(edge2.shape) // for each intersection point for (let pt of ip) { addToIntPoints(edge1, pt, intersections.int_points1) addToIntPoints(edge2, pt, intersections.int_points2) } } } return intersections } function getNotIntersectedFaces(poly: Polygon, int_points: Intersection[]) { let notIntersected = [] for (let face of poly.faces) { if (!int_points.find((ip) => ip.face === face)) { notIntersected.push(face) } } return notIntersected } function calcInclusionForNotIntersectedFaces(notIntersectedFaces: Face[], poly2: Polygon) { for (let face of notIntersectedFaces) { face.first.bv = face.first.bvStart = face.first.bvEnd = undefined face.first.setInclusion(poly2) } } function fixBoundaryConflicts( poly1: Polygon, poly2: Polygon, int_points1: Intersection[], int_points1_sorted: Intersection[], int_points2: Intersection[], intersections: Intersections ) { let cur_face let first_int_point_in_face_id let next_int_point1 let num_int_points = int_points1_sorted.length let iterate_more = false for (let i = 0; i < num_int_points; i++) { let cur_int_point1 = int_points1_sorted[i] // Find boundary chain in the polygon1 if (cur_int_point1.face !== cur_face) { // next chain started first_int_point_in_face_id = i // cur_int_point1; cur_face = cur_int_point1.face } // Skip duplicated points with same in "cur_int_point1" pool let int_points_cur_pool_start = i let int_points_cur_pool_num = intPointsPoolCount(int_points1_sorted, i, cur_face) let next_int_point_id if ( int_points_cur_pool_start + int_points_cur_pool_num < num_int_points && int_points1_sorted[int_points_cur_pool_start + int_points_cur_pool_num].face === cur_face ) { next_int_point_id = int_points_cur_pool_start + int_points_cur_pool_num } else { // get first point from the same face next_int_point_id = first_int_point_in_face_id } // From all points with same ,x,y. in 'next_int_point1' pool choose one that // has same face both in res_poly and in wrk_poly let int_points_next_pool_num = intPointsPoolCount(int_points1_sorted, next_int_point_id, cur_face) next_int_point1 = null for (let j = next_int_point_id; j < next_int_point_id + int_points_next_pool_num; j++) { let next_int_point1_tmp = int_points1_sorted[j] if ( next_int_point1_tmp.face === cur_face && int_points2[next_int_point1_tmp.id].face === int_points2[cur_int_point1.id].face ) { next_int_point1 = next_int_point1_tmp break } } if (next_int_point1 === null) continue let edge_from1 = cur_int_point1.edge_after let edge_to1 = next_int_point1.edge_before // Case #1. One of the ends is not boundary - probably tiny edge wrongly marked as boundary if (edge_from1.bv === Inclusion.BOUNDARY && edge_to1.bv != Inclusion.BOUNDARY) { edge_from1.bv = edge_to1.bv continue } if (edge_from1.bv != Inclusion.BOUNDARY && edge_to1.bv === Inclusion.BOUNDARY) { edge_to1.bv = edge_from1.bv continue } // Set up all boundary values for middle edges. Need for cases 2 and 3 if ( (edge_from1.bv === Inclusion.BOUNDARY && edge_to1.bv === Inclusion.BOUNDARY && edge_from1 != edge_to1) || (edge_from1.bv === Inclusion.INSIDE && edge_to1.bv === Inclusion.OUTSIDE) || (edge_from1.bv === Inclusion.OUTSIDE && edge_to1.bv === Inclusion.INSIDE) ) { let edge_tmp = edge_from1.next while (edge_tmp != edge_to1) { edge_tmp.bvStart = undefined edge_tmp.bvEnd = undefined edge_tmp.bv = undefined edge_tmp.setInclusion(poly2) edge_tmp = edge_tmp.next } } // Case #2. Both of the ends boundary. Check all the edges in the middle // If some edges in the middle are not boundary then update bv of 'from' and 'to' edges if (edge_from1.bv === Inclusion.BOUNDARY && edge_to1.bv === Inclusion.BOUNDARY && edge_from1 != edge_to1) { let edge_tmp = edge_from1.next let new_bv while (edge_tmp != edge_to1) { if (edge_tmp.bv != Inclusion.BOUNDARY) { if (new_bv === undefined) { // first not boundary edge between from and to new_bv = edge_tmp.bv } else { // another not boundary edge between from and to if (edge_tmp.bv != new_bv) { // and it has different bv - can't resolve conflict throw Errors.UNRESOLVED_BOUNDARY_CONFLICT } } } edge_tmp = edge_tmp.next } if (new_bv != undefined) { edge_from1.bv = new_bv edge_to1.bv = new_bv } continue // all middle edges are boundary, proceed with this } // Case 3. One of the ends is inner, another is outer if ( (edge_from1.bv === Inclusion.INSIDE && edge_to1.bv === Inclusion.OUTSIDE) || (edge_from1.bv === Inclusion.OUTSIDE && edge_to1.bv === Inclusion.INSIDE) ) { let edge_tmp = edge_from1 // Find missing intersection point while (edge_tmp != edge_to1) { if (edge_tmp.bvStart === edge_from1.bv && edge_tmp.bvEnd === edge_to1.bv) { let [dist, segment] = edge_tmp.shape.distanceTo(poly2) as [number, Segment] if (dist < 10 * Utils.getTolerance()) { // it should be very close // let pt = edge_tmp.end; // add to the list of intersections of poly1 addToIntPoints(edge_tmp, segment.start, int_points1) // split edge_tmp in poly1 if need let int_point1 = int_points1[int_points1.length - 1] if (int_point1.is_vertex & START_VERTEX) { // nothing to split int_point1.edge_after = edge_tmp int_point1.edge_before = edge_tmp.prev edge_tmp.bvStart = Inclusion.BOUNDARY edge_tmp.bv = undefined edge_tmp.setInclusion(poly2) } else if (int_point1.is_vertex & END_VERTEX) { // nothing to split int_point1.edge_after = edge_tmp.next edge_tmp.bvEnd = Inclusion.BOUNDARY edge_tmp.bv = undefined edge_tmp.setInclusion(poly2) } else { // split edge here let newEdge1 = poly2.addVertex(int_point1.pt, edge_tmp) int_point1.edge_before = newEdge1 int_point1.edge_after = newEdge1.next newEdge1.setInclusion(poly2) newEdge1.next.bvStart = Inclusion.BOUNDARY newEdge1.next.bvEnd = undefined newEdge1.next.bv = undefined newEdge1.next.setInclusion(poly2) } // add to the list of intersections of poly2 let edge2 = poly2.findEdgeByPoint(segment.end) addToIntPoints(edge2, segment.end, int_points2) // split edge2 in poly2 if need let int_point2 = int_points2[int_points2.length - 1] if (int_point2.is_vertex & START_VERTEX) { // nothing to split int_point2.edge_after = edge2 int_point2.edge_before = edge2.prev } else if (int_point2.is_vertex & END_VERTEX) { // nothing to split int_point2.edge_after = edge2.next } else { // split edge here // first locate int_points that may refer to edge2 as edge.after // let int_point2_edge_before = int_points2.find( int_point => int_point.edge_before === edge2) let int_point2_edge_after = int_points2.find((int_point) => int_point.edge_after === edge2) let newEdge2 = poly2.addVertex(int_point2.pt, edge2) int_point2.edge_before = newEdge2 int_point2.edge_after = newEdge2.next if (int_point2_edge_after) int_point2_edge_after.edge_after = newEdge2 newEdge2.bvStart = undefined newEdge2.bvEnd = Inclusion.BOUNDARY newEdge2.bv = undefined newEdge2.setInclusion(poly1) newEdge2.next.bvStart = Inclusion.BOUNDARY newEdge2.next.bvEnd = undefined newEdge2.next.bv = undefined newEdge2.next.setInclusion(poly1) } sortIntersections(intersections) iterate_more = true break } } edge_tmp = edge_tmp.next } // we changed intersections inside loop, have to exit and repair again if (iterate_more) break throw Errors.UNRESOLVED_BOUNDARY_CONFLICT } } return iterate_more } export function removeNotRelevantChains( polygon: Polygon, op: BooleanOp, int_points: Intersection[], is_res_polygon: boolean, ) { if (!int_points) return let cur_face = undefined let first_int_point_in_face_num = undefined let int_point_current: Intersection let int_point_next: Intersection for (let i = 0; i < int_points.length; i++) { int_point_current = int_points[i] if (int_point_current.face !== cur_face) { // next face started first_int_point_in_face_num = i cur_face = int_point_current.face } if (cur_face.isEmpty()) // ?? continue // Get next int point from the same face that current // Count how many duplicated points with same in "points from" pool ? let int_points_from_pull_start = i let int_points_from_pull_num = intPointsPoolCount(int_points, i, cur_face) let next_int_point_num if ( int_points_from_pull_start + int_points_from_pull_num < int_points.length && int_points[int_points_from_pull_start + int_points_from_pull_num].face === int_point_current.face ) { next_int_point_num = int_points_from_pull_start + int_points_from_pull_num } else { // get first point from the same face next_int_point_num = first_int_point_in_face_num } int_point_next = int_points[next_int_point_num] /* Count how many duplicated points with same in "points to" pull ? */ let int_points_to_pull_start = next_int_point_num let int_points_to_pull_num = intPointsPoolCount(int_points, int_points_to_pull_start, cur_face) let edge_from = int_point_current.edge_after let edge_to = int_point_next.edge_before if ( (edge_from.bv === Inclusion.INSIDE && edge_to.bv === Inclusion.INSIDE && op === BooleanOp.UNION) || (edge_from.bv === Inclusion.OUTSIDE && edge_to.bv === Inclusion.OUTSIDE && op === BooleanOp.INTERSECT) || ((edge_from.bv === Inclusion.OUTSIDE || edge_to.bv === Inclusion.OUTSIDE) && op === BooleanOp.SUBTRACT && !is_res_polygon) || ((edge_from.bv === Inclusion.INSIDE || edge_to.bv === Inclusion.INSIDE) && op === BooleanOp.SUBTRACT && is_res_polygon) || (edge_from.bv === Inclusion.BOUNDARY && edge_to.bv === Inclusion.BOUNDARY && edge_from.overlap & Overlap.SAME && is_res_polygon) || (edge_from.bv === Inclusion.BOUNDARY && edge_to.bv === Inclusion.BOUNDARY && edge_from.overlap & Overlap.OPPOSITE) ) { polygon.removeChain(cur_face, edge_from, edge_to) /* update all points in "points from" pull */ for (let k = int_points_from_pull_start; k < int_points_from_pull_start + int_points_from_pull_num; k++) { int_points[k].edge_after = undefined } /* update all points in "points to" pull */ for (let k = int_points_to_pull_start; k < int_points_to_pull_start + int_points_to_pull_num; k++) { int_points[k].edge_before = undefined } } /* skip to the last point in "points from" group */ i += int_points_from_pull_num - 1 } } function copyWrkToRes(res_polygon: Polygon, wrk_polygon: Polygon, op: BooleanOp, int_points: Intersection[]) { for (let face of wrk_polygon.faces) { for (let edge of face) { res_polygon.edges.add(edge) } // If union - add face from wrk_polygon that is not intersected with res_polygon if ( /*(op === BooleanOp.UNION || op == BooleanOp.SUBTRACT) &&*/ int_points.find((ip) => ip.face === face) === undefined ) { res_polygon.addFace(face.first, face.last) } } } function swapLinks(res_polygon: Polygon, wrk_polygon: Polygon, intersections: Intersections) { if (intersections.int_points1.length === 0) return for (let i = 0; i < intersections.int_points1.length; i++) { let int_point1 = intersections.int_points1[i] let int_point2 = intersections.int_points2[i] // Simple case - find continuation on the other polygon // Process edge from res_polygon if (int_point1.edge_before !== undefined && int_point1.edge_after === undefined) { // swap need if (int_point2.edge_before === undefined && int_point2.edge_after !== undefined) { // simple case // Connect edges int_point1.edge_before.next = int_point2.edge_after int_point2.edge_after.prev = int_point1.edge_before // Fill in missed links in intersection points int_point1.edge_after = int_point2.edge_after int_point2.edge_before = int_point1.edge_before } } // Process edge from wrk_polygon if (int_point2.edge_before !== undefined && int_point2.edge_after === undefined) { // swap need if (int_point1.edge_before === undefined && int_point1.edge_after !== undefined) { // simple case // Connect edges int_point2.edge_before.next = int_point1.edge_after int_point1.edge_after.prev = int_point2.edge_before // Complete missed links int_point2.edge_after = int_point1.edge_after int_point1.edge_before = int_point2.edge_before } } // Continuation not found - complex case // Continuation will be found on the same polygon. // It happens when intersection point is actually touching point // Polygon1 if (int_point1.edge_before !== undefined && int_point1.edge_after === undefined) { // still swap need for (let int_point of intersections.int_points1_sorted) { if (int_point === int_point1) continue // skip same if (int_point.edge_before === undefined && int_point.edge_after !== undefined) { if (int_point.pt.equalTo(int_point1.pt)) { // Connect edges int_point1.edge_before.next = int_point.edge_after int_point.edge_after.prev = int_point1.edge_before // Complete missed links int_point1.edge_after = int_point.edge_after int_point.edge_before = int_point1.edge_before } } } } // Polygon2 if (int_point2.edge_before !== undefined && int_point2.edge_after === undefined) { // still swap need for (let int_point of intersections.int_points2_sorted) { if (int_point === int_point2) continue // skip same if (int_point.edge_before === undefined && int_point.edge_after !== undefined) { if (int_point.pt.equalTo(int_point2.pt)) { // Connect edges int_point2.edge_before.next = int_point.edge_after int_point.edge_after.prev = int_point2.edge_before // Complete missed links int_point2.edge_after = int_point.edge_after int_point.edge_before = int_point2.edge_before } } } } } // Sanity check that no dead ends left } export function removeOldFaces(polygon: Polygon, int_points: Intersection[]) { for (let int_point of int_points) { polygon.faces.delete(int_point.face) int_point.face = undefined if (int_point.edge_before) int_point.edge_before.face = undefined if (int_point.edge_after) int_point.edge_after.face = undefined } } export function restoreFaces(polygon: Polygon, int_points: Intersection[], other_int_points: Intersection[]) { // For each intersection point - create new face for (let int_point of int_points) { if (int_point.edge_before === undefined || int_point.edge_after === undefined) // completely deleted continue if (int_point.face) // already restored continue if (int_point.edge_after.face || int_point.edge_before.face) // Face already created. Possible case in duplicated intersection points continue let first = int_point.edge_after // face start let last = int_point.edge_before // face end; LinkedList.testInfiniteLoop(first) // check and throw error if infinite loop found let face = polygon.addFace(first, last) // Mark intersection points from the newly create face // to avoid multiple creation of the same face // Face was assigned to each edge of new face in addFace function for (let int_point_tmp of int_points) { if ( int_point_tmp.edge_before && int_point_tmp.edge_after && int_point_tmp.edge_before.face === face && int_point_tmp.edge_after.face === face ) { int_point_tmp.face = face } } // Mark other intersection points as well for (let int_point_tmp of other_int_points) { if ( int_point_tmp.edge_before && int_point_tmp.edge_after && int_point_tmp.edge_before.face === face && int_point_tmp.edge_after.face === face ) { int_point_tmp.face = face } } } } function removeNotRelevantNotIntersectedFaces(polygon: Polygon, notIntersectedFaces: Face[], op: BooleanOp, is_res_polygon: boolean) { for (let face of notIntersectedFaces) { let rel = face.first.bv if ( (op === BooleanOp.UNION && rel === Inclusion.INSIDE) || (op === BooleanOp.SUBTRACT && rel === Inclusion.INSIDE && is_res_polygon) || (op === BooleanOp.SUBTRACT && rel === Inclusion.OUTSIDE && !is_res_polygon) || (op === BooleanOp.INTERSECT && rel === Inclusion.OUTSIDE) ) { polygon.deleteFace(face) } } } function mergeRelevantNotIntersectedFaces(res_polygon: Polygon, wrk_polygon: Polygon) { // All not relevant faces should be already deleted from wrk_polygon for (let face of wrk_polygon.faces) { res_polygon.addFace(face) } }