UNPKG

region2d

Version:

A JavaScript implementation of the Region abstract data type, which GUIs use to perform constructive solid geometry with 2-D rectangles.

1,190 lines (1,034 loc) 41.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.RegionError = exports.Region2D = exports.Region1D = undefined; var _region1d = require("./region1d"); /** * Region2D objects are semi-opaque data structures that represent a 2-dimensional * set in the plane, described using axis-aligned rectangles of included points. * * ------------------------------------------------------------------------------------------------ * * Region2D objects are capable of performing most major set-theoretic operations, including: * * result = a.union(b); // Return a new set that is the logical union of the two sets. * result = a.intersect(b); // Return a new set that is the logical intersection of the two sets. * result = a.subtract(b); // Return the logical subtraction of the two sets, i.e., the * // equivalent of a.union(b.not()), but computed more efficiently. * result = a.xor(b); // Return the exclusive-or of the two sets, i.e., those ranges * // which exist in one set or the other but not both. * result = a.not(); // Return the logical complement of the set (which may include infinity). * result = a.isEmpty(); // Return true/false if the set is empty. * result = a.isFinite(); // Return true/false if the set is finite (doesn't stretch to infinity). * result = a.isInfinite(); // Return true/false if the set stretches to infinity in any direction. * result = a.isRectangular(); // Return true/false if the set can be described by a single rectangle. * result = a.isPointIn(x, y); // Return true if the given point is contained within the set. * result = a.doesIntersect(b); // Return true if the logical intersection of the two sets is nonempty. This is * // more efficient than performing "!a.intersect(b).isEmpty()". * result = a.equals(b); // Return true if the sets are identical. * result = a.getCount(); // Return the number of nonoverlapping rectangles that would describe this Region2D. * result = a.getRects(); // Return an array of nonoverlapping rectangles describing the Region2D. * result = a.getBounds(b); // Return a boundary rectangle containing all of the points of the Region2D. * * All Region2D operations are carefully written to be bounded in both time and * space, and all will run in no worse than O(n) or O(n+m) time. * * ------------------------------------------------------------------------------------------------ * * Under the hood, this is partially implemented using Region1D. Each Region2D consists of an * array of Region1D "rows" or "bands," which represent sets of rectangles with identical * minY/maxY coordinates. Each of the rows must be nonempty and must be unique (i.e., a successive * row's spans must not equal a previous row spans, if the maxY of the previous row equals the minY * of the successive row). * * Representing regions like this is how X Windows does it, and while this design may not always * result in the most optimized set of rectangles, the operations to work with these kinds of * regions are provably efficient: This design trades space for time. * * As a rather nice side-effect of the design, calls to getRects() will always result in a set * of rectangles that go from top-to-bottom, left-to-right on the screen, which can be beneficial * in some rendering scenarios. * * This implementation also has performance optimizations to avoid combining regions when the * operations are meaningless or would result in the empty set, and there are various kinds of * boundary checks to early-out operations wherever possible. */ var Region2D = function () { var infinite = void 0, empty = void 0; var //--------------------------------------------------------------------------------------------- // Global constants. // Precache positive/negative infinity locally. pInf = Number.POSITIVE_INFINITY, nInf = Number.NEGATIVE_INFINITY, regionError = _region1d.RegionError, //--------------------------------------------------------------------------------------------- // Helper functions. /** * Construct a wrapper around the given private data that makes it opaque except for * those with access to the 'expectedKey'. */ makeProtectedData = function makeProtectedData(protectedData, expectedKey) { return function (actualKey) { if (actualKey === expectedKey) return protectedData;else throw new regionError("Illegal access"); }; }, /** * Determine if the given object is an array. This is provided in newer JavaScript environs, * but is notably lacking in older ones. We avoid a dependency on a huge package like CoreJS * by just defining the shim here. */ isArray = function isArray(o) { return Array.isArray ? Array.isArray(o) : o instanceof Array; }, //--------------------------------------------------------------------------------------------- // Region core internals. /** * Make a 'generator' function that, upon each invocation, will return the next * pair of rows that need to be combined, as the form { row1:, row2:, minY:, maxY: }, * where row1 and row2 are the original Region1D objects, and minY and maxY should * be the Y coordinates of the resulting combined row. This is actually a lot simpler * than it looks, but many separate cases need to be handled. * * On each separate invocation, the generator will return a new pair object until it * runs out of source rows, and then it will return null. */ makeRowPairGenerator = function makeRowPairGenerator(rows1, rows2) { var rowIndex1 = 0; var rowIndex2 = 0; var lastY = nInf; var empty = _region1d.Region1D.empty; return function () { //------------------------------------------------------------------------------------- // Step 1. First, see if we've run out of data in either set. if (rowIndex1 >= rows1.length) { // No more left in rows1, so just take whatever's left of rows2. if (rowIndex2 >= rows2.length) return null;else { var _result = { row1: empty, row2: rows2[rowIndex2].region, minY: Math.max(rows2[rowIndex2].minY, lastY), maxY: lastY = rows2[rowIndex2].maxY }; rowIndex2++; return _result; } } else if (rowIndex2 >= rows2.length) { // No more left in rows2, so just take whatever's left of rows1. var _result2 = { row1: rows1[rowIndex1].region, row2: empty, minY: Math.max(rows1[rowIndex1].minY, lastY), maxY: lastY = rows1[rowIndex1].maxY }; rowIndex1++; return _result2; } else {} // We have remaining rows in both rows1 and rows2, so now we need // to do the general case. //------------------------------------------------------------------------------------- // Step 2. Extract out the next row pair. This is a somewhat-straightforward // decision-tree approach, and is very fast, but since there are many possible // cases, there are a lot of conditionals below to test for all of them. var row1 = rows1[rowIndex1]; var row2 = rows2[rowIndex2]; var nextY1 = Math.max(row1.minY, lastY); var nextY2 = Math.max(row2.minY, lastY); var da = void 0, db = void 0, minY = void 0, maxY = void 0; if (nextY1 === nextY2) { // The A-side and B-side rows having a matching top edge. minY = nextY1; // These match the first half of the conditionals described below. da = row1.region, db = row2.region; if (row2.maxY < row1.maxY) { lastY = maxY = row2.maxY; rowIndex2++; } else if (row2.maxY === row1.maxY) { lastY = maxY = row1.maxY; rowIndex1++, rowIndex2++; } else { lastY = maxY = row1.maxY; rowIndex1++; } } else if (nextY1 < nextY2) { // The A-side row is strictly above the B-side row. minY = nextY1; // These match the second half of the conditionals described below. da = row1.region, db = empty; if (nextY2 >= row1.maxY) { lastY = maxY = row1.maxY; rowIndex1++; } else { lastY = maxY = nextY2; } } else { // The B-side row is strictly above the A-side row. minY = nextY2; // These match the second half of the conditionals described below, inverted. da = empty, db = row2.region; if (nextY1 >= row2.maxY) { lastY = maxY = row2.maxY; rowIndex2++; } else { lastY = maxY = nextY1; } } //------------------------------------------------------------------------------------- // Step 3. Emit the result for this row pair. var result = { row1: da, row2: db, minY: minY, maxY: maxY }; return result; /* //------------------------------------------------------------------------------------- // Step 2, in detail. Both sides follow the same basic algorithm, as // explained below: // // Find the maxY, and iterate whichever side is the next one that requires // iteration (possibly both). if (ay === by) { // Top edges are equal, so we're consuming part or all of both rows. // // Case 1. +-------+ +-------+ <--- top equal // | a | | b | // Three possibilities: rb.maxY is above, equal to, or below ra.maxY. if (rb.maxY < ra.maxY) { // Case 1a. +-------+ +-------+ <--- top equal // | a | | b | // | | +-------+ <--- bottom above // +-------+ // Consume all of rb, but only the top part of ra. lastY = maxY = rb.maxY; da = ra.region; db = rb.region; ib++; } else if (rb.maxY === ra.maxY) { // Case 1b. +-------+ +-------+ <--- top equal // | a | | b | // +-------+ +-------+ <--- bottom equal // Consume both ra and rb. lastY = maxY = ra.maxY; da = ra.region; db = rb.region; ia++; ib++; } else { // Case 1c. +-------+ +-------+ <--- top equal // | a | | b | // +-------+ | | // +-------+ <--- bottom below // Consume all of ra, but only the top part of rb. lastY = maxY = ra.maxY; da = ra.region; db = rb.region; ia++; } } else if (by >= ra.maxY) { // Degenerate case: by is past ra.maxY, so there // is no overlap at all. // // Case 2. +-------+ // | a | // +-------+ // +-------+ <--- top entirely below a // | b | // +-------+ // Consume all of ra, and none of rb. lastY = maxY = ra.maxY; da = ra.region; db = empty; ia++; } else { // Top edge of rb is below the top edge of ra, but there's definitely // overlap. So we now need to decide how much overlap. // // Case 3. +-------+ // | a | +-------+ <--- top below // | | | b | // // Consume the next part of ra through by, but none of rb. lastY = maxY = by; da = ra.region; db = empty; } */ }; }, /** * Combine two regions together, returning a new region that is the result of having * combined them, using the provided rowTransform to mutate their individual rows. * * This spins over the rows of the regions in parallel, "peeling off" each successive * unique pair of rows with identical Y coordinates, and then invokes the transform * to perform the actual combination. The transformed row is then added to the pile * of output rows, with a few interesting caveats to maintain the region invariants: * * - We don't add a row that is identical to the previous row; we expand the previous row. * - We don't add empty rows at all. * - We do track the boundary min/max X coordinates as we go. * - We compute the overall region hash as we go. * - We only compute the boundary min/max Y coordinates after all rows are added. * * Neither input region may be empty. * * The result is always a valid region if the two input regions are valid regions. */ combineData = function combineData(array1, array2, rowTransform) { // Make the generator that spits out pairs of rows to combine. var pairGenerator = makeRowPairGenerator(array1, array2); // Spin over all the pairs of input rows, and combine them together to produce // the output region. var lastResult = null; var result = []; var minX = pInf, maxX = nInf; var hash = 0; var count = 0; for (var pair; pair = pairGenerator();) { // Perform the 1-dimensional version of the transform. var resultRow = rowTransform(pair.row1, pair.row2); // If the result is empty, we don't add it. if (resultRow.isEmpty()) continue; // If the result is the same as the previous row's result, and they're immediately // adjacent, then just expand the previous row: Don't add a new one. if (lastResult && resultRow.equals(lastResult.region) && lastResult.maxY == pair.minY) { lastResult.maxY = pair.maxY; continue; } // New result row, and it's valid content, so add it to the result. result.push(lastResult = { region: resultRow, minY: pair.minY, maxY: pair.maxY }); // Update the rectangle count. count += resultRow.getCount(); // Update the minima and maxima for this 2-D region based on the new row. var rowBounds = resultRow.getBounds(); if (rowBounds.min < minX) minX = rowBounds.min; if (rowBounds.max > maxX) maxX = rowBounds.max; // Update the hash (checksum) for the 2-D region based on the 1-D row hash. hash *= 23; hash += resultRow.getHashCode() | 0; hash &= ~0; } // Finally, generate the 2-D region data itself. var newRegionData = { array: result, count: count, minX: minX, minY: result.length ? result[0].minY : pInf, maxX: maxX, maxY: result.length ? result[result.length - 1].maxY : nInf, hash: hash }; return newRegionData; }, /** * Calculate the union of the given arrays of 2-D region data. * Returns a new array that contains the 2-D union. */ unionData = function unionData(array1, array2) { return combineData(array1, array2, function (r1, r2) { return r1.union(r2); }); }, /** * Calculate the intersection of the given arrays of 2-D region data. * Returns a new array that contains the 2-D intersection. */ intersectData = function intersectData(array1, array2) { return combineData(array1, array2, function (r1, r2) { return r1.intersect(r2); }); }, /** * Calculate the exclusive-or of the given arrays of 2-D region data. * Returns a new array that contains the 2-D exclusive-or. */ xorData = function xorData(array1, array2) { return combineData(array1, array2, function (r1, r2) { return r1.xor(r2); }); }, /** * Calculate the difference of the given arrays of 2-D region data. * Returns a new array that contains the 2-D difference. */ subtractData = function subtractData(array1, array2) { return combineData(array1, array2, function (r1, r2) { return r1.subtract(r2); }); }, //--------------------------------------------------------------------------------------------- // Support for generation of paths/windings. /** * Extract the edges of this region. The edges are fairly-easily extracted from the row data: * All vertical lines in each row are valid edges, and horizontal lines are valid wherever * the XOR with the adjacent row is nonempty. */ generateEdges = function generateEdges(array) { var edges = []; if (array.length === 1) { // Degenerate case: Only one row. var spans = array[0].region.getRawSpans(); var y1 = array[0].minY; var y2 = array[0].maxY; for (var i = 0; i < spans.length; i += 2) { edges.push({ x1: spans[i], y1: y1, x2: spans[i + 1], y2: y1, kind: "top", key1: null, key2: null, next: null, prev: null, used: false }); edges.push({ x1: spans[i + 1], y1: y1, x2: spans[i + 1], y2: y2, kind: "right", key1: null, key2: null, next: null, prev: null, used: false }); edges.push({ x1: spans[i + 1], y1: y2, x2: spans[i], y2: y2, kind: "bottom", key1: null, key2: null, next: null, prev: null, used: false }); edges.push({ x1: spans[i], y1: y2, x2: spans[i], y2: y1, kind: "left", key1: null, key2: null, next: null, prev: null, used: false }); } return edges; } else { // Main case: N rows, N > 1 // First, emit the top edge(s) and verticals. var _spans = array[0].region.getRawSpans(); var _y = array[0].minY; var _y2 = array[0].maxY; for (var _i = 0; _i < _spans.length; _i += 2) { edges.push({ x1: _spans[_i], y1: _y, x2: _spans[_i + 1], y2: _y, kind: "top", key1: null, key2: null, next: null, prev: null, used: false }); edges.push({ x1: _spans[_i + 1], y1: _y, x2: _spans[_i + 1], y2: _y2, kind: "right", key1: null, key2: null, next: null, prev: null, used: false }); edges.push({ x1: _spans[_i], y1: _y2, x2: _spans[_i], y2: _y, kind: "left", key1: null, key2: null, next: null, prev: null, used: false }); } // Now handle the interior rows. for (var rowIndex = 1, numRows = array.length; rowIndex < numRows; rowIndex++) { _y = array[rowIndex].minY; _y2 = array[rowIndex].maxY; if (_y > array[rowIndex - 1].maxY) { // Emit bottom edges for the previous row verbatim, since it doesn't touch this row. for (var _i2 = 0; _i2 < _spans.length; _i2 += 2) { edges.push({ x1: _spans[_i2 + 1], y1: array[rowIndex - 1].maxY, x2: _spans[_i2], y2: array[rowIndex - 1].maxY, kind: "bottom", key1: null, key2: null, next: null, prev: null, used: false }); } // Emit top edges for this row verbatim, since it doesn't touch the previous row. _spans = array[rowIndex].region.getRawSpans(); for (var _i3 = 0; _i3 < _spans.length; _i3 += 2) { edges.push({ x1: _spans[_i3], y1: _y, x2: _spans[_i3 + 1], y2: _y, kind: "top", key1: null, key2: null, next: null, prev: null, used: false }); } } else { // Emit bottom edges for the previous row by subtracting away this row. var interiorEdges = array[rowIndex - 1].region.subtract(array[rowIndex].region); _spans = interiorEdges.getRawSpans(); for (var _i4 = 0; _i4 < _spans.length; _i4 += 2) { edges.push({ x1: _spans[_i4 + 1], y1: _y, x2: _spans[_i4], y2: _y, kind: "bottom", key1: null, key2: null, next: null, prev: null, used: false }); } // Emit top edges for this row by subtracting away the previous row. interiorEdges = array[rowIndex].region.subtract(array[rowIndex - 1].region); _spans = interiorEdges.getRawSpans(); for (var _i5 = 0; _i5 < _spans.length; _i5 += 2) { edges.push({ x1: _spans[_i5], y1: _y, x2: _spans[_i5 + 1], y2: _y, kind: "top", key1: null, key2: null, next: null, prev: null, used: false }); } } // Emit verticals everywhere on this row. _spans = array[rowIndex].region.getRawSpans(); for (var _i6 = 0; _i6 < _spans.length; _i6 += 2) { edges.push({ x1: _spans[_i6 + 1], y1: _y, x2: _spans[_i6 + 1], y2: _y2, kind: "right", key1: null, key2: null, next: null, prev: null, used: false }); edges.push({ x1: _spans[_i6], y1: _y2, x2: _spans[_i6], y2: _y, kind: "left", key1: null, key2: null, next: null, prev: null, used: false }); } } // Finally, emit the bottom edge(s) for the last row. for (var _i7 = 0; _i7 < _spans.length; _i7 += 2) { edges.push({ x1: _spans[_i7 + 1], y1: _y2, x2: _spans[_i7], y2: _y2, kind: "bottom", key1: null, key2: null, next: null, prev: null, used: false }); } } return edges; }, /** * Make a lookup table that finds edges quickly (O(1)) by either endpoint, and set up the * edges as a linked list so it's easy to quickly (O(1)) find any un-consumed edge. */ makeEdgeTable = function makeEdgeTable(edges) { var table = {}; for (var i = 0, l = edges.length; i < l; i++) { var edge = edges[i]; edge.key1 = edge.x1 + "," + edge.y1; edge.key2 = edge.x2 + "," + edge.y2; edge.prev = i > 0 ? edges[i - 1] : null; edge.next = i < l - 1 ? edges[i + 1] : null; // We only add the 'start' endpoint to the lookup table, because that's // the only point we want to follow to. if (!(edge.key1 in table)) table[edge.key1] = [edge];else table[edge.key1].push(edge); } return table; }, /** * Make the windings, clockwise polygons that are formed from adjacent edges. */ makeWindings = function makeWindings(edges, table) { // Algorithm: // // Starting with a top edge, follow its endpoints clockwise until we reach that same // start edge. Wherever duplicate points are found, prefer following top->right, // right->bottom, bottom->left, and left->top. Remove each edge from the source set // as we follow it. When we reach the start edge, if there are edges left, repeat the // same whole algorithm until no edges are left. var allWindings = []; // This will be the linked-list of all unconsumed edges. var firstEdge = edges[0], lastEdge = edges[edges.length - 1]; // Consume an edge: Remove it from the list, and mark it as 'used'. var consumeEdge = function consumeEdge(edge) { if (edge.next) edge.next.prev = edge.prev;else lastEdge = edge.prev; if (edge.prev) edge.prev.next = edge.next;else firstEdge = edge.next; edge.used = true; }; // Find the next edge to follow given a set of possible matches. var findBestPossibleEdge = function findBestPossibleEdge(edge, possibleEdges) { // Easy degenerate case: If there's only one edge, take it. if (possibleEdges.length === 1 && !possibleEdges.used) return possibleEdges[0]; // First, prefer following top->right, right->bottom, bottom->left, and left->top, // if there's a matching edge. for (var i = 0, l = possibleEdges.length; i < l; i++) { if (possibleEdges[i].used) continue; switch (edge.kind) { case 'top': if (possibleEdges[i].kind === 'right') return possibleEdges[i]; break; case 'right': if (possibleEdges[i].kind === 'bottom') return possibleEdges[i]; break; case 'bottom': if (possibleEdges[i].kind === 'left') return possibleEdges[i]; break; case 'left': if (possibleEdges[i].kind === 'top') return possibleEdges[i]; break; } } // We can't follow our preferred direction, so just take whatever's available. for (var _i8 = 0, _l = possibleEdges.length; _i8 < _l; _i8++) { if (possibleEdges[_i8].used) continue; return possibleEdges[_i8]; } // Shouldn't get here. throw new regionError("Edge generation failure."); }; // Main loop: We do this until we run out of edges. Each iteration of the loop // will generate one whole polygon. This whole thing is fairly complex-looking, // but it will run in O(n) time. while (firstEdge) { var winding = []; // First, find any top edge. This *could* be up to O(n) in a pathological case, but // average time is O(1) because of how we generated the edges in the first place. var startEdge = firstEdge; while (startEdge.kind !== 'top') { startEdge = startEdge.next; } // Consume and emit the start edge. consumeEdge(startEdge); winding.push({ x: startEdge.x1, y: startEdge.y1 }); // Now walk forward from the current edge, following its end point to successive // start points until we reach the startEdge's start point. var currentEdge = startEdge; var prevX = startEdge.x1, prevPrevX = null; while (currentEdge.key2 !== startEdge.key1) { // First, find the set of possible edges to follow, which should be nonempty. var possibleEdges = table[currentEdge.key2]; // Move to the edge that is the best one to follow. currentEdge = findBestPossibleEdge(currentEdge, possibleEdges); // Consume it, now that we found it. consumeEdge(currentEdge); // Emit the next point in the winding. if (currentEdge.x1 === prevX && prevX === prevPrevX) { // This vertical edge was preceded by another vertical edge, so this edge piece extends it. winding[winding.length - 1].y = currentEdge.y1; } else { winding.push({ x: currentEdge.x1, y: currentEdge.y1 }); } // Record where we've been so we know if we have to extend this edge. prevPrevX = prevX; prevX = currentEdge.x1; } // If the last edge was vertical, and it generated an extra point between its // start and the winding's first point, remove its extra point. if (winding[0].x === prevX && prevX === prevPrevX) { winding.pop(); } // Finished a whole polygon. allWindings.push(winding); } return allWindings; }, /** * Calculate a minimal set of nonoverlapping nonadjoining clockwise polygons that describe this region. * The result will be an array of arrays of points, like this: * [ * [{x:1, y:2}, {x:3, y:2}, {x:3, y:6}, {x:1, y:6}], // Polygon 1 * [{x:7, y:5}, {x:8, y:5}, {x:8, y:8}, {x:10, y:8}, {x:10, y:9}, {x:7, y:9}] // Polygon 2 * ] */ makePath = function makePath(array) { if (!array.length) return []; var edges = generateEdges(array); var table = makeEdgeTable(edges); var windings = makeWindings(edges, table); return windings; }, //--------------------------------------------------------------------------------------------- // Region miscellaneous support. /** * Calculate a new region whose coordinates have all been translated/scaled by the given amounts. */ transformData = function transformData(array, ratioX, ratioY, deltaX, deltaY) { deltaX = Number(deltaX); deltaY = Number(deltaY); if (!(nInf < deltaX && deltaX < pInf) || !(nInf < deltaY && deltaY < pInf)) // Catches other NaNs as well as infinities. throw new regionError("Invalid translation delta"); ratioX = Number(ratioX); ratioY = Number(ratioY); if (!(nInf < ratioX && ratioX < pInf) || ratioX === 0 || !(nInf < ratioY && ratioY < pInf) || ratioY === 0) // Catches other NaNs as well as infinities. throw new regionError("Invalid scale ratio"); var newArray = []; for (var i = 0, l = array.length; i < l; i++) { var row = array[i]; newArray[i] = { region: row.region.transform(ratioX, deltaX), minY: row.minY * ratioY + deltaY, maxY: row.maxY * ratioY + deltaY }; } return newArray; }, /** * Determine if the bounding rectangles of each region actually overlap. If they * don't overlap, we can often treat region operations as special degenerate cases. * This runs in O(1) time. */ doBoundsOverlap = function doBoundsOverlap(data1, data2) { return !(data1.minX > data2.maxX || data1.maxX < data2.minX || data1.minY > data2.maxY || data1.maxY < data2.minY); }, cannotConstructMessage = "Cannot construct a Region2D from ", invalidRectangleDataMessage = cannotConstructMessage + "invalid rectangle data.", invalidRectangleSizeMessage = cannotConstructMessage + "a rectangle of zero or negative size.", /** * Make region data from a single rectangle, in one of the four major rectangle forms: * - An object with { x:, y:, width:, height: } properties. * - An object with { left:, top:, right:, bottom: } properties. * - An array with [x, y, width, height] values. * - A DOM element's bounding box. * * This is fairly straightforward, and runs in O(1) time. */ makeRegionDataFromOneRect = function makeRegionDataFromOneRect(rect) { // Calculate the actual rectangle coordinates from whatever object was passed in. var minX = void 0, maxX = void 0, minY = void 0, maxY = void 0; if (typeof HTMLElement !== 'undefined' && rect instanceof HTMLElement) { var clientRect = rect.getBoundingClientRect(); minX = window.scrollX + clientRect.left, minY = window.scrollY + clientRect.top; maxX = window.scrollX + clientRect.right, maxY = window.scrollY + clientRect.bottom; } else if (isArray(rect)) { if (rect.length !== 4) { throw new regionError(invalidRectangleDataMessage); } minX = Number(rect[0]), minY = Number(rect[1]); maxX = Number(rect[2]), maxY = Number(rect[3]); } else if ("left" in rect) { minX = Number(rect.left), minY = Number(rect.top); maxX = Number(rect.right), maxY = Number(rect.bottom); } else if ("x" in rect) { minX = Number(rect.x), minY = Number(rect.y); maxX = minX + Number(rect.width), maxY = minY + Number(rect.height); } else { throw new regionError(invalidRectangleDataMessage); } // Validate the rectangle data. if (maxX <= minX || maxY <= minY) { throw new regionError(invalidRectangleSizeMessage); } // Construct the new row containing that rectangle. var region1D = new _region1d.Region1D([minX, maxX]); // Now make the actual region data for this single-rect region. var data = { array: [{ region: region1D, minY: minY, maxY: maxY }], count: 1, minX: minX, minY: minY, maxX: maxX, maxY: maxY, hash: region1D.getHashCode() }; return data; }, /** * Construct an empty region consisting of no rectangles at all. */ makeEmptyRegionData = function makeEmptyRegionData() { return { array: [], count: 0, minX: pInf, minY: pInf, maxX: nInf, maxY: nInf, hash: 0 }; }, rowDataErrorMessage = "Invalid row data for row ", /** * Construct a region from raw band data. This simply checks the band data for correctness, * and then fills in the appropriate metadata. This runs in O(n) time with respect to the * number of bands. */ makeDataFromRows = function makeDataFromRows(bands) { // These will collect all the statistical metadata about the region. var prevMax = nInf; var count = 0; var minX = pInf, maxX = nInf; var hash = 0; // Clone the band data, validate it, and collect the metadata. This is O(n) with respect // to the number of bands; the number of rectangles per band is irrelevant. var array = []; for (var i = 0, l = bands.length; i < l; i++) { // Check the band. var band = bands[i]; if (band.minY < prevMax || band.minY >= band.maxY || !(band.region instanceof _region1d.Region1D)) { throw new _region1d.RegionError(rowDataErrorMessage + i); } // Push a cloned copy of its data. array.push({ region: band.region, minY: band.minY, maxY: band.maxY }); // Collect statistics about the band. var rowCount = band.region.getCount(); if (!rowCount) { throw new _region1d.RegionError(rowDataErrorMessage + i); } count += rowCount; // Update the region's X boundaries. var bounds = band.region.getBounds(); if (bounds.min < minX) minX = bounds.min; if (bounds.max > maxX) maxX = bounds.max; // Update the region's hash code (for fast inequality tests). hash *= 23; hash += band.region.getHashCode() | 0; hash &= ~0; prevMax = band.maxY; } // Create the region data from the resulting rows and the metadata. return { array: array, count: count, minX: minX, minY: array.length ? array[0].minY : pInf, maxX: maxX, maxY: array.length ? array[array.length - 1].maxY : nInf, hash: hash }; }, /** * Create a simple rectangle from the given region's internal bounding rect. */ getBoundsFromData = function getBoundsFromData(data) { return { x: data.minX, y: data.minY, width: data.maxX - data.minX, height: data.maxY - data.minY, left: data.minX, top: data.minY, right: data.maxX, bottom: data.maxY }; }, /** * Get a copy of the raw row data. */ _getRawRows = function _getRawRows(srcArray) { var destArray = []; for (var i = 0, l = srcArray.length; i < l; i++) { var src = srcArray[i]; destArray.push({ minY: src.minY, maxY: src.maxY, region: src.region }); } return destArray; }, /** * Get all of the rectangle data for this entire region. */ makeRects = function makeRects(array) { var result = []; for (var i = 0, l = array.length; i < l; i++) { var row = array[i]; row.region.getAsRects(row.minY, row.maxY, result); } return result; }, /** * Determine whether this region stretches to infinity in any direction. */ _isInfinite = function _isInfinite(data) { return data.minX === nInf || data.minY === nInf || data.maxX === pInf || data.maxY === pInf; }, /** * Compare the Region1D data found in each array instance to each other for equality. */ arrayEquals = function arrayEquals(array1, array2) { if (array1.length != array2.length) return false; for (var i = 0, l = array1.length; i < l; i++) { if (array1[i].minY !== array2[i].minY || array1[i].maxY !== array2[i].maxY) return false; if (!array1[i].region.equals(array2[i].region)) return false; } return true; }, /** * Determine if the data of region1 intersects the data of region2, and do so more efficiently * than simply performing "!a.intersect(b).isEmpty()". */ doesIntersectData = function doesIntersectData(data1, data2) { // TODO: Implement this better than the quick-and-dirty solution below. Ideally, // this should just test the data and early-out on the first hit, rather than // actually *doing* all the work and then discarding the result. return !!intersectData(data1.array, data2.array).array.length; }, /** * Determine if the given point lies within the given region data. This first performs * some easy boundary checks, then efficiently finds the matching row (if any), and then * invokes Region1D.isPointIn() to efficiently answer the question for real. This runs in * O(lg n) time, where 'n' is the number of rectangles in the region. */ isPointInData = function isPointInData(data, x, y) { var array = data.array; // It can't be in the empty set. if (!array.length) return false; // If it's outside the bounds, it's definitely not in. if (y < data.minY || y > data.maxY || x < data.minX || x > data.maxX) return false; if (array.length <= 5) { // Spin over all the rows in a simple linear search. for (var i = 0, l = array.length; i < l; i++) { if (y >= array[i].minY && y < array[i].maxY) { // Found the row. return array[i].region.isPointIn(x); } } return false; } else { // Binary search to find the row that y is within. var start = 0, end = array.length; while (start < end) { var midpt = (start + end) / 2 & ~0; var row = array[midpt]; if (y >= row.minY && y < row.maxY) { // Found the row, so see if 'x' lies within its spans. return row.region.isPointIn(x); } else if (y < row.minY) { end = midpt; } else { start = midpt + 1; } } return false; } }, /** * Check to ensure that the given object is actually a Region2D, and abort if it is not. */ verifyRegion2DType = function verifyRegion2DType(obj) { if (!(obj instanceof Region2D)) { throw new regionError("Object must be a Region2D instance."); } }, //--------------------------------------------------------------------------------------------- // Public construction interface. /** * A special private object used to flag internal constructions in such a way that * external callers' data must be validated, but internal data can skip those checks. */ privateKey = {}, /** * Access the internal data, if this is an allowed thing to do. */ getData = function getData(region) { return region._opaque(privateKey); }; /** * Construct a 2-D region either from either nothing or from the given rectangle. * * Usage: * var empty = new Region2D(); * var rectRegion = new Region2D(rect); * * The rectangle may be expressed as any of the following three forms: * - An object with { x:, y:, width:, height: } properties. * - An object with { left:, top:, right:, bottom: } properties. * - An array with [x, y, width, height] values. * * Alternative internal invocation: * var region = new Region2D(regionData, privateKey); */ function Region2D(rect, key) { var data = key === privateKey ? rect : typeof rect !== 'undefined' ? makeRegionDataFromOneRect(rect) : makeEmptyRegionData(); this._opaque = makeProtectedData(data, privateKey); }; /** * The region's prototype contains helpers that simply invoke the private operations * to do all the hard work. */ Region2D.prototype = { union: function union(other) { verifyRegion2DType(other); var data = getData(this), otherData = getData(other); return new Region2D(unionData(data.array, otherData.array), privateKey); }, intersect: function intersect(other) { verifyRegion2DType(other); var data = getData(this), otherData = getData(other); if (!doBoundsOverlap(data, otherData)) return empty; return new Region2D(intersectData(data.array, otherData.array), privateKey); }, subtract: function subtract(other) { verifyRegion2DType(other); var data = getData(this), otherData = getData(other); if (!doBoundsOverlap(data, otherData)) return this; return new Region2D(subtractData(data.array, otherData.array), privateKey); }, xor: function xor(other) { verifyRegion2DType(other); var data = getData(this), otherData = getData(other); return new Region2D(xorData(data.array, otherData.array), privateKey); }, not: function not() { // Lazy implementation of 'not': Simply 'xor' with an infinite region. // A better implementation would take advantage of the efficient Region1D#not() method. var data = getData(this), otherData = getData(infinite); return new Region2D(xorData(data.array, otherData.array), privateKey); }, transform: function transform(scaleX, scaleY, offsetX, offsetY) { var data = getData(this); return new Region2D(makeDataFromRows(transformData(data.array, scaleX, scaleY, offsetX, offsetY)), privateKey); }, translate: function translate(offsetX, offsetY) { var data = getData(this); return new Region2D(makeDataFromRows(transformData(data.array, 1.0, 1.0, offsetX, offsetY)), privateKey); }, scale: function scale(scaleX, scaleY) { var data = getData(this); return new Region2D(makeDataFromRows(transformData(data.array, scaleX, scaleY, 0, 0)), privateKey); }, isEmpty: function isEmpty() { return !getData(this).array.length; }, isInfinite: function isInfinite() { return _isInfinite(getData(this)); }, isFinite: function isFinite() { return !_isInfinite(getData(this)); }, isRectangular: function isRectangular() { return getData(this).count === 1; }, doesIntersect: function doesIntersect(other) { verifyRegion2DType(other); return doesIntersectData(getData(this), getData(other)); }, isPointIn: function isPointIn(x, y) { return isPointInData(getData(this), Number(x), Number(y)); }, equals: function equals(other) { verifyRegion2DType(other); var data = getData(this), otherData = getData(other); if (data === otherData) return true; if (data.hash !== otherData.hash || data.count !== otherData.count) return false; return arrayEquals(data.array, otherData.array); }, getCount: function getCount() { return getData(this).count; }, getRects: function getRects() { return makeRects(getData(this).array); }, getRawRows: function getRawRows() { return _getRawRows(getData(this).array); }, getBounds: function getBounds() { return getBoundsFromData(getData(this)); }, getPath: function getPath() { return makePath(getData(this).array); }, getHashCode: function getHashCode() { return getData(this).hash; } }; /** * A reusable infinite instance. */ Region2D.infinite = infinite = new Region2D([nInf, nInf, pInf, pInf]); /** * A reusable empty instance. */ Region2D.empty = empty = new Region2D(); /** * Static helper function for creating complex regions from arrays of rectangles. */ Region2D.fromRects = function (rects) { if (!rects.length) return empty; var region = new Region2D(rects[0]); for (var i = 1, l = rects.length; i < l; i++) { region = region.union(new Region2D(rects[i])); } return region; }; /** * Static helper function for creating complex regions from pre-constructed row data. * This is the fastest way to create a complex region, as it runs in O(n) time (with * respect to the number of rows), but it has strict requirements on the shape of the * row data. * * @param rows {Array} - An array of objects, where each object describes a row of the * region. The row objects must have the properties 'region' {Region1D}, 'minY' {Number}, * and 'maxY' {Number}. The 'maxY' of each row must be strictly greater than the 'minY' * of that row, and the 'minY' of each row must be greater than or equal to the 'maxY' of * the previous row. Each row's Region1D must also be nonempty. */ Region2D.fromRawRows = function (rows) { return new Region2D(makeDataFromRows(rows), privateKey); }; return Region2D; }(); exports.default = Region2D; exports.Region1D = _region1d.Region1D; exports.Region2D = Region2D; exports.RegionError = _region1d.RegionError; //# sourceMappingURL=region2d.js.map