2086 lines
57 KiB
JavaScript
2086 lines
57 KiB
JavaScript
/**
|
|
* martinez v0.7.4
|
|
* Martinez polygon clipping algorithm, does boolean operation on polygons (multipolygons, polygons with holes etc): intersection, union, difference, xor
|
|
*
|
|
* @author Alex Milevski <info@w8r.name>
|
|
* @license MIT
|
|
* @preserve
|
|
*/
|
|
|
|
(function (global, factory) {
|
|
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
|
|
typeof define === 'function' && define.amd ? define(['exports'], factory) :
|
|
(global = global || self, factory(global.martinez = {}));
|
|
}(this, (function (exports) { 'use strict';
|
|
|
|
function DEFAULT_COMPARE (a, b) { return a > b ? 1 : a < b ? -1 : 0; }
|
|
|
|
var SplayTree = function SplayTree(compare, noDuplicates) {
|
|
if ( compare === void 0 ) compare = DEFAULT_COMPARE;
|
|
if ( noDuplicates === void 0 ) noDuplicates = false;
|
|
|
|
this._compare = compare;
|
|
this._root = null;
|
|
this._size = 0;
|
|
this._noDuplicates = !!noDuplicates;
|
|
};
|
|
|
|
var prototypeAccessors = { size: { configurable: true } };
|
|
|
|
|
|
SplayTree.prototype.rotateLeft = function rotateLeft (x) {
|
|
var y = x.right;
|
|
if (y) {
|
|
x.right = y.left;
|
|
if (y.left) { y.left.parent = x; }
|
|
y.parent = x.parent;
|
|
}
|
|
|
|
if (!x.parent) { this._root = y; }
|
|
else if (x === x.parent.left) { x.parent.left = y; }
|
|
else { x.parent.right = y; }
|
|
if (y) { y.left = x; }
|
|
x.parent = y;
|
|
};
|
|
|
|
|
|
SplayTree.prototype.rotateRight = function rotateRight (x) {
|
|
var y = x.left;
|
|
if (y) {
|
|
x.left = y.right;
|
|
if (y.right) { y.right.parent = x; }
|
|
y.parent = x.parent;
|
|
}
|
|
|
|
if (!x.parent) { this._root = y; }
|
|
else if(x === x.parent.left) { x.parent.left = y; }
|
|
else { x.parent.right = y; }
|
|
if (y) { y.right = x; }
|
|
x.parent = y;
|
|
};
|
|
|
|
|
|
SplayTree.prototype._splay = function _splay (x) {
|
|
while (x.parent) {
|
|
var p = x.parent;
|
|
if (!p.parent) {
|
|
if (p.left === x) { this.rotateRight(p); }
|
|
else { this.rotateLeft(p); }
|
|
} else if (p.left === x && p.parent.left === p) {
|
|
this.rotateRight(p.parent);
|
|
this.rotateRight(p);
|
|
} else if (p.right === x && p.parent.right === p) {
|
|
this.rotateLeft(p.parent);
|
|
this.rotateLeft(p);
|
|
} else if (p.left === x && p.parent.right === p) {
|
|
this.rotateRight(p);
|
|
this.rotateLeft(p);
|
|
} else {
|
|
this.rotateLeft(p);
|
|
this.rotateRight(p);
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
SplayTree.prototype.splay = function splay (x) {
|
|
var p, gp, ggp, l, r;
|
|
|
|
while (x.parent) {
|
|
p = x.parent;
|
|
gp = p.parent;
|
|
|
|
if (gp && gp.parent) {
|
|
ggp = gp.parent;
|
|
if (ggp.left === gp) { ggp.left= x; }
|
|
else { ggp.right = x; }
|
|
x.parent = ggp;
|
|
} else {
|
|
x.parent = null;
|
|
this._root = x;
|
|
}
|
|
|
|
l = x.left; r = x.right;
|
|
|
|
if (x === p.left) { // left
|
|
if (gp) {
|
|
if (gp.left === p) {
|
|
/* zig-zig */
|
|
if (p.right) {
|
|
gp.left = p.right;
|
|
gp.left.parent = gp;
|
|
} else { gp.left = null; }
|
|
|
|
p.right = gp;
|
|
gp.parent = p;
|
|
} else {
|
|
/* zig-zag */
|
|
if (l) {
|
|
gp.right = l;
|
|
l.parent = gp;
|
|
} else { gp.right = null; }
|
|
|
|
x.left = gp;
|
|
gp.parent = x;
|
|
}
|
|
}
|
|
if (r) {
|
|
p.left = r;
|
|
r.parent = p;
|
|
} else { p.left = null; }
|
|
|
|
x.right= p;
|
|
p.parent = x;
|
|
} else { // right
|
|
if (gp) {
|
|
if (gp.right === p) {
|
|
/* zig-zig */
|
|
if (p.left) {
|
|
gp.right = p.left;
|
|
gp.right.parent = gp;
|
|
} else { gp.right = null; }
|
|
|
|
p.left = gp;
|
|
gp.parent = p;
|
|
} else {
|
|
/* zig-zag */
|
|
if (r) {
|
|
gp.left = r;
|
|
r.parent = gp;
|
|
} else { gp.left = null; }
|
|
|
|
x.right = gp;
|
|
gp.parent = x;
|
|
}
|
|
}
|
|
if (l) {
|
|
p.right = l;
|
|
l.parent = p;
|
|
} else { p.right = null; }
|
|
|
|
x.left = p;
|
|
p.parent = x;
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
SplayTree.prototype.replace = function replace (u, v) {
|
|
if (!u.parent) { this._root = v; }
|
|
else if (u === u.parent.left) { u.parent.left = v; }
|
|
else { u.parent.right = v; }
|
|
if (v) { v.parent = u.parent; }
|
|
};
|
|
|
|
|
|
SplayTree.prototype.minNode = function minNode (u) {
|
|
if ( u === void 0 ) u = this._root;
|
|
|
|
if (u) { while (u.left) { u = u.left; } }
|
|
return u;
|
|
};
|
|
|
|
|
|
SplayTree.prototype.maxNode = function maxNode (u) {
|
|
if ( u === void 0 ) u = this._root;
|
|
|
|
if (u) { while (u.right) { u = u.right; } }
|
|
return u;
|
|
};
|
|
|
|
|
|
SplayTree.prototype.insert = function insert (key, data) {
|
|
var z = this._root;
|
|
var p = null;
|
|
var comp = this._compare;
|
|
var cmp;
|
|
|
|
if (this._noDuplicates) {
|
|
while (z) {
|
|
p = z;
|
|
cmp = comp(z.key, key);
|
|
if (cmp === 0) { return; }
|
|
else if (comp(z.key, key) < 0) { z = z.right; }
|
|
else { z = z.left; }
|
|
}
|
|
} else {
|
|
while (z) {
|
|
p = z;
|
|
if (comp(z.key, key) < 0) { z = z.right; }
|
|
else { z = z.left; }
|
|
}
|
|
}
|
|
|
|
z = { key: key, data: data, left: null, right: null, parent: p };
|
|
|
|
if (!p) { this._root = z; }
|
|
else if (comp(p.key, z.key) < 0) { p.right = z; }
|
|
else { p.left= z; }
|
|
|
|
this.splay(z);
|
|
this._size++;
|
|
return z;
|
|
};
|
|
|
|
|
|
SplayTree.prototype.find = function find (key) {
|
|
var z = this._root;
|
|
var comp = this._compare;
|
|
while (z) {
|
|
var cmp = comp(z.key, key);
|
|
if (cmp < 0) { z = z.right; }
|
|
else if (cmp > 0) { z = z.left; }
|
|
else { return z; }
|
|
}
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* Whether the tree contains a node with the given key
|
|
* @param{Key} key
|
|
* @return {boolean} true/false
|
|
*/
|
|
SplayTree.prototype.contains = function contains (key) {
|
|
var node = this._root;
|
|
var comparator = this._compare;
|
|
while (node){
|
|
var cmp = comparator(key, node.key);
|
|
if (cmp === 0) { return true; }
|
|
else if (cmp < 0) { node = node.left; }
|
|
else { node = node.right; }
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
|
|
SplayTree.prototype.remove = function remove (key) {
|
|
var z = this.find(key);
|
|
|
|
if (!z) { return false; }
|
|
|
|
this.splay(z);
|
|
|
|
if (!z.left) { this.replace(z, z.right); }
|
|
else if (!z.right) { this.replace(z, z.left); }
|
|
else {
|
|
var y = this.minNode(z.right);
|
|
if (y.parent !== z) {
|
|
this.replace(y, y.right);
|
|
y.right = z.right;
|
|
y.right.parent = y;
|
|
}
|
|
this.replace(z, y);
|
|
y.left = z.left;
|
|
y.left.parent = y;
|
|
}
|
|
|
|
this._size--;
|
|
return true;
|
|
};
|
|
|
|
|
|
SplayTree.prototype.removeNode = function removeNode (z) {
|
|
if (!z) { return false; }
|
|
|
|
this.splay(z);
|
|
|
|
if (!z.left) { this.replace(z, z.right); }
|
|
else if (!z.right) { this.replace(z, z.left); }
|
|
else {
|
|
var y = this.minNode(z.right);
|
|
if (y.parent !== z) {
|
|
this.replace(y, y.right);
|
|
y.right = z.right;
|
|
y.right.parent = y;
|
|
}
|
|
this.replace(z, y);
|
|
y.left = z.left;
|
|
y.left.parent = y;
|
|
}
|
|
|
|
this._size--;
|
|
return true;
|
|
};
|
|
|
|
|
|
SplayTree.prototype.erase = function erase (key) {
|
|
var z = this.find(key);
|
|
if (!z) { return; }
|
|
|
|
this.splay(z);
|
|
|
|
var s = z.left;
|
|
var t = z.right;
|
|
|
|
var sMax = null;
|
|
if (s) {
|
|
s.parent = null;
|
|
sMax = this.maxNode(s);
|
|
this.splay(sMax);
|
|
this._root = sMax;
|
|
}
|
|
if (t) {
|
|
if (s) { sMax.right = t; }
|
|
else { this._root = t; }
|
|
t.parent = sMax;
|
|
}
|
|
|
|
this._size--;
|
|
};
|
|
|
|
/**
|
|
* Removes and returns the node with smallest key
|
|
* @return {?Node}
|
|
*/
|
|
SplayTree.prototype.pop = function pop () {
|
|
var node = this._root, returnValue = null;
|
|
if (node) {
|
|
while (node.left) { node = node.left; }
|
|
returnValue = { key: node.key, data: node.data };
|
|
this.remove(node.key);
|
|
}
|
|
return returnValue;
|
|
};
|
|
|
|
|
|
/* eslint-disable class-methods-use-this */
|
|
|
|
/**
|
|
* Successor node
|
|
* @param{Node} node
|
|
* @return {?Node}
|
|
*/
|
|
SplayTree.prototype.next = function next (node) {
|
|
var successor = node;
|
|
if (successor) {
|
|
if (successor.right) {
|
|
successor = successor.right;
|
|
while (successor && successor.left) { successor = successor.left; }
|
|
} else {
|
|
successor = node.parent;
|
|
while (successor && successor.right === node) {
|
|
node = successor; successor = successor.parent;
|
|
}
|
|
}
|
|
}
|
|
return successor;
|
|
};
|
|
|
|
|
|
/**
|
|
* Predecessor node
|
|
* @param{Node} node
|
|
* @return {?Node}
|
|
*/
|
|
SplayTree.prototype.prev = function prev (node) {
|
|
var predecessor = node;
|
|
if (predecessor) {
|
|
if (predecessor.left) {
|
|
predecessor = predecessor.left;
|
|
while (predecessor && predecessor.right) { predecessor = predecessor.right; }
|
|
} else {
|
|
predecessor = node.parent;
|
|
while (predecessor && predecessor.left === node) {
|
|
node = predecessor;
|
|
predecessor = predecessor.parent;
|
|
}
|
|
}
|
|
}
|
|
return predecessor;
|
|
};
|
|
/* eslint-enable class-methods-use-this */
|
|
|
|
|
|
/**
|
|
* @param{forEachCallback} callback
|
|
* @return {SplayTree}
|
|
*/
|
|
SplayTree.prototype.forEach = function forEach (callback) {
|
|
var current = this._root;
|
|
var s = [], done = false, i = 0;
|
|
|
|
while (!done) {
|
|
// Reach the left most Node of the current Node
|
|
if (current) {
|
|
// Place pointer to a tree node on the stack
|
|
// before traversing the node's left subtree
|
|
s.push(current);
|
|
current = current.left;
|
|
} else {
|
|
// BackTrack from the empty subtree and visit the Node
|
|
// at the top of the stack; however, if the stack is
|
|
// empty you are done
|
|
if (s.length > 0) {
|
|
current = s.pop();
|
|
callback(current, i++);
|
|
|
|
// We have visited the node and its left
|
|
// subtree. Now, it's right subtree's turn
|
|
current = current.right;
|
|
} else { done = true; }
|
|
}
|
|
}
|
|
return this;
|
|
};
|
|
|
|
|
|
/**
|
|
* Walk key range from `low` to `high`. Stops if `fn` returns a value.
|
|
* @param{Key} low
|
|
* @param{Key} high
|
|
* @param{Function} fn
|
|
* @param{*?} ctx
|
|
* @return {SplayTree}
|
|
*/
|
|
SplayTree.prototype.range = function range (low, high, fn, ctx) {
|
|
var Q = [];
|
|
var compare = this._compare;
|
|
var node = this._root, cmp;
|
|
|
|
while (Q.length !== 0 || node) {
|
|
if (node) {
|
|
Q.push(node);
|
|
node = node.left;
|
|
} else {
|
|
node = Q.pop();
|
|
cmp = compare(node.key, high);
|
|
if (cmp > 0) {
|
|
break;
|
|
} else if (compare(node.key, low) >= 0) {
|
|
if (fn.call(ctx, node)) { return this; } // stop if smth is returned
|
|
}
|
|
node = node.right;
|
|
}
|
|
}
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Returns all keys in order
|
|
* @return {Array<Key>}
|
|
*/
|
|
SplayTree.prototype.keys = function keys () {
|
|
var current = this._root;
|
|
var s = [], r = [], done = false;
|
|
|
|
while (!done) {
|
|
if (current) {
|
|
s.push(current);
|
|
current = current.left;
|
|
} else {
|
|
if (s.length > 0) {
|
|
current = s.pop();
|
|
r.push(current.key);
|
|
current = current.right;
|
|
} else { done = true; }
|
|
}
|
|
}
|
|
return r;
|
|
};
|
|
|
|
|
|
/**
|
|
* Returns `data` fields of all nodes in order.
|
|
* @return {Array<Value>}
|
|
*/
|
|
SplayTree.prototype.values = function values () {
|
|
var current = this._root;
|
|
var s = [], r = [], done = false;
|
|
|
|
while (!done) {
|
|
if (current) {
|
|
s.push(current);
|
|
current = current.left;
|
|
} else {
|
|
if (s.length > 0) {
|
|
current = s.pop();
|
|
r.push(current.data);
|
|
current = current.right;
|
|
} else { done = true; }
|
|
}
|
|
}
|
|
return r;
|
|
};
|
|
|
|
|
|
/**
|
|
* Returns node at given index
|
|
* @param{number} index
|
|
* @return {?Node}
|
|
*/
|
|
SplayTree.prototype.at = function at (index) {
|
|
// removed after a consideration, more misleading than useful
|
|
// index = index % this.size;
|
|
// if (index < 0) index = this.size - index;
|
|
|
|
var current = this._root;
|
|
var s = [], done = false, i = 0;
|
|
|
|
while (!done) {
|
|
if (current) {
|
|
s.push(current);
|
|
current = current.left;
|
|
} else {
|
|
if (s.length > 0) {
|
|
current = s.pop();
|
|
if (i === index) { return current; }
|
|
i++;
|
|
current = current.right;
|
|
} else { done = true; }
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* Bulk-load items. Both array have to be same size
|
|
* @param{Array<Key>} keys
|
|
* @param{Array<Value>}[values]
|
|
* @param{Boolean} [presort=false] Pre-sort keys and values, using
|
|
* tree's comparator. Sorting is done
|
|
* in-place
|
|
* @return {AVLTree}
|
|
*/
|
|
SplayTree.prototype.load = function load (keys, values, presort) {
|
|
if ( keys === void 0 ) keys = [];
|
|
if ( values === void 0 ) values = [];
|
|
if ( presort === void 0 ) presort = false;
|
|
|
|
if (this._size !== 0) { throw new Error('bulk-load: tree is not empty'); }
|
|
var size = keys.length;
|
|
if (presort) { sort(keys, values, 0, size - 1, this._compare); }
|
|
this._root = loadRecursive(null, keys, values, 0, size);
|
|
this._size = size;
|
|
return this;
|
|
};
|
|
|
|
|
|
SplayTree.prototype.min = function min () {
|
|
var node = this.minNode(this._root);
|
|
if (node) { return node.key; }
|
|
else { return null; }
|
|
};
|
|
|
|
|
|
SplayTree.prototype.max = function max () {
|
|
var node = this.maxNode(this._root);
|
|
if (node) { return node.key; }
|
|
else { return null; }
|
|
};
|
|
|
|
SplayTree.prototype.isEmpty = function isEmpty () { return this._root === null; };
|
|
prototypeAccessors.size.get = function () { return this._size; };
|
|
|
|
|
|
/**
|
|
* Create a tree and load it with items
|
|
* @param{Array<Key>} keys
|
|
* @param{Array<Value>?} [values]
|
|
|
|
* @param{Function?} [comparator]
|
|
* @param{Boolean?} [presort=false] Pre-sort keys and values, using
|
|
* tree's comparator. Sorting is done
|
|
* in-place
|
|
* @param{Boolean?} [noDuplicates=false] Allow duplicates
|
|
* @return {SplayTree}
|
|
*/
|
|
SplayTree.createTree = function createTree (keys, values, comparator, presort, noDuplicates) {
|
|
return new SplayTree(comparator, noDuplicates).load(keys, values, presort);
|
|
};
|
|
|
|
Object.defineProperties( SplayTree.prototype, prototypeAccessors );
|
|
|
|
|
|
function loadRecursive (parent, keys, values, start, end) {
|
|
var size = end - start;
|
|
if (size > 0) {
|
|
var middle = start + Math.floor(size / 2);
|
|
var key = keys[middle];
|
|
var data = values[middle];
|
|
var node = { key: key, data: data, parent: parent };
|
|
node.left = loadRecursive(node, keys, values, start, middle);
|
|
node.right = loadRecursive(node, keys, values, middle + 1, end);
|
|
return node;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
|
|
function sort(keys, values, left, right, compare) {
|
|
if (left >= right) { return; }
|
|
|
|
var pivot = keys[(left + right) >> 1];
|
|
var i = left - 1;
|
|
var j = right + 1;
|
|
|
|
while (true) {
|
|
do { i++; } while (compare(keys[i], pivot) < 0);
|
|
do { j--; } while (compare(keys[j], pivot) > 0);
|
|
if (i >= j) { break; }
|
|
|
|
var tmp = keys[i];
|
|
keys[i] = keys[j];
|
|
keys[j] = tmp;
|
|
|
|
tmp = values[i];
|
|
values[i] = values[j];
|
|
values[j] = tmp;
|
|
}
|
|
|
|
sort(keys, values, left, j, compare);
|
|
sort(keys, values, j + 1, right, compare);
|
|
}
|
|
|
|
var NORMAL = 0;
|
|
var NON_CONTRIBUTING = 1;
|
|
var SAME_TRANSITION = 2;
|
|
var DIFFERENT_TRANSITION = 3;
|
|
|
|
var INTERSECTION = 0;
|
|
var UNION = 1;
|
|
var DIFFERENCE = 2;
|
|
var XOR = 3;
|
|
|
|
/**
|
|
* @param {SweepEvent} event
|
|
* @param {SweepEvent} prev
|
|
* @param {Operation} operation
|
|
*/
|
|
function computeFields (event, prev, operation) {
|
|
// compute inOut and otherInOut fields
|
|
if (prev === null) {
|
|
event.inOut = false;
|
|
event.otherInOut = true;
|
|
|
|
// previous line segment in sweepline belongs to the same polygon
|
|
} else {
|
|
if (event.isSubject === prev.isSubject) {
|
|
event.inOut = !prev.inOut;
|
|
event.otherInOut = prev.otherInOut;
|
|
|
|
// previous line segment in sweepline belongs to the clipping polygon
|
|
} else {
|
|
event.inOut = !prev.otherInOut;
|
|
event.otherInOut = prev.isVertical() ? !prev.inOut : prev.inOut;
|
|
}
|
|
|
|
// compute prevInResult field
|
|
if (prev) {
|
|
event.prevInResult = (!inResult(prev, operation) || prev.isVertical())
|
|
? prev.prevInResult : prev;
|
|
}
|
|
}
|
|
|
|
// check if the line segment belongs to the Boolean operation
|
|
var isInResult = inResult(event, operation);
|
|
if (isInResult) {
|
|
event.resultTransition = determineResultTransition(event, operation);
|
|
} else {
|
|
event.resultTransition = 0;
|
|
}
|
|
}
|
|
|
|
|
|
/* eslint-disable indent */
|
|
function inResult(event, operation) {
|
|
switch (event.type) {
|
|
case NORMAL:
|
|
switch (operation) {
|
|
case INTERSECTION:
|
|
return !event.otherInOut;
|
|
case UNION:
|
|
return event.otherInOut;
|
|
case DIFFERENCE:
|
|
// return (event.isSubject && !event.otherInOut) ||
|
|
// (!event.isSubject && event.otherInOut);
|
|
return (event.isSubject && event.otherInOut) ||
|
|
(!event.isSubject && !event.otherInOut);
|
|
case XOR:
|
|
return true;
|
|
}
|
|
break;
|
|
case SAME_TRANSITION:
|
|
return operation === INTERSECTION || operation === UNION;
|
|
case DIFFERENT_TRANSITION:
|
|
return operation === DIFFERENCE;
|
|
case NON_CONTRIBUTING:
|
|
return false;
|
|
}
|
|
return false;
|
|
}
|
|
/* eslint-enable indent */
|
|
|
|
|
|
function determineResultTransition(event, operation) {
|
|
var thisIn = !event.inOut;
|
|
var thatIn = !event.otherInOut;
|
|
|
|
var isIn;
|
|
switch (operation) {
|
|
case INTERSECTION:
|
|
isIn = thisIn && thatIn; break;
|
|
case UNION:
|
|
isIn = thisIn || thatIn; break;
|
|
case XOR:
|
|
isIn = thisIn ^ thatIn; break;
|
|
case DIFFERENCE:
|
|
if (event.isSubject) {
|
|
isIn = thisIn && !thatIn;
|
|
} else {
|
|
isIn = thatIn && !thisIn;
|
|
}
|
|
break;
|
|
}
|
|
return isIn ? +1 : -1;
|
|
}
|
|
|
|
var SweepEvent = function SweepEvent (point, left, otherEvent, isSubject, edgeType) {
|
|
|
|
/**
|
|
* Is left endpoint?
|
|
* @type {Boolean}
|
|
*/
|
|
this.left = left;
|
|
|
|
/**
|
|
* @type {Array.<Number>}
|
|
*/
|
|
this.point = point;
|
|
|
|
/**
|
|
* Other edge reference
|
|
* @type {SweepEvent}
|
|
*/
|
|
this.otherEvent = otherEvent;
|
|
|
|
/**
|
|
* Belongs to source or clipping polygon
|
|
* @type {Boolean}
|
|
*/
|
|
this.isSubject = isSubject;
|
|
|
|
/**
|
|
* Edge contribution type
|
|
* @type {Number}
|
|
*/
|
|
this.type = edgeType || NORMAL;
|
|
|
|
|
|
/**
|
|
* In-out transition for the sweepline crossing polygon
|
|
* @type {Boolean}
|
|
*/
|
|
this.inOut = false;
|
|
|
|
|
|
/**
|
|
* @type {Boolean}
|
|
*/
|
|
this.otherInOut = false;
|
|
|
|
/**
|
|
* Previous event in result?
|
|
* @type {SweepEvent}
|
|
*/
|
|
this.prevInResult = null;
|
|
|
|
/**
|
|
* Type of result transition (0 = not in result, +1 = out-in, -1, in-out)
|
|
* @type {Number}
|
|
*/
|
|
this.resultTransition = 0;
|
|
|
|
// connection step
|
|
|
|
/**
|
|
* @type {Number}
|
|
*/
|
|
this.otherPos = -1;
|
|
|
|
/**
|
|
* @type {Number}
|
|
*/
|
|
this.outputContourId = -1;
|
|
|
|
this.isExteriorRing = true; // TODO: Looks unused, remove?
|
|
};
|
|
|
|
var prototypeAccessors$1 = { inResult: { configurable: true } };
|
|
|
|
|
|
/**
|
|
* @param{Array.<Number>}p
|
|
* @return {Boolean}
|
|
*/
|
|
SweepEvent.prototype.isBelow = function isBelow (p) {
|
|
var p0 = this.point, p1 = this.otherEvent.point;
|
|
return this.left
|
|
? (p0[0] - p[0]) * (p1[1] - p[1]) - (p1[0] - p[0]) * (p0[1] - p[1]) > 0
|
|
// signedArea(this.point, this.otherEvent.point, p) > 0 :
|
|
: (p1[0] - p[0]) * (p0[1] - p[1]) - (p0[0] - p[0]) * (p1[1] - p[1]) > 0;
|
|
//signedArea(this.otherEvent.point, this.point, p) > 0;
|
|
};
|
|
|
|
|
|
/**
|
|
* @param{Array.<Number>}p
|
|
* @return {Boolean}
|
|
*/
|
|
SweepEvent.prototype.isAbove = function isAbove (p) {
|
|
return !this.isBelow(p);
|
|
};
|
|
|
|
|
|
/**
|
|
* @return {Boolean}
|
|
*/
|
|
SweepEvent.prototype.isVertical = function isVertical () {
|
|
return this.point[0] === this.otherEvent.point[0];
|
|
};
|
|
|
|
|
|
/**
|
|
* Does event belong to result?
|
|
* @return {Boolean}
|
|
*/
|
|
prototypeAccessors$1.inResult.get = function () {
|
|
return this.resultTransition !== 0;
|
|
};
|
|
|
|
|
|
SweepEvent.prototype.clone = function clone () {
|
|
var copy = new SweepEvent(
|
|
this.point, this.left, this.otherEvent, this.isSubject, this.type);
|
|
|
|
copy.contourId = this.contourId;
|
|
copy.resultTransition = this.resultTransition;
|
|
copy.prevInResult = this.prevInResult;
|
|
copy.isExteriorRing = this.isExteriorRing;
|
|
copy.inOut = this.inOut;
|
|
copy.otherInOut = this.otherInOut;
|
|
|
|
return copy;
|
|
};
|
|
|
|
Object.defineProperties( SweepEvent.prototype, prototypeAccessors$1 );
|
|
|
|
function equals(p1, p2) {
|
|
if (p1[0] === p2[0]) {
|
|
if (p1[1] === p2[1]) {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// const EPSILON = 1e-9;
|
|
// const abs = Math.abs;
|
|
// TODO https://github.com/w8r/martinez/issues/6#issuecomment-262847164
|
|
// Precision problem.
|
|
//
|
|
// module.exports = function equals(p1, p2) {
|
|
// return abs(p1[0] - p2[0]) <= EPSILON && abs(p1[1] - p2[1]) <= EPSILON;
|
|
// };
|
|
|
|
var epsilon = 1.1102230246251565e-16;
|
|
var splitter = 134217729;
|
|
var resulterrbound = (3 + 8 * epsilon) * epsilon;
|
|
|
|
// fast_expansion_sum_zeroelim routine from oritinal code
|
|
function sum(elen, e, flen, f, h) {
|
|
var Q, Qnew, hh, bvirt;
|
|
var enow = e[0];
|
|
var fnow = f[0];
|
|
var eindex = 0;
|
|
var findex = 0;
|
|
if ((fnow > enow) === (fnow > -enow)) {
|
|
Q = enow;
|
|
enow = e[++eindex];
|
|
} else {
|
|
Q = fnow;
|
|
fnow = f[++findex];
|
|
}
|
|
var hindex = 0;
|
|
if (eindex < elen && findex < flen) {
|
|
if ((fnow > enow) === (fnow > -enow)) {
|
|
Qnew = enow + Q;
|
|
hh = Q - (Qnew - enow);
|
|
enow = e[++eindex];
|
|
} else {
|
|
Qnew = fnow + Q;
|
|
hh = Q - (Qnew - fnow);
|
|
fnow = f[++findex];
|
|
}
|
|
Q = Qnew;
|
|
if (hh !== 0) {
|
|
h[hindex++] = hh;
|
|
}
|
|
while (eindex < elen && findex < flen) {
|
|
if ((fnow > enow) === (fnow > -enow)) {
|
|
Qnew = Q + enow;
|
|
bvirt = Qnew - Q;
|
|
hh = Q - (Qnew - bvirt) + (enow - bvirt);
|
|
enow = e[++eindex];
|
|
} else {
|
|
Qnew = Q + fnow;
|
|
bvirt = Qnew - Q;
|
|
hh = Q - (Qnew - bvirt) + (fnow - bvirt);
|
|
fnow = f[++findex];
|
|
}
|
|
Q = Qnew;
|
|
if (hh !== 0) {
|
|
h[hindex++] = hh;
|
|
}
|
|
}
|
|
}
|
|
while (eindex < elen) {
|
|
Qnew = Q + enow;
|
|
bvirt = Qnew - Q;
|
|
hh = Q - (Qnew - bvirt) + (enow - bvirt);
|
|
enow = e[++eindex];
|
|
Q = Qnew;
|
|
if (hh !== 0) {
|
|
h[hindex++] = hh;
|
|
}
|
|
}
|
|
while (findex < flen) {
|
|
Qnew = Q + fnow;
|
|
bvirt = Qnew - Q;
|
|
hh = Q - (Qnew - bvirt) + (fnow - bvirt);
|
|
fnow = f[++findex];
|
|
Q = Qnew;
|
|
if (hh !== 0) {
|
|
h[hindex++] = hh;
|
|
}
|
|
}
|
|
if (Q !== 0 || hindex === 0) {
|
|
h[hindex++] = Q;
|
|
}
|
|
return hindex;
|
|
}
|
|
|
|
function estimate(elen, e) {
|
|
var Q = e[0];
|
|
for (var i = 1; i < elen; i++) { Q += e[i]; }
|
|
return Q;
|
|
}
|
|
|
|
function vec(n) {
|
|
return new Float64Array(n);
|
|
}
|
|
|
|
var ccwerrboundA = (3 + 16 * epsilon) * epsilon;
|
|
var ccwerrboundB = (2 + 12 * epsilon) * epsilon;
|
|
var ccwerrboundC = (9 + 64 * epsilon) * epsilon * epsilon;
|
|
|
|
var B = vec(4);
|
|
var C1 = vec(8);
|
|
var C2 = vec(12);
|
|
var D = vec(16);
|
|
var u = vec(4);
|
|
|
|
function orient2dadapt(ax, ay, bx, by, cx, cy, detsum) {
|
|
var acxtail, acytail, bcxtail, bcytail;
|
|
var bvirt, c, ahi, alo, bhi, blo, _i, _j, _0, s1, s0, t1, t0, u3;
|
|
|
|
var acx = ax - cx;
|
|
var bcx = bx - cx;
|
|
var acy = ay - cy;
|
|
var bcy = by - cy;
|
|
|
|
s1 = acx * bcy;
|
|
c = splitter * acx;
|
|
ahi = c - (c - acx);
|
|
alo = acx - ahi;
|
|
c = splitter * bcy;
|
|
bhi = c - (c - bcy);
|
|
blo = bcy - bhi;
|
|
s0 = alo * blo - (s1 - ahi * bhi - alo * bhi - ahi * blo);
|
|
t1 = acy * bcx;
|
|
c = splitter * acy;
|
|
ahi = c - (c - acy);
|
|
alo = acy - ahi;
|
|
c = splitter * bcx;
|
|
bhi = c - (c - bcx);
|
|
blo = bcx - bhi;
|
|
t0 = alo * blo - (t1 - ahi * bhi - alo * bhi - ahi * blo);
|
|
_i = s0 - t0;
|
|
bvirt = s0 - _i;
|
|
B[0] = s0 - (_i + bvirt) + (bvirt - t0);
|
|
_j = s1 + _i;
|
|
bvirt = _j - s1;
|
|
_0 = s1 - (_j - bvirt) + (_i - bvirt);
|
|
_i = _0 - t1;
|
|
bvirt = _0 - _i;
|
|
B[1] = _0 - (_i + bvirt) + (bvirt - t1);
|
|
u3 = _j + _i;
|
|
bvirt = u3 - _j;
|
|
B[2] = _j - (u3 - bvirt) + (_i - bvirt);
|
|
B[3] = u3;
|
|
|
|
var det = estimate(4, B);
|
|
var errbound = ccwerrboundB * detsum;
|
|
if (det >= errbound || -det >= errbound) {
|
|
return det;
|
|
}
|
|
|
|
bvirt = ax - acx;
|
|
acxtail = ax - (acx + bvirt) + (bvirt - cx);
|
|
bvirt = bx - bcx;
|
|
bcxtail = bx - (bcx + bvirt) + (bvirt - cx);
|
|
bvirt = ay - acy;
|
|
acytail = ay - (acy + bvirt) + (bvirt - cy);
|
|
bvirt = by - bcy;
|
|
bcytail = by - (bcy + bvirt) + (bvirt - cy);
|
|
|
|
if (acxtail === 0 && acytail === 0 && bcxtail === 0 && bcytail === 0) {
|
|
return det;
|
|
}
|
|
|
|
errbound = ccwerrboundC * detsum + resulterrbound * Math.abs(det);
|
|
det += (acx * bcytail + bcy * acxtail) - (acy * bcxtail + bcx * acytail);
|
|
if (det >= errbound || -det >= errbound) { return det; }
|
|
|
|
s1 = acxtail * bcy;
|
|
c = splitter * acxtail;
|
|
ahi = c - (c - acxtail);
|
|
alo = acxtail - ahi;
|
|
c = splitter * bcy;
|
|
bhi = c - (c - bcy);
|
|
blo = bcy - bhi;
|
|
s0 = alo * blo - (s1 - ahi * bhi - alo * bhi - ahi * blo);
|
|
t1 = acytail * bcx;
|
|
c = splitter * acytail;
|
|
ahi = c - (c - acytail);
|
|
alo = acytail - ahi;
|
|
c = splitter * bcx;
|
|
bhi = c - (c - bcx);
|
|
blo = bcx - bhi;
|
|
t0 = alo * blo - (t1 - ahi * bhi - alo * bhi - ahi * blo);
|
|
_i = s0 - t0;
|
|
bvirt = s0 - _i;
|
|
u[0] = s0 - (_i + bvirt) + (bvirt - t0);
|
|
_j = s1 + _i;
|
|
bvirt = _j - s1;
|
|
_0 = s1 - (_j - bvirt) + (_i - bvirt);
|
|
_i = _0 - t1;
|
|
bvirt = _0 - _i;
|
|
u[1] = _0 - (_i + bvirt) + (bvirt - t1);
|
|
u3 = _j + _i;
|
|
bvirt = u3 - _j;
|
|
u[2] = _j - (u3 - bvirt) + (_i - bvirt);
|
|
u[3] = u3;
|
|
var C1len = sum(4, B, 4, u, C1);
|
|
|
|
s1 = acx * bcytail;
|
|
c = splitter * acx;
|
|
ahi = c - (c - acx);
|
|
alo = acx - ahi;
|
|
c = splitter * bcytail;
|
|
bhi = c - (c - bcytail);
|
|
blo = bcytail - bhi;
|
|
s0 = alo * blo - (s1 - ahi * bhi - alo * bhi - ahi * blo);
|
|
t1 = acy * bcxtail;
|
|
c = splitter * acy;
|
|
ahi = c - (c - acy);
|
|
alo = acy - ahi;
|
|
c = splitter * bcxtail;
|
|
bhi = c - (c - bcxtail);
|
|
blo = bcxtail - bhi;
|
|
t0 = alo * blo - (t1 - ahi * bhi - alo * bhi - ahi * blo);
|
|
_i = s0 - t0;
|
|
bvirt = s0 - _i;
|
|
u[0] = s0 - (_i + bvirt) + (bvirt - t0);
|
|
_j = s1 + _i;
|
|
bvirt = _j - s1;
|
|
_0 = s1 - (_j - bvirt) + (_i - bvirt);
|
|
_i = _0 - t1;
|
|
bvirt = _0 - _i;
|
|
u[1] = _0 - (_i + bvirt) + (bvirt - t1);
|
|
u3 = _j + _i;
|
|
bvirt = u3 - _j;
|
|
u[2] = _j - (u3 - bvirt) + (_i - bvirt);
|
|
u[3] = u3;
|
|
var C2len = sum(C1len, C1, 4, u, C2);
|
|
|
|
s1 = acxtail * bcytail;
|
|
c = splitter * acxtail;
|
|
ahi = c - (c - acxtail);
|
|
alo = acxtail - ahi;
|
|
c = splitter * bcytail;
|
|
bhi = c - (c - bcytail);
|
|
blo = bcytail - bhi;
|
|
s0 = alo * blo - (s1 - ahi * bhi - alo * bhi - ahi * blo);
|
|
t1 = acytail * bcxtail;
|
|
c = splitter * acytail;
|
|
ahi = c - (c - acytail);
|
|
alo = acytail - ahi;
|
|
c = splitter * bcxtail;
|
|
bhi = c - (c - bcxtail);
|
|
blo = bcxtail - bhi;
|
|
t0 = alo * blo - (t1 - ahi * bhi - alo * bhi - ahi * blo);
|
|
_i = s0 - t0;
|
|
bvirt = s0 - _i;
|
|
u[0] = s0 - (_i + bvirt) + (bvirt - t0);
|
|
_j = s1 + _i;
|
|
bvirt = _j - s1;
|
|
_0 = s1 - (_j - bvirt) + (_i - bvirt);
|
|
_i = _0 - t1;
|
|
bvirt = _0 - _i;
|
|
u[1] = _0 - (_i + bvirt) + (bvirt - t1);
|
|
u3 = _j + _i;
|
|
bvirt = u3 - _j;
|
|
u[2] = _j - (u3 - bvirt) + (_i - bvirt);
|
|
u[3] = u3;
|
|
var Dlen = sum(C2len, C2, 4, u, D);
|
|
|
|
return D[Dlen - 1];
|
|
}
|
|
|
|
function orient2d(ax, ay, bx, by, cx, cy) {
|
|
var detleft = (ay - cy) * (bx - cx);
|
|
var detright = (ax - cx) * (by - cy);
|
|
var det = detleft - detright;
|
|
|
|
if (detleft === 0 || detright === 0 || (detleft > 0) !== (detright > 0)) { return det; }
|
|
|
|
var detsum = Math.abs(detleft + detright);
|
|
if (Math.abs(det) >= ccwerrboundA * detsum) { return det; }
|
|
|
|
return -orient2dadapt(ax, ay, bx, by, cx, cy, detsum);
|
|
}
|
|
|
|
/**
|
|
* Signed area of the triangle (p0, p1, p2)
|
|
* @param {Array.<Number>} p0
|
|
* @param {Array.<Number>} p1
|
|
* @param {Array.<Number>} p2
|
|
* @return {Number}
|
|
*/
|
|
function signedArea(p0, p1, p2) {
|
|
var res = orient2d(p0[0], p0[1], p1[0], p1[1], p2[0], p2[1]);
|
|
if (res > 0) { return -1; }
|
|
if (res < 0) { return 1; }
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* @param {SweepEvent} e1
|
|
* @param {SweepEvent} e2
|
|
* @return {Number}
|
|
*/
|
|
function compareEvents(e1, e2) {
|
|
var p1 = e1.point;
|
|
var p2 = e2.point;
|
|
|
|
// Different x-coordinate
|
|
if (p1[0] > p2[0]) { return 1; }
|
|
if (p1[0] < p2[0]) { return -1; }
|
|
|
|
// Different points, but same x-coordinate
|
|
// Event with lower y-coordinate is processed first
|
|
if (p1[1] !== p2[1]) { return p1[1] > p2[1] ? 1 : -1; }
|
|
|
|
return specialCases(e1, e2, p1);
|
|
}
|
|
|
|
|
|
/* eslint-disable no-unused-vars */
|
|
function specialCases(e1, e2, p1, p2) {
|
|
// Same coordinates, but one is a left endpoint and the other is
|
|
// a right endpoint. The right endpoint is processed first
|
|
if (e1.left !== e2.left)
|
|
{ return e1.left ? 1 : -1; }
|
|
|
|
// const p2 = e1.otherEvent.point, p3 = e2.otherEvent.point;
|
|
// const sa = (p1[0] - p3[0]) * (p2[1] - p3[1]) - (p2[0] - p3[0]) * (p1[1] - p3[1])
|
|
// Same coordinates, both events
|
|
// are left endpoints or right endpoints.
|
|
// not collinear
|
|
if (signedArea(p1, e1.otherEvent.point, e2.otherEvent.point) !== 0) {
|
|
// the event associate to the bottom segment is processed first
|
|
return (!e1.isBelow(e2.otherEvent.point)) ? 1 : -1;
|
|
}
|
|
|
|
return (!e1.isSubject && e2.isSubject) ? 1 : -1;
|
|
}
|
|
/* eslint-enable no-unused-vars */
|
|
|
|
/**
|
|
* @param {SweepEvent} se
|
|
* @param {Array.<Number>} p
|
|
* @param {Queue} queue
|
|
* @return {Queue}
|
|
*/
|
|
function divideSegment(se, p, queue) {
|
|
var r = new SweepEvent(p, false, se, se.isSubject);
|
|
var l = new SweepEvent(p, true, se.otherEvent, se.isSubject);
|
|
|
|
/* eslint-disable no-console */
|
|
if (equals(se.point, se.otherEvent.point)) {
|
|
console.warn('what is that, a collapsed segment?', se);
|
|
}
|
|
/* eslint-enable no-console */
|
|
|
|
r.contourId = l.contourId = se.contourId;
|
|
|
|
// avoid a rounding error. The left event would be processed after the right event
|
|
if (compareEvents(l, se.otherEvent) > 0) {
|
|
se.otherEvent.left = true;
|
|
l.left = false;
|
|
}
|
|
|
|
// avoid a rounding error. The left event would be processed after the right event
|
|
// if (compareEvents(se, r) > 0) {}
|
|
|
|
se.otherEvent.otherEvent = l;
|
|
se.otherEvent = r;
|
|
|
|
queue.push(l);
|
|
queue.push(r);
|
|
|
|
return queue;
|
|
}
|
|
|
|
//const EPS = 1e-9;
|
|
|
|
/**
|
|
* Finds the magnitude of the cross product of two vectors (if we pretend
|
|
* they're in three dimensions)
|
|
*
|
|
* @param {Object} a First vector
|
|
* @param {Object} b Second vector
|
|
* @private
|
|
* @returns {Number} The magnitude of the cross product
|
|
*/
|
|
function crossProduct(a, b) {
|
|
return (a[0] * b[1]) - (a[1] * b[0]);
|
|
}
|
|
|
|
/**
|
|
* Finds the dot product of two vectors.
|
|
*
|
|
* @param {Object} a First vector
|
|
* @param {Object} b Second vector
|
|
* @private
|
|
* @returns {Number} The dot product
|
|
*/
|
|
function dotProduct(a, b) {
|
|
return (a[0] * b[0]) + (a[1] * b[1]);
|
|
}
|
|
|
|
/**
|
|
* Finds the intersection (if any) between two line segments a and b, given the
|
|
* line segments' end points a1, a2 and b1, b2.
|
|
*
|
|
* This algorithm is based on Schneider and Eberly.
|
|
* http://www.cimec.org.ar/~ncalvo/Schneider_Eberly.pdf
|
|
* Page 244.
|
|
*
|
|
* @param {Array.<Number>} a1 point of first line
|
|
* @param {Array.<Number>} a2 point of first line
|
|
* @param {Array.<Number>} b1 point of second line
|
|
* @param {Array.<Number>} b2 point of second line
|
|
* @param {Boolean=} noEndpointTouch whether to skip single touchpoints
|
|
* (meaning connected segments) as
|
|
* intersections
|
|
* @returns {Array.<Array.<Number>>|Null} If the lines intersect, the point of
|
|
* intersection. If they overlap, the two end points of the overlapping segment.
|
|
* Otherwise, null.
|
|
*/
|
|
function intersection (a1, a2, b1, b2, noEndpointTouch) {
|
|
// The algorithm expects our lines in the form P + sd, where P is a point,
|
|
// s is on the interval [0, 1], and d is a vector.
|
|
// We are passed two points. P can be the first point of each pair. The
|
|
// vector, then, could be thought of as the distance (in x and y components)
|
|
// from the first point to the second point.
|
|
// So first, let's make our vectors:
|
|
var va = [a2[0] - a1[0], a2[1] - a1[1]];
|
|
var vb = [b2[0] - b1[0], b2[1] - b1[1]];
|
|
// We also define a function to convert back to regular point form:
|
|
|
|
/* eslint-disable arrow-body-style */
|
|
|
|
function toPoint(p, s, d) {
|
|
return [
|
|
p[0] + s * d[0],
|
|
p[1] + s * d[1]
|
|
];
|
|
}
|
|
|
|
/* eslint-enable arrow-body-style */
|
|
|
|
// The rest is pretty much a straight port of the algorithm.
|
|
var e = [b1[0] - a1[0], b1[1] - a1[1]];
|
|
var kross = crossProduct(va, vb);
|
|
var sqrKross = kross * kross;
|
|
var sqrLenA = dotProduct(va, va);
|
|
//const sqrLenB = dotProduct(vb, vb);
|
|
|
|
// Check for line intersection. This works because of the properties of the
|
|
// cross product -- specifically, two vectors are parallel if and only if the
|
|
// cross product is the 0 vector. The full calculation involves relative error
|
|
// to account for possible very small line segments. See Schneider & Eberly
|
|
// for details.
|
|
if (sqrKross > 0/* EPS * sqrLenB * sqLenA */) {
|
|
// If they're not parallel, then (because these are line segments) they
|
|
// still might not actually intersect. This code checks that the
|
|
// intersection point of the lines is actually on both line segments.
|
|
var s = crossProduct(e, vb) / kross;
|
|
if (s < 0 || s > 1) {
|
|
// not on line segment a
|
|
return null;
|
|
}
|
|
var t = crossProduct(e, va) / kross;
|
|
if (t < 0 || t > 1) {
|
|
// not on line segment b
|
|
return null;
|
|
}
|
|
if (s === 0 || s === 1) {
|
|
// on an endpoint of line segment a
|
|
return noEndpointTouch ? null : [toPoint(a1, s, va)];
|
|
}
|
|
if (t === 0 || t === 1) {
|
|
// on an endpoint of line segment b
|
|
return noEndpointTouch ? null : [toPoint(b1, t, vb)];
|
|
}
|
|
return [toPoint(a1, s, va)];
|
|
}
|
|
|
|
// If we've reached this point, then the lines are either parallel or the
|
|
// same, but the segments could overlap partially or fully, or not at all.
|
|
// So we need to find the overlap, if any. To do that, we can use e, which is
|
|
// the (vector) difference between the two initial points. If this is parallel
|
|
// with the line itself, then the two lines are the same line, and there will
|
|
// be overlap.
|
|
//const sqrLenE = dotProduct(e, e);
|
|
kross = crossProduct(e, va);
|
|
sqrKross = kross * kross;
|
|
|
|
if (sqrKross > 0 /* EPS * sqLenB * sqLenE */) {
|
|
// Lines are just parallel, not the same. No overlap.
|
|
return null;
|
|
}
|
|
|
|
var sa = dotProduct(va, e) / sqrLenA;
|
|
var sb = sa + dotProduct(va, vb) / sqrLenA;
|
|
var smin = Math.min(sa, sb);
|
|
var smax = Math.max(sa, sb);
|
|
|
|
// this is, essentially, the FindIntersection acting on floats from
|
|
// Schneider & Eberly, just inlined into this function.
|
|
if (smin <= 1 && smax >= 0) {
|
|
|
|
// overlap on an end point
|
|
if (smin === 1) {
|
|
return noEndpointTouch ? null : [toPoint(a1, smin > 0 ? smin : 0, va)];
|
|
}
|
|
|
|
if (smax === 0) {
|
|
return noEndpointTouch ? null : [toPoint(a1, smax < 1 ? smax : 1, va)];
|
|
}
|
|
|
|
if (noEndpointTouch && smin === 0 && smax === 1) { return null; }
|
|
|
|
// There's overlap on a segment -- two points of intersection. Return both.
|
|
return [
|
|
toPoint(a1, smin > 0 ? smin : 0, va),
|
|
toPoint(a1, smax < 1 ? smax : 1, va)
|
|
];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @param {SweepEvent} se1
|
|
* @param {SweepEvent} se2
|
|
* @param {Queue} queue
|
|
* @return {Number}
|
|
*/
|
|
function possibleIntersection (se1, se2, queue) {
|
|
// that disallows self-intersecting polygons,
|
|
// did cost us half a day, so I'll leave it
|
|
// out of respect
|
|
// if (se1.isSubject === se2.isSubject) return;
|
|
var inter = intersection(
|
|
se1.point, se1.otherEvent.point,
|
|
se2.point, se2.otherEvent.point
|
|
);
|
|
|
|
var nintersections = inter ? inter.length : 0;
|
|
if (nintersections === 0) { return 0; } // no intersection
|
|
|
|
// the line segments intersect at an endpoint of both line segments
|
|
if ((nintersections === 1) &&
|
|
(equals(se1.point, se2.point) ||
|
|
equals(se1.otherEvent.point, se2.otherEvent.point))) {
|
|
return 0;
|
|
}
|
|
|
|
if (nintersections === 2 && se1.isSubject === se2.isSubject) {
|
|
// if(se1.contourId === se2.contourId){
|
|
// console.warn('Edges of the same polygon overlap',
|
|
// se1.point, se1.otherEvent.point, se2.point, se2.otherEvent.point);
|
|
// }
|
|
//throw new Error('Edges of the same polygon overlap');
|
|
return 0;
|
|
}
|
|
|
|
// The line segments associated to se1 and se2 intersect
|
|
if (nintersections === 1) {
|
|
|
|
// if the intersection point is not an endpoint of se1
|
|
if (!equals(se1.point, inter[0]) && !equals(se1.otherEvent.point, inter[0])) {
|
|
divideSegment(se1, inter[0], queue);
|
|
}
|
|
|
|
// if the intersection point is not an endpoint of se2
|
|
if (!equals(se2.point, inter[0]) && !equals(se2.otherEvent.point, inter[0])) {
|
|
divideSegment(se2, inter[0], queue);
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
// The line segments associated to se1 and se2 overlap
|
|
var events = [];
|
|
var leftCoincide = false;
|
|
var rightCoincide = false;
|
|
|
|
if (equals(se1.point, se2.point)) {
|
|
leftCoincide = true; // linked
|
|
} else if (compareEvents(se1, se2) === 1) {
|
|
events.push(se2, se1);
|
|
} else {
|
|
events.push(se1, se2);
|
|
}
|
|
|
|
if (equals(se1.otherEvent.point, se2.otherEvent.point)) {
|
|
rightCoincide = true;
|
|
} else if (compareEvents(se1.otherEvent, se2.otherEvent) === 1) {
|
|
events.push(se2.otherEvent, se1.otherEvent);
|
|
} else {
|
|
events.push(se1.otherEvent, se2.otherEvent);
|
|
}
|
|
|
|
if ((leftCoincide && rightCoincide) || leftCoincide) {
|
|
// both line segments are equal or share the left endpoint
|
|
se2.type = NON_CONTRIBUTING;
|
|
se1.type = (se2.inOut === se1.inOut)
|
|
? SAME_TRANSITION : DIFFERENT_TRANSITION;
|
|
|
|
if (leftCoincide && !rightCoincide) {
|
|
// honestly no idea, but changing events selection from [2, 1]
|
|
// to [0, 1] fixes the overlapping self-intersecting polygons issue
|
|
divideSegment(events[1].otherEvent, events[0].point, queue);
|
|
}
|
|
return 2;
|
|
}
|
|
|
|
// the line segments share the right endpoint
|
|
if (rightCoincide) {
|
|
divideSegment(events[0], events[1].point, queue);
|
|
return 3;
|
|
}
|
|
|
|
// no line segment includes totally the other one
|
|
if (events[0] !== events[3].otherEvent) {
|
|
divideSegment(events[0], events[1].point, queue);
|
|
divideSegment(events[1], events[2].point, queue);
|
|
return 3;
|
|
}
|
|
|
|
// one line segment includes the other one
|
|
divideSegment(events[0], events[1].point, queue);
|
|
divideSegment(events[3].otherEvent, events[2].point, queue);
|
|
|
|
return 3;
|
|
}
|
|
|
|
/**
|
|
* @param {SweepEvent} le1
|
|
* @param {SweepEvent} le2
|
|
* @return {Number}
|
|
*/
|
|
function compareSegments(le1, le2) {
|
|
if (le1 === le2) { return 0; }
|
|
|
|
// Segments are not collinear
|
|
if (signedArea(le1.point, le1.otherEvent.point, le2.point) !== 0 ||
|
|
signedArea(le1.point, le1.otherEvent.point, le2.otherEvent.point) !== 0) {
|
|
|
|
// If they share their left endpoint use the right endpoint to sort
|
|
if (equals(le1.point, le2.point)) { return le1.isBelow(le2.otherEvent.point) ? -1 : 1; }
|
|
|
|
// Different left endpoint: use the left endpoint to sort
|
|
if (le1.point[0] === le2.point[0]) { return le1.point[1] < le2.point[1] ? -1 : 1; }
|
|
|
|
// has the line segment associated to e1 been inserted
|
|
// into S after the line segment associated to e2 ?
|
|
if (compareEvents(le1, le2) === 1) { return le2.isAbove(le1.point) ? -1 : 1; }
|
|
|
|
// The line segment associated to e2 has been inserted
|
|
// into S after the line segment associated to e1
|
|
return le1.isBelow(le2.point) ? -1 : 1;
|
|
}
|
|
|
|
if (le1.isSubject === le2.isSubject) { // same polygon
|
|
var p1 = le1.point, p2 = le2.point;
|
|
if (p1[0] === p2[0] && p1[1] === p2[1]/*equals(le1.point, le2.point)*/) {
|
|
p1 = le1.otherEvent.point; p2 = le2.otherEvent.point;
|
|
if (p1[0] === p2[0] && p1[1] === p2[1]) { return 0; }
|
|
else { return le1.contourId > le2.contourId ? 1 : -1; }
|
|
}
|
|
} else { // Segments are collinear, but belong to separate polygons
|
|
return le1.isSubject ? -1 : 1;
|
|
}
|
|
|
|
return compareEvents(le1, le2) === 1 ? 1 : -1;
|
|
}
|
|
|
|
function subdivide(eventQueue, subject, clipping, sbbox, cbbox, operation) {
|
|
var sweepLine = new SplayTree(compareSegments);
|
|
var sortedEvents = [];
|
|
|
|
var rightbound = Math.min(sbbox[2], cbbox[2]);
|
|
|
|
var prev, next, begin;
|
|
|
|
while (eventQueue.length !== 0) {
|
|
var event = eventQueue.pop();
|
|
sortedEvents.push(event);
|
|
|
|
// optimization by bboxes for intersection and difference goes here
|
|
if ((operation === INTERSECTION && event.point[0] > rightbound) ||
|
|
(operation === DIFFERENCE && event.point[0] > sbbox[2])) {
|
|
break;
|
|
}
|
|
|
|
if (event.left) {
|
|
next = prev = sweepLine.insert(event);
|
|
begin = sweepLine.minNode();
|
|
|
|
if (prev !== begin) { prev = sweepLine.prev(prev); }
|
|
else { prev = null; }
|
|
|
|
next = sweepLine.next(next);
|
|
|
|
var prevEvent = prev ? prev.key : null;
|
|
var prevprevEvent = (void 0);
|
|
computeFields(event, prevEvent, operation);
|
|
if (next) {
|
|
if (possibleIntersection(event, next.key, eventQueue) === 2) {
|
|
computeFields(event, prevEvent, operation);
|
|
computeFields(next.key, event, operation);
|
|
}
|
|
}
|
|
|
|
if (prev) {
|
|
if (possibleIntersection(prev.key, event, eventQueue) === 2) {
|
|
var prevprev = prev;
|
|
if (prevprev !== begin) { prevprev = sweepLine.prev(prevprev); }
|
|
else { prevprev = null; }
|
|
|
|
prevprevEvent = prevprev ? prevprev.key : null;
|
|
computeFields(prevEvent, prevprevEvent, operation);
|
|
computeFields(event, prevEvent, operation);
|
|
}
|
|
}
|
|
} else {
|
|
event = event.otherEvent;
|
|
next = prev = sweepLine.find(event);
|
|
|
|
if (prev && next) {
|
|
|
|
if (prev !== begin) { prev = sweepLine.prev(prev); }
|
|
else { prev = null; }
|
|
|
|
next = sweepLine.next(next);
|
|
sweepLine.remove(event);
|
|
|
|
if (next && prev) {
|
|
possibleIntersection(prev.key, next.key, eventQueue);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return sortedEvents;
|
|
}
|
|
|
|
var Contour = function Contour() {
|
|
this.points = [];
|
|
this.holeIds = [];
|
|
this.holeOf = null;
|
|
this.depth = null;
|
|
};
|
|
|
|
Contour.prototype.isExterior = function isExterior () {
|
|
return this.holeOf == null;
|
|
};
|
|
|
|
/**
|
|
* @param {Array.<SweepEvent>} sortedEvents
|
|
* @return {Array.<SweepEvent>}
|
|
*/
|
|
function orderEvents(sortedEvents) {
|
|
var event, i, len, tmp;
|
|
var resultEvents = [];
|
|
for (i = 0, len = sortedEvents.length; i < len; i++) {
|
|
event = sortedEvents[i];
|
|
if ((event.left && event.inResult) ||
|
|
(!event.left && event.otherEvent.inResult)) {
|
|
resultEvents.push(event);
|
|
}
|
|
}
|
|
// Due to overlapping edges the resultEvents array can be not wholly sorted
|
|
var sorted = false;
|
|
while (!sorted) {
|
|
sorted = true;
|
|
for (i = 0, len = resultEvents.length; i < len; i++) {
|
|
if ((i + 1) < len &&
|
|
compareEvents(resultEvents[i], resultEvents[i + 1]) === 1) {
|
|
tmp = resultEvents[i];
|
|
resultEvents[i] = resultEvents[i + 1];
|
|
resultEvents[i + 1] = tmp;
|
|
sorted = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
for (i = 0, len = resultEvents.length; i < len; i++) {
|
|
event = resultEvents[i];
|
|
event.otherPos = i;
|
|
}
|
|
|
|
// imagine, the right event is found in the beginning of the queue,
|
|
// when his left counterpart is not marked yet
|
|
for (i = 0, len = resultEvents.length; i < len; i++) {
|
|
event = resultEvents[i];
|
|
if (!event.left) {
|
|
tmp = event.otherPos;
|
|
event.otherPos = event.otherEvent.otherPos;
|
|
event.otherEvent.otherPos = tmp;
|
|
}
|
|
}
|
|
|
|
return resultEvents;
|
|
}
|
|
|
|
|
|
/**
|
|
* @param {Number} pos
|
|
* @param {Array.<SweepEvent>} resultEvents
|
|
* @param {Object>} processed
|
|
* @return {Number}
|
|
*/
|
|
function nextPos(pos, resultEvents, processed, origPos) {
|
|
var newPos = pos + 1,
|
|
p = resultEvents[pos].point,
|
|
p1;
|
|
var length = resultEvents.length;
|
|
|
|
if (newPos < length)
|
|
{ p1 = resultEvents[newPos].point; }
|
|
|
|
while (newPos < length && p1[0] === p[0] && p1[1] === p[1]) {
|
|
if (!processed[newPos]) {
|
|
return newPos;
|
|
} else {
|
|
newPos++;
|
|
}
|
|
if (newPos < length) {
|
|
p1 = resultEvents[newPos].point;
|
|
}
|
|
}
|
|
|
|
newPos = pos - 1;
|
|
|
|
while (processed[newPos] && newPos > origPos) {
|
|
newPos--;
|
|
}
|
|
|
|
return newPos;
|
|
}
|
|
|
|
|
|
function initializeContourFromContext(event, contours, contourId) {
|
|
var contour = new Contour();
|
|
if (event.prevInResult != null) {
|
|
var prevInResult = event.prevInResult;
|
|
// Note that it is valid to query the "previous in result" for its output contour id,
|
|
// because we must have already processed it (i.e., assigned an output contour id)
|
|
// in an earlier iteration, otherwise it wouldn't be possible that it is "previous in
|
|
// result".
|
|
var lowerContourId = prevInResult.outputContourId;
|
|
var lowerResultTransition = prevInResult.resultTransition;
|
|
if (lowerResultTransition > 0) {
|
|
// We are inside. Now we have to check if the thing below us is another hole or
|
|
// an exterior contour.
|
|
var lowerContour = contours[lowerContourId];
|
|
if (lowerContour.holeOf != null) {
|
|
// The lower contour is a hole => Connect the new contour as a hole to its parent,
|
|
// and use same depth.
|
|
var parentContourId = lowerContour.holeOf;
|
|
contours[parentContourId].holeIds.push(contourId);
|
|
contour.holeOf = parentContourId;
|
|
contour.depth = contours[lowerContourId].depth;
|
|
} else {
|
|
// The lower contour is an exterior contour => Connect the new contour as a hole,
|
|
// and increment depth.
|
|
contours[lowerContourId].holeIds.push(contourId);
|
|
contour.holeOf = lowerContourId;
|
|
contour.depth = contours[lowerContourId].depth + 1;
|
|
}
|
|
} else {
|
|
// We are outside => this contour is an exterior contour of same depth.
|
|
contour.holeOf = null;
|
|
contour.depth = contours[lowerContourId].depth;
|
|
}
|
|
} else {
|
|
// There is no lower/previous contour => this contour is an exterior contour of depth 0.
|
|
contour.holeOf = null;
|
|
contour.depth = 0;
|
|
}
|
|
return contour;
|
|
}
|
|
|
|
/**
|
|
* @param {Array.<SweepEvent>} sortedEvents
|
|
* @return {Array.<*>} polygons
|
|
*/
|
|
function connectEdges(sortedEvents) {
|
|
var i, len;
|
|
var resultEvents = orderEvents(sortedEvents);
|
|
|
|
// "false"-filled array
|
|
var processed = {};
|
|
var contours = [];
|
|
|
|
var loop = function ( ) {
|
|
|
|
if (processed[i]) {
|
|
return;
|
|
}
|
|
|
|
var contourId = contours.length;
|
|
var contour = initializeContourFromContext(resultEvents[i], contours, contourId);
|
|
|
|
// Helper function that combines marking an event as processed with assigning its output contour ID
|
|
var markAsProcessed = function (pos) {
|
|
processed[pos] = true;
|
|
if (pos < resultEvents.length && resultEvents[pos]) {
|
|
resultEvents[pos].outputContourId = contourId;
|
|
}
|
|
};
|
|
|
|
var pos = i;
|
|
var origPos = i;
|
|
|
|
var initial = resultEvents[i].point;
|
|
contour.points.push(initial);
|
|
|
|
/* eslint no-constant-condition: "off" */
|
|
while (true) {
|
|
markAsProcessed(pos);
|
|
|
|
pos = resultEvents[pos].otherPos;
|
|
|
|
markAsProcessed(pos);
|
|
contour.points.push(resultEvents[pos].point);
|
|
|
|
pos = nextPos(pos, resultEvents, processed, origPos);
|
|
|
|
if (pos == origPos || pos >= resultEvents.length || !resultEvents[pos]) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
contours.push(contour);
|
|
};
|
|
|
|
for (i = 0, len = resultEvents.length; i < len; i++) loop( );
|
|
|
|
return contours;
|
|
}
|
|
|
|
var tinyqueue = TinyQueue;
|
|
var default_1 = TinyQueue;
|
|
|
|
function TinyQueue(data, compare) {
|
|
if (!(this instanceof TinyQueue)) { return new TinyQueue(data, compare); }
|
|
|
|
this.data = data || [];
|
|
this.length = this.data.length;
|
|
this.compare = compare || defaultCompare;
|
|
|
|
if (this.length > 0) {
|
|
for (var i = (this.length >> 1) - 1; i >= 0; i--) { this._down(i); }
|
|
}
|
|
}
|
|
|
|
function defaultCompare(a, b) {
|
|
return a < b ? -1 : a > b ? 1 : 0;
|
|
}
|
|
|
|
TinyQueue.prototype = {
|
|
|
|
push: function (item) {
|
|
this.data.push(item);
|
|
this.length++;
|
|
this._up(this.length - 1);
|
|
},
|
|
|
|
pop: function () {
|
|
if (this.length === 0) { return undefined; }
|
|
|
|
var top = this.data[0];
|
|
this.length--;
|
|
|
|
if (this.length > 0) {
|
|
this.data[0] = this.data[this.length];
|
|
this._down(0);
|
|
}
|
|
this.data.pop();
|
|
|
|
return top;
|
|
},
|
|
|
|
peek: function () {
|
|
return this.data[0];
|
|
},
|
|
|
|
_up: function (pos) {
|
|
var data = this.data;
|
|
var compare = this.compare;
|
|
var item = data[pos];
|
|
|
|
while (pos > 0) {
|
|
var parent = (pos - 1) >> 1;
|
|
var current = data[parent];
|
|
if (compare(item, current) >= 0) { break; }
|
|
data[pos] = current;
|
|
pos = parent;
|
|
}
|
|
|
|
data[pos] = item;
|
|
},
|
|
|
|
_down: function (pos) {
|
|
var data = this.data;
|
|
var compare = this.compare;
|
|
var halfLength = this.length >> 1;
|
|
var item = data[pos];
|
|
|
|
while (pos < halfLength) {
|
|
var left = (pos << 1) + 1;
|
|
var right = left + 1;
|
|
var best = data[left];
|
|
|
|
if (right < this.length && compare(data[right], best) < 0) {
|
|
left = right;
|
|
best = data[right];
|
|
}
|
|
if (compare(best, item) >= 0) { break; }
|
|
|
|
data[pos] = best;
|
|
pos = left;
|
|
}
|
|
|
|
data[pos] = item;
|
|
}
|
|
};
|
|
tinyqueue.default = default_1;
|
|
|
|
var max = Math.max;
|
|
var min = Math.min;
|
|
|
|
var contourId = 0;
|
|
|
|
|
|
function processPolygon(contourOrHole, isSubject, depth, Q, bbox, isExteriorRing) {
|
|
var i, len, s1, s2, e1, e2;
|
|
for (i = 0, len = contourOrHole.length - 1; i < len; i++) {
|
|
s1 = contourOrHole[i];
|
|
s2 = contourOrHole[i + 1];
|
|
e1 = new SweepEvent(s1, false, undefined, isSubject);
|
|
e2 = new SweepEvent(s2, false, e1, isSubject);
|
|
e1.otherEvent = e2;
|
|
|
|
if (s1[0] === s2[0] && s1[1] === s2[1]) {
|
|
continue; // skip collapsed edges, or it breaks
|
|
}
|
|
|
|
e1.contourId = e2.contourId = depth;
|
|
if (!isExteriorRing) {
|
|
e1.isExteriorRing = false;
|
|
e2.isExteriorRing = false;
|
|
}
|
|
if (compareEvents(e1, e2) > 0) {
|
|
e2.left = true;
|
|
} else {
|
|
e1.left = true;
|
|
}
|
|
|
|
var x = s1[0], y = s1[1];
|
|
bbox[0] = min(bbox[0], x);
|
|
bbox[1] = min(bbox[1], y);
|
|
bbox[2] = max(bbox[2], x);
|
|
bbox[3] = max(bbox[3], y);
|
|
|
|
// Pushing it so the queue is sorted from left to right,
|
|
// with object on the left having the highest priority.
|
|
Q.push(e1);
|
|
Q.push(e2);
|
|
}
|
|
}
|
|
|
|
|
|
function fillQueue(subject, clipping, sbbox, cbbox, operation) {
|
|
var eventQueue = new tinyqueue(null, compareEvents);
|
|
var polygonSet, isExteriorRing, i, ii, j, jj; //, k, kk;
|
|
|
|
for (i = 0, ii = subject.length; i < ii; i++) {
|
|
polygonSet = subject[i];
|
|
for (j = 0, jj = polygonSet.length; j < jj; j++) {
|
|
isExteriorRing = j === 0;
|
|
if (isExteriorRing) { contourId++; }
|
|
processPolygon(polygonSet[j], true, contourId, eventQueue, sbbox, isExteriorRing);
|
|
}
|
|
}
|
|
|
|
for (i = 0, ii = clipping.length; i < ii; i++) {
|
|
polygonSet = clipping[i];
|
|
for (j = 0, jj = polygonSet.length; j < jj; j++) {
|
|
isExteriorRing = j === 0;
|
|
if (operation === DIFFERENCE) { isExteriorRing = false; }
|
|
if (isExteriorRing) { contourId++; }
|
|
processPolygon(polygonSet[j], false, contourId, eventQueue, cbbox, isExteriorRing);
|
|
}
|
|
}
|
|
|
|
return eventQueue;
|
|
}
|
|
|
|
var EMPTY = [];
|
|
|
|
|
|
function trivialOperation(subject, clipping, operation) {
|
|
var result = null;
|
|
if (subject.length * clipping.length === 0) {
|
|
if (operation === INTERSECTION) {
|
|
result = EMPTY;
|
|
} else if (operation === DIFFERENCE) {
|
|
result = subject;
|
|
} else if (operation === UNION ||
|
|
operation === XOR) {
|
|
result = (subject.length === 0) ? clipping : subject;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
|
|
function compareBBoxes(subject, clipping, sbbox, cbbox, operation) {
|
|
var result = null;
|
|
if (sbbox[0] > cbbox[2] ||
|
|
cbbox[0] > sbbox[2] ||
|
|
sbbox[1] > cbbox[3] ||
|
|
cbbox[1] > sbbox[3]) {
|
|
if (operation === INTERSECTION) {
|
|
result = EMPTY;
|
|
} else if (operation === DIFFERENCE) {
|
|
result = subject;
|
|
} else if (operation === UNION ||
|
|
operation === XOR) {
|
|
result = subject.concat(clipping);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
|
|
function boolean(subject, clipping, operation) {
|
|
if (typeof subject[0][0][0] === 'number') {
|
|
subject = [subject];
|
|
}
|
|
if (typeof clipping[0][0][0] === 'number') {
|
|
clipping = [clipping];
|
|
}
|
|
var trivial = trivialOperation(subject, clipping, operation);
|
|
if (trivial) {
|
|
return trivial === EMPTY ? null : trivial;
|
|
}
|
|
var sbbox = [Infinity, Infinity, -Infinity, -Infinity];
|
|
var cbbox = [Infinity, Infinity, -Infinity, -Infinity];
|
|
|
|
// console.time('fill queue');
|
|
var eventQueue = fillQueue(subject, clipping, sbbox, cbbox, operation);
|
|
//console.timeEnd('fill queue');
|
|
|
|
trivial = compareBBoxes(subject, clipping, sbbox, cbbox, operation);
|
|
if (trivial) {
|
|
return trivial === EMPTY ? null : trivial;
|
|
}
|
|
// console.time('subdivide edges');
|
|
var sortedEvents = subdivide(eventQueue, subject, clipping, sbbox, cbbox, operation);
|
|
//console.timeEnd('subdivide edges');
|
|
|
|
// console.time('connect vertices');
|
|
var contours = connectEdges(sortedEvents);
|
|
//console.timeEnd('connect vertices');
|
|
|
|
// Convert contours to polygons
|
|
var polygons = [];
|
|
for (var i = 0; i < contours.length; i++) {
|
|
var contour = contours[i];
|
|
if (contour.isExterior()) {
|
|
// The exterior ring goes first
|
|
var rings = [contour.points];
|
|
// Followed by holes if any
|
|
for (var j = 0; j < contour.holeIds.length; j++) {
|
|
var holeId = contour.holeIds[j];
|
|
rings.push(contours[holeId].points);
|
|
}
|
|
polygons.push(rings);
|
|
}
|
|
}
|
|
|
|
return polygons;
|
|
}
|
|
|
|
function union (subject, clipping) {
|
|
return boolean(subject, clipping, UNION);
|
|
}
|
|
|
|
function diff (subject, clipping) {
|
|
return boolean(subject, clipping, DIFFERENCE);
|
|
}
|
|
|
|
function xor (subject, clipping) {
|
|
return boolean(subject, clipping, XOR);
|
|
}
|
|
|
|
function intersection$1 (subject, clipping) {
|
|
return boolean(subject, clipping, INTERSECTION);
|
|
}
|
|
|
|
/**
|
|
* @enum {Number}
|
|
*/
|
|
var operations = { UNION: UNION, DIFFERENCE: DIFFERENCE, INTERSECTION: INTERSECTION, XOR: XOR };
|
|
|
|
exports.diff = diff;
|
|
exports.intersection = intersection$1;
|
|
exports.operations = operations;
|
|
exports.union = union;
|
|
exports.xor = xor;
|
|
|
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
|
})));
|
|
//# sourceMappingURL=martinez.umd.js.map
|