Include new, improved point in path algorithm based on winding number.

It's also possible to switch to using the canvas's native isPointInPath() through options.nativeContains
This commit is contained in:
Jürg Lehni 2013-10-18 14:22:59 +02:00
parent eae526f38c
commit 4f27be8f12
7 changed files with 226 additions and 126 deletions

View file

@ -286,8 +286,19 @@
strokeWidth: 1
};
function convertToPath(item) {
if (item instanceof Shape) {
var path = item.toPath();
item.remove();
return path;
}
return item;
}
// Better if path1 and path2 fit nicely inside a 200x200 pixels rect
function testBooleanStatic(path1, path2) {
path1 = convertToPath(path1);
path2 = convertToPath(path2);
path1.style = path2.style = pathStyleNormal;
if (operations.union) {

View file

@ -227,7 +227,7 @@ var BlendMode = new function() {
// Now test for the new blend modes. Just seeing if globalCompositeOperation
// is sticky is not enough, as Chome 27 pretends for blend-modes to work,
// but does not actually apply them.
// but does not actually apply them.
var ctx = CanvasProvider.getContext(1, 1);
Base.each(modes, function(func, mode) {
// Blend #330000 (51) and #aa0000 (170):

View file

@ -22,5 +22,6 @@ var options = {
svg: true,
fatline: true,
paperscript: true,
nativeContains: false,
debug: false
};

View file

@ -192,19 +192,42 @@ var CompoundPath = PathItem.extend(/** @lends CompoundPath# */{
* Item#contains() is prepared for such a result.
*/
_contains: function(point) {
/*#*/ if (options.nativeContains) {
// To compare with native canvas approach:
var ctx = CanvasProvider.getContext(1, 1),
children = this._children,
param = Base.merge({ compound: true });
// Return early if the compound path doesn't have any children:
if (children.length === 0)
return [];
ctx.beginPath();
for (var i = 0, l = children.length; i < l; i++)
children[i]._draw(ctx, param);
var res = ctx.isPointInPath(point.x, point.y);
CanvasProvider.release(ctx);
return res && children;
/*#*/ } // options.nativeContains
// Compound paths are a little complex: In order to determine whether a
// point is inside a path or not due to the even-odd rule, we need to
// check all the children and count how many intersect. If it's an odd
// number, the point is inside the path. Once we know it's inside the
// path, _hitTest also needs access to the first intersecting element,
// for the HitResult, so we collect and return a list here.
var children = [];
var total = 0,
children = [];
for (var i = 0, l = this._children.length; i < l; i++) {
var child = this._children[i];
if (child.contains(point))
var child = this._children[i],
winding = child._getWinding(point);
total += winding;
/*
if (winding & 1)
children.push(child);
*/
if (winding)
children.push(child);
}
return (children.length & 1) == 1 && children;
return total && children; // <- non-zero // even-odd: (total & 1) && children;
},
_hitTest: function _hitTest(point, options) {

View file

@ -535,7 +535,7 @@ statics: {
// Converts from the point coordinates (p1, c1, c2, p2) for one axis to
// the polynomial coefficients and solves the polynomial for val
solveCubic: function (v, coord, val, roots) {
solveCubic: function (v, coord, val, roots, min, max) {
var p1 = v[coord],
c1 = v[coord + 2],
c2 = v[coord + 4],
@ -543,7 +543,7 @@ statics: {
c = 3 * (c1 - p1),
b = 3 * (c2 - c1) - c,
a = p2 - p1 - c - b;
return Numerical.solveCubic(a, b, c, p1 - val, roots);
return Numerical.solveCubic(a, b, c, p1 - val, roots, min, max);
},
getParameterOf: function(v, x, y) {
@ -642,68 +642,8 @@ statics: {
return new Rectangle(min[0], min[1], max[0] - min[0], max[1] - min[1]);
},
_getCrossings: function(v, prev, x, y, roots) {
// Implementation of the crossing number algorithm:
// http://en.wikipedia.org/wiki/Point_in_polygon
// Solve the y-axis cubic polynomial for y and count all solutions
// to the right of x as crossings.
var count = Curve.solveCubic(v, 1, y, roots),
crossings = 0,
tolerance = /*#=*/ Numerical.TOLERANCE,
abs = Math.abs;
// Checks the y-slope between the current curve and the previous for a
// change of orientation, when a solution is found at t == 0
function changesOrientation(tangent) {
return Curve.evaluate(prev, 1, 1).y
* tangent.y > 0;
}
if (count === -1) {
// Infinite solutions, so we have a horizontal curve.
// Find parameter through getParameterOf()
roots[0] = Curve.getParameterOf(v, x, y);
count = roots[0] !== null ? 1 : 0;
}
for (var i = 0; i < count; i++) {
var t = roots[i];
if (t > -tolerance && t < 1 - tolerance) {
var pt = Curve.evaluate(v, t, 0);
if (x < pt.x + tolerance) {
// Pass 1 for Curve.evaluate() type to calculate tangent
var tan = Curve.evaluate(v, t, 1);
// Handle all kind of edge cases when points are on
// contours or rays are touching countours, to termine
// whether the crossing counts or not.
// See if the actual point is on the countour:
if (abs(pt.x - x) < tolerance) {
// Do not count the crossing if it is on the left hand
// side of the shape (tangent pointing upwards), since
// the ray will go out the other end, count as crossing
// there, and the point is on the contour, so to be
// considered inside.
var angle = tan.getAngle();
if (angle > -180 && angle < 0
// Handle special case where point is on a corner,
// in which case this crossing is skipped if both
// tangents have the same orientation.
&& (t > tolerance || changesOrientation(tan)))
continue;
} else {
// Skip touching stationary points:
if (abs(tan.y) < tolerance
// Check derivate for stationary points. If root is
// close to 0 and not changing vertical orientation
// from the previous curve, do not count this root,
// as it's touching a corner.
|| t < tolerance && !changesOrientation(tan))
continue;
}
crossings++;
}
}
}
return crossings;
isClockwise: function(v) {
return Curve._getEdgeSum(v) > 0;
},
/**
@ -750,6 +690,131 @@ statics: {
+ t * t * t * v3,
padding);
}
},
_getWinding: function(v, x, y, roots1, roots2) {
var tolerance = /*#=*/ Numerical.TOLERANCE,
abs = Math.abs;
// Implementation of the crossing number algorithm:
// http://en.wikipedia.org/wiki/Point_in_polygon
// Solve the y-axis cubic polynomial for y and count all solutions
// to the right of x as crossings.
if (Curve.isLinear(v)) {
// Special case for handling lines.
var y0 = v[1],
y1 = v[7],
dir = 1;
if (y0 > y1) {
var tmp = y0;
y0 = y1;
y1 = tmp;
dir = -1;
}
if (y < y0 || y > y1)
return 0;
var cross = (v[6] - v[0]) * (y - v[1]) - (v[7] - v[1]) * (x - v[0]);
if ((cross < -tolerance ? -1 : 1) == dir)
dir = 0;
return dir;
}
// Handle bezier curves. We need to chop them into smaller curves with
// defined orientation, by solving the derrivative curve for Y extrema.
var y0 = v[1],
y1 = v[3],
y2 = v[5],
y3 = v[7];
// Split the curve at Y extremas, to get mono bezier curves
var a = 3 * (y1 - y2) - y0 + y3,
b = 2 * (y0 + y2) - 4 * y1,
c = y1 - y0,
// Keep then range to 0 .. 1 (excluding) in the search for y extrema
count = Numerical.solveQuadratic(a, b, c, roots1, tolerance,
1 - tolerance);
var winding = 0,
left,
right = v;
var t = roots1[0];
for (var i = 0; i <= count; i++) {
if (i === count) {
left = right;
} else {
// Divide the curve at t.
var curves = Curve.subdivide(right, t);
left = curves[0];
right = curves[1];
t = roots1[i];
// TODO: Watch for divide by 0
// Now renormalize t to the range of the next iteration.
t = (roots1[i + 1] - t) / (1 - t);
}
// Make sure that the connecting y extrema are flat
if (i > 0)
left[3] = left[1]; // curve2.handle1.y = curve2.point1.y;
if (i < count)
left[5] = right[1]; // curve1.handle2.y = curve2.point1.y;
var dir = 1;
if (left[1] > left[7]) {
left = [
left[6], left[7],
left[4], left[5],
left[2], left[3],
left[0], left[1]
];
dir = -1;
}
if (y < left[1] || y > left[7])
continue;
// Adjust start and end range depending on if curve was flipped.
// In normal orientation we exclude the end point since it's also
// the start point of the next curve. If flipped, we have to exclude
// the end point instead.
var min = -tolerance * dir,
root,
xt;
if (Curve.solveCubic(left, 1, y, roots2, min, 1 + min) === 1) {
root = roots2[0];
xt = Curve.evaluate(left, root, 0).x;
} else {
var mid = (left[1] + left[7]) / 2;
xt = y < mid ? left[0] : left[6];
root = y < mid ? 0 : 1;
// Filter out end points based on direction.
if (dir < 0 && root === 0 && abs(y - left[1]) < tolerance ||
dir > 0 && root === 1 && abs(y - left[7]) < tolerance)
continue;
}
// See if we're touching a horizontal stationary point.
var flat = abs(Curve.evaluate(left, root, 1).y) < tolerance;
// Calculate compare tolerance based on curve orientation (dir), to
// add a bit of tolerance when considering points lying on the curve
// as inside. But if we're touching a horizontal stationary point,
// set compare tolerance to -tolerance, since we don't want to step
// side-ways in tolerance based on orientation. This is needed e.g.
// when touching the bottom tip of a circle.
// Pass 1 for Curve.evaluate() type to calculate tangent
if (x >= xt + (flat ? -tolerance : tolerance * dir)) {
// If this is a horizontal stationary point, and we're at the
// end of the curve, flip the orientation of dir.
winding += flat && abs(root - 1) < tolerance ? -dir : dir;
}
}
return winding;
},
_getEdgeSum: function(v) {
// Method derived from:
// http://stackoverflow.com/questions/1165647
// We treat the curve points and handles as the outline of a polygon of
// which we determine the orientation using the method of calculating
// the sum over the edges. This will work even with non-convex polygons,
// telling you whether it's mostly clockwise
var sum = 0;
for (var j = 2; j < 8; j += 2)
sum += (v[j - 2] - v[j]) * (v[j + 1] + v[j - 1]);
return sum;
}
}}, Base.each(['getBounds', 'getStrokeBounds', 'getHandleBounds', 'getRoughBounds'],
// Note: Although Curve.getBounds() exists, we are using Path.getBounds() to

View file

@ -1684,7 +1684,7 @@ var Path = PathItem.extend(/** @lends Path# */{
return null;
},
_contains: function(point) {
_getWinding: function(point) {
var closed = this._closed;
// If the path is not closed, we should not bail out in case it has a
// fill color!
@ -1692,44 +1692,55 @@ var Path = PathItem.extend(/** @lends Path# */{
// We need to call the internal _getBounds, to get non-
// transformed bounds.
|| !this._getBounds('getRoughBounds')._containsPoint(point))
return false;
// Note: This only works correctly with even-odd fill rule, or paths
// that do not overlap with themselves.
// TODO: Find out how to implement the "Point In Polygon" problem for
// non-zero fill rule.
return 0;
// Use the crossing number algorithm, by counting the crossings of the
// beam in right y-direction with the shape, and see if it's an odd
// number, meaning the starting point is inside the shape.
// http://en.wikipedia.org/wiki/Point_in_polygon
var curves = this.getCurves(),
segments = this._segments,
crossings = 0,
// Reuse one array for root-finding, give garbage collector a break
roots = new Array(3),
winding = 0,
// Reuse arrays for root-finding, give garbage collector a break
roots1 = [],
roots2 = [],
last = (closed
? curves[curves.length - 1]
// Create a straight closing line for open paths, just like
// how filling open paths works.
: new Curve(segments[segments.length - 1]._point,
segments[0]._point)).getValues(),
previous = last;
segments[0]._point)).getValues();
for (var i = 0, l = curves.length; i < l; i++) {
var vals = curves[i].getValues(),
x = vals[0],
y = vals[1];
// Filter out curves with 0-lenght (all 4 points in the same place):
// Filter out curves with 0-length (all 4 points in the same place):
if (!(x === vals[2] && y === vals[3] && x === vals[4]
&& y === vals[5] && x === vals[6] && y === vals[7])) {
crossings += Curve._getCrossings(vals, previous,
point.x, point.y, roots);
previous = vals;
winding += Curve._getWinding(vals, point.x, point.y,
roots1, roots2);
}
}
if (!closed) {
crossings += Curve._getCrossings(last, previous, point.x, point.y,
roots);
winding += Curve._getWinding(last, point.x, point.y,
roots1, roots2);
}
return (crossings & 1) === 1;
return winding;
},
_contains: function(point) {
/*#*/ if (options.nativeContains) {
// To compare with native canvas approach:
var ctx = CanvasProvider.getContext(1, 1);
this._draw(ctx, Base.merge({ clip: true }));
var res = ctx.isPointInPath(point.x, point.y);
CanvasProvider.release(ctx);
return res;
/*#*/ } // options.nativeContains
// even-odd:
// return !!(this._getWinding(point) & 1);
// non-zero:
return !!this._getWinding(point);
},
_hitTest: function(point, options) {
@ -1779,16 +1790,13 @@ var Path = PathItem.extend(/** @lends Path# */{
function isInArea(point) {
var length = area.length,
previous = getAreaCurve(length - 1),
roots = new Array(3),
crossings = 0;
for (var i = 0; i < length; i++) {
var curve = getAreaCurve(i);
crossings += Curve._getCrossings(curve, previous,
point.x, point.y, roots);
previous = curve;
}
return (crossings & 1) === 1;
roots1 = [],
roots2 = [],
winding = 0;
for (var i = 0; i < length; i++)
winding += Curve._getWinding(getAreaCurve(i), point.x, point.y,
roots1, roots2);
return !!winding;
}
function checkSegmentStroke(segment) {
@ -2410,35 +2418,11 @@ statics: {
* @private
*/
isClockwise: function(segments) {
var sum = 0,
xPre, yPre,
add = false;
function edge(x, y) {
if (add)
sum += (xPre - x) * (y + yPre);
xPre = x;
yPre = y;
add = true;
}
// Method derived from:
// http://stackoverflow.com/questions/1165647
// We treat the curve points and handles as the outline of a polygon of
// which we determine the orientation using the method of calculating
// the sum over the edges. This will work even with non-convex polygons,
// telling you whether it's mostly clockwise
var sum = 0;
// TODO: Check if this works correctly for all open paths.
for (var i = 0, l = segments.length; i < l; i++) {
var seg1 = segments[i],
seg2 = segments[i + 1 < l ? i + 1 : 0],
point1 = seg1._point,
handle1 = seg1._handleOut,
handle2 = seg2._handleIn,
point2 = seg2._point;
edge(point1._x, point1._y);
edge(point1._x + handle1._x, point1._y + handle1._y);
edge(point2._x + handle2._x, point2._y + handle2._y);
edge(point2._x, point2._y);
}
for (var i = 0, l = segments.length; i < l; i++)
sum += Curve._getEdgeSum(Curve.getValues(segments[i],
segments[i + 1 < l ? i + 1 : 0]));
return sum > 0;
},
@ -2690,7 +2674,9 @@ statics: {
// possible.
var strokeWidth = style.getStrokeColor() ? style.getStrokeWidth() : 0,
joinWidth = strokeWidth;
if (strokeWidth > 0) {
if (strokeWidth === 0) {
strokeWidth = /*#=*/ Numerical.TOLERANCE;
} else {
if (style.getStrokeJoin() === 'miter')
joinWidth = strokeWidth * style.getMiterLimit();
if (style.getStrokeCap() === 'square')

View file

@ -153,3 +153,17 @@ test('Path#contains() (Rotated Rectangle Contours)', function() {
testPoint(path, curves[i].getPoint(0.5), true);
}
});
test('Path#contains() (touching stationary point with changing orientation)', function() {
var path = new Path({
segments: [
new Segment([100, 100]),
new Segment([200, 200], [-50, 0], [50, 0]),
new Segment([300, 300]),
new Segment([300, 100])
],
closed: true
});
testPoint(path, new Point(200, 200), true);
})