@avdl/martinez
Version:
TypeScript library for polygon boolean operations
1,319 lines (1,300 loc) • 49.8 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
/**
* Represents a line segment with begin and end points
*/
var Segment = /*#__PURE__*/function () {
function Segment(beginPoint, endPoint) {
this.beginPoint = beginPoint;
this.endPoint = endPoint;
}
var _proto = Segment.prototype;
_proto.begin = function begin() {
return this.beginPoint;
};
_proto.end = function end() {
return this.endPoint;
};
return Segment;
}();
/**
* Represents a contour (closed polygon boundary) with multiple vertices
*/
var Contour = /*#__PURE__*/function () {
function Contour(points) {
if (points === void 0) {
points = [];
}
this.points = [];
this.holeOf = null; // Index of parent contour if this is a hole
this.holeIds = []; // Indices of child holes
this.depth = 0; // Nesting depth for validation
this.points = [].concat(points);
}
var _proto = Contour.prototype;
_proto.nvertices = function nvertices() {
return this.points.length;
};
_proto.segment = function segment(i) {
var p1 = this.points[i];
var p2 = this.points[(i + 1) % this.points.length];
return new Segment(p1, p2);
};
_proto.addPoint = function addPoint(point) {
this.points.push(point);
};
_proto.getPoints = function getPoints() {
return [].concat(this.points);
};
_proto.setPoints = function setPoints(points) {
this.points = [].concat(points);
}
/**
* Set this contour as a hole of another contour
*/;
_proto.setHoleOf = function setHoleOf(parentIndex) {
this.holeOf = parentIndex;
}
/**
* Get the parent contour index if this is a hole
*/;
_proto.getHoleOf = function getHoleOf() {
return this.holeOf;
}
/**
* Add a hole to this contour
*/;
_proto.addHole = function addHole(holeIndex) {
this.holeIds.push(holeIndex);
}
/**
* Get all hole indices for this contour
*/;
_proto.getHoles = function getHoles() {
return [].concat(this.holeIds);
}
/**
* Set the nesting depth
*/;
_proto.setDepth = function setDepth(depth) {
this.depth = depth;
}
/**
* Get the nesting depth
*/;
_proto.getDepth = function getDepth() {
return this.depth;
}
/**
* Check if this contour is a hole
*/;
_proto.isHole = function isHole() {
return this.holeOf !== null;
};
return Contour;
}();
function _arrayLikeToArray(r, a) {
(null == a || a > r.length) && (a = r.length);
for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e];
return n;
}
function _defineProperties(e, r) {
for (var t = 0; t < r.length; t++) {
var o = r[t];
o.enumerable = o.enumerable || !1, o.configurable = !0, "value" in o && (o.writable = !0), Object.defineProperty(e, _toPropertyKey(o.key), o);
}
}
function _createClass(e, r, t) {
return r && _defineProperties(e.prototype, r), t && _defineProperties(e, t), Object.defineProperty(e, "prototype", {
writable: !1
}), e;
}
function _createForOfIteratorHelperLoose(r, e) {
var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"];
if (t) return (t = t.call(r)).next.bind(t);
if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) {
t && (r = t);
var o = 0;
return function () {
return o >= r.length ? {
done: !0
} : {
done: !1,
value: r[o++]
};
};
}
throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
function _toPrimitive(t, r) {
if ("object" != typeof t || !t) return t;
var e = t[Symbol.toPrimitive];
if (void 0 !== e) {
var i = e.call(t, r || "default");
if ("object" != typeof i) return i;
throw new TypeError("@@toPrimitive must return a primitive value.");
}
return ("string" === r ? String : Number)(t);
}
function _toPropertyKey(t) {
var i = _toPrimitive(t, "string");
return "symbol" == typeof i ? i : i + "";
}
function _unsupportedIterableToArray(r, a) {
if (r) {
if ("string" == typeof r) return _arrayLikeToArray(r, a);
var t = {}.toString.call(r).slice(8, -1);
return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0;
}
}
/**
* Represents a polygon with multiple contours and bounding box calculations
*/
var Polygon = /*#__PURE__*/function () {
function Polygon(contours) {
if (contours === void 0) {
contours = [];
}
this.contours = [];
this.contours = [].concat(contours);
}
var _proto = Polygon.prototype;
_proto.contourCount = function contourCount() {
return this.contours.length;
};
_proto.contour = function contour(index) {
return this.contours[index];
};
_proto.boundingbox = function boundingbox(min, max) {
if (this.contours.length === 0) return;
var minX = Infinity,
minY = Infinity;
var maxX = -Infinity,
maxY = -Infinity;
for (var _iterator = _createForOfIteratorHelperLoose(this.contours), _step; !(_step = _iterator()).done;) {
var contour = _step.value;
for (var i = 0; i < contour.nvertices(); i++) {
var seg = contour.segment(i);
var p1 = seg.begin();
var p2 = seg.end();
minX = Math.min(minX, p1.x, p2.x);
minY = Math.min(minY, p1.y, p2.y);
maxX = Math.max(maxX, p1.x, p2.x);
maxY = Math.max(maxY, p1.y, p2.y);
}
}
min.x = minX;
min.y = minY;
max.x = maxX;
max.y = maxY;
};
_proto.pushbackContour = function pushbackContour() {
var contour = new Contour();
this.contours.push(contour);
return contour;
};
_proto.addContour = function addContour(points) {
var contour = new Contour(points);
this.contours.push(contour);
return contour;
};
_proto.addContours = function addContours(points) {
var _this = this;
return points.map(function (pointList) {
var contour = new Contour(structuredClone(pointList));
_this.contours.push(contour);
return contour;
});
};
_proto.getContours = function getContours() {
return this.contours.map(function (c) {
return c.getPoints();
});
};
return Polygon;
}();
/**
* Boolean operation types for polygon clipping
*/
(function (BooleanOperationType) {
BooleanOperationType[BooleanOperationType["INTERSECTION"] = 0] = "INTERSECTION";
BooleanOperationType[BooleanOperationType["UNION"] = 1] = "UNION";
BooleanOperationType[BooleanOperationType["DIFFERENCE"] = 2] = "DIFFERENCE";
BooleanOperationType[BooleanOperationType["XOR"] = 3] = "XOR";
})(exports.BooleanOperationType || (exports.BooleanOperationType = {}));
/**
* Edge types for sweep line algorithm
*/
(function (EdgeType) {
EdgeType[EdgeType["NORMAL"] = 0] = "NORMAL";
EdgeType[EdgeType["NON_CONTRIBUTING"] = 1] = "NON_CONTRIBUTING";
EdgeType[EdgeType["SAME_TRANSITION"] = 2] = "SAME_TRANSITION";
EdgeType[EdgeType["DIFFERENT_TRANSITION"] = 3] = "DIFFERENT_TRANSITION";
})(exports.EdgeType || (exports.EdgeType = {}));
/**
* Polygon types for boolean operations
*/
(function (PolygonType) {
PolygonType[PolygonType["SUBJECT"] = 0] = "SUBJECT";
PolygonType[PolygonType["CLIPPING"] = 1] = "CLIPPING";
})(exports.PolygonType || (exports.PolygonType = {}));
/**
* Priority Queue implementation for event queue management
* Uses a binary heap with custom comparison function
*/
var PriorityQueue = /*#__PURE__*/function () {
function PriorityQueue(compareFn) {
this.items = [];
this.compareFn = compareFn;
}
var _proto = PriorityQueue.prototype;
_proto.push = function push(item) {
this.items.push(item);
this.bubbleUp(this.items.length - 1);
};
_proto.pop = function pop() {
if (this.items.length === 0) return undefined;
if (this.items.length === 1) return this.items.pop();
var top = this.items[0];
this.items[0] = this.items.pop();
this.bubbleDown(0);
return top;
};
_proto.size = function size() {
return this.items.length;
};
_proto.isEmpty = function isEmpty() {
return this.items.length === 0;
};
_proto.bubbleUp = function bubbleUp(index) {
while (index > 0) {
var parentIndex = Math.floor((index - 1) / 2);
if (!this.compareFn(this.items[parentIndex], this.items[index])) {
break;
}
this.swap(parentIndex, index);
index = parentIndex;
}
};
_proto.bubbleDown = function bubbleDown(index) {
while (true) {
var minIndex = index;
var leftChild = 2 * index + 1;
var rightChild = 2 * index + 2;
if (leftChild < this.items.length && this.compareFn(this.items[minIndex], this.items[leftChild])) {
minIndex = leftChild;
}
if (rightChild < this.items.length && this.compareFn(this.items[minIndex], this.items[rightChild])) {
minIndex = rightChild;
}
if (minIndex === index) break;
this.swap(index, minIndex);
index = minIndex;
}
};
_proto.swap = function swap(i, j) {
var _ref = [this.items[j], this.items[i]];
this.items[i] = _ref[0];
this.items[j] = _ref[1];
};
return PriorityQueue;
}();
/**
* Ordered Set implementation for maintaining sweep line status
* Uses binary search for insertion and maintains sorted order
*/
var OrderedSet = /*#__PURE__*/function () {
function OrderedSet(compareFn) {
this.items = [];
this.compareFn = compareFn;
}
var _proto = OrderedSet.prototype;
_proto.insert = function insert(item) {
// Find the correct position to insert while maintaining order
var left = 0;
var right = this.items.length;
while (left < right) {
var mid = Math.floor((left + right) / 2);
if (this.compareFn(this.items[mid], item)) {
left = mid + 1;
} else {
right = mid;
}
}
this.items.splice(left, 0, item);
return left;
};
_proto["delete"] = function _delete(item) {
var index = this.items.indexOf(item);
if (index !== -1) {
this.items.splice(index, 1);
return true;
}
return false;
};
_proto.indexOf = function indexOf(item) {
return this.items.indexOf(item);
};
_proto.at = function at(index) {
return this.items[index];
};
_proto.size = function size() {
return this.items.length;
};
_proto.isEmpty = function isEmpty() {
return this.items.length === 0;
};
_proto.clear = function clear() {
this.items = [];
};
return OrderedSet;
}();
/**
* Calculate the signed area of a triangle formed by three points
* @param firstPoint First point of the triangle
* @param secondPoint Second point of the triangle
* @param thirdPoint Third point of the triangle
* @returns Signed area (positive for counter-clockwise, negative for clockwise)
*/
function calculateSignedArea(firstPoint, secondPoint, thirdPoint) {
return (firstPoint.x - thirdPoint.x) * (secondPoint.y - thirdPoint.y) - (secondPoint.x - thirdPoint.x) * (firstPoint.y - thirdPoint.y);
}
/**
* Find intersection between two line segments
* @param segment1 First line segment
* @param segment2 Second line segment
* @param intersectionPoint1 Output: first intersection point
* @param intersectionPoint2 Output: second intersection point (for overlapping segments)
* @returns Number of intersections (0, 1, or 2)
*/
function findSegmentIntersection(segment1, segment2, intersectionPoint1, intersectionPoint2) {
var p0 = segment1.begin();
var p1 = segment1.end();
var p2 = segment2.begin();
var p3 = segment2.end();
// Direction vectors
var d0x = p1.x - p0.x;
var d0y = p1.y - p0.y;
var d1x = p3.x - p2.x;
var d1y = p3.y - p2.y;
// Vector from p0 to p2
var Ex = p2.x - p0.x;
var Ey = p2.y - p0.y;
// Cross product of direction vectors
var kross = d0x * d1y - d0y * d1x;
var sqrEpsilon = 0.0000001;
var sqrLen0 = d0x * d0x + d0y * d0y;
var sqrLen1 = d1x * d1x + d1y * d1y;
if (kross * kross > sqrEpsilon * sqrLen0 * sqrLen1) {
// Lines are not parallel - check for intersection
var s = (Ex * d1y - Ey * d1x) / kross;
if (s < 0 || s > 1) {
return 0; // No intersection within segment1
}
var t = (Ex * d0y - Ey * d0x) / kross;
if (t < 0 || t > 1) {
return 0; // No intersection within segment2
}
// Intersection point
intersectionPoint1.x = p0.x + s * d0x;
intersectionPoint1.y = p0.y + s * d0y;
// Snap to exact endpoints if very close (for numerical stability)
var snapDistance = 0.00000001;
if (Math.abs(intersectionPoint1.x - p0.x) < snapDistance && Math.abs(intersectionPoint1.y - p0.y) < snapDistance) {
intersectionPoint1.x = p0.x;
intersectionPoint1.y = p0.y;
} else if (Math.abs(intersectionPoint1.x - p1.x) < snapDistance && Math.abs(intersectionPoint1.y - p1.y) < snapDistance) {
intersectionPoint1.x = p1.x;
intersectionPoint1.y = p1.y;
} else if (Math.abs(intersectionPoint1.x - p2.x) < snapDistance && Math.abs(intersectionPoint1.y - p2.y) < snapDistance) {
intersectionPoint1.x = p2.x;
intersectionPoint1.y = p2.y;
} else if (Math.abs(intersectionPoint1.x - p3.x) < snapDistance && Math.abs(intersectionPoint1.y - p3.y) < snapDistance) {
intersectionPoint1.x = p3.x;
intersectionPoint1.y = p3.y;
}
return 1;
}
// Lines are parallel or collinear
var sqrLenE = Ex * Ex + Ey * Ey;
var krossE = Ex * d0y - Ey * d0x;
var sqrKrossE = krossE * krossE;
if (sqrKrossE > sqrEpsilon * sqrLen0 * sqrLenE) {
// Lines are parallel but different - no intersection
return 0;
}
// Lines are collinear - check for segment overlap
var s0 = (d0x * Ex + d0y * Ey) / sqrLen0;
var s1 = s0 + (d0x * d1x + d0y * d1y) / sqrLen0;
var smin = Math.min(s0, s1);
var smax = Math.max(s0, s1);
// Find intersection of [0,1] with [smin, smax]
var wmin = Math.max(0.0, smin);
var wmax = Math.min(1.0, smax);
if (wmin > wmax) {
return 0; // No overlap
}
if (wmin === wmax) {
// Single point overlap
intersectionPoint1.x = p0.x + wmin * d0x;
intersectionPoint1.y = p0.y + wmin * d0y;
return 1;
} else {
// Segment overlap
intersectionPoint1.x = p0.x + wmin * d0x;
intersectionPoint1.y = p0.y + wmin * d0y;
intersectionPoint2.x = p0.x + wmax * d0x;
intersectionPoint2.y = p0.y + wmax * d0y;
return 2;
}
}
/**
* Check if a point lies on a line segment
* @param segmentStart Start point of the segment
* @param segmentEnd End point of the segment
* @param testPoint Point to test
* @returns True if the point lies on the segment
*/
function isPointOnSegment(segmentStart, segmentEnd, testPoint) {
return testPoint.x <= Math.max(segmentStart.x, segmentEnd.x) && testPoint.x >= Math.min(segmentStart.x, segmentEnd.x) && testPoint.y <= Math.max(segmentStart.y, segmentEnd.y) && testPoint.y >= Math.min(segmentStart.y, segmentEnd.y);
}
/**
* Represents a sweep event in the Martinez-Rueda clipping algorithm
*/
var SweepEvent = /*#__PURE__*/function () {
function SweepEvent(point, isLeftEndpoint, polygonLabel, otherEvent, edgeType) {
if (edgeType === void 0) {
edgeType = exports.EdgeType.NORMAL;
}
this._point = point;
this._isLeftEndpoint = isLeftEndpoint;
this._polygonLabel = polygonLabel;
this._otherEvent = otherEvent;
this._edgeType = edgeType;
this._positionInSweepLine = null;
this._isInsideOutsideTransition = false;
this._isInsideOtherPolygon = false;
}
/**
* Return the line segment associated to the SweepEvent
*/
var _proto = SweepEvent.prototype;
_proto.getSegment = function getSegment() {
return new Segment(this.point, this._otherEvent.point);
}
/**
* Check if the line segment (point, other->point) is below a given point
*/;
_proto.isSegmentBelowPoint = function isSegmentBelowPoint(testPoint) {
if (this._isLeftEndpoint) {
return calculateSignedArea(this.point, this._otherEvent.point, testPoint) > 0;
} else {
return calculateSignedArea(this._otherEvent.point, this.point, testPoint) > 0;
}
}
/**
* Check if the line segment (point, other->point) is above a given point
*/;
_proto.isSegmentAbovePoint = function isSegmentAbovePoint(testPoint) {
return !this.isSegmentBelowPoint(testPoint);
}
// Backward compatibility aliases
;
_proto.segment = function segment() {
return this.getSegment();
};
_proto.below = function below(testPoint) {
return this.isSegmentBelowPoint(testPoint);
};
_proto.above = function above(testPoint) {
return this.isSegmentAbovePoint(testPoint);
};
return _createClass(SweepEvent, [{
key: "point",
get: function get() {
return this._point;
},
set: function set(value) {
this._point = value;
}
}, {
key: "isLeftEndpoint",
get: function get() {
return this._isLeftEndpoint;
},
set: function set(value) {
this._isLeftEndpoint = value;
}
}, {
key: "polygonLabel",
get: function get() {
return this._polygonLabel;
},
set: function set(value) {
this._polygonLabel = value;
}
}, {
key: "otherEvent",
get: function get() {
return this._otherEvent;
},
set: function set(value) {
this._otherEvent = value;
}
}, {
key: "isInsideOutsideTransition",
get: function get() {
return this._isInsideOutsideTransition;
},
set: function set(value) {
this._isInsideOutsideTransition = value;
}
}, {
key: "edgeType",
get: function get() {
return this._edgeType;
},
set: function set(value) {
this._edgeType = value;
}
}, {
key: "isInsideOtherPolygon",
get: function get() {
return this._isInsideOtherPolygon;
},
set: function set(value) {
this._isInsideOtherPolygon = value;
}
}, {
key: "positionInSweepLine",
get: function get() {
return this._positionInSweepLine;
},
set: function set(value) {
this._positionInSweepLine = value;
}
}]);
}();
/**
* Comparator for SweepEvent ordering in the event queue
*/
var SweepEventComparator = /*#__PURE__*/function () {
function SweepEventComparator() {}
/**
* Compare two sweep events for priority queue ordering
* @param firstEvent First sweep event
* @param secondEvent Second sweep event
* @returns True if firstEvent has higher priority than secondEvent
*/
SweepEventComparator.compare = function compare(firstEvent, secondEvent) {
if (firstEvent.point.x > secondEvent.point.x) {
// Different x-coordinate
return true;
}
if (secondEvent.point.x > firstEvent.point.x) {
// Different x-coordinate
return false;
}
if (firstEvent.point.x !== secondEvent.point.x || firstEvent.point.y !== secondEvent.point.y) {
// Different points, but same x-coordinate
return firstEvent.point.y > secondEvent.point.y;
}
if (firstEvent.isLeftEndpoint !== secondEvent.isLeftEndpoint) {
// Same point, but one is left endpoint and other is right endpoint
return firstEvent.isLeftEndpoint;
}
// Same point, both events are left endpoints or both are right endpoints
return firstEvent.isSegmentAbovePoint(secondEvent.otherEvent.point);
};
return SweepEventComparator;
}();
/**
* Comparator for segment ordering in the sweep line status
*/
var SegmentComparator = /*#__PURE__*/function () {
function SegmentComparator() {}
/**
* Compare two segments (represented by their sweep events) for sweep line ordering
* @param firstEvent First segment's sweep event
* @param secondEvent Second segment's sweep event
* @returns True if firstEvent's segment should come before secondEvent's segment
*/
SegmentComparator.compare = function compare(firstEvent, secondEvent) {
if (firstEvent === secondEvent) return false;
if (calculateSignedArea(firstEvent.point, firstEvent.otherEvent.point, secondEvent.point) !== 0 || calculateSignedArea(firstEvent.point, firstEvent.otherEvent.point, secondEvent.otherEvent.point) !== 0) {
// Segments are not collinear
// If they share their left endpoint use the right endpoint to sort
if (firstEvent.point.x === secondEvent.point.x && firstEvent.point.y === secondEvent.point.y) {
return firstEvent.isSegmentBelowPoint(secondEvent.otherEvent.point);
}
// Different points
if (SweepEventComparator.compare(firstEvent, secondEvent)) {
// has the line segment associated to firstEvent been inserted into S after secondEvent?
return secondEvent.isSegmentAbovePoint(firstEvent.point);
}
// The line segment associated to secondEvent has been inserted into S after firstEvent
return firstEvent.isSegmentBelowPoint(secondEvent.point);
}
// Segments are collinear. Just a consistent criterion is used
if (firstEvent.point.x === secondEvent.point.x && firstEvent.point.y === secondEvent.point.y) {
// Use object comparison for consistent ordering
return firstEvent < secondEvent;
}
return SweepEventComparator.compare(firstEvent, secondEvent);
};
return SegmentComparator;
}();
/**
* Represents a chain of connected points forming part of a polygon contour
* Direct translation of PointChain class from connector.h and connector.cpp
*/
var PointChain = /*#__PURE__*/function () {
function PointChain() {
// Backward compatibility aliases
this.init = this.initializeWithSegment;
this.closed = this.isClosed;
this.begin = this.getPoints;
this.end = this.getPoints;
this.clear = this.clearChain;
this.size = this.getSize;
this.pointList = [];
this.isChainClosed = false;
this.prevInResult = null;
this.resultTransition = false;
}
/**
* Initialize the chain with a segment
*/
var _proto = PointChain.prototype;
_proto.initializeWithSegment = function initializeWithSegment(segment) {
this.pointList.push(segment.begin());
this.pointList.push(segment.end());
}
/**
* Try to link a segment to this chain
* @param segment Segment to link
* @returns True if the segment was successfully linked
*/;
_proto.linkSegment = function linkSegment(segment) {
var segmentBegin = segment.begin();
var segmentEnd = segment.end();
if (segmentBegin.x === this.pointList[0].x && segmentBegin.y === this.pointList[0].y) {
// segment.begin() == pointList.front()
if (segmentEnd.x === this.pointList[this.pointList.length - 1].x && segmentEnd.y === this.pointList[this.pointList.length - 1].y)
// segment.end() == pointList.back()
this.isChainClosed = true;else this.pointList.unshift(segmentEnd); // push_front
return true;
}
if (segmentEnd.x === this.pointList[this.pointList.length - 1].x && segmentEnd.y === this.pointList[this.pointList.length - 1].y) {
// segment.end() == pointList.back()
if (segmentBegin.x === this.pointList[0].x && segmentBegin.y === this.pointList[0].y)
// segment.begin() == pointList.front()
this.isChainClosed = true;else this.pointList.push(segmentBegin); // push_back
return true;
}
if (segmentEnd.x === this.pointList[0].x && segmentEnd.y === this.pointList[0].y) {
// segment.end() == pointList.front()
if (segmentBegin.x === this.pointList[this.pointList.length - 1].x && segmentBegin.y === this.pointList[this.pointList.length - 1].y)
// segment.begin() == pointList.back()
this.isChainClosed = true;else this.pointList.unshift(segmentBegin); // push_front
return true;
}
if (segmentBegin.x === this.pointList[this.pointList.length - 1].x && segmentBegin.y === this.pointList[this.pointList.length - 1].y) {
// segment.begin() == pointList.back()
if (segmentEnd.x === this.pointList[0].x && segmentEnd.y === this.pointList[0].y)
// segment.end() == pointList.front()
this.isChainClosed = true;else this.pointList.push(segmentEnd); // push_back
return true;
}
return false;
}
/**
* Try to link another point chain to this chain
* @param chain Point chain to link
* @returns True if the chain was successfully linked
*/;
_proto.linkPointChain = function linkPointChain(chain) {
if (chain.pointList[0].x === this.pointList[this.pointList.length - 1].x && chain.pointList[0].y === this.pointList[this.pointList.length - 1].y) {
var _this$pointList;
// chain.pointList.front() == pointList.back()
chain.pointList.shift(); // pop_front
(_this$pointList = this.pointList).splice.apply(_this$pointList, [this.pointList.length, 0].concat(chain.pointList)); // splice at end
return true;
}
if (chain.pointList[chain.pointList.length - 1].x === this.pointList[0].x && chain.pointList[chain.pointList.length - 1].y === this.pointList[0].y) {
var _this$pointList2;
// chain.pointList.back() == pointList.front()
this.pointList.shift(); // pop_front
(_this$pointList2 = this.pointList).splice.apply(_this$pointList2, [0, 0].concat(chain.pointList)); // splice at begin
return true;
}
if (chain.pointList[0].x === this.pointList[0].x && chain.pointList[0].y === this.pointList[0].y) {
var _this$pointList3;
// chain.pointList.front() == pointList.front()
this.pointList.shift(); // pop_front
chain.pointList.reverse(); // reverse
(_this$pointList3 = this.pointList).splice.apply(_this$pointList3, [0, 0].concat(chain.pointList)); // splice at begin
return true;
}
if (chain.pointList[chain.pointList.length - 1].x === this.pointList[this.pointList.length - 1].x && chain.pointList[chain.pointList.length - 1].y === this.pointList[this.pointList.length - 1].y) {
var _this$pointList4;
// chain.pointList.back() == pointList.back()
this.pointList.pop(); // pop_back
chain.pointList.reverse(); // reverse
(_this$pointList4 = this.pointList).splice.apply(_this$pointList4, [this.pointList.length, 0].concat(chain.pointList)); // splice at end
return true;
}
return false;
}
/**
* Check if the chain is closed
*/;
_proto.isClosed = function isClosed() {
return this.isChainClosed;
}
/**
* Get the points in the chain
*/;
_proto.getPoints = function getPoints() {
return this.pointList;
}
/**
* Clear the chain
*/;
_proto.clearChain = function clearChain() {
this.pointList = [];
}
/**
* Get the number of points in the chain
*/;
_proto.getSize = function getSize() {
return this.pointList.length;
}
/**
* Set spatial context information for hierarchy classification
*/;
_proto.setSpatialContext = function setSpatialContext(prevInResult, resultTransition) {
this.prevInResult = prevInResult;
this.resultTransition = resultTransition;
}
/**
* Get spatial context information
*/;
_proto.getSpatialContext = function getSpatialContext() {
return {
prevInResult: this.prevInResult,
resultTransition: this.resultTransition
};
};
return _createClass(PointChain, [{
key: "list",
get: function get() {
return this.pointList;
}
}, {
key: "_closed",
get: function get() {
return this.isChainClosed;
},
set: function set(value) {
this.isChainClosed = value;
}
}]);
}();
/**
* Connects segments to form complete polygon contours
* Direct translation of Connector class from connector.h and connector.cpp
*/
var Connector = /*#__PURE__*/function () {
function Connector() {
// Backward compatibility aliases
this.add = this.addSegment;
this.begin = this.getClosedChains;
this.end = this.getClosedChains;
this.clear = this.clearAll;
this.size = this.getClosedChainCount;
this.openPolygonChains = [];
this.closedPolygonChains = [];
this.prevInResult = null;
}
/**
* Add a segment to the connector, linking it to existing chains or creating a new one
* @param segment Segment to add
* @param resultTransition Optional transition information for hierarchy classification
*/
var _proto = Connector.prototype;
_proto.addSegment = function addSegment(segment, resultTransition) {
var chainIndex = 0;
while (chainIndex < this.openPolygonChains.length) {
if (this.openPolygonChains[chainIndex].linkSegment(segment)) {
if (this.openPolygonChains[chainIndex].isClosed()) {
// Move from openPolygonChains to closedPolygonChains (equivalent to splice operation)
var closedChain = this.openPolygonChains.splice(chainIndex, 1)[0];
// Set spatial context for hierarchy classification
if (resultTransition !== undefined) {
closedChain.setSpatialContext(this.prevInResult, resultTransition);
}
this.closedPolygonChains.push(closedChain);
// Update prevInResult to point to this newly closed contour
this.prevInResult = this.closedPolygonChains.length - 1;
} else {
// Try to link with other open polygons
var otherChainIndex = chainIndex + 1;
while (otherChainIndex < this.openPolygonChains.length) {
if (this.openPolygonChains[chainIndex].linkPointChain(this.openPolygonChains[otherChainIndex])) {
this.openPolygonChains.splice(otherChainIndex, 1); // erase
break;
}
otherChainIndex++;
}
}
return;
}
chainIndex++;
}
// The segment cannot be connected with any open polygon
var newChain = new PointChain();
newChain.initializeWithSegment(segment);
this.openPolygonChains.push(newChain);
}
/**
* Get the closed polygon chains
*/;
_proto.getClosedChains = function getClosedChains() {
return this.closedPolygonChains;
}
/**
* Clear all chains
*/;
_proto.clearAll = function clearAll() {
this.closedPolygonChains = [];
this.openPolygonChains = [];
}
/**
* Get the number of closed chains
*/;
_proto.getClosedChainCount = function getClosedChainCount() {
return this.closedPolygonChains.length;
}
/**
* Convert the closed chains to a polygon with hierarchy classification
* @param targetPolygon Polygon to populate with the chains
*/;
_proto.toPolygon = function toPolygon(targetPolygon) {
// First pass: create all contours
for (var _iterator = _createForOfIteratorHelperLoose(this.closedPolygonChains), _step; !(_step = _iterator()).done;) {
var _chain = _step.value;
var _contour = targetPolygon.pushbackContour();
for (var _iterator2 = _createForOfIteratorHelperLoose(_chain.getPoints()), _step2; !(_step2 = _iterator2()).done;) {
var point = _step2.value;
_contour.addPoint(point);
}
}
// Second pass: establish hierarchy relationships based on spatial context
for (var i = 0; i < this.closedPolygonChains.length; i++) {
var chain = this.closedPolygonChains[i];
var contour = targetPolygon.contour(i);
var spatialContext = chain.getSpatialContext();
if (spatialContext.prevInResult !== null) {
this.classifyContourHierarchy(i, spatialContext, targetPolygon);
}
}
}
/**
* Classify contour hierarchy based on spatial context
*/;
_proto.classifyContourHierarchy = function classifyContourHierarchy(contourIndex, spatialContext, targetPolygon) {
var prevInResult = spatialContext.prevInResult,
resultTransition = spatialContext.resultTransition;
if (prevInResult === null) {
// No spatial context - treat as exterior contour
return;
}
var currentContour = targetPolygon.contour(contourIndex);
var prevContour = targetPolygon.contour(prevInResult);
// Apply hierarchy rules based on transition direction and spatial context
if (resultTransition) {
// Positive transition = entering a contour (moving from outside to inside)
// This means we're starting inside another contour, so this is a hole
if (prevContour.isHole()) {
// If the lower contour is a hole, connect to the same parent
var parentIndex = prevContour.getHoleOf();
if (parentIndex !== null) {
currentContour.setHoleOf(parentIndex);
targetPolygon.contour(parentIndex).addHole(contourIndex);
currentContour.setDepth(prevContour.getDepth());
}
} else {
// The lower contour is exterior - this hole becomes its child
currentContour.setHoleOf(prevInResult);
prevContour.addHole(contourIndex);
currentContour.setDepth(prevContour.getDepth() + 1);
}
} else {
// Negative transition = exiting a contour (moving from inside to outside)
// This means we're starting outside, so this is an exterior contour
currentContour.setDepth(prevContour.getDepth());
}
};
return Connector;
}();
/**
* Direct TypeScript translation of the Martinez C++ implementation
* Based on martinez.h and martinez.cpp from the example folder
*/
/**
* Martinez Boolean Operations Algorithm
* Implements the Martinez-Rueda clipping algorithm for polygon boolean operations
*/
var Martinez = /*#__PURE__*/function () {
/**
* Constructor
* @param subjectPolygon First input polygon
* @param clippingPolygon Second input polygon
*/
function Martinez(subjectPolygon, clippingPolygon) {
this.eventQueue = new PriorityQueue(SweepEventComparator.compare);
this.eventStorage = [];
this.subjectPolygon = subjectPolygon;
this.clippingPolygon = clippingPolygon;
this.intersectionCount = 0;
}
/**
* Get the number of intersections found during computation (for statistics)
* @returns Number of intersections
*/
var _proto = Martinez.prototype;
_proto.getIntersectionCount = function getIntersectionCount() {
return this.intersectionCount;
}
/**
* Compute the boolean operation between the two input polygons
* @param operation The boolean operation type to perform
* @returns The resulting polygon from the boolean operation
*/;
_proto.computeBooleanOperation = function computeBooleanOperation(operation) {
var resultPolygon = new Polygon();
// Test 1 for trivial result case
if (this.subjectPolygon.contourCount() * this.clippingPolygon.contourCount() === 0) {
// At least one polygon is empty
if (operation === exports.BooleanOperationType.DIFFERENCE) {
resultPolygon.addContours(this.subjectPolygon.getContours());
}
if (operation === exports.BooleanOperationType.UNION) {
var source = this.subjectPolygon.contourCount() === 0 ? this.clippingPolygon : this.subjectPolygon;
resultPolygon.addContours(source.getContours());
}
return resultPolygon;
}
// Test 2 for trivial result case
var minsubj = {
x: 0,
y: 0
};
var maxsubj = {
x: 0,
y: 0
};
var minclip = {
x: 0,
y: 0
};
var maxclip = {
x: 0,
y: 0
};
this.subjectPolygon.boundingbox(minsubj, maxsubj);
this.clippingPolygon.boundingbox(minclip, maxclip);
if (minsubj.x > maxclip.x || minclip.x > maxsubj.x || minsubj.y > maxclip.y || minclip.y > maxsubj.y) {
// the bounding boxes do not overlap
if (operation === exports.BooleanOperationType.DIFFERENCE) {
resultPolygon.addContours(this.subjectPolygon.getContours());
}
if (operation === exports.BooleanOperationType.UNION) {
resultPolygon.addContours(this.subjectPolygon.getContours());
resultPolygon.addContours(this.clippingPolygon.getContours());
}
return resultPolygon;
}
// Boolean operation is not trivial
// Insert all the endpoints associated to the line segments into the event queue
for (var i = 0; i < this.subjectPolygon.contourCount(); i++) {
for (var j = 0; j < this.subjectPolygon.contour(i).nvertices(); j++) {
this.processSegment(this.subjectPolygon.contour(i).segment(j), exports.PolygonType.SUBJECT);
}
}
for (var _i = 0; _i < this.clippingPolygon.contourCount(); _i++) {
for (var _j = 0; _j < this.clippingPolygon.contour(_i).nvertices(); _j++) {
this.processSegment(this.clippingPolygon.contour(_i).segment(_j), exports.PolygonType.CLIPPING);
}
}
var connector = new Connector(); // to connect the edge solutions
var S = new OrderedSet(SegmentComparator.compare); // Status line
var MINMAXX = Math.min(maxsubj.x, maxclip.x); // for optimization 1
// No need to sort - PriorityQueue maintains order automatically
while (!this.eventQueue.isEmpty()) {
var e = this.eventQueue.pop(); // Get highest priority element from priority queue
// optimization 1
if (operation === exports.BooleanOperationType.INTERSECTION && e.point.x > MINMAXX || operation === exports.BooleanOperationType.DIFFERENCE && e.point.x > maxsubj.x) {
connector.toPolygon(resultPolygon);
return resultPolygon;
}
if (operation === exports.BooleanOperationType.UNION && e.point.x > MINMAXX) {
// add all the non-processed line segments to the result
if (!e.isLeftEndpoint) connector.add(e.segment(), e.otherEvent.isInsideOutsideTransition);
while (!this.eventQueue.isEmpty()) {
var e2 = this.eventQueue.pop(); // Use pop() to maintain priority order
if (!e2.isLeftEndpoint) connector.add(e2.segment(), e2.otherEvent.isInsideOutsideTransition);
}
connector.toPolygon(resultPolygon);
return resultPolygon;
}
// end of optimization 1
if (e.isLeftEndpoint) {
// the line segment must be inserted into S
var index = S.insert(e);
e.positionInSweepLine = index;
// Update positions of all events after the inserted one
for (var _i2 = index + 1; _i2 < S.size(); _i2++) {
var event = S.at(_i2);
if (event && event.positionInSweepLine !== null) {
event.positionInSweepLine = _i2;
}
}
// Find previous and next elements using the ordered set
var prev = index > 0 ? S.at(index - 1) : null;
var next = index < S.size() - 1 ? S.at(index + 1) : null;
// Compute the inside and inOut flags
if (prev === null || prev === undefined) {
// there is not a previous line segment in S?
e.isInsideOtherPolygon = e.isInsideOutsideTransition = false;
} else if (prev.edgeType !== exports.EdgeType.NORMAL) {
if (index <= 1) {
// e overlaps with prev or is at the beginning
e.isInsideOtherPolygon = true; // it is not relevant to set true or false
e.isInsideOutsideTransition = false;
} else {
// the previous two line segments in S are overlapping line segments
var prevPrev = S.at(index - 2);
if (prevPrev && prev.polygonLabel === e.polygonLabel) {
e.isInsideOutsideTransition = !prev.isInsideOutsideTransition;
e.isInsideOtherPolygon = !prevPrev.isInsideOutsideTransition;
} else if (prevPrev) {
e.isInsideOutsideTransition = !prevPrev.isInsideOutsideTransition;
e.isInsideOtherPolygon = !prev.isInsideOutsideTransition;
}
}
} else if (e.polygonLabel === prev.polygonLabel) {
// previous line segment in S belongs to the same polygon
e.isInsideOtherPolygon = prev.isInsideOtherPolygon;
e.isInsideOutsideTransition = !prev.isInsideOutsideTransition;
} else {
// previous line segment in S belongs to a different polygon
e.isInsideOtherPolygon = !prev.isInsideOutsideTransition;
e.isInsideOutsideTransition = prev.isInsideOtherPolygon;
}
// Process a possible intersection between "e" and its next neighbor in S
if (next !== null && next !== undefined) {
this.possibleIntersection(e, next);
}
// Process a possible intersection between "e" and its previous neighbor in S
if (prev !== null && prev !== undefined) {
this.possibleIntersection(prev, e);
}
} else {
// the line segment must be removed from S
var otherEvent = e.otherEvent;
var _index = otherEvent.positionInSweepLine; // Get the stored position
var _prev = _index > 0 ? S.at(_index - 1) : null;
var _next = _index < S.size() - 1 ? S.at(_index + 1) : null;
// Check if the line segment belongs to the Boolean operation
switch (e.edgeType) {
case exports.EdgeType.NORMAL:
switch (operation) {
case exports.BooleanOperationType.INTERSECTION:
if (e.otherEvent.isInsideOtherPolygon) connector.add(e.segment(), e.otherEvent.isInsideOutsideTransition);
break;
case exports.BooleanOperationType.UNION:
if (!e.otherEvent.isInsideOtherPolygon) connector.add(e.segment(), e.otherEvent.isInsideOutsideTransition);
break;
case exports.BooleanOperationType.DIFFERENCE:
if (e.polygonLabel === exports.PolygonType.SUBJECT && !e.otherEvent.isInsideOtherPolygon || e.polygonLabel === exports.PolygonType.CLIPPING && e.otherEvent.isInsideOtherPolygon) connector.add(e.segment(), e.otherEvent.isInsideOutsideTransition);
break;
case exports.BooleanOperationType.XOR:
connector.add(e.segment(), e.otherEvent.isInsideOutsideTransition);
break;
}
break;
case exports.EdgeType.SAME_TRANSITION:
if (operation === exports.BooleanOperationType.INTERSECTION || operation === exports.BooleanOperationType.UNION) connector.add(e.segment(), e.otherEvent.isInsideOutsideTransition);
break;
case exports.EdgeType.DIFFERENT_TRANSITION:
if (operation === exports.BooleanOperationType.DIFFERENCE) connector.add(e.segment(), e.otherEvent.isInsideOutsideTransition);
break;
}
// delete line segment associated to e from S and check for intersection between neighbors
S["delete"](otherEvent);
// Update positions of all events after the deleted one
for (var _i3 = _index; _i3 < S.size(); _i3++) {
var _event = S.at(_i3);
if (_event && _event.positionInSweepLine !== null) {
_event.positionInSweepLine = _i3;
}
}
if (_next !== null && _prev !== null && _next !== undefined && _prev !== undefined) {
this.possibleIntersection(_prev, _next);
}
}
}
connector.toPolygon(resultPolygon);
return resultPolygon;
}
// Private methods
;
_proto.processSegment = function processSegment(s, pl) {
var begin = s.begin();
var end = s.end();
if (begin.x === end.x && begin.y === end.y) {
// if the two edge endpoints are equal, discard
return;
}
var e1 = this.storeSweepEvent(new SweepEvent(begin, true, pl, null));
var e2 = this.storeSweepEvent(new SweepEvent(end, true, pl, e1));
e1.otherEvent = e2;
if (e1.point.x < e2.point.x) {
e2.isLeftEndpoint = false;
} else if (e1.point.x > e2.point.x) {
e1.isLeftEndpoint = false;
} else if (e1.point.y < e2.point.y) {
// the line segment is vertical. The bottom endpoint is the left endpoint
e2.isLeftEndpoint = false;
} else {
e1.isLeftEndpoint = false;
}
this.eventQueue.push(e1);
this.eventQueue.push(e2);
};
_proto.possibleIntersection = function possibleIntersection(e1, e2) {
var ip1 = {
x: 0,
y: 0
};
var ip2 = {
x: 0,
y: 0
};
var nintersections = findSegmentIntersection(e1.segment(), e2.segment(), ip1, ip2);
if (nintersections === 0) {
return;
}
if (nintersections === 1 && (e1.point.x === e2.point.x && e1.point.y === e2.point.y || e1.otherEvent.point.x === e2.otherEvent.point.x && e1.otherEvent.point.y === e2.otherEvent.point.y)) {
return; // the line segments intersect at an endpoint of both line segments
}
if (nintersections === 2 && e1.polygonLabel === e2.polygonLabel) {
return; // the line segments overlap, but they belong to the same polygon
}
// The line segments associated to e1 and e2 intersect
this.intersectionCount += nintersections;
if (nintersections === 1) {
if ((e1.point.x !== ip1.x || e1.point.y !== ip1.y) && (e1.otherEvent.point.x !== ip1.x || e1.otherEvent.point.y !== ip1.y)) {
// if ip1 is not an endpoint of e1
this.divideSegment(e1, ip1);
}
if ((e2.point.x !== ip1.x || e2.point.y !== ip1.y) && (e2.otherEvent.point.x !== ip1.x || e2.otherEvent.point.y !== ip1.y)) {
// if ip1 is not an endpoint of e2
this.divideSegment(e2, ip1);
}
return;
}
// The line segments overlap
var sortedEvents = [];
if (e1.point.x === e2.point.x && e1.point.y === e2.point.y) {
sortedEvents.push(null);
} else if (SweepEventComparator.compare(e1, e2)) {
sortedEvents.push(e2);
sortedEvents.push(e1);
} else {
sortedEvents.push(e1);
sortedEvents.push(e2);
}
if (e1.otherEvent.point.x === e2.otherEvent.point.x && e1.otherEvent.point.y === e2.otherEvent.point.y) {
sortedEvents.push(null);
} else if (SweepEventComparator.compare(e1.otherEvent, e2.otherEvent)) {
sortedEvents.push(e2.otherEvent);
sortedEvents.push(e1.otherEvent);
} else {
sortedEvents.push(e1.otherEvent);
sortedEvents.push(e2.otherEvent);
}
if (sortedEvents.length === 2) {
// are both line segments equal?
e1.edgeType = e1.otherEvent.edgeType = exports.EdgeType.NON_CONTRIBUTING;
e2.edgeType = e2.otherEvent.edgeType = e1.isInsideOutsideTransition === e2.isInsideOutsideTransition ? exports.EdgeType.SAME_TRANSITION : exports.EdgeType.DIFFERENT_TRANSITION;
return;
}
if (sortedEvents.length === 3) {
// the line segments share an endpoint
sortedEvents[1].edgeType = sortedEvents[1].otherEvent.edgeType = exports.EdgeType.NON_CONTRIBUTING;
if (sortedEvents[0])
// is the right endpoint the shared point?
sortedEvents[0].otherEvent.edgeType = e1.isInsideOutsideTransition === e2.isInsideOutsideTransition ? exports.EdgeType.SAME_TRANSITION : exports.EdgeType.DIFFERENT_TRANSITION;
// the shared point is the left endpoint
else sortedEvents[2].otherEvent.edgeType = e1.isInsideOutsideTransition === e2.isInsideOutsideTransition ? exports.EdgeType.SAME_TRANSITION : exports.EdgeType.DIFFERENT_TRANSITION;
this.divideSegment(sortedEvents[0] ? sortedEvents[0] : sortedEvents[2].otherEvent, sortedEvents[1].point);
return;
}
if (sortedEvents[0] !== sortedEvents[3].otherEvent) {
// no line segment includes totally the other one
sortedEvents[1].edgeType = exports.EdgeType.NON_CONTRIBUTING;
sortedEvents[2].edgeType = e1.isInsideOutsideTransition === e2.isInsideOutsideTransition ? exports.EdgeType.SAME_TRANSITION : exports.EdgeType.DIFFERENT_TRANSITION;
this.divideSegment(sortedEvents[0], sortedEvents[1].point);
this.divideSegment(sortedEvents[1], sortedEvents[2].point);
return;
}
// one line segment includes the other one
sortedEvents[1].edgeType = sortedEvents[1].otherEvent.edgeType = exports.EdgeType.NON_CONTRIBUTING;
this.divideSegment(sortedEvents[0], sortedEvents[1].point);
sortedEvents[3].otherEvent.edgeType = e1.isInsideOutsideTransition === e2.isInsideOutsideTransition ? exports.EdgeType.SAME_TRANSITION : exports.EdgeType.DIFFERENT_TRANSITION;
this.divideSegment(sortedEvents[3].otherEvent, sortedEvents[2].point);
};
_proto.divideSegment = function divideSegment(e, p) {
// "Right event" of the "left line segment" resulting from dividing e
var r = this.storeSweepEvent(new SweepEvent(p, false, e.polygonLabel, e, e.edgeType));
// "Left event" of the "right line segment" resulting from dividing e
var l = this.storeSweepEvent(new SweepEvent(p, true, e.polygonLabel, e.otherEvent, e.otherEvent.edgeType));
if (SweepEventComparator.compare(l, e.otherEvent)) {
// avoid a rounding error
console.log("Oops");
e.otherEvent.isLeftEndpoint = true;
l.isLeftEndpoint = false;
}
if (SweepEventComparator.compare(e, r)) {
// avoid a rounding error
console.log("Oops2");
}
e.otherEvent.otherEvent = l;
e.otherEvent = r;
this.eventQueue.push(l);
this.eventQueue.push(r);
};
_proto.storeSweepEvent = function storeSweepEvent(e) {
this.eventStorage.push(e);
return this.eventStorage[this.eventStorage.length - 1];
};
return Martinez;
}();
exports.Connector = Connector;
exports.Contour = Contour;
exports.Martinez = Martinez;
exports.OrderedSet = OrderedSet;
exports.PointChain = PointChain;
exports.Polygon = Polygon;
exports.PriorityQueue = PriorityQueue;
exports.Segment = Segment;
exports.SegmentComparator = SegmentComparator;
exports.SweepEvent = SweepEvent;
exports.SweepEventComparator = SweepEventComparator;
exports.calculateSignedArea = calculateSignedArea;
exports.findSegmentIntersection = findSegmentIntersection;
exports.isPointOnSegment = isPointOnSegment;
//# sourceMappingURL=martinez.cjs.development.js.map