Merge remote-tracking branch 'origin/master' into boolean-operations

Conflicts:
	src/path/PathItem.Boolean.js
This commit is contained in:
Jürg Lehni 2015-01-04 21:29:50 +01:00
commit d522e4aec2
8 changed files with 198 additions and 66 deletions

View file

@ -166,7 +166,7 @@ var Line = Base.extend(/** @lends Line# */{
vy -= py;
}
return Numerical.isZero(vx)
? vy >= 0 ? py - x : x - px
? vy >= 0 ? px - x : x - px
: Numerical.isZero(vy)
? vx >= 0 ? y - py : py - y
: -(vy * x - vx * y - px * (py + vy) + py * (px + vx)) /

View file

@ -1073,7 +1073,6 @@ new function() { // Scope for methods that require numerical integration
// Let P be the first curve and Q be the second
var q0x = v2[0], q0y = v2[1], q3x = v2[6], q3y = v2[7],
tolerance = /*#=*/Numerical.TOLERANCE,
epsilon = 1e-10, // /*#=*/Numerical.EPSILON,
// Calculate the fat-line L for Q is the baseline l and two
// offsets which completely encloses the curve P.
d1 = getSignedDistance(q0x, q0y, q3x, q3y, v2[2], v2[3]) || 0,
@ -1089,9 +1088,7 @@ new function() { // Scope for methods that require numerical integration
dp2 = getSignedDistance(q0x, q0y, q3x, q3y, v1[4], v1[5]),
dp3 = getSignedDistance(q0x, q0y, q3x, q3y, v1[6], v1[7]),
tMinNew, tMaxNew, tDiff;
// NOTE: the recursion threshold of 4 is needed to prevent issue #571
// from occurring: https://github.com/paperjs/paper.js/issues/571
if (q0x === q3x && uMax - uMin <= epsilon && recursion > 4) {
if (q0x === q3x && uMax - uMin <= tolerance && recursion > 3) {
// The fatline of Q has converged to a point, the clipping is not
// reliable. Return the value we have even though we will miss the
// precision.
@ -1154,7 +1151,7 @@ new function() { // Scope for methods that require numerical integration
curve1, t1, Curve.evaluate(v1, t1, 0),
curve2, t2, Curve.evaluate(v2, t2, 0));
}
} else { // Iterate
} else if (tDiff > 0) { // Iterate
addCurveIntersections(v2, v1, curve2, curve1, locations, include,
uMin, uMax, tMinNew, tMaxNew, tDiff, !reverse, ++recursion);
}
@ -1192,7 +1189,7 @@ new function() { // Scope for methods that require numerical integration
var vx = l2x - l1x,
vy = l2y - l1y;
if (Numerical.isZero(vx))
return vy >= 0 ? l1y - x : x - l1x;
return vy >= 0 ? l1x - x : x - l1x;
var m = vy / vx, // slope
b = l1y - m * l1x; // y offset
// Distance to the linear equation
@ -1411,11 +1408,11 @@ new function() { // Scope for methods that require numerical integration
},
filterIntersections: function(locations, _expand) {
var max = locations.length - 1,
var last = locations.length - 1,
tMax = 1 - /*#=*/Numerical.TOLERANCE;
// Merge intersections very close to the end of a curve to the
// beginning of the next curve.
for (var i = max; i >= 0; i--) {
for (var i = last; i >= 0; i--) {
var loc = locations[i],
next = loc._curve.getNext(),
next2 = loc._curve2.getNext();
@ -1442,18 +1439,18 @@ new function() { // Scope for methods that require numerical integration
: path1._id - path2._id;
}
if (max > 0) {
if (last > 0) {
locations.sort(compare);
// Filter out duplicate locations
for (var i = max; i >= 1; i--) {
if (locations[i].equals(locations[i === 0 ? max : i - 1])) {
// Filter out duplicate locations.
for (var i = last; i > 0; i--) {
if (locations[i].equals(locations[i - 1])) {
locations.splice(i, 1);
max--;
last--;
}
}
}
if (_expand) {
for (var i = max; i >= 0; i--)
for (var i = last; i >= 0; i--)
locations.push(locations[i].getIntersection());
locations.sort(compare);
}

View file

@ -32,30 +32,49 @@
*/
PathItem.inject(new function() {
var operators = {
unite: function(w) {
return w === 1 || w === 0;
},
intersect: function(w) {
return w === 2;
},
subtract: function(w) {
return w === 1;
},
exclude: function(w) {
return w === 1;
}
};
// Boolean operators return true if a curve with the given winding
// contribution contributes to the final result or not. They are called
// for each curve in the graph after curves in the operands are
// split at intersections.
function computeBoolean(path1, path2, operator, subtract) {
function computeBoolean(path1, path2, operation) {
var operator = operators[operation];
// Creates a cloned version of the path that we can modify freely, with
// its matrix applied to its geometry. Calls #reduce() to simplify
// compound paths and remove empty curves, and #reorient() to make sure
// all paths have correct winding direction.
function preparePath(path) {
return path.clone(false).reduce().reorient().transform(null, true);
return path.clone(false).reduce().reorient().transform(null, true,
true);
}
// We do not modify the operands themselves
// The result might not belong to the same type
// We do not modify the operands themselves, but create copies instead,
// fas produced by the calls to preparePath().
// Note that the result paths might not belong to the same type
// i.e. subtraction(A:Path, B:Path):CompoundPath etc.
var _path1 = preparePath(path1),
_path2 = path2 && path1 !== path2 && preparePath(path2);
// Do operator specific calculations before we begin
// Make both paths at clockwise orientation, except when subtract = true
// We need both paths at opposite orientation for subtraction.
if (!_path1.isClockwise())
_path1.reverse();
if (_path2 && !(subtract ^ _path2.isClockwise()))
// Give both paths the same orientation except for subtraction
// and exclusion, where we need them at opposite orientation.
if (_path2 && /^(subtract|exclude)$/.test(operation)
^ (_path2.isClockwise() !== _path1.isClockwise()))
_path2.reverse();
// Split curves at intersections on both paths. Note that for self
// intersection, _path2 will be null and getIntersections() handles it.
@ -134,7 +153,7 @@ PathItem.inject(new function() {
// While subtracting, we need to omit this curve if this
// curve is contributing to the second operand and is
// outside the first operand.
windingSum += subtract && _path2
windingSum += operation === 'subtract' && _path2
&& (path === _path1 && _path2._getWinding(pt, hor)
|| path === _path2 && !_path1._getWinding(pt, hor))
? 0
@ -172,7 +191,8 @@ PathItem.inject(new function() {
* @param {CurveLocation[]} intersections Array of CurveLocation objects
*/
function splitPath(intersections) {
var tolerance = /*#=*/Numerical.TOLERANCE,
var tMin = /*#=*/Numerical.TOLERANCE,
tMax = 1 - tMin,
linearHandles;
function resetLinear() {
@ -182,23 +202,22 @@ PathItem.inject(new function() {
linearHandles[i].set(0, 0);
}
for (var i = intersections.length - 1, curve, prevLoc; i >= 0; i--) {
for (var i = intersections.length - 1, curve, prev; i >= 0; i--) {
var loc = intersections[i],
t = loc._parameter;
// Check if we are splitting same curve multiple times
if (prevLoc && prevLoc._curve === loc._curve
// Avoid dividing with zero
&& prevLoc._parameter > 0) {
// Check if we are splitting same curve multiple times, but avoid
// dividing with zero.
if (prev && prev._curve === loc._curve && prev._parameter > 0) {
// Scale parameter after previous split.
t /= prevLoc._parameter;
t /= prev._parameter;
} else {
curve = loc._curve;
if (linearHandles)
resetLinear();
curve = loc._curve;
linearHandles = curve.isLinear() && [];
if (linearHandles)
linearHandles.push(curve._segment1._handleOut,
curve._segment2._handleIn);
linearHandles = curve.isLinear() ? [
curve._segment1._handleOut,
curve._segment2._handleIn
] : null;
}
var newCurve,
segment;
@ -209,9 +228,9 @@ PathItem.inject(new function() {
if (linearHandles)
linearHandles.push(segment._handleOut, segment._handleIn);
} else {
segment = t < tolerance
segment = t < tMin
? curve._segment1
: t > 1 - tolerance
: t > tMax
? curve._segment2
: curve.getPartLength(0, t) < curve.getPartLength(t, 1)
? curve._segment1
@ -220,7 +239,7 @@ PathItem.inject(new function() {
// Link the new segment with the intersection on the other curve
segment._intersection = loc.getIntersection();
loc._segment = segment;
prevLoc = loc;
prev = loc;
}
if (linearHandles)
resetLinear();
@ -231,14 +250,7 @@ PathItem.inject(new function() {
* with respect to a given set of monotone curves.
*/
function getWinding(point, curves, horizontal, testContains) {
// We need to use a smaller tolerance here than in the rest of the
// library when dealing with curve time parameters and coordinates, in
// order to get really precise values for winding tests. 1e-7 was
// determined through a lot of trial and error, and boolean-test suites.
// Further decreasing it produces new errors.
// The value of 1e-7 also solves issue #559:
// https://github.com/paperjs/paper.js/issues/559
var tolerance = 1e-7,
var tolerance = /*#=*/Numerical.TOLERANCE,
tMin = tolerance,
tMax = 1 - tMin,
x = point.x,
@ -344,10 +356,6 @@ PathItem.inject(new function() {
* @return {Path[]} the contours traced
*/
function tracePaths(segments, operator, selfOp) {
// Choose a default operator which will return all contours
operator = operator || function() {
return true;
};
var paths = [],
// Values for getTangentAt() that are almost 0 and 1.
// TODO: Correctly support getTangentAt(0) / (1)?
@ -485,9 +493,7 @@ PathItem.inject(new function() {
* @return {PathItem} the resulting path item
*/
unite: function(path) {
return computeBoolean(this, path, function(w) {
return w === 1 || w === 0;
}, false);
return computeBoolean(this, path, 'unite');
},
/**
@ -498,9 +504,7 @@ PathItem.inject(new function() {
* @return {PathItem} the resulting path item
*/
intersect: function(path) {
return computeBoolean(this, path, function(w) {
return w === 2;
}, false);
return computeBoolean(this, path, 'intersect');
},
/**
@ -511,9 +515,7 @@ PathItem.inject(new function() {
* @return {PathItem} the resulting path item
*/
subtract: function(path) {
return computeBoolean(this, path, function(w) {
return w === 1;
}, true);
return computeBoolean(this, path, 'subtract');
},
// Compound boolean operators combine the basic boolean operations such
@ -526,7 +528,7 @@ PathItem.inject(new function() {
* @return {Group} the resulting group item
*/
exclude: function(path) {
return new Group([this.subtract(path), path.subtract(this)]);
return computeBoolean(this, path, 'exclude');
},
/**

View file

@ -63,7 +63,7 @@ var Numerical = new function() {
cos = Math.cos,
PI = Math.PI,
isFinite = Number.isFinite,
TOLERANCE = 1e-5,
TOLERANCE = 1e-6,
EPSILON = 1e-13,
MACHINE_EPSILON = 1.12e-16;

View file

@ -72,9 +72,9 @@ var comparators = {
},
Number: function(actual, expected, message, options) {
// Compare with a default tolerance of Numerical.TOLERANCE:
// Compare with a default tolerance of 1e-5:
var ok = Math.abs(actual - expected)
<= Base.pick(options && options.tolerance, Numerical.TOLERANCE);
<= Base.pick(options && options.tolerance, 1e-5);
QUnit.push(ok, ok ? expected : actual, expected, message);
},

View file

@ -0,0 +1,31 @@
/*
* Paper.js - The Swiss Army Knife of Vector Graphics Scripting.
* http://paperjs.org/
*
* Copyright (c) 2011 - 2014, Juerg Lehni & Jonathan Puckey
* http://scratchdisk.com/ & http://jonathanpuckey.com/
*
* Distributed under the MIT license. See LICENSE file for details.
*
* All rights reserved.
*/
module('Path Boolean Operations');
test('path.unite(); #609', function() {
// https://github.com/paperjs/paper.js/issues/609
// path1 and path2 are half circles, applying unite should result in a circle
var path1 = new Path();
path1.moveTo(new Point(100, 100));
path1.arcTo(new Point(100, 200));
path1.closePath();
var path2 = new Path();
path2.moveTo(new Point(100, 200));
path2.arcTo(new Point(100, 100));
path2.closePath();
var path3 = path1.unite(path2);
equals(path3.pathData, 'M100,100c27.61424,0 50,22.38576 50,50c0,27.61424 -22.38576,50 -50,50z M100,200c-27.61424,0 -50,-22.38576 -50,-50c0,-27.61424 22.38576,-50 50,-50z', 'path3.pathData');
});

View file

@ -0,0 +1,100 @@
/*
* Paper.js - The Swiss Army Knife of Vector Graphics Scripting.
* http://paperjs.org/
*
* Copyright (c) 2011 - 2014, Juerg Lehni & Jonathan Puckey
* http://scratchdisk.com/ & http://jonathanpuckey.com/
*
* Distributed under the MIT license. See LICENSE file for details.
*
* All rights reserved.
*/
module('Path Intersections');
function testIntersection(intersections, results) {
equals(intersections.length, results.length, 'intersections.length');
for (var i = 0; i < results.length; i++) {
var inter = intersections[i];
var values = results[i];
var name = 'intersections[' + i + ']';
equals(inter.point, new Point(values.point), name + '.point');
equals(inter.index, values.index, name + '.index');
equals(inter.parameter, values.parameter || 0, name + '.parameter');
}
}
test('path.getIntersections(); #565', function() {
// https://github.com/paperjs/paper.js/issues/565
var crv1 = new Curve(new Point(421.75945, 416.40481), new Point(-181.49299, -224.94946), new Point(44.52004, -194.13319), new Point(397.47615, 331.34712));
var crv2 = new Curve(new Point(360.09446, 350.97254), new Point(-58.58867, -218.45806), new Point(-109.55091, -220.99561), new Point(527.83582, 416.79948));
var pth1 = new Path([crv1.segment1, crv1.segment2]);
var pth2 = new Path([crv2.segment1, crv2.segment2]);
testIntersection(crv1.getIntersections(crv2), [
{ point: { x: 354.13635, y: 220.81369 }, index: 0, parameter: 0.46725 },
{ point: { x: 390.24772, y: 224.27351 }, index: 0, parameter: 0.71605 }
]);
// Alternative pair of curves that has the same issue
var crv1 = new Curve(new Point(484.9026237381622, 404.11001967731863), new Point(-265.1185871567577, -204.00749347172678), new Point(-176.7118886578828, 111.96015905588865), new Point(438.8191690435633, 429.0297837462276));
var crv2 = new Curve(new Point(388.25280445162207, 490.95032326877117), new Point(-194.0586572047323, -50.77360603027046), new Point(-184.71034923568368, -260.5346686206758), new Point(498.41401199810207, 455.55853731930256)); var pth1 = new Path([crv1.segment1, crv1.segment2]);
var pth1 = new Path([crv1.segment1, crv1.segment2]);
var pth2 = new Path([crv2.segment1, crv2.segment2]);
testIntersection(crv1.getIntersections(crv2), [
{ point: { x: 335.62744, y: 338.15939 }, index: 0, parameter: 0.26516 }
]);
});
test('path.getIntersections(); #568', function() {
// https://github.com/paperjs/paper.js/issues/568
var crv1 = new Curve(new Point(509.05465863179415, 440.1211663847789), new Point(233.6728838738054, -245.8216403145343), new Point(-270.755685120821, 53.14275110140443), new Point(514.079892472364, 481.95262297522277));
var crv2 = new Curve(new Point(542.1666181180626, 451.06309361290187), new Point(179.91238399408758, 148.68241581134498), new Point(193.42650789767504, -47.97609066590667), new Point(423.66228222381324, 386.3876062911004));
var pth1 = new Path([crv1.segment1, crv1.segment2]);
var pth2 = new Path([crv2.segment1, crv2.segment2]);
testIntersection(crv1.getIntersections(crv2), [
{ point: { x: 547.96568, y: 396.66339 }, index: 0, parameter: 0.07024 },
{ point: { x: 504.79973, y: 383.37886 }, index: 0, parameter: 0.48077 }
]);
var crv1 = new Curve(new Point(0, 0), new Point(20, 40) , new Point (-30, -50), new Point(50, 50));
var crv2 = new Curve(new Point(50, 50), new Point(20, 100), new Point (-30, -120), new Point(250, 250));
var pth1 = new Path([crv1.segment1, crv1.segment2]);
var pth2 = new Path([crv2.segment1, crv2.segment2]);
testIntersection(crv1.getIntersections(crv2), [
{ point: { x: 50, y: 50 }, index: 0, parameter: 1 }
]);
});
test('path.getIntersections(); #570', function() {
// https://github.com/paperjs/paper.js/issues/570
var crv1 = new Curve(new Point(171, 359), new Point(65.26926656546078, 62.85188632229557), new Point(-37.43795644844329, 7.813022000754188), new Point(311.16034791674826, 406.2985255840872));
var crv2 = new Curve(new Point(311.16034791674826, 406.2985255840872), new Point(39.997020018940304, -8.347079462067768), new Point(-73.86292504547487, -77.47859270504358), new Point(465, 467));
var pth1 = new Path([crv1.segment1, crv1.segment2]);
var pth2 = new Path([crv2.segment1, crv2.segment2]);
var ints = crv1.getIntersections(crv2);
testIntersection(crv1.getIntersections(crv2), [
{ point: { x: 311.16035, y: 406.29853 }, index: 0, parameter: 1 }
]);
});
test('path.getIntersections(); #571', function() {
// https://github.com/paperjs/paper.js/issues/571
var crv1 = new Curve(new Point(171, 359), new Point(205.3908899553486, -14.994581100305595), new Point(5.767644819815757, 28.49094950835297), new Point(420.1235851920127, 275.8351912321666));
var crv2 = new Curve(new Point(420.1235851920127, 275.8351912321666), new Point(-10.77224553077383, -53.21262197949682), new Point(-259.2129470250785, -258.56165821345775), new Point(465, 467));
var pth1 = new Path([crv1.segment1, crv1.segment2]);
var pth2 = new Path([crv2.segment1, crv2.segment2]);
testIntersection(crv1.getIntersections(crv2), [
{ point: { x: 352.39945, y: 330.44135 }, index: 0, parameter: 0.41159 },
{ point: { x: 420.12359, y: 275.83519 }, index: 0, parameter: 1 }
]);
});
test('path.getIntersections(); overlapping circles', function() {
var c1 = new Path.Circle(new paper.Point(50, 50), 50);
var c2 = new Path.Circle(new paper.Point(100, 100), 50);
testIntersection(c1.getIntersections(c2), [
{ point: { x: 100, y: 50 }, index: 2 },
{ point: { x: 50, y: 100 }, index: 3 }
]);
});

View file

@ -35,6 +35,8 @@
/*#*/ include('Path_Curves.js');
/*#*/ include('Path_Bounds.js');
/*#*/ include('Path_Length.js');
/*#*/ include('Path_Intersections.js');
/*#*/ include('Path_Boolean.js');
/*#*/ include('Curve.js');
/*#*/ include('CurveLocation.js');