First implementation of sweep and prune (#1740)

This commit is contained in:
waruyama 2019-12-13 14:32:31 +01:00 committed by Jürg Lehni
parent 871531b46a
commit 1f39b1df98
5 changed files with 436 additions and 70 deletions

View file

@ -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');

View file

@ -2104,50 +2104,53 @@ new function() { // Scope for bezier intersection using fat-line clipping
function getIntersections(curves1, curves2, include, matrix1, matrix2,
_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);
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;
values1 = new Array(length1),
values2 = self ? values1 : new Array(length2),
locations = [];
arrays.push(locations);
for (var i = 0; i < length1; i++) {
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++) {
// 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;
getCurveIntersections(values1, values2[j], curve1, curves2[j],
locations, include);
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;
}

View file

@ -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,31 +341,41 @@ 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(),
indicesI = collisions[i];
if (indicesI) {
var entry1 = lookup[path1._id],
point = null; // interior point, only get it if required
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
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;
entry1.container = entry2.exclude ?
entry2.container : path2;
break;
}
}
}
}
// Only keep paths if the "insideness" changes when crossing the
// path, e.g. the inside of the path is filled and the outside
// is not, or vice versa.
@ -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);
},
/**

View file

@ -722,19 +722,23 @@ 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;
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;
}
}
}
}
// Each path in path2 needs to be matched at least once.
ok = ok && count === length2;
}

View file

@ -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 <jan.boesenberg@gmail.com>
*/
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 <jan.boesenberg@gmail.com>
*/
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 <jan.boesenberg@gmail.com>
*/
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;
}
};