Winding: Add back support for open paths.

Includes refactoring of some related code.
This commit is contained in:
Jürg Lehni 2016-07-16 20:01:36 +02:00
parent 7d675dab13
commit b1037f89f1
2 changed files with 180 additions and 118 deletions

View file

@ -622,49 +622,56 @@ statics: /** @lends Curve */{
]; ];
}, },
/** /**
* Splits the specified curve values into segments of curves that are * Splits the specified curve values into curves that are monotone in the
* monotone in the specified coordinate direction (0: monotone in * specified coordinate direction.
* x-direction, 1: monotone in y-direction. If the curve is already *
* monotone, an array only containing the original values will be returned. * @param {Number[]} v the curve values, as returned by
* {@link Curve#getValues()}
* @param {Number} [dir=0] the direction in which the curves should be
* monotone, `0`: monotone in x-direction, `1`: monotone in y-direction
* @return {Number[][]} an array of curve value arrays of the resulting
* monotone curve. If the original curve was already monotone, an array
* only containing its values are returned.
*/ */
getMonoCurves: function(v, coord) { getMonoCurves: function(v, dir) {
var curves = []; var curves = [];
// #getLength() is a rather expensive operation, therefore we test two // #getLength() is a rather expensive operation, therefore we test two
// cheap preconditions first. // cheap preconditions first.
if (v[0] === v[6] && v[1] === v[7] && Curve.getLength(v) === 0) if (v[0] !== v[6] || v[1] === v[7] || Curve.getLength(v) !== 0) {
return curves; // Determine the ordinate index in the curve values array.
var o0 = v[1 - coord], var io = dir ? 0 : 1,
o1 = v[3 - coord], o0 = v[io],
o2 = v[5 - coord], o1 = v[io + 2],
o3 = v[7 - coord]; o2 = v[io + 4],
if ((o0 >= o1) === (o1 >= o2) && (o1 >= o2) === (o2 >= o3) o3 = v[io + 6];
|| Curve.isStraight(v)) { if ((o0 >= o1) === (o1 >= o2) && (o1 >= o2) === (o2 >= o3)
// Straight curves and curves with points and control points ordered || Curve.isStraight(v)) {
// in coordinate direction are guaranteed to be monotone. // Straight curves and curves with all involved points ordered
curves.push(v); // in coordinate direction are guaranteed to be monotone.
} else {
var a = 3 * (o1 - o2) - o0 + o3,
b = 2 * (o0 + o2) - 4 * o1,
c = o1 - o0,
tMin = 4e-7,
tMax = 1 - tMin,
roots = [],
n = Numerical.solveQuadratic(a, b, c, roots, tMin, tMax);
if (n === 0) {
curves.push(v); curves.push(v);
} else { } else {
roots.sort(); var a = 3 * (o1 - o2) - o0 + o3,
var t = roots[0], b = 2 * (o0 + o2) - 4 * o1,
parts = Curve.subdivide(v, t); c = o1 - o0,
curves.push(parts[0]); tMin = 4e-7,
if (n > 1) { tMax = 1 - tMin,
t = (roots[1] - t) / (1 - t); roots = [],
parts = Curve.subdivide(parts[1], t); n = Numerical.solveQuadratic(a, b, c, roots, tMin, tMax);
if (n === 0) {
curves.push(v);
} else {
roots.sort();
var t = roots[0],
parts = Curve.subdivide(v, t);
curves.push(parts[0]); curves.push(parts[0]);
if (n > 1) {
t = (roots[1] - t) / (1 - t);
parts = Curve.subdivide(parts[1], t);
curves.push(parts[0]);
}
curves.push(parts[1]);
} }
curves.push(parts[1]);
} }
} }
return curves; return curves;

View file

@ -295,7 +295,7 @@ PathItem.inject(new function() {
return results || locations; return results || locations;
} }
function getWinding(point, curves, horizontal) { function getWinding(point, curves, dir) {
var epsilon = /*#=*/Numerical.WINDING_EPSILON, var epsilon = /*#=*/Numerical.WINDING_EPSILON,
abs = Math.abs, abs = Math.abs,
windingL = 0, windingL = 0,
@ -305,34 +305,39 @@ PathItem.inject(new function() {
onPathWinding = 0, onPathWinding = 0,
isOnPath = false, isOnPath = false,
prevV, prevV,
coord = horizontal ? 1 : 0, closeV,
po = horizontal ? point.x : point.y, // point's abscissa // Determine the index of the abscissa and ordinate values in the
pa = horizontal ? point.y : point.x, // point's ordinate // curve values arrays, based on the direction:
ia = dir ? 1 : 0, // the abscissa index
io = dir ? 0 : 1, // the ordinate index
pv = [point.x, point.y];
pa = pv[ia], // the point's abscissa
po = pv[io], // the point's ordinate
aBefore = pa - epsilon, aBefore = pa - epsilon,
aAfter = pa + epsilon; aAfter = pa + epsilon;
function addWinding(v) { function addWinding(v) {
var vo0 = v[1 - coord], var o0 = v[io],
vo3 = v[7 - coord]; o3 = v[io + 6];
if (vo0 > po && vo3 > po || if (o0 > po && o3 > po ||
vo0 < po && vo3 < po) { o0 < po && o3 < po) {
// If curve is outside the ordinates' range, no intersection // If curve is outside the ordinates' range, no intersection
// with the ray is possible. // with the ray is possible.
return v; return v;
} }
var va0 = v[coord], var a0 = v[ia],
va1 = v[2 + coord], a1 = v[ia + 2],
va2 = v[4 + coord], a2 = v[ia + 4],
va3 = v[6 + coord]; a3 = v[ia + 6];
if (vo0 === vo3) { if (o0 === o3) {
// A horizontal curve is not necessarily between two non- // A horizontal curve is not necessarily between two non-
// horizontal curves. We have to take cases like these into // horizontal curves. We have to take cases like these into
// account: // account:
// +-----+ // +-----+
// ----+ | // +----+ |
// +-----+ // +-----+
if (va1 <= aAfter && va3 >= aBefore || if (a1 <= aAfter && a3 >= aBefore ||
va3 <= aAfter && va1 >= aBefore) { a3 <= aAfter && a1 >= aBefore) {
isOnPath = true; isOnPath = true;
} }
// If curve does not change in ordinate direction, windings will // If curve does not change in ordinate direction, windings will
@ -340,20 +345,20 @@ PathItem.inject(new function() {
return prevV; return prevV;
} }
var roots = [], var roots = [],
a = po === vo0 ? va0 a = po === o0 ? a0
: po === vo3 ? va3 : po === o3 ? a3
: ( va0 < aBefore && va1 < aBefore && : ( a0 < aBefore && a1 < aBefore &&
va2 < aBefore && va3 < aBefore) || a2 < aBefore && a3 < aBefore) ||
( va0 > aAfter && va1 > aAfter && ( a0 > aAfter && a1 > aAfter &&
va2 > aAfter && va3 > aAfter) a2 > aAfter && a3 > aAfter)
? (va0 + va3) / 2 ? (a0 + a3) / 2
: Curve.solveCubic(v, coord ? 0 : 1, po, roots, 0, 1) === 1 : Curve.solveCubic(v, io, po, roots, 0, 1) === 1
? Curve.getPoint(v, roots[0])[coord ? 'y' : 'x'] ? Curve.getPoint(v, roots[0])[dir ? 'y' : 'x']
: (va0 + va3) / 2; : (a0 + a3) / 2;
var winding = vo0 > vo3 ? 1 : -1, var winding = o0 > o3 ? 1 : -1,
prevWinding = prevV[1 - coord] > prevV[7 - coord] ? 1 : -1, prevWinding = prevV[io] > prevV[io + 6] ? 1 : -1,
prevAEnd = prevV[6 + coord]; prevAEnd = prevV[ia + 6];
if (po !== vo0) { if (po !== o0) {
// Standard case, curve is crossed by not at its start point. // Standard case, curve is crossed by not at its start point.
if (a < aBefore) { if (a < aBefore) {
pathWindingL += winding; pathWindingL += winding;
@ -379,7 +384,7 @@ PathItem.inject(new function() {
// horizontal and the current curve. // horizontal and the current curve.
isOnPath = true; isOnPath = true;
if (prevAEnd < aBefore) { if (prevAEnd < aBefore) {
// left winding was added before, now add right winding // left winding was added before, now add right winding.
pathWindingR += winding; pathWindingR += winding;
} else if (prevAEnd > aAfter) { } else if (prevAEnd > aAfter) {
// right winding was added before, not add left winding. // right winding was added before, not add left winding.
@ -389,47 +394,83 @@ PathItem.inject(new function() {
return v; return v;
} }
function handleCurve(v) {
// Get the ordinates:
var o0 = v[io],
o1 = v[io + 2],
o2 = v[io + 4],
o3 = v[io + 6];
// Only handle curves that can cross the point's ordinate.
if ((o0 >= po || o1 >= po || o2 >= po || o3 >= po) &&
(o0 <= po || o1 <= po || o2 <= po || o3 <= po)) {
// Get the abscissas:
var a0 = v[ia],
a1 = v[ia + 2],
a2 = v[ia + 4],
a3 = v[ia + 6],
// Get monotone curves. If the curve is outside the point's
// abscissa, it can be treated as a monotone curve:
monoCurves = ( a0 < aBefore && a1 < aBefore &&
a2 < aBefore && a3 < aBefore) ||
( a0 > aAfter && a1 > aAfter &&
a2 > aAfter && a3 > aAfter)
? [v] : Curve.getMonoCurves(v, dir);
for (var i = 0; i < monoCurves.length; i++) {
prevV = addWinding(monoCurves[i]);
}
}
}
for (var i = 0, l = curves.length; i < l; i++) { for (var i = 0, l = curves.length; i < l; i++) {
var curve = curves[i], var curve = curves[i],
path = curve.getPath(); path = curve._path,
if (i === 0 || curves[i - 1].getPath() !== path) { v = curve.getValues();
// On new path, determine values of last non-horizontal curve. if (i === 0 || curves[i - 1]._path !== path) {
// We're on a new (sub-)path, so we need to determine values of
// the last non-horizontal curve on this path.
prevV = null; prevV = null;
var curvePrev = curve.getPrevious(); // If the path is not closed, connect the end points with a
while (!prevV && curvePrev && curvePrev != curve) { // straight curve, just like how filling open paths works.
var v2 = curvePrev.getValues(); if (!path._closed) {
if (v2[1 - coord] != v2[7 - coord]) var p1 = path.getLastCurve().getPoint2(),
prevV = v2; p2 = curve.getPoint1(),
curvePrev = curvePrev.getPrevious(); x1 = p1._x, y1 = p1._y,
x2 = p2._x, y2 = p2._y;
closeV = [x1, y1, x1, y1, x2, y2, x2, y2];
// The closing curve is a potential candidate for the last
// non-horizontal curve.
if (closeV[io] !== closeV[io + 6]) {
prevV = closeV;
}
} }
prevV = prevV || curve.getValues();
} if (!prevV) {
var v = curve.getValues(), // Walk backwards through list of the path's curves until we
// Get the ordinates: // find one that is not horizontal.
vo0 = v[1 - coord], // Fall-back to the first curve's values if none is found:
vo1 = v[3 - coord], prevV = v;
vo2 = v[5 - coord], var prev = path.getLastCurve();
vo3 = v[7 - coord]; while (prev && prev !== curve) {
// Only handle curves that can cross the point's ordinate var v2 = prev.getValues();
if ((vo0 >= po || vo1 >= po || vo2 >= po || vo3 >= po) && if (v2[io] !== v2[io + 6]) {
(vo0 <= po || vo1 <= po || vo2 <= po || vo3 <= po)) { prevV = v2;
var va0 = v[coord], break;
va1 = v[2 + coord], }
va2 = v[4 + coord], prev = prev.getPrevious();
va3 = v[6 + coord]; }
// Get monotone curves. If the curve is outside the point's
// abscissa, it can be treated as a monotone curve
var monoCurves = ( va0 < aBefore && va1 < aBefore &&
va2 < aBefore && va3 < aBefore) ||
( va0 > aAfter && va1 > aAfter &&
va2 > aAfter && va3 > aAfter)
? [v] : Curve.getMonoCurves(v, coord);
for (var j = 0; j < monoCurves.length; j++) {
prevV = addWinding(monoCurves[j]);
} }
} }
var nextCurve = curves[i + 1];
if (!nextCurve || nextCurve.getPath() !== path) { handleCurve(v);
if (i + 1 === l || curves[i + 1]._path !== path) {
// We're at the last curve of the current (sub-)path. If a
// closing curve was calculated at the beginning of it, handle
// it now to treat the path as closed:
if (closeV) {
handleCurve(closeV);
closeV = null;
}
if (!pathWindingL && !pathWindingR && isOnPath) { if (!pathWindingL && !pathWindingR && isOnPath) {
// Use the on-path windings if no other intersections // Use the on-path windings if no other intersections
// were found or if they canceled each other. // were found or if they canceled each other.
@ -487,16 +528,20 @@ PathItem.inject(new function() {
parent = path._parent, parent = path._parent,
t = curve.getTimeAt(length), t = curve.getTimeAt(length),
pt = curve.getPointAtTime(t), pt = curve.getPointAtTime(t),
hor = Math.abs(curve.getTangentAtTime(t).normalize().y) < 0.5; // Determine whether to check winding in horizontal or
// vertical direction from the point based on the curve's
// direction at the given point.
dir = Math.abs(curve.getTangentAtTime(t).normalize().y)
< 0.5 ? 1 : 0;
if (parent instanceof CompoundPath) if (parent instanceof CompoundPath)
path = parent; path = parent;
// While subtracting, we need to omit this curve if it is // While subtracting, we need to omit this curve if it is
// contributing to the second operand and is outside the // contributing to the second operand and is outside the
// first operand. // first operand.
winding = !(operator.subtract && path2 && ( winding = !(operator.subtract && path2 && (
path === path1 && path2._getWinding(pt, hor) || path === path1 && path2._getWinding(pt, dir) ||
path === path2 && !path1._getWinding(pt, hor))) path === path2 && !path1._getWinding(pt, dir)))
? getWinding(pt, curves, hor) ? getWinding(pt, curves, dir)
: { winding: 0 }; : { winding: 0 };
break; break;
} }
@ -714,8 +759,8 @@ PathItem.inject(new function() {
* part of a horizontal curve * part of a horizontal curve
* @return {Number} the winding number * @return {Number} the winding number
*/ */
_getWinding: function(point, horizontal) { _getWinding: function(point, dir) {
return getWinding(point, this.getCurves(), horizontal).winding; return getWinding(point, this.getCurves(), dir).winding;
}, },
/** /**
@ -972,16 +1017,25 @@ Path.inject(/** @lends Path# */{
prevWinding = 0; prevWinding = 0;
// Get values for all y-monotone curves that intersect the ray at y. // Get values for all y-monotone curves that intersect the ray at y.
for (var i = 0, l = curves.length; i < l; i++) { for (var i = 0, l = curves.length; i < l; i++) {
var v = curves[i].getValues(); var v = curves[i].getValues(),
if ((v[1] <= y || v[3] <= y || v[5] <= y || v[7] <= y) && o0 = v[1],
(v[1] >= y || v[3] >= y || v[5] >= y || v[7] >= y)) { o1 = v[3],
var monos = Curve.getMonoCurves(curves[i].getValues(), 0); o2 = v[5],
o3 = v[7];
if ((o0 <= y || o1 <= y || o2 <= y || o3 <= y) &&
(o0 >= y || o1 >= y || o2 >= y || o3 >= y)) {
var monos = Curve.getMonoCurves(v);
for (var j = 0, m = monos.length; j < m; j++) { for (var j = 0, m = monos.length; j < m; j++) {
var v = monos[j]; var mono = monos[j],
if (y >= v[1] && y <= v[7] || y >= v[7] && y <= v[1]) { mo0 = mono[1],
var winding = v[1] > v[7] ? 1 : v[1] < v[7] ? -1 : 0; mo3 = mono[7];
if (y >= mo0 && y <= mo3 || y >= mo3 && y <= mo0) {
var winding = mo0 > mo3 ? 1 : mo0 < mo3 ? -1 : 0;
if (winding) { if (winding) {
monoCurves.push({ values: v, winding: winding }); monoCurves.push({
values: mono,
winding: winding
});
prevWinding = winding; prevWinding = winding;
} }
} }
@ -992,8 +1046,9 @@ Path.inject(/** @lends Path# */{
if (!monoCurves.length) if (!monoCurves.length)
return point; return point;
for (var i = 0, l = monoCurves.length; i < l; i++) { for (var i = 0, l = monoCurves.length; i < l; i++) {
var v = monoCurves[i].values, var entry = monoCurves[i],
winding = monoCurves[i].winding, v = entry.values,
winding = entry.winding,
x = y === v[1] ? v[0] x = y === v[1] ? v[0]
: y === v[7] ? v[6] : y === v[7] ? v[6]
: Curve.solveCubic(v, 1, y, roots, 0, 1) === 1 : Curve.solveCubic(v, 1, y, roots, 0, 1) === 1