greiner-hormann
Version:
Greiner-Hormann clipping algorithm
534 lines (440 loc) • 12.9 kB
JavaScript
/**
* greiner-hormann v1.4.2
* Greiner-Hormann clipping algorithm
*
* @author Alexander 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) :
(factory((global.greinerHormann = {})));
}(this, (function (exports) { 'use strict';
var Vertex = function Vertex (x, y) {
if (arguments.length === 1) {
// Coords
if (Array.isArray(x)) {
y = x[1];
x = x[0];
} else {
y = x.y;
x = x.x;
}
}
/**
* X coordinate
* @type {Number}
*/
this.x = x;
/**
* Y coordinate
* @type {Number}
*/
this.y = y;
/**
* Next node
* @type {Vertex}
*/
this.next = null;
/**
* Previous vertex
* @type {Vertex}
*/
this.prev = null;
/**
* Corresponding intersection in other polygon
*/
this._corresponding = null;
/**
* Distance from previous
*/
this._distance = 0.0;
/**
* Entry/exit point in another polygon
* @type {Boolean}
*/
this._isEntry = true;
/**
* Intersection vertex flag
* @type {Boolean}
*/
this._isIntersection = false;
/**
* Loop check
* @type {Boolean}
*/
this._visited = false;
};
/**
* Creates intersection vertex
* @param{Number} x
* @param{Number} y
* @param{Number} distance
* @return {Vertex}
*/
Vertex.createIntersection = function createIntersection (x, y, distance) {
var vertex = new Vertex(x, y);
vertex._distance = distance;
vertex._isIntersection = true;
vertex._isEntry = false;
return vertex;
};
/**
* Mark as visited
*/
Vertex.prototype.visit = function visit () {
this._visited = true;
if (this._corresponding !== null && !this._corresponding._visited) {
this._corresponding.visit();
}
};
/**
* Convenience
* @param{Vertex}v
* @return {Boolean}
*/
Vertex.prototype.equals = function equals (v) {
return this.x === v.x && this.y === v.y;
};
/**
* Check if vertex is inside a polygon by odd-even rule:
* If the number of intersections of a ray out of the point and polygon
* segments is odd - the point is inside.
* @param {Polygon} poly
* @return {Boolean}
*/
Vertex.prototype.isInside = function isInside (poly) {
var oddNodes = false;
var vertex = poly.first;
var next = vertex.next;
var x = this.x;
var y = this.y;
do {
if ((vertex.y < y && next.y >= y ||
next.y < y && vertex.y >= y) &&
(vertex.x <= x || next.x <= x)) {
oddNodes ^= (vertex.x + (y - vertex.y) /
(next.y - vertex.y) * (next.x - vertex.x) < x);
}
vertex = vertex.next;
next = vertex.next || poly.first;
} while (!vertex.equals(poly.first));
return oddNodes;
};
var Intersection = function Intersection(s1, s2, c1, c2) {
/**
* @type {Number}
*/
this.x = 0.0;
/**
* @type {Number}
*/
this.y = 0.0;
/**
* @type {Number}
*/
this.toSource = 0.0;
/**
* @type {Number}
*/
this.toClip = 0.0;
var d = (c2.y - c1.y) * (s2.x - s1.x) - (c2.x - c1.x) * (s2.y - s1.y);
if (d === 0) { return; }
/**
* @type {Number}
*/
this.toSource = ((c2.x - c1.x) * (s1.y - c1.y) - (c2.y - c1.y) * (s1.x - c1.x)) / d;
/**
* @type {Number}
*/
this.toClip = ((s2.x - s1.x) * (s1.y - c1.y) - (s2.y - s1.y) * (s1.x - c1.x)) / d;
if (this.valid()) {
this.x = s1.x + this.toSource * (s2.x - s1.x);
this.y = s1.y + this.toSource * (s2.y - s1.y);
}
};
/**
* @return {Boolean}
*/
Intersection.prototype.valid = function valid () {
return (0 < this.toSource && this.toSource < 1) && (0 < this.toClip && this.toClip < 1);
};
var Polygon = function Polygon (p, arrayVertices) {
var this$1 = this;
/**
* @type {Vertex}
*/
this.first = null;
/**
* @type {Number}
*/
this.vertices = 0;
/**
* @type {Vertex}
*/
this._lastUnprocessed = null;
/**
* Whether to handle input and output as [x,y] or {x:x,y:y}
* @type {Boolean}
*/
this._arrayVertices = (typeof arrayVertices === "undefined") ?
Array.isArray(p[0]) :
arrayVertices;
for (var i = 0, len = p.length; i < len; i++) {
this$1.addVertex(new Vertex(p[i]));
}
};
/**
* Add a vertex object to the polygon
* (vertex is added at the 'end' of the list')
*
* @param vertex
*/
Polygon.prototype.addVertex = function addVertex (vertex) {
if (this.first === null) {
this.first = vertex;
this.first.next = vertex;
this.first.prev = vertex;
} else {
var next = this.first;
var prev = next.prev;
next.prev = vertex;
vertex.next = next;
vertex.prev = prev;
prev.next = vertex;
}
this.vertices++;
};
/**
* Inserts a vertex inbetween start and end
*
* @param {Vertex} vertex
* @param {Vertex} start
* @param {Vertex} end
*/
Polygon.prototype.insertVertex = function insertVertex (vertex, start, end) {
var prev, curr = start;
while (!curr.equals(end) && curr._distance < vertex._distance) {
curr = curr.next;
}
vertex.next = curr;
prev = curr.prev;
vertex.prev = prev;
prev.next = vertex;
curr.prev = vertex;
this.vertices++;
};
/**
* Get next non-intersection point
* @param{Vertex} v
* @return {Vertex}
*/
Polygon.prototype.getNext = function getNext (v) {
var c = v;
while (c._isIntersection) { c = c.next; }
return c;
};
/**
* Unvisited intersection
* @return {Vertex}
*/
Polygon.prototype.getFirstIntersect = function getFirstIntersect () {
var v = this._firstIntersect || this.first;
do {
if (v._isIntersection && !v._visited) { break; }
v = v.next;
} while (!v.equals(this.first));
this._firstIntersect = v;
return v;
};
/**
* Does the polygon have unvisited vertices
* @return {Boolean} [description]
*/
Polygon.prototype.hasUnprocessed = function hasUnprocessed () {
var this$1 = this;
var v = this._lastUnprocessed || this.first;
do {
if (v._isIntersection && !v._visited) {
this$1._lastUnprocessed = v;
return true;
}
v = v.next;
} while (!v.equals(this.first));
this._lastUnprocessed = null;
return false;
};
/**
* The output depends on what you put in, arrays or objects
* @return {Array.<Array<Number>|Array.<Object>}
*/
Polygon.prototype.getPoints = function getPoints () {
var points = [];
var v = this.first;
if (this._arrayVertices) {
do {
points.push([v.x, v.y]);
v = v.next;
} while (v !== this.first);
} else {
do {
points.push({
x: v.x,
y: v.y
});
v = v.next;
} while (v !== this.first);
}
return points;
};
/**
* Clip polygon against another one.
* Result depends on algorithm direction:
*
* Intersection: forwards forwards
* Union: backwars backwards
* Diff: backwards forwards
*
* @param {Polygon} clip
* @param {Boolean} sourceForwards
* @param {Boolean} clipForwards
*/
Polygon.prototype.clip = function clip (clip$1, sourceForwards, clipForwards) {
var this$1 = this;
var sourceVertex = this.first;
var clipVertex = clip$1.first;
var sourceInClip, clipInSource;
var isUnion = !sourceForwards && !clipForwards;
var isIntersection = sourceForwards && clipForwards;
// calculate and mark intersections
do {
if (!sourceVertex._isIntersection) {
do {
if (!clipVertex._isIntersection) {
var i = new Intersection(
sourceVertex,
this$1.getNext(sourceVertex.next),
clipVertex, clip$1.getNext(clipVertex.next)
);
if (i.valid()) {
var sourceIntersection = Vertex.createIntersection(i.x, i.y, i.toSource);
var clipIntersection = Vertex.createIntersection(i.x, i.y, i.toClip);
sourceIntersection._corresponding = clipIntersection;
clipIntersection._corresponding = sourceIntersection;
this$1.insertVertex(sourceIntersection, sourceVertex, this$1.getNext(sourceVertex.next));
clip$1.insertVertex(clipIntersection, clipVertex, clip$1.getNext(clipVertex.next));
}
}
clipVertex = clipVertex.next;
} while (!clipVertex.equals(clip$1.first));
}
sourceVertex = sourceVertex.next;
} while (!sourceVertex.equals(this.first));
// phase two - identify entry/exit points
sourceVertex = this.first;
clipVertex = clip$1.first;
sourceInClip = sourceVertex.isInside(clip$1);
clipInSource = clipVertex.isInside(this);
sourceForwards ^= sourceInClip;
clipForwards ^= clipInSource;
do {
if (sourceVertex._isIntersection) {
sourceVertex._isEntry = sourceForwards;
sourceForwards = !sourceForwards;
}
sourceVertex = sourceVertex.next;
} while (!sourceVertex.equals(this.first));
do {
if (clipVertex._isIntersection) {
clipVertex._isEntry = clipForwards;
clipForwards = !clipForwards;
}
clipVertex = clipVertex.next;
} while (!clipVertex.equals(clip$1.first));
// phase three - construct a list of clipped polygons
var list = [];
while (this.hasUnprocessed()) {
var current = this$1.getFirstIntersect();
// keep format
var clipped = new Polygon([], this$1._arrayVertices);
clipped.addVertex(new Vertex(current.x, current.y));
do {
current.visit();
if (current._isEntry) {
do {
current = current.next;
clipped.addVertex(new Vertex(current.x, current.y));
} while (!current._isIntersection);
} else {
do {
current = current.prev;
clipped.addVertex(new Vertex(current.x, current.y));
} while (!current._isIntersection);
}
current = current._corresponding;
} while (!current._visited);
list.push(clipped.getPoints());
}
if (list.length === 0) {
if (isUnion) {
if (sourceInClip) { list.push(clip$1.getPoints()); }
else if (clipInSource) { list.push(this.getPoints()); }
else { list.push(this.getPoints(), clip$1.getPoints()); }
} else if (isIntersection) { // intersection
if (sourceInClip) { list.push(this.getPoints()); }
else if (clipInSource) { list.push(clip$1.getPoints()); }
} else { // diff
if (sourceInClip) { list.push(clip$1.getPoints(), this.getPoints()); }
else if (clipInSource) { list.push(this.getPoints(), clip$1.getPoints()); }
else { list.push(this.getPoints()); }
}
if (list.length === 0) { list = null; }
}
return list;
};
/**
* Clip driver
* @param {Array.<Array.<Number>>} polygonA
* @param {Array.<Array.<Number>>} polygonB
* @param {Boolean} sourceForwards
* @param {Boolean} clipForwards
* @return {Array.<Array.<Number>>}
*/
function boolean (polygonA, polygonB, eA, eB) {
var source = new Polygon(polygonA);
var clip = new Polygon(polygonB);
return source.clip(clip, eA, eB);
}
/**
* @param {Array.<Array.<Number>|Array.<Object>} polygonA
* @param {Array.<Array.<Number>|Array.<Object>} polygonB
* @return {Array.<Array.<Number>>|Array.<Array.<Object>|Null}
*/
function union (polygonA, polygonB) {
return boolean(polygonA, polygonB, false, false);
}
/**
* @param {Array.<Array.<Number>|Array.<Object>} polygonA
* @param {Array.<Array.<Number>|Array.<Object>} polygonB
* @return {Array.<Array.<Number>>|Array.<Array.<Object>>|Null}
*/
function intersection (polygonA, polygonB) {
return boolean(polygonA, polygonB, true, true);
}
/**
* @param {Array.<Array.<Number>|Array.<Object>} polygonA
* @param {Array.<Array.<Number>|Array.<Object>} polygonB
* @return {Array.<Array.<Number>>|Array.<Array.<Object>>|Null}
*/
function diff (polygonA, polygonB) {
return boolean(polygonA, polygonB, false, true);
}
var clip = boolean;
exports.union = union;
exports.intersection = intersection;
exports.diff = diff;
exports.clip = clip;
Object.defineProperty(exports, '__esModule', { value: true });
})));
//# sourceMappingURL=greiner-hormann.js.map