diff --git a/src/paper.js b/src/paper.js index fc00e683..de0350eb 100644 --- a/src/paper.js +++ b/src/paper.js @@ -42,6 +42,7 @@ var paper = function(self, undefined) { /*#*/ include('core/PaperScope.js'); /*#*/ include('core/PaperScopeItem.js'); +/*#*/ include('util/CollisionDetection.js'); /*#*/ include('util/Formatter.js'); /*#*/ include('util/Numerical.js'); /*#*/ include('util/UID.js'); diff --git a/src/path/Curve.js b/src/path/Curve.js index ac23371b..5cabea60 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -2103,52 +2103,55 @@ new function() { // Scope for bezier intersection using fat-line clipping } function getIntersections(curves1, curves2, include, matrix1, matrix2, - _returnFirst) { + _returnFirst) { + var epsilon = Numerical.GEOMETRIC_EPSILON; var self = !curves2; if (self) curves2 = curves1; var length1 = curves1.length, length2 = curves2.length, - values2 = [], - arrays = [], - locations, - current; - // Cache values for curves2 as we re-iterate them for each in curves1. - for (var i = 0; i < length2; i++) - values2[i] = curves2[i].getValues(matrix2); + values1 = new Array(length1), + values2 = self ? values1 : new Array(length2), + locations = []; + for (var i = 0; i < length1; i++) { - var curve1 = curves1[i], - values1 = self ? values2[i] : curve1.getValues(matrix1), - path1 = curve1.getPath(); - // NOTE: Due to the nature of getCurveIntersections(), we use - // separate location arrays per path1, to make sure the circularity - // checks are not getting confused by locations on separate paths. - // The separate arrays are then flattened in the end. - if (path1 !== current) { - current = path1; - locations = []; - arrays.push(locations); + var v = curves1[i].getValues(matrix1); + values1[i] = v; + } + if (!self) { + for (var i = 0; i < length2; i++) { + var v = curves2[i].getValues(matrix2); + values2[i] = v; } + } + var boundsCollisions = CollisionDetection.findCurveBoundsCollisions( + values1, self ? null : values2, epsilon); + for (var index1 = 0; index1 < length1; index1++) { + var curve1 = curves1[index1], + v1 = values1[index1]; if (self) { // First check for self-intersections within the same curve. - getSelfIntersection(values1, curve1, locations, include); + getSelfIntersection(v1, curve1, locations, include); } - // Check for intersections with other curves. - // For self-intersection, we can start at i + 1 instead of 0. - for (var j = self ? i + 1 : 0; j < length2; j++) { - // There might be already one location from the above - // self-intersection check: - if (_returnFirst && locations.length) - return locations; - getCurveIntersections(values1, values2[j], curve1, curves2[j], - locations, include); + // Check for intersections with potentially intersecting curves. + var collisions1 = boundsCollisions[index1]; + if (collisions1) { + for (var j = 0; j < collisions1.length; j++) { + // There might be already one location from the above + // self-intersection check: + if (_returnFirst && locations.length) + return locations; + var index2 = collisions1[j]; + if (!self || index2 > index1) { + var curve2 = curves2[index2], + v2 = values2[index2]; + getCurveIntersections( + v1, v2, curve1, curve2, locations, include + ); + } + } } - } - // Flatten the list of location arrays to one array and return it. - locations = []; - for (var i = 0, l = arrays.length; i < l; i++) { - Base.push(locations, arrays[i]); - } + } return locations; } diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index a38aac3d..b97dbb29 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -156,19 +156,61 @@ PathItem.inject(new function() { collect(paths1); if (paths2) collect(paths2); + + var curvesValues = new Array(curves.length); + for (var i = 0, l = curves.length; i < l; i++) { + curvesValues[i] = curves[i].getValues(); + } + var horCurveCollisions = + CollisionDetection.findCurveBoundsCollisions( + curvesValues, curvesValues, 0, false, true); + var horCurvesMap = {}; + for (var i = 0; i < curves.length; i++) { + var curve = curves[i], + collidingCurves = [], + collisionIndices = horCurveCollisions[i]; + if (collisionIndices) { + for (var j = 0; j < collisionIndices.length; j++) { + collidingCurves.push(curves[collisionIndices[j]]); + } + } + var pathId = curve.getPath().getId(); + horCurvesMap[pathId] = horCurvesMap[pathId] || {}; + horCurvesMap[pathId][curve.getIndex()] = collidingCurves; + } + + var vertCurveCollisions = + CollisionDetection.findCurveBoundsCollisions( + curvesValues, curvesValues, 0, true, true); + var vertCurvesMap = {}; + for (var i = 0; i < curves.length; i++) { + var curve = curves[i], + collidingCurves = [], + collisionIndices = vertCurveCollisions[i]; + if (collisionIndices) { + for (var j = 0; j < collisionIndices.length; j++) { + collidingCurves.push(curves[collisionIndices[j]]); + } + } + var pathId = curve.getPath().getId(); + vertCurvesMap[pathId] = vertCurvesMap[pathId] || {}; + vertCurvesMap[pathId][curve.getIndex()] = collidingCurves; + } + // Propagate the winding contribution. Winding contribution of // curves does not change between two crossings. // First, propagate winding contributions for curve chains starting // in all crossings: for (var i = 0, l = crossings.length; i < l; i++) { - propagateWinding(crossings[i]._segment, _path1, _path2, curves, - operator); + propagateWinding(crossings[i]._segment, _path1, _path2, + horCurvesMap, vertCurvesMap, operator); } for (var i = 0, l = segments.length; i < l; i++) { var segment = segments[i], inter = segment._intersection; if (!segment._winding) { - propagateWinding(segment, _path1, _path2, curves, operator); + propagateWinding(segment, _path1, _path2, + horCurvesMap, vertCurvesMap, operator); } // See if all encountered segments in a path are overlaps. if (!(inter && inter._overlap)) @@ -186,7 +228,6 @@ PathItem.inject(new function() { return !!operator[w]; }); } - return createResult(paths, true, path1, path2, options); } @@ -300,29 +341,39 @@ PathItem.inject(new function() { // Get reference to the first, largest path and insert it // already. first = sorted[0]; + // create lookup containing potentially overlapping path bounds + var collisions = CollisionDetection.findItemBoundsCollisions(sorted, + null, Numerical.GEOMETRIC_EPSILON); if (clockwise == null) clockwise = first.isClockwise(); // Now determine the winding for each path, from large to small. for (var i = 0; i < length; i++) { var path1 = sorted[i], - entry1 = lookup[path1._id], - point = path1.getInteriorPoint(), - containerWinding = 0; - for (var j = i - 1; j >= 0; j--) { - var path2 = sorted[j]; - // As we run through the paths from largest to smallest, for - // any current path, all potentially containing paths have - // already been processed and their orientation fixed. - // To achieve correct orientation of contained paths based - // on winding, we have to find one containing path with - // different "insideness" and set opposite orientation. - if (path2.contains(point)) { - var entry2 = lookup[path2._id]; - containerWinding = entry2.winding; - entry1.winding += containerWinding; - entry1.container = entry2.exclude ? entry2.container - : path2; - break; + indicesI = collisions[i]; + if (indicesI) { + var entry1 = lookup[path1._id], + point = null; // interior point, only get it if required + containerWinding = 0; + for (var j = indicesI.length - 1; j >= 0; j--) { + if (indicesI[j] < i) { + point = point || path1.getInteriorPoint(); + var path2 = sorted[indicesI[j]]; + // As we run through the paths from largest to + // smallest, for any current path, all potentially + // containing paths have already been processed and + // their orientation fixed. To achieve correct + // orientation of contained paths based on winding, + // we have to find one containing path with + // different "insideness" and set opposite orientation. + if (path2.contains(point)) { + var entry2 = lookup[path2._id]; + containerWinding = entry2.winding; + entry1.winding += containerWinding; + entry1.container = entry2.exclude ? + entry2.container : path2; + break; + } + } } } // Only keep paths if the "insideness" changes when crossing the @@ -483,9 +534,16 @@ PathItem.inject(new function() { * * @param {Point} point the location for which to determine the winding * contribution - * @param {Curve[]} curves the curves that describe the shape against which + * @param {Curve[]} curvesH The curves that describe the shape against which * to check, as returned by {@link Path#curves} or - * {@link CompoundPath#curves} + * {@link CompoundPath#curves}. This only has to contain those curves + * that can be crossed by a horizontal line through the point to be + * checked. + * @param {Curve[]} curvesV The curves that describe the shape against which + * to check, as returned by {@link Path#curves} or + * {@link CompoundPath#curves}. This only has to contain those curves + * that can be crossed by a vertical line through the point to be + * checked. * @param {Boolean} [dir=false] the direction in which to determine the * winding contribution, `false`: in x-direction, `true`: in y-direction * @param {Boolean} [closed=false] determines how areas should be closed @@ -498,7 +556,8 @@ PathItem.inject(new function() { * well as an indication whether the point was situated on the contour * @private */ - function getWinding(point, curves, dir, closed, dontFlip) { + function getWinding(point, curvesH, curvesV, dir, closed, dontFlip) { + var curves = !dir ? curvesV : curvesH; // Determine the index of the abscissa and ordinate values in the curve // values arrays, based on the direction: var ia = dir ? 1 : 0, // the abscissa index @@ -613,7 +672,7 @@ PathItem.inject(new function() { // again with flipped direction and return that result instead. return !dontFlip && a > paL && a < paR && Curve.getTangent(v, t)[dir ? 'x' : 'y'] === 0 - && getWinding(point, curves, !dir, closed, true); + && getWinding(point, curvesH, curvesV, !dir, closed, true); } function handleCurve(v) { @@ -734,7 +793,8 @@ PathItem.inject(new function() { }; } - function propagateWinding(segment, path1, path2, curves, operator) { + function propagateWinding(segment, path1, path2, horCurveCollisionsMap, + vertCurveCollisionsMap, operator) { // Here we try to determine the most likely winding number contribution // for the curve-chain starting with this segment. Once we have enough // confidence in the winding contribution, we can propagate it until the @@ -801,7 +861,12 @@ PathItem.inject(new function() { } } } - wind = wind || getWinding(pt, curves, dir, true); + var pathId = path.getId(); + var curveIndex = curve.getIndex(); + var hCollisions = horCurveCollisionsMap[pathId][curveIndex]; + var vCollisions = vertCurveCollisionsMap[pathId][curveIndex]; + wind = wind || + getWinding(pt, hCollisions, vCollisions, dir, true); if (wind.quality > winding.quality) winding = wind; break; @@ -1077,7 +1142,8 @@ PathItem.inject(new function() { * @return {Number} the winding number */ _getWinding: function(point, dir, closed) { - return getWinding(point, this.getCurves(), dir, closed); + let curves = this.getCurves(); + return getWinding(point, curves, curves, dir, closed); }, /** diff --git a/src/path/PathItem.js b/src/path/PathItem.js index 01000361..03115233 100644 --- a/src/path/PathItem.js +++ b/src/path/PathItem.js @@ -722,16 +722,20 @@ var PathItem = Item.extend(/** @lends PathItem# */{ matched = [], count = 0; ok = true; + var boundsOverlaps = CollisionDetection.findBoundsOverlaps(paths1, paths2, Numerical.GEOMETRIC_EPSILON); for (var i1 = length1 - 1; i1 >= 0 && ok; i1--) { var path1 = paths1[i1]; ok = false; - for (var i2 = length2 - 1; i2 >= 0 && !ok; i2--) { - if (path1.compare(paths2[i2])) { - if (!matched[i2]) { - matched[i2] = true; - count++; + var pathBoundsOverlaps = boundsOverlaps[i1]; + if (pathBoundsOverlaps) { + for (var i2 = pathBoundsOverlaps.length - 1; i2 >= 0 && !ok; i2--) { + if (path1.compare(paths2[pathBoundsOverlaps[i2]])) { + if (!matched[pathBoundsOverlaps[i2]]) { + matched[pathBoundsOverlaps[i2]] = true; + count++; + } + ok = true; } - ok = true; } } } diff --git a/src/util/CollisionDetection.js b/src/util/CollisionDetection.js new file mode 100644 index 00000000..37e26ea6 --- /dev/null +++ b/src/util/CollisionDetection.js @@ -0,0 +1,292 @@ +/* + * Paper.js - The Swiss Army Knife of Vector Graphics Scripting. + * http://paperjs.org/ + * + * Copyright (c) 2011 - 2019, Juerg Lehni & Jonathan Puckey + * http://scratchdisk.com/ & https://puckey.studio/ + * + * Distributed under the MIT license. See LICENSE file for details. + * + * All rights reserved. + */ + +/** + * @name CollisionDetection + * @namespace + * @private + */ +var CollisionDetection = /** @lends CollisionDetection */{ + + /** + * Finds collisions between axis aligned bounding boxes of items. + * + * This function takes the bounds of all items in the items1 and items2 + * arrays and calls findBoundsCollisions(). + * + * @param {Array} itemsA Array of curve values for which collisions should + * be found. + * @param {Array} [itemsA] Array of curve values that the first array should + * be compared with. If not provided, collisions between items within + * the first arrray will be returned. + * @param {Number} [tolerance] If provided, the tolerance will be added to + * all sides of each bounds when checking for collisions. + * @param {Boolean} [sweepVertical] If set to true, the sweep is done + * along the y axis. + * @param {Boolean} [onlySweepAxisCollisionss] If set to true, no collision + * checks will be done on the secondary axis. + * @returns {Array} Array containing for the bounds at thes same index in + * itemsA an array of the indexes of colliding bounds in itemsB + * + * @author Jan Boesenberg + */ + findItemBoundsCollisions: function(itemsA, itemsB, tolerance, + sweepVertical, onlySweepAxisCollisions) { + var boundsArr1 = new Array(itemsA.length), + boundsArr2; + for (var i = 0; i < boundsArr1.length; i++) { + var bounds = itemsA[i].bounds; + boundsArr1[i] = [bounds.left, bounds.top, bounds.right, + bounds.bottom]; + } + if (itemsB) { + if (itemsB === itemsA) { + boundsArr2 = boundsArr1; + } else { + boundsArr2 = new Array(itemsB.length); + for (var i = 0; i < boundsArr2.length; i++) { + var bounds = itemsB[i].bounds; + boundsArr2[i] = [bounds.left, bounds.top, bounds.right, + bounds.bottom]; + } + } + } + return this.findBoundsCollisions(boundsArr1, boundsArr2, tolerance || 0, + sweepVertical, onlySweepAxisCollisions); + }, + + /** + * Finds collisions between curves bounds. For performance reasons this + * uses broad bounds of the curve, which can be calculated much faster than + * the actual bounds. Broad bounds guarantee to contain the full curve, + * but they are usually larger than the actual bounds of a curve. + * + * This function takes the broad bounds of all curve values in the + * curveValues1 and curveValues2 arrays and calls findBoundsCollisions(). + * + * @param {Array} curvesValues1 Array of curve values for which collisions + * should be found. + * @param {Array} [curvesValues2] Array of curve values that the first + * array should be compared with. If not provided, collisions between + * curve bounds within the first arrray will be returned. + * @param {Number} [tolerance] If provided, the tolerance will be added to + * all sides of each bounds when checking for collisions. + * @param {Boolean} [sweepVertical] If set to true, the sweep is done + * along the y axis. + * @param {Boolean} [onlySweepAxisCollisionss] If set to true, no collision + * checks will be done on the secondary axis. + * @returns {Array} Array containing for the bounds at thes same index in + * curveValuesA an array of the indexes of colliding bounds in + * curveValuesB + * + * @author Jan Boesenberg + */ + findCurveBoundsCollisions: function(curvesValues1, curvesValues2, + tolerance, sweepVertical, onlySweepAxisCollisions) { + var min = Math.min, + max = Math.max, + boundsArr1 = new Array(curvesValues1.length), + boundsArr2; + for (var i = 0; i < boundsArr1.length; i++) { + var v1 = curvesValues1[i]; + boundsArr1[i] = [ + min(v1[0], v1[2], v1[4], v1[6]), + min(v1[1], v1[3], v1[5], v1[7]), + max(v1[0], v1[2], v1[4], v1[6]), + max(v1[1], v1[3], v1[5], v1[7]) + ]; + } + if (curvesValues2) { + if (curvesValues2 === curvesValues1) { + boundsArr2 = boundsArr1; + } else { + boundsArr2 = new Array(curvesValues2.length); + for (var i = 0; i < boundsArr2.length; i++) { + var v2 = curvesValues2[i]; + boundsArr2[i] = [ + min(v2[0], v2[2], v2[4], v2[6]), + min(v2[1], v2[3], v2[5], v2[7]), + max(v2[0], v2[2], v2[4], v2[6]), + max(v2[1], v2[3], v2[5], v2[7]) + ]; + } + } + } + return this.findBoundsCollisions(boundsArr1, boundsArr2, + tolerance || 0, sweepVertical, onlySweepAxisCollisions); + }, + + /** + * Finds collisions between two sets of bounding rectangles. + * + * The collision detection is implemented as a sweep and prune algorithm + * with sweep either along the x or y axis (primary axis) and immediate + * check on secondary axis for potential pairs. + * + * Each entry in the bounds arrays must be an array of length 4 with + * x0, y0, x1, and y1 as the array elements. + * + * The returned array has the same length as boundsArr1. Each entry + * contains an array with all indices of overlapping bounds of + * boundsArr2 (or boundsArr1 if boundsArr2 is not provided) sorted + * in ascending order. + * + * If the second bounds array parameter is null, collisions between bounds + * within the first bounds array will be found. In this case the indexed + * returned for each bounds will not contain the bounds' own index. + * + * + * @param {Array} boundsArr1 Array of bounds objects for which collisions + * should be found. + * @param {Array} [boundsArr2] Array of bounds that the first array should + * be compared with. If not provided, collisions between bounds within + * the first arrray will be returned. + * @param {Number} [tolerance] If provided, the tolerance will be added to + * all sides of each bounds when checking for collisions. + * @param {Boolean} [sweepVertical] If set to true, the sweep is done + * along the y axis. + * @param {Boolean} [onlySweepAxisCollisionss] If set to true, no collision + * checks will be done on the secondary axis. + * @returns {Array} Array containing for the bounds at thes same index in + * boundsA an array of the indexes of colliding bounds in boundsB + * + * @author Jan Boesenberg + */ + findBoundsCollisions: function(boundsA, boundsB, tolerance, + sweepVertical, onlySweepAxisCollisions) { + // Binary search utility function. + // For multiple same entries, this returns the rightmost entry. + // https://en.wikipedia.org/wiki/Binary_search_algorithm#Procedure_for_finding_the_rightmost_element + var lo, hi; + var binarySearch = function(indices, coordinateValue, coordinate) { + lo = 0; + hi = indices.length; + while (lo < hi) { + var mid = (hi + lo) >>> 1; // same as Math.floor((hi+lo)/2) + if (allBounds[indices[mid]][coordinate] < coordinateValue) { + lo = mid + 1; + } else { + hi = mid; + } + } + return lo - 1; + }; + + // + var self = !boundsB || boundsA === boundsB, + allBounds = self ? boundsA : boundsA.concat(boundsB), + countA = boundsA.length, + countAll = allBounds.length; + // Set coordinates for primary and secondary axis depending on sweep + // direction. By default we sweep in horizontal direction, which + // means x is the primary axis. + var coordP0 = sweepVertical ? 1 : 0, + coordP1 = coordP0 + 2, + coordS0 = sweepVertical ? 0 : 1, + coordS1 = coordS0 + 2; + // Create array with all indices sorted by lower boundary on primary + // axis. + var allIndicesByP0 = new Array(countAll); + for (var i = 0; i < countAll; i++) { + allIndicesByP0[i] = i; + } + allIndicesByP0.sort(function(i1, i2) { + return allBounds[i1][coordP0] - allBounds[i2][coordP0]; + }); + // Sweep along primary axis. Indices of active bounds are kept in an + // array sorted by higher boundary on primary axis. + var activeIndicesByP1 = [], + allCollisions = new Array(countA); + for (var i = 0; i < countAll; i++) { + var currentIndex = allIndicesByP0[i], + currentBounds = allBounds[currentIndex]; + currentOriginalIndex = self ? currentIndex + : currentIndex - countA, // index in boundsA or boundsB array + isCurrentA = currentIndex < countA, + isCurrentB = self || currentIndex >= countA, + currentCollisions = isCurrentA ? [] : null; + if (activeIndicesByP1.length) { + // remove (prune) indices that are no longer active + var pruneCount = binarySearch(activeIndicesByP1, + currentBounds[coordP0] - tolerance, coordP1) + 1; + activeIndicesByP1.splice(0, pruneCount); + // add collisions for current index + if (self && onlySweepAxisCollisions) { + // All active indexes can be added, no further checks needed + currentCollisions = currentCollisions.concat( + activeIndicesByP1.slice()); + // Add current index to collisions of all active indexes + for (var j = 0; j < activeIndicesByP1.length; j++) { + var activeIndex = activeIndicesByP1[j]; + allCollisions[activeIndex].push(currentOriginalIndex); + } + } else { + var currentS1 = currentBounds[coordS1], + currentS0 = currentBounds[coordS0]; + for (var j = 0; j < activeIndicesByP1.length; j++) { + var activeIndex = activeIndicesByP1[j], + isActiveA = activeIndex < countA, + isActiveB = self || activeIndex >= countA; + // Check secondary axis bounds if necessary + if (onlySweepAxisCollisions || + (((isCurrentA && isActiveB) || + (isCurrentB && isActiveA)) && + currentS1 >= + allBounds[activeIndex][coordS0] - + tolerance && + currentS0 <= + allBounds[activeIndex][coordS1] + + tolerance)) { + // Add current index to collisions of active + // indices and vice versa. + if (isCurrentA && isActiveB) { + currentCollisions.push( + self ? activeIndex : activeIndex - countA); + } + if (isCurrentB && isActiveA) { + allCollisions[activeIndex].push( + currentOriginalIndex); + } + } + } + } + } + if (isCurrentA) { + if (boundsA === boundsB) { + // if both arrays are the same, add self collision + currentCollisions.push(currentIndex); + } + // add collisions for current index + allCollisions[currentIndex] = currentCollisions; + } + // add current index to active indices. Keep array sorted by + // their higher boundary on the primary axis + if (activeIndicesByP1.length) { + var currentP1 = currentBounds[coordP1], + insertIndex = + binarySearch(activeIndicesByP1, currentP1, coordP1) + 1; + activeIndicesByP1.splice(insertIndex, 0, currentIndex); + } else { + activeIndicesByP1.push(currentIndex); + } + } + // Sort collision indioes in ascending order + for (var i = 0; i < allCollisions.length; i++) { + if (allCollisions[i]) { + allCollisions[i].sort(function(i1, i2) { + return i1 - i2; + }); + } + } + return allCollisions; + } +}; \ No newline at end of file