UNPKG

@zenghawtin/graph2d

Version:

Javascript library for 2d geometry

1,517 lines (1,300 loc) 292 kB
/** * Created by Alex Bol on 2/18/2017. */ /** * Floating point comparison tolerance. * Default value is 0.000001 (10e-6) * @type {number} */ let DP_TOL = 0.000001; /** * Set new floating point comparison tolerance * @param {number} tolerance */ function setTolerance(tolerance) {DP_TOL = tolerance;} /** * Get floating point comparison tolerance * @returns {number} */ function getTolerance() {return DP_TOL;} const DECIMALS = 3; /** * Returns *true* if value comparable to zero * @param {number} x * @param {number} y * @return {boolean} */ function EQ_0(x) { return (x < DP_TOL && x > -DP_TOL); } /** * Returns *true* if two values are equal up to DP_TOL * @param {number} x * @param {number} y * @return {boolean} */ function EQ(x, y) { return (x - y < DP_TOL && x - y > -DP_TOL); } /** * Returns *true* if first argument greater than second argument up to DP_TOL * @param {number} x * @param {number} y * @return {boolean} */ function GT(x, y) { return (x - y > DP_TOL); } /** * Returns *true* if first argument greater than or equal to second argument up to DP_TOL * @param {number} x * @param {number} y * @returns {boolean} */ function GE(x, y) { return (x - y > -DP_TOL); } /** * Returns *true* if first argument less than second argument up to DP_TOL * @param {number} x * @param {number} y * @return {boolean} */ function LT(x, y) { return (x - y < -DP_TOL) } /** * Returns *true* if first argument less than or equal to second argument up to DP_TOL * @param {number} x * @param {number} y * @return {boolean} */ function LE(x, y) { return (x - y < DP_TOL); } var Utils$1 = /*#__PURE__*/Object.freeze({ __proto__: null, DECIMALS: DECIMALS, EQ: EQ, EQ_0: EQ_0, GE: GE, GT: GT, LE: LE, LT: LT, getTolerance: getTolerance, setTolerance: setTolerance }); /** * Global constant CCW defines counterclockwise direction of arc * @type {boolean} */ const CCW = true; /** * Global constant CW defines clockwise direction of arc * @type {boolean} */ const CW = false; /** * Defines orientation for face of the polygon: clockwise, counterclockwise * or not orientable in the case of self-intersection * @type {{CW: number, CCW: number, NOT_ORIENTABLE: number}} */ const ORIENTATION = {CCW:-1, CW:1, NOT_ORIENTABLE: 0}; const PIx2 = 2 * Math.PI; const INSIDE$2 = 1; const OUTSIDE$1 = 0; const BOUNDARY$1 = 2; const CONTAINS = 3; const INTERLACE = 4; const OVERLAP_SAME$1 = 1; const OVERLAP_OPPOSITE$1 = 2; const NOT_VERTEX$1 = 0; const START_VERTEX$1 = 1; const END_VERTEX$1 = 2; var Constants = /*#__PURE__*/Object.freeze({ __proto__: null, BOUNDARY: BOUNDARY$1, CCW: CCW, CONTAINS: CONTAINS, CW: CW, END_VERTEX: END_VERTEX$1, INSIDE: INSIDE$2, INTERLACE: INTERLACE, NOT_VERTEX: NOT_VERTEX$1, ORIENTATION: ORIENTATION, OUTSIDE: OUTSIDE$1, OVERLAP_OPPOSITE: OVERLAP_OPPOSITE$1, OVERLAP_SAME: OVERLAP_SAME$1, PIx2: PIx2, START_VERTEX: START_VERTEX$1 }); let Flatten = { Utils: Utils$1, Errors: undefined, Matrix: undefined, Planar_set: undefined, Point: undefined, Vector: undefined, Line: undefined, Circle: undefined, Segment: undefined, Arc: undefined, Box: undefined, Edge: undefined, Face: undefined, Ray: undefined, Ray_shooting: undefined, Multiline: undefined, Polygon: undefined, Distance: undefined, Inversion: undefined, }; for (let c in Constants) { Flatten[c] = Constants[c]; } Object.defineProperty(Flatten, "DP_TOL", { get: function () { return getTolerance(); }, set: function (value) { setTolerance(value); }, }); /** * Created by Alex Bol on 2/19/2017. */ /** * Class of system errors */ class Errors { /** * Throw error ILLEGAL_PARAMETERS when cannot instantiate from given parameter * @returns {ReferenceError} */ static get ILLEGAL_PARAMETERS() { return new ReferenceError('Illegal Parameters'); } /** * Throw error ZERO_DIVISION to catch situation of zero division * @returns {Error} */ static get ZERO_DIVISION() { return new Error('Zero division'); } /** * Error to throw from BooleanOperations module in case when fixBoundaryConflicts not capable to fix it * @returns {Error} */ static get UNRESOLVED_BOUNDARY_CONFLICT() { return new Error('Unresolved boundary conflict in boolean operation'); } /** * Error to throw from LinkedList:testInfiniteLoop static method * in case when circular loop detected in linked list * @returns {Error} */ static get INFINITE_LOOP() { return new Error('Infinite loop'); } static get CANNOT_COMPLETE_BOOLEAN_OPERATION() { return new Error('Cannot complete boolean operation') } static get CANNOT_INVOKE_ABSTRACT_METHOD() { return new Error('Abstract method cannot be invoked'); } static get OPERATION_IS_NOT_SUPPORTED() { return new Error('Operation is not supported') } } Flatten.Errors = Errors; /** * Class implements bidirectional non-circular linked list. <br/> * LinkedListElement - object of any type that has properties next and prev. */ class LinkedList { constructor(first, last) { this.first = first; this.last = last || this.first; } [Symbol.iterator]() { let value = undefined; return { next: () => { value = value ? value.next : this.first; return {value: value, done: value === undefined}; } }; }; /** * Return number of elements in the list * @returns {number} */ get size() { let counter = 0; for (let edge of this) { counter++; } return counter; } /** * Return array of elements from start to end, * If start or end not defined, take first as start, last as end * @returns {Array} */ toArray(start=undefined, end=undefined) { let elements = []; let from = start || this.first; let to = end || this.last; let element = from; if (element === undefined) return elements; do { elements.push(element); element = element.next; } while (element !== to.next); return elements; } /** * Append new element to the end of the list * @param {LinkedListElement} element * @returns {LinkedList} */ append(element) { if (this.isEmpty()) { this.first = element; } else { element.prev = this.last; this.last.next = element; } // update edge to be last this.last = element; // nullify non-circular links this.last.next = undefined; this.first.prev = undefined; return this; } /** * Insert new element to the list after elementBefore * @param {LinkedListElement} newElement * @param {LinkedListElement} elementBefore * @returns {LinkedList} */ insert(newElement, elementBefore) { if (this.isEmpty()) { this.first = newElement; this.last = newElement; } else if (elementBefore === null || elementBefore === undefined) { newElement.next = this.first; this.first.prev = newElement; this.first = newElement; } else { /* set links to new element */ let elementAfter = elementBefore.next; elementBefore.next = newElement; if (elementAfter) elementAfter.prev = newElement; /* set links from new element */ newElement.prev = elementBefore; newElement.next = elementAfter; /* extend list if new element added after the last element */ if (this.last === elementBefore) this.last = newElement; } // nullify non-circular links this.last.next = undefined; this.first.prev = undefined; return this; } /** * Remove element from the list * @param {LinkedListElement} element * @returns {LinkedList} */ remove(element) { // special case if last edge removed if (element === this.first && element === this.last) { this.first = undefined; this.last = undefined; } else { // update linked list if (element.prev) element.prev.next = element.next; if (element.next) element.next.prev = element.prev; // update first if need if (element === this.first) { this.first = element.next; } // update last if need if (element === this.last) { this.last = element.prev; } } return this; } /** * Return true if list is empty * @returns {boolean} */ isEmpty() { return this.first === undefined; } /** * Throw an error if circular loop detected in the linked list * @param {LinkedListElement} first element to start iteration * @throws {Errors.INFINITE_LOOP} */ static testInfiniteLoop(first) { let edge = first; let controlEdge = first; do { if (edge != first && edge === controlEdge) { throw Errors.INFINITE_LOOP; // new Error("Infinite loop") } edge = edge.next; controlEdge = controlEdge.next.next; } while (edge != first) } } const defaultAttributes = { stroke: "black" }; class SVGAttributes { constructor(args = defaultAttributes) { for(const property in args) { this[property] = args[property]; } this.stroke = args.stroke ?? defaultAttributes.stroke; } toAttributesString() { return Object.keys(this) .reduce( (acc, key) => acc + (this[key] !== undefined ? this.toAttrString(key, this[key]) : "") , ``) } toAttrString(key, value) { const SVGKey = key === "className" ? "class" : this.convertCamelToKebabCase(key); return value === null ? `${SVGKey} ` : `${SVGKey}="${value.toString()}" ` } convertCamelToKebabCase(str) { return str .match(/[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g) .join('-') .toLowerCase(); } } function convertToString(attrs) { return new SVGAttributes(attrs).toAttributesString() } /** * Class Multiline represent connected path of [edges]{@link Flatten.Edge}, where each edge may be * [segment]{@link Flatten.Segment}, [arc]{@link Flatten.Arc}, [line]{@link Flatten.Line} or [ray]{@link Flatten.Ray} */ class Multiline extends LinkedList { constructor(...args) { super(); if (args.length === 0) { return; } if (args.length === 1) { if (args[0] instanceof Array) { let shapes = args[0]; if (shapes.length === 0) return; // TODO: more strict validation: // there may be only one line // only first and last may be rays shapes.every((shape) => { return shape instanceof Flatten.Segment || shape instanceof Flatten.Arc || shape instanceof Flatten.Ray || shape instanceof Flatten.Line }); for (let shape of shapes) { let edge = new Flatten.Edge(shape); this.append(edge); } this.setArcLength(); } } } /** * (Getter) Return array of edges * @returns {Edge[]} */ get edges() { return [...this]; } /** * (Getter) Return bounding box of the multiline * @returns {Box} */ get box() { return this.edges.reduce( (acc,edge) => acc.merge(edge.box), new Flatten.Box() ); } /** * (Getter) Returns array of vertices * @returns {Point[]} */ get vertices() { let v = this.edges.map(edge => edge.start); v.push(this.last.end); return v; } /** * Return new cloned instance of Multiline * @returns {Multiline} */ clone() { return new Multiline(this.toShapes()); } /** * Set arc_length property for each of the edges in the face. * Arc_length of the edge it the arc length from the first edge of the face */ setArcLength() { for (let edge of this) { this.setOneEdgeArcLength(edge); } } setOneEdgeArcLength(edge) { if (edge === this.first) { edge.arc_length = 0.0; } else { edge.arc_length = edge.prev.arc_length + edge.prev.length; } } /** * Split edge and add new vertex, return new edge inserted * @param {Point} pt - point on edge that will be added as new vertex * @param {Edge} edge - edge to split * @returns {Edge} */ addVertex(pt, edge) { let shapes = edge.shape.split(pt); // if (shapes.length < 2) return; if (shapes[0] === null) // point incident to edge start vertex, return previous edge return edge.prev; if (shapes[1] === null) // point incident to edge end vertex, return edge itself return edge; let newEdge = new Flatten.Edge(shapes[0]); let edgeBefore = edge.prev; /* Insert first split edge into linked list after edgeBefore */ this.insert(newEdge, edgeBefore); // edge.face ? // Update edge shape with second split edge keeping links edge.shape = shapes[1]; return newEdge; } getChain(edgeFrom, edgeTo) { let edges = []; for (let edge = edgeFrom; edge !== edgeTo.next; edge = edge.next) { edges.push(edge); } return edges } /** * Split edges of multiline with intersection points and return mutated multiline * @param {Point[]} ip - array of points to be added as new vertices * @returns {Multiline} */ split(ip) { for (let pt of ip) { let edge = this.findEdgeByPoint(pt); this.addVertex(pt, edge); } return this; } /** * Returns edge which contains given point * @param {Point} pt * @returns {Edge} */ findEdgeByPoint(pt) { let edgeFound; for (let edge of this) { if (edge.shape.contains(pt)) { edgeFound = edge; break; } } return edgeFound; } /** * Returns new multiline translated by vector vec * @param {Vector} vec * @returns {Multiline} */ translate(vec) { return new Multiline(this.edges.map( edge => edge.shape.translate(vec))); } /** * Return new multiline rotated by given angle around given point * If point omitted, rotate around origin (0,0) * Positive value of angle defines rotation counterclockwise, negative - clockwise * @param {number} angle - rotation angle in radians * @param {Point} center - rotation center, default is (0,0) * @returns {Multiline} - new rotated polygon */ rotate(angle = 0, center = new Flatten.Point()) { return new Multiline(this.edges.map( edge => edge.shape.rotate(angle, center) )); } /** * Return new multiline transformed using affine transformation matrix * Method does not support unbounded shapes * @param {Matrix} matrix - affine transformation matrix * @returns {Multiline} - new multiline */ transform(matrix = new Flatten.Matrix()) { return new Multiline(this.edges.map( edge => edge.shape.transform(matrix))); } /** * Transform multiline into array of shapes * @returns {Shape[]} */ toShapes() { return this.edges.map(edge => edge.shape.clone()) } /** * This method returns an object that defines how data will be * serialized when called JSON.stringify() method * @returns {Object} */ toJSON() { return this.edges.map(edge => edge.toJSON()); } /** * Return string to draw multiline in svg * @param attrs - an object with attributes for svg path element * TODO: support semi-infinite Ray and infinite Line * @returns {string} */ svg(attrs = {}) { let svgStr = `\n<path ${convertToString({fill: "none", ...attrs})} d="`; svgStr += `\nM${this.first.start.x},${this.first.start.y}`; for (let edge of this) { svgStr += edge.svg(); } svgStr += `" >\n</path>`; return svgStr; } } Flatten.Multiline = Multiline; /** * Shortcut function to create multiline * @param args */ const multiline = (...args) => new Flatten.Multiline(...args); Flatten.multiline = multiline; /* Smart intersections describe intersection points that refers to the edges they intersect This function are supposed for internal usage by morphing and relation methods between */ function addToIntPoints(edge, pt, int_points) { let id = int_points.length; let shapes = edge.shape.split(pt); // if (shapes.length < 2) return; if (shapes.length === 0) return; // Point does not belong to edge ? let len = 0; if (shapes[0] === null) { // point incident to edge start vertex len = 0; } else if (shapes[1] === null) { // point incident to edge end vertex len = edge.shape.length; } else { // Edge was split into to edges len = shapes[0].length; } let is_vertex = NOT_VERTEX$1; if (EQ(len, 0)) { is_vertex |= START_VERTEX$1; } if (EQ(len, edge.shape.length)) { is_vertex |= END_VERTEX$1; } // Fix intersection point which is end point of the last edge let arc_length; if (len === Infinity) { arc_length = shapes[0].coord(pt); } else { arc_length = (is_vertex & END_VERTEX$1) && edge.next && edge.next.arc_length === 0 ? 0 : edge.arc_length + len; } int_points.push({ id: id, pt: pt, arc_length: arc_length, edge_before: edge, edge_after: undefined, face: edge.face, is_vertex: is_vertex }); } function sortIntersections(intersections) { // augment intersections with new sorted arrays intersections.int_points1_sorted = getSortedArray(intersections.int_points1); intersections.int_points2_sorted = getSortedArray(intersections.int_points2); } function getSortedArray(int_points) { let faceMap = new Map; let id = 0; // Create integer id's for faces for (let ip of int_points) { if (!faceMap.has(ip.face)) { faceMap.set(ip.face, id); id++; } } // Augment intersection points with face id's for (let ip of int_points) { ip.faceId = faceMap.get(ip.face); } // Clone and sort let int_points_sorted = int_points.slice().sort(compareFn); return int_points_sorted; } function compareFn(ip1, ip2) { // compare face id's if (ip1.faceId < ip2.faceId) { return -1; } if (ip1.faceId > ip2.faceId) { return 1; } // same face - compare arc_length if (ip1.arc_length < ip2.arc_length) { return -1; } if (ip1.arc_length > ip2.arc_length) { return 1; } return 0; } function filterDuplicatedIntersections(intersections) { if (intersections.int_points1.length < 2) return; let do_squeeze = false; let int_point_ref1; let int_point_ref2; let int_point_cur1; let int_point_cur2; for (let i = 0; i < intersections.int_points1_sorted.length; i++) { if (intersections.int_points1_sorted[i].id === -1) continue; int_point_ref1 = intersections.int_points1_sorted[i]; int_point_ref2 = intersections.int_points2[int_point_ref1.id]; for (let j=i+1; j < intersections.int_points1_sorted.length; j++) { int_point_cur1 = intersections.int_points1_sorted[j]; if (!EQ(int_point_cur1.arc_length, int_point_ref1.arc_length)) { break; } if (int_point_cur1.id === -1) continue; int_point_cur2 = intersections.int_points2[int_point_cur1.id]; if (int_point_cur2.id === -1) continue; if (int_point_cur1.edge_before === int_point_ref1.edge_before && int_point_cur1.edge_after === int_point_ref1.edge_after && int_point_cur2.edge_before === int_point_ref2.edge_before && int_point_cur2.edge_after === int_point_ref2.edge_after) { int_point_cur1.id = -1; /* to be deleted */ int_point_cur2.id = -1; /* to be deleted */ do_squeeze = true; } } } int_point_ref2 = intersections.int_points2_sorted[0]; int_point_ref1 = intersections.int_points1[int_point_ref2.id]; for (let i = 1; i < intersections.int_points2_sorted.length; i++) { let int_point_cur2 = intersections.int_points2_sorted[i]; if (int_point_cur2.id === -1) continue; /* already deleted */ if (int_point_ref2.id === -1 || /* can't be reference if already deleted */ !(EQ(int_point_cur2.arc_length, int_point_ref2.arc_length))) { int_point_ref2 = int_point_cur2; int_point_ref1 = intersections.int_points1[int_point_ref2.id]; continue; } let int_point_cur1 = intersections.int_points1[int_point_cur2.id]; if (int_point_cur1.edge_before === int_point_ref1.edge_before && int_point_cur1.edge_after === int_point_ref1.edge_after && int_point_cur2.edge_before === int_point_ref2.edge_before && int_point_cur2.edge_after === int_point_ref2.edge_after) { int_point_cur1.id = -1; /* to be deleted */ int_point_cur2.id = -1; /* to be deleted */ do_squeeze = true; } } if (do_squeeze) { intersections.int_points1 = intersections.int_points1.filter((int_point) => int_point.id >= 0); intersections.int_points2 = intersections.int_points2.filter((int_point) => int_point.id >= 0); // update id's intersections.int_points1.forEach((int_point, index) => int_point.id = index); intersections.int_points2.forEach((int_point, index) => int_point.id = index); } } function initializeInclusionFlags(int_points) { for (let int_point of int_points) { if (int_point.edge_before) { int_point.edge_before.bvStart = undefined; int_point.edge_before.bvEnd = undefined; int_point.edge_before.bv = undefined; int_point.edge_before.overlap = undefined; } if (int_point.edge_after) { int_point.edge_after.bvStart = undefined; int_point.edge_after.bvEnd = undefined; int_point.edge_after.bv = undefined; int_point.edge_after.overlap = undefined; } } for (let int_point of int_points) { if (int_point.edge_before) int_point.edge_before.bvEnd = BOUNDARY$1; if (int_point.edge_after) int_point.edge_after.bvStart = BOUNDARY$1; } } function calculateInclusionFlags(int_points, polygon) { for (let int_point of int_points) { if (int_point.edge_before) int_point.edge_before.setInclusion(polygon); if (int_point.edge_after) int_point.edge_after.setInclusion(polygon); } } function setOverlappingFlags(intersections) { let cur_face = undefined; let first_int_point_in_face_id = undefined; let next_int_point1 = undefined; let num_int_points = intersections.int_points1.length; for (let i = 0; i < num_int_points; i++) { let cur_int_point1 = intersections.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 <x,y> in "cur_int_point1" pool let int_points_cur_pool_start = i; let int_points_cur_pool_num = intPointsPoolCount(intersections.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 && intersections.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(intersections.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 = intersections.int_points1_sorted[j]; if (next_int_point1_tmp.face === cur_face && intersections.int_points2[next_int_point1_tmp.id].face === intersections.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; if (!(edge_from1.bv === BOUNDARY$1 && edge_to1.bv === BOUNDARY$1)) // not a boundary chain - skip continue; if (edge_from1 !== edge_to1) // one edge chain TODO: support complex case continue; /* Find boundary chain in polygon2 between same intersection points */ let cur_int_point2 = intersections.int_points2[cur_int_point1.id]; let next_int_point2 = intersections.int_points2[next_int_point1.id]; let edge_from2 = cur_int_point2.edge_after; let edge_to2 = next_int_point2.edge_before; /* if [edge_from2..edge_to2] is not a boundary chain, invert it */ /* check also that chain consist of one or two edges */ if (!(edge_from2.bv === BOUNDARY$1 && edge_to2.bv === BOUNDARY$1 && edge_from2 === edge_to2)) { cur_int_point2 = intersections.int_points2[next_int_point1.id]; next_int_point2 = intersections.int_points2[cur_int_point1.id]; edge_from2 = cur_int_point2.edge_after; edge_to2 = next_int_point2.edge_before; } if (!(edge_from2.bv === BOUNDARY$1 && edge_to2.bv === BOUNDARY$1 && edge_from2 === edge_to2)) continue; // not an overlapping chain - skip TODO: fix boundary conflict // Set overlapping flag - one-to-one case edge_from1.setOverlap(edge_from2); } } function intPointsPoolCount(int_points, cur_int_point_num, cur_face) { let int_point_current; let int_point_next; let int_points_pool_num = 1; if (int_points.length === 1) return 1; int_point_current = int_points[cur_int_point_num]; for (let i = cur_int_point_num + 1; i < int_points.length; i++) { if (int_point_current.face !== cur_face) { /* next face started */ break; } int_point_next = int_points[i]; if (!(int_point_next.pt.equalTo(int_point_current.pt) && int_point_next.edge_before === int_point_current.edge_before && int_point_next.edge_after === int_point_current.edge_after)) { break; /* next point is different - break and exit */ } int_points_pool_num++; /* duplicated intersection point - increase counter */ } return int_points_pool_num; } function splitByIntersections(polygon, int_points) { if (!int_points) return; for (let int_point of int_points) { let edge = int_point.edge_before; // recalculate vertex flag: it may be changed after previous split int_point.is_vertex = NOT_VERTEX$1; if (edge.shape.start && edge.shape.start.equalTo(int_point.pt)) { int_point.is_vertex |= START_VERTEX$1; } if (edge.shape.end && edge.shape.end.equalTo(int_point.pt)) { int_point.is_vertex |= END_VERTEX$1; } if (int_point.is_vertex & START_VERTEX$1) { // nothing to split int_point.edge_before = edge.prev; if (edge.prev) { int_point.is_vertex = END_VERTEX$1; // polygon } continue; } if (int_point.is_vertex & END_VERTEX$1) { // nothing to split continue; } let newEdge = polygon.addVertex(int_point.pt, edge); int_point.edge_before = newEdge; } for (let int_point of int_points) { if (int_point.edge_before) { int_point.edge_after = int_point.edge_before.next; } else { if (polygon instanceof Multiline && int_point.is_vertex & START_VERTEX$1) { int_point.edge_after = polygon.first; } } } } function insertBetweenIntPoints(int_point1, int_point2, new_edges) { const edge_before = int_point1.edge_before; const edge_after = int_point2.edge_after; const len = new_edges.length; edge_before.next = new_edges[0]; new_edges[0].prev = edge_before; new_edges[len-1].next = edge_after; edge_after.prev = new_edges[len-1]; } var smart_intersections = /*#__PURE__*/Object.freeze({ __proto__: null, addToIntPoints: addToIntPoints, calculateInclusionFlags: calculateInclusionFlags, filterDuplicatedIntersections: filterDuplicatedIntersections, getSortedArray: getSortedArray, initializeInclusionFlags: initializeInclusionFlags, insertBetweenIntPoints: insertBetweenIntPoints, intPointsPoolCount: intPointsPoolCount, setOverlappingFlags: setOverlappingFlags, sortIntersections: sortIntersections, splitByIntersections: splitByIntersections }); /** * Created by Alex Bol on 12/02/2018. */ /** * @module BooleanOperations */ const {INSIDE: INSIDE$1, OUTSIDE, BOUNDARY, OVERLAP_SAME, OVERLAP_OPPOSITE} = Constants; const {NOT_VERTEX, START_VERTEX, END_VERTEX} = Constants; const BOOLEAN_UNION = 1; const BOOLEAN_INTERSECT = 2; const BOOLEAN_SUBTRACT = 3; /** * Unify two polygons polygons and returns new polygon. <br/> * Point belongs to the resulted polygon if it belongs to the first OR to the second polygon * @param {Polygon} polygon1 - first operand * @param {Polygon} polygon2 - second operand * @returns {Polygon} */ function unify(polygon1, polygon2) { let [res_poly, wrk_poly] = booleanOpBinary(polygon1, polygon2, BOOLEAN_UNION, true); return res_poly; } /** * 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 * @param {Polygon} polygon1 - first operand * @param {Polygon} polygon2 - second operand * @returns {Polygon} */ function subtract(polygon1, polygon2) { let polygon2_tmp = polygon2.clone(); let polygon2_reversed = polygon2_tmp.reverse(); let [res_poly, wrk_poly] = booleanOpBinary(polygon1, polygon2_reversed, BOOLEAN_SUBTRACT, true); return res_poly; } /** * Intersect two polygons and returns new polygon * Point belongs to the resulted polygon is it belongs to the first AND to the second polygon * @param {Polygon} polygon1 - first operand * @param {Polygon} polygon2 - second operand * @returns {Polygon} */ function intersect$1(polygon1, polygon2) { let [res_poly, wrk_poly] = booleanOpBinary(polygon1, polygon2, BOOLEAN_INTERSECT, true); return res_poly; } /** * Returns boundary of intersection between two polygons as two arrays of shapes (Segments/Arcs) <br/> * The first array are shapes from the first polygon, the second array are shapes from the second * @param {Polygon} polygon1 - first operand * @param {Polygon} polygon2 - second operand * @returns {Shape[][]} */ function innerClip(polygon1, polygon2) { let [res_poly, wrk_poly] = booleanOpBinary(polygon1, polygon2, BOOLEAN_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 * @param {Polygon} polygon1 - first operand * @param {Polygon} polygon2 - second operand * @returns {Shape[]} */ function outerClip(polygon1, polygon2) { let [res_poly, wrk_poly] = booleanOpBinary(polygon1, polygon2, BOOLEAN_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 <br/> * 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 * @param {Polygon} polygon1 - first operand * @param {Polygon} polygon2 - second operand * @returns {Point[][]} */ function calculateIntersections(polygon1, polygon2) { 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, wrk_poly, intersections, op) { // 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, wrk_poly, intersections, op) { // 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, polygon2, op, restore) { 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, polygon2) { let intersections = { int_points1: [], int_points2: [] }; // 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, int_points) { let notIntersected = []; for (let face of poly.faces) { if (!int_points.find((ip) => ip.face === face)) { notIntersected.push(face); } } return notIntersected; } function calcInclusionForNotIntersectedFaces(notIntersectedFaces, poly2) { for (let face of notIntersectedFaces) { face.first.bv = face.first.bvStart = face.first.bvEnd = undefined; face.first.setInclusion(poly2); } } function fixBoundaryConflicts(poly1, poly2, int_points1, int_points1_sorted, int_points2, 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 <x,y> 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 === BOUNDARY && edge_to1.bv != BOUNDARY) { edge_from1.bv = edge_to1.bv; continue; } if (edge_from1.bv != BOUNDARY && edge_to1.bv === 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 === BOUNDARY && edge_to1.bv === BOUNDARY && edge_from1 != edge_to1) || (edge_from1.bv === INSIDE$1 && edge_to1.bv === OUTSIDE || edge_from1.bv === OUTSIDE && edge_to1.bv === INSIDE$1 ) ) { 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 === BOUNDARY && edge_to1.bv === BOUNDARY && edge_from1 != edge_to1) { let edge_tmp = edge_from1.next; let new_bv; while (edge_tmp != edge_to1) { if (edge_tmp.bv != 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 === INSIDE$1 && edge_to1.bv === OUTSIDE || edge_from1.bv === OUTSIDE && edge_to1.bv === INSIDE$1 ) { 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); if (dist < 10*Flatten.DP_TOL) { // it should be very close // let pt = edge_tmp.end; // add to the list of intersections of poly1 addToIntPoints(edge_tmp, segment.ps, 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 = 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 = 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 = 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.pe); addToIntPoints(edge2, segment.pe, 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 = BOUNDARY; newEdge2.bv = undefined; newEdge2.setInclusion(poly1); newEdge2.next.bvStart = 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; } function removeNotRelevantChains(polygon, op, int_points, is_res_polygon) { if (!int_points) return; let cur_face = undefined; let first_int_point_in_face_num = undefined; let int_point_current; let int_point_next; 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.is