UNPKG

@avdl/martinez

Version:

TypeScript library for polygon boolean operations

1,305 lines (1,288 loc) 49.2 kB
/** * 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 */ var BooleanOperationType; (function (BooleanOperationType) { BooleanOperationType[BooleanOperationType["INTERSECTION"] = 0] = "INTERSECTION"; BooleanOperationType[BooleanOperationType["UNION"] = 1] = "UNION"; BooleanOperationType[BooleanOperationType["DIFFERENCE"] = 2] = "DIFFERENCE"; BooleanOperationType[BooleanOperationType["XOR"] = 3] = "XOR"; })(BooleanOperationType || (BooleanOperationType = {})); /** * Edge types for sweep line algorithm */ var EdgeType; (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"; })(EdgeType || (EdgeType = {})); /** * Polygon types for boolean operations */ var PolygonType; (function (PolygonType) { PolygonType[PolygonType["SUBJECT"] = 0] = "SUBJECT"; PolygonType[PolygonType["CLIPPING"] = 1] = "CLIPPING"; })(PolygonType || (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 = 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 === BooleanOperationType.DIFFERENCE) { resultPolygon.addContours(this.subjectPolygon.getContours()); } if (operation === 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 === BooleanOperationType.DIFFERENCE) { resultPolygon.addContours(this.subjectPolygon.getContours()); } if (operation === 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), 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), 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 === BooleanOperationType.INTERSECTION && e.point.x > MINMAXX || operation === BooleanOperationType.DIFFERENCE && e.point.x > maxsubj.x) { connector.toPolygon(resultPolygon); return resultPolygon; } if (operation === 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 !== 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 EdgeType.NORMAL: switch (operation) { case BooleanOperationType.INTERSECTION: if (e.otherEvent.isInsideOtherPolygon) connector.add(e.segment(), e.otherEvent.isInsideOutsideTransition); break; case BooleanOperationType.UNION: if (!e.otherEvent.isInsideOtherPolygon) connector.add(e.segment(), e.otherEvent.isInsideOutsideTransition); break; case BooleanOperationType.DIFFERENCE: if (e.polygonLabel === PolygonType.SUBJECT && !e.otherEvent.isInsideOtherPolygon || e.polygonLabel === PolygonType.CLIPPING && e.otherEvent.isInsideOtherPolygon) connector.add(e.segment(), e.otherEvent.isInsideOutsideTransition); break; case BooleanOperationType.XOR: connector.add(e.segment(), e.otherEvent.isInsideOutsideTransition); break; } break; case EdgeType.SAME_TRANSITION: if (operation === BooleanOperationType.INTERSECTION || operation === BooleanOperationType.UNION) connector.add(e.segment(), e.otherEvent.isInsideOutsideTransition); break; case EdgeType.DIFFERENT_TRANSITION: if (operation === 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 = EdgeType.NON_CONTRIBUTING; e2.edgeType = e2.otherEvent.edgeType = e1.isInsideOutsideTransition === e2.isInsideOutsideTransition ? EdgeType.SAME_TRANSITION : EdgeType.DIFFERENT_TRANSITION; return; } if (sortedEvents.length === 3) { // the line segments share an endpoint sortedEvents[1].edgeType = sortedEvents[1].otherEvent.edgeType = EdgeType.NON_CONTRIBUTING; if (sortedEvents[0]) // is the right endpoint the shared point? sortedEvents[0].otherEvent.edgeType = e1.isInsideOutsideTransition === e2.isInsideOutsideTransition ? EdgeType.SAME_TRANSITION : EdgeType.DIFFERENT_TRANSITION; // the shared point is the left endpoint else sortedEvents[2].otherEvent.edgeType = e1.isInsideOutsideTransition === e2.isInsideOutsideTransition ? EdgeType.SAME_TRANSITION : 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 = EdgeType.NON_CONTRIBUTING; sortedEvents[2].edgeType = e1.isInsideOutsideTransition === e2.isInsideOutsideTransition ? EdgeType.SAME_TRANSITION : 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 = EdgeType.NON_CONTRIBUTING; this.divideSegment(sortedEvents[0], sortedEvents[1].point); sortedEvents[3].otherEvent.edgeType = e1.isInsideOutsideTransition === e2.isInsideOutsideTransition ? EdgeType.SAME_TRANSITION : 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; }(); export { BooleanOperationType, Connector, Contour, EdgeType, Martinez, OrderedSet, PointChain, Polygon, PolygonType, PriorityQueue, Segment, SegmentComparator, SweepEvent, SweepEventComparator, calculateSignedArea, findSegmentIntersection, isPointOnSegment }; //# sourceMappingURL=martinez.esm.js.map