region2d
Version:
A JavaScript implementation of the Region abstract data type, which GUIs use to perform constructive solid geometry with 2-D rectangles.
1,408 lines (1,209 loc) • 62.3 kB
JavaScript
"use strict";
/**
* First, a custom error type for regions, to make tracking and logging errors easier.
*/
function RegionError(message) {
this.message = (this.name = "RegionError") + ": " + message;
var stackPieces = String(new Error().stack).split('\n');
stackPieces.shift();
stackPieces.shift();
this.stack = stackPieces.join('\n');
};
RegionError.prototype = Object.create ? Object.create(Error.prototype) : new Error();
/**
* Region1D objects are semi-opaque data structures that represent a 1-dimensional
* set on the number line, described using "spans" of included points.
*
* ------------------------------------------------------------------------------------------------
*
* Each span in the region has an inclusive start and an exclusive end. Spans may
* not overlap, and always must appear in sorted order. So, for example, consider
* this set:
*
* ##### #### ########### #### #
* |----+----|----+----|----+----|----+----|----+----|
* 0 5 10 15 20 25 30 35 40 45 50
*
* This set (inclusively) contains the numbers 3-7, 10-13, 20-30, 38-41, and 47.
* Its Region1D representation (using only integer values) would therefore
* consist of these ranges:
*
* [3,8); [10,14); [20,31); [38,42); [47,48)
*
* And thus the resulting data array stored by the Region1D object would be:
*
* [3, 8, 10, 14, 20, 31, 38, 42, 47, 48]
*
* Note that when you construct a Region1D, you *must* provide the data array
* in sorted order, or the Region1D's constraints will be violated.
*
* ------------------------------------------------------------------------------------------------
*
* Region1Ds provide many operations that can be used to manipulate their
* data as formal sets, 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.isPointIn(x); // Return true if the given coordinate 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.getBounds(b); // Return { min:, max: } of the Region1D.
* result = a.getAsRects(minY, maxY); // Return an array of { x:, y:, width:, height: } rectangles describing the Region1D.
* result = a.getRawSpans(); // Return a raw array of numbers, the same kind that was used to construct the Region1D.
*
* All Region1D 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.
*/
var Region1D = function () {
var
//---------------------------------------------------------------------------------------------
// Global constants.
// Precache positive/negative infinity locally.
pInf = Number.POSITIVE_INFINITY,
nInf = Number.NEGATIVE_INFINITY,
regionError = 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;
},
//---------------------------------------------------------------------------------------------
// 1-D raw-data-manipulation functions.
/**
* Make a function that generates successive lowest values from each of the two given arrays.
*/
makeCoordinateGenerator = function makeCoordinateGenerator(array1, array2) {
var i1 = 0,
i2 = 0;
// Get the next coordinate with the lowest value from either array, keeping
// track of whether it is a begin (+1) or end (-1) coordinate of its span. O(1).
return function () {
if (i1 >= array1.length && i2 >= array2.length) return null;else if (i1 >= array1.length) return { x: array2[i2], kind: i2++ & 1 ? -1 : +1, src: 2 };else if (i2 >= array2.length || array1[i1] < array2[i2]) return { x: array1[i1], kind: i1++ & 1 ? -1 : +1, src: 1 };else return { x: array2[i2], kind: i2++ & 1 ? -1 : +1, src: 2 };
};
},
/**
* Calculate the combination of the given (sorted!) arrays of 1-D region data.
* Returns a new array that contains the 1-D combination.
*/
combineData = function combineData(array1, array2, op) {
// Special case: Nothin' from nothin' gives nothin'.
if (!array1.length && !array2.length) return [];
// Get the next coordinate with the lowest value from either array, keeping
// track of whether it is a begin (+1) or end (-1) coordinate of its span. O(1).
var getNext = makeCoordinateGenerator(array1, array2);
var depth1 = 0,
depth2 = 0;
var state = 0,
lastState = 0;
// Do whatever needs to happen at the very first coordinate.
var coord = getNext();
// Process all of the coordinates until both arrays are empty, collecting
// new spans in the 'result' array. O(n+m).
var result = [];
do {
// Do whatever happens at this coordinate.
if (coord.src === 1) depth1 += coord.kind;else depth2 += coord.kind;
// Process any subsequent coordinates at the same 'x' offset,
// also collecting the one after it.
var nextCoord = void 0;
while ((nextCoord = getNext()) && nextCoord.x === coord.x) {
if (nextCoord.src === 1) depth1 += nextCoord.kind;else depth2 += nextCoord.kind;
}
// Change the state to match whatever happened here.
lastState = state;
state = op(depth1, depth2);
// If we entered/exited a new span, emit a start/end X value.
if (state !== lastState) {
result.push(coord.x);
}
coord = nextCoord;
} while (coord);
return result;
},
/**
* Calculate the union of the given arrays of 1-D region data.
* Returns a new array that contains the 1-D union.
*/
unionData = function unionData(array1, array2) {
return combineData(array1, array2, function (depth1, depth2) {
return depth1 | depth2;
});
},
/**
* Calculate the intersection of the given arrays of 1-D region data.
* Returns a new array that contains the 1-D intersection.
*/
intersectData = function intersectData(array1, array2) {
return combineData(array1, array2, function (depth1, depth2) {
return depth1 & depth2;
});
},
/**
* Calculate the exclusive-or of the given arrays of 1-D region data.
* Returns a new array that contains the 1-D exclusive-or.
*/
xorData = function xorData(array1, array2) {
return combineData(array1, array2, function (depth1, depth2) {
return depth1 ^ depth2;
});
},
/**
* Calculate the difference of the given arrays of 1-D region data.
* Returns a new array that contains the 1-D difference.
*/
subtractData = function subtractData(array1, array2) {
return combineData(array1, array2, function (depth1, depth2) {
return depth1 & ~depth2;
});
},
/**
* Calculate whether the given arrays of 1-D region data intersect, and
* if so, how. This requires constant memory, but it may take O(n+m) time.
*
* If 'earlyOut' is true, this will return only '' or 'intersects', and
* it will return that answer as soon as it possibly can, skipping testing
* successive data if possible.
*
* Returns one of:
* '': no intersection
* 'intersect': there is at least some kind of intersection
* 'a-contain-b': array1 is a proper superset of array2
* 'b-contain-a': array2 is a proper superset of array1
* 'equal': array1 and array2 represent the same exact region
*/
relateData = function relateData(array1, array2, earlyOut) {
// If either is empty, there's no intersection.
if (!array1.length || !array2.length) return '';
// If all of the spans of one are before all of the spans of another, there's no intersection.
if (array1[array1.length - 1] < array2[0] || array2[array2.length - 1] < array1[0]) return '';
// Test all the spans against each other.
var depth1 = 0,
depth2 = 0;
var getNext = makeCoordinateGenerator(array1, array2);
// Do whatever needs to happen at the very first coordinate.
var coord = getNext();
// Bit flags: We start out assuming A and B both contain each other, but there
// is not yet an intersection. It's weird, but go with it.
var result = 3;
do {
// Do whatever happens at this coordinate.
if (coord.src === 1) depth1 += coord.kind;else depth2 += coord.kind;
// Process any subsequent coordinates at the same 'x' offset,
// also collecting the one after it.
var nextCoord = void 0;
while ((nextCoord = getNext()) && nextCoord.x === coord.x) {
if (nextCoord.src === 1) depth1 += nextCoord.kind;else depth2 += nextCoord.kind;
}
// Change the state to match whatever happened here.
if (depth1 & depth2) {
// Got an intersection.
result |= 4;
if (earlyOut) return 'intersect';
} else if (depth2 & ~depth1) {
// A does not contain B.
result &= ~1;
} else if (depth1 & ~depth2) {
// B does not contain A.
result &= ~2;
}
coord = nextCoord;
} while (coord);
// Choose an answer based on the resulting flag bits.
switch (result) {
case 4:
return 'intersect'; // 1 0 0
case 5:
return 'a-contain-b'; // 1 0 1
case 6:
return 'b-contain-a'; // 1 1 0
case 7:
return 'equal'; // 1 1 1
default:
return ''; // 0 * *
}
},
/**
* Determine whether the given point lies within the spans of the Region1D data.
*/
isPointInData = function isPointInData(array, x) {
// It can't be in the empty set.
if (!array.length) return false;
// If it's outside the bounds, it's not anywhere within any of the spans.
if (x < array[0] || x > array[array.length - 1]) return false;
if (array.length <= 8) {
// Spin over all the spans in a simple linear search.
for (var i = 0, l = array.length; i < l; i += 2) {
if (x >= array[i] && x < array[i + 1]) return true;
}
return false;
} else {
// Binary search to find the array index that x is either after or at.
var start = 0,
end = array.length;
var index = 0;
while (start < end) {
var midpt = (start + end) / 2 & ~0;
var value = array[midpt];
if (x === value) {
index = midpt;
break;
} else if (x < value) {
end = midpt;
} else {
index = midpt;
start = midpt + 1;
}
}
// 'index' now is the closest value at or before 'x', so we just need to see if
// it's an odd or even array index to know if 'x' is inside the span or outside it.
return !(index & 1);
}
},
/**
* Calculate a complement of the 1-D (sorted!) region data.
* This is easy:
* If it starts with -Inf, remove that; otherwise, prepend -Inf.
* If it ends with +Inf, remove that; otherwise, append +Inf.
* Returns a new array that contains the 1D complement.
*/
notData = function notData(array) {
var newArray = [];
var src = 0;
if (!array.length) {
newArray.push(nInf);
newArray.push(pInf);
return newArray;
} else {
if (array[src] != nInf) newArray.push(nInf);else src++;
}
while (src < array.length - 1) {
newArray.push(array[src++]);
}
if (array[src] != pInf) {
newArray.push(array[src++]);
newArray.push(pInf);
}
return newArray;
},
/**
* Calculate a new region whose coordinates have all been translated/scaled by the given amounts.
*/
transformData = function transformData(array, ratio, delta) {
delta = Number(delta);
if (!(nInf < delta && delta < pInf)) // Catches other NaNs as well as infinities.
throw new regionError("Invalid translation delta");
ratio = Number(ratio);
if (!(nInf < ratio && ratio < pInf) || ratio === 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++) {
newArray[i] = array[i] * ratio + delta;
}
return newArray;
},
/**
* Determine if two arrays of (sorted!) 1-D region data are equivalent.
* Returns true if they are the same, false if they are different.
*/
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] !== array2[i]) return false;
}
return true;
},
/**
* Transform a set of 1-D region data into an array of rectangles with
* the given same y and height values.
*
* Returns a new array that contains rectangles of the form { x:, y:, width:, height:, left:, top:, right:, bottom: }.
*/
makeRects = function makeRects(array, minY, maxY, result) {
var height = maxY - minY;
for (var i = 0, l = array.length; i < l; i += 2) {
var minX = array[i];
var maxX = array[i + 1];
result.push({
x: minX, y: minY, width: maxX - minX, height: height,
left: minX, top: minY, right: maxX, bottom: maxY
});
}
return result;
},
/**
* Clone a set of 1-D region data into a raw array.
* Returns a new array that contains pairs of points.
*/
makeRawSpans = function makeRawSpans(array) {
var result = [];
for (var i = 0, l = array.length; i < l; i += 2) {
result.push(array[i]);
result.push(array[i + 1]);
}
return result;
},
/**
* Calculate a hash that (loosely) describes the given Region1D of data, so that we
* can readily tell whether it is different from another.
*/
makeHashCode = function makeHashCode(array) {
var hash = 0;
for (var i = 0, l = array.length; i < l; i++) {
hash *= 23;
hash += array[i] | 0;
hash &= ~0;
}
return hash;
},
/**
* Check to ensure that the given object is actually a Region1D, and abort if it is not.
*/
verifyRegion1DType = function verifyRegion1DType(obj) {
if (!(obj instanceof Region1D)) {
throw new regionError("Object must be a Region1D instance.");
}
},
/**
* Check the given data to make sure that it consists of an array of ordered pairs
* of span start/end points.
*/
validateData = function validateData(array) {
var typeErrorMsg = "Expected an ordered array of numeric start/end pairs.";
var dataErrorMsg = "Array start/end pairs are not in strictly ascending order.";
// Make sure it's an array of even length.
if (!isArray(array) || array.length & 1) {
throw new regionError(typeErrorMsg);
}
// Empty array is always valid.
if (array.length == 0) return;
// Get the first entry, and make sure it's a number.
var prev = array[0];
if (typeof prev !== 'number') {
throw new regionError(typeErrorMsg);
}
// Check each successive entry to make sure that it's (A) a number and (B) strictly
// greater than the entry before it.
for (var i = 1, l = array.length; i < l; i++) {
var cur = array[i];
if (typeof cur !== 'number') {
throw new regionError(typeErrorMsg);
}
if (cur <= prev) {
throw new regionError(dataErrorMsg);
}
prev = cur;
}
},
//---------------------------------------------------------------------------------------------
// Public 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 1-D region from the given array of start/end X coordinates. This is a
* proper object, with prototype methods for performing operations like
* union/intersect/subtract/xor.
*
* Usage: new Region1D(array)
*
* @param array {Array} - The array of span endpoints, in pairs of start (inclusive)
* and end (exclusive) X-coordinates.
*/
function Region1D(array, key, hash) {
// Internal-only second parameter: A 'key' flag, indicating this data came from an
// internal operation and does not require validation for correctness.
if (key === privateKey) {
// Internal-only third parameter: A hash for comparisons.
if (typeof hash !== 'number') hash = makeHashCode(array);
} else if (typeof key !== 'undefined' || typeof hash !== 'undefined') {
// You're not allowed to specify a key unless it's the right one.
throw new regionError("Illegal access");
} else {
// Verify that the user passed us data that makes sense.
validateData(array);
hash = makeHashCode(array);
}
this._opaque = makeProtectedData({
array: array,
min: array.length ? array[0] : pInf,
max: array.length ? array[array.length - 1] : nInf,
hash: hash
}, privateKey);
};
/**
* The row's prototype contains helpers that simply invoke the private operations
* to do all the hard work.
*/
Region1D.prototype = {
union: function union(other) {
verifyRegion1DType(other);
var data = getData(this),
otherData = getData(other);
return new Region1D(unionData(data.array, otherData.array), privateKey);
},
intersect: function intersect(other) {
verifyRegion1DType(other);
var data = getData(this),
otherData = getData(other);
return new Region1D(intersectData(data.array, otherData.array), privateKey);
},
subtract: function subtract(other) {
verifyRegion1DType(other);
var data = getData(this),
otherData = getData(other);
return new Region1D(subtractData(data.array, otherData.array), privateKey);
},
xor: function xor(other) {
verifyRegion1DType(other);
var data = getData(this),
otherData = getData(other);
return new Region1D(xorData(data.array, otherData.array), privateKey);
},
not: function not() {
var data = getData(this);
return new Region1D(notData(data.array), privateKey);
},
transform: function transform(scale, offset) {
var data = getData(this);
return new Region1D(transformData(data.array, scale, offset)); // No privateKey forces a data check, since we could have lost precision.
},
translate: function translate(offset) {
var data = getData(this);
return new Region1D(transformData(data.array, 1.0, offset)); // No privateKey forces a data check, since we could have lost precision.
},
scale: function scale(_scale) {
var data = getData(this);
return new Region1D(transformData(data.array, _scale, 0)); // No privateKey forces a data check, since we could have lost precision.
},
isEmpty: function isEmpty() {
return !getData(this).array.length;
},
getCount: function getCount() {
return getData(this).array.length >> 1;
},
doesIntersect: function doesIntersect(other) {
verifyRegion1DType(other);
return !!relateData(getData(this).array, getData(other).array, true);
},
relate: function relate(other) {
verifyRegion1DType(other);
return relateData(getData(this).array, getData(other).array, false);
},
isPointIn: function isPointIn(x) {
return isPointInData(getData(this).array, Number(x));
},
equals: function equals(other) {
verifyRegion1DType(other);
var data = getData(this),
otherData = getData(other);
if (data === otherData) return true;
if (data.hash !== otherData.hash) return false;
return arrayEquals(data.array, otherData.array);
},
getRawSpans: function getRawSpans() {
var data = getData(this);
return makeRawSpans(data.array);
},
getAsRects: function getAsRects(minY, maxY, destArray) {
var data = getData(this);
return makeRects(data.array, minY, maxY, destArray || []);
},
getBounds: function getBounds() {
var data = getData(this);
return { min: data.min, max: data.max };
},
getHashCode: function getHashCode() {
return getData(this).hash;
}
};
// Construct a convenient shareable 'empty' instance.
Region1D.empty = new Region1D([], privateKey, 0);
return 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 = 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.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 = n