@zenghawtin/graph2d
Version:
Javascript library for 2d geometry
1,517 lines (1,300 loc) • 292 kB
JavaScript
/**
* 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