Revamp winding calculation so it can determine the winding in horizontal and vertical direction. Monotonic curves are new only created on demand.

This commit is contained in:
iconexperience 2016-06-16 10:13:10 +02:00
parent 2a668dd04e
commit a328f5b04b
2 changed files with 252 additions and 234 deletions

View file

@ -822,6 +822,50 @@ statics: /** @lends Curve */{
+ t * t * t * v3,
padding);
}
},
/**
* Splits the specified curve values into segments of curves that are monotone in the specified
* coordinate direction (0: monotone in x-direction, 1: monotone in y-direction. If the curve is
* already monotone, an array only containing the original values will be returned.
*/
splitToMonoCurves: function(v, coord) {
var vMono = [];
// getLength is a rather expensive operation, therefore we test two cheap preconditions first
if (v[0] === v[6] && v[1] === v[7] && Curve.getLength(v) === 0)
return vMono;
var o0 = v[1 - coord],
o1 = v[3 - coord],
o2 = v[5 - coord],
o3 = v[7 - coord];
if (o0 >= o1 === o1 >= o2 && o1 >= o2 === o2 >= o3 || Curve.isStraight(v)) {
// Straight curves and curves with points and control points ordered
// in coordinate direction are guaranteed to be monotone.
vMono.push(v);
} 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) {
vMono.push(v);
} else {
roots.sort();
var t = roots[0],
parts = Curve.subdivide(v, t);
vMono.push(parts[0]);
if (n > 1) {
t = (roots[1] - t) / (1 - t);
parts = Curve.subdivide(parts[1], t);
vMono.push(parts[0]);
}
vMono.push(parts[1]);
}
}
return vMono;
}
}}, Base.each(
['getBounds', 'getStrokeBounds', 'getHandleBounds'],

View file

@ -100,7 +100,7 @@ PathItem.inject(new function() {
for (var i = 0, l = paths.length; i < l; i++) {
var path = paths[i];
segments.push.apply(segments, path._segments);
monoCurves.push.apply(monoCurves, path._getMonoCurves());
monoCurves.push.apply(monoCurves, path._getCurves());
// Keep track if there are valid intersections other than
// overlaps in each path.
path._overlapsOnly = path._validOverlapsOnly = true;
@ -295,143 +295,183 @@ PathItem.inject(new function() {
return results || locations;
}
/**
* Private method that returns the winding contribution of the given point
* with respect to a given set of monotonic curves.
* Adds the winding contribution of a curve to the already found windings. The curve does not have
* to be a monotone curve.
*
* @param v the values of the curve
* @param vPrev
* @param px x coordinate of the point to be examined
* @param py y coordinate of the point to be examined
* @param windings an array of length 2. Index 0 is the windings to the left, index 1 to the right.
* @param onCurveWinding
* @param coord The coordinate direction of the cast ray (0 = x, 1 = y)
*/
function addWindingContribution(v, vPrev, px, py, windings, onCurveWinding, coord) {
var epsilon = 2e-7;
var pa = coord ? py : px; // point's abscissa
var po = coord ? px : py; // point's ordinate
var vo0 = v[1 - coord],
vo3 = v[7 - coord];
if ((vo0 > po && vo3 > po) ||
(vo0 < po && vo3 < po)) {
// if curve is outside the ordinates' range, no intersection with the ray is possible
return v;
}
var aBefore = pa - epsilon;
var aAfter = pa + epsilon;
var va0 = v[coord],
va1 = v[2 + coord],
va2 = v[4 + coord],
va3 = v[6 + coord];
if (vo0 === vo3) {
if (aAfter > va1 && aBefore < va3 || aAfter > va3 && aBefore < va1)
onCurveWinding[0] = (onCurveWinding[0] || 0);
// if curve does not change in ordinate direction, windings will be added by adjacent curves
return vPrev;
}
var roots = [];
var a = (va0 < aBefore && va1 < aBefore && va2 < aBefore && va3 < aBefore) ||
(va0 > aAfter && va1 > aAfter && va2 > aAfter && va3 > aAfter)
? (va0 + va3) / 2
: po === vo0 ? va0
: po === vo3 ? va3
: Curve.solveCubic(v, coord ? 0 : 1, po, roots, 0, 1) === 1
? Curve.getPoint(v, roots[0])[coord ? 'y' : 'x']
: (va0 + va3) / 2;
var winding = vo0 < vo3 ? 1 : -1;
var prevWinding = vPrev[1 - coord] < vPrev[7 - coord] ? 1 : -1;
var prevAEnd = vPrev[6 + coord];
var prevAStart = vPrev[coord];
if (a != null) {
if (po !== vo0) {
// standard case, the ray crosses the curve, but not at the start point
if (a < aBefore) {
windings[0] += winding;
} else if (a > aAfter) {
windings[1] += winding;
} else {
onCurveWinding[0] = (onCurveWinding[0] || 0) + winding;
windings[0] += winding;
windings[1] += winding;
}
} else if (a >= aBefore && a <= aAfter) {
if (prevAEnd >= aBefore && prevAEnd <= aAfter) {
if (winding !== prevWinding) {
onCurveWinding[0] = (onCurveWinding[0] || 0) + winding;
if (prevAStart < v[6]) { // ToDo: This should be done with comparing tangens
windings[1] += winding;
} else if (prevAStart > v[6]) {
windings[0] += winding;
} else {
windings[0] += winding;
windings[1] += winding;
}
}
} else {
onCurveWinding[0] = (onCurveWinding[0] || 0) + winding;
if (a > prevAEnd) {
windings[1] += winding;
} else {
windings[0] += winding;
}
}
} else if (prevAEnd >= aBefore && prevAEnd <= aAfter) {
if (winding !== prevWinding) {
onCurveWinding[0] = (onCurveWinding[0] || 0) + winding;
if (a < aBefore) {
windings[0] += 2 * winding;
} else if (a > aAfter) {
windings[1] += 2 * winding;
}
}
} else if ((pa - prevAEnd) * (pa - a) < 0) {
onCurveWinding[0] = (onCurveWinding[0] || 0);
if (a < aBefore) {
windings[0] += winding;
} else if (a > aAfter) {
windings[1] += winding;
}
} else if (winding !== prevWinding) {
if (a < aBefore) {
windings[0] += winding;
} else if (a > aAfter) {
windings[1] += winding;
}
}
}
return v;
}
function getWinding(point, curves, horizontal) {
var epsilon = /*#=*/Numerical.WINDING_EPSILON,
px = point.x,
py = point.y,
windLeft = 0,
windRight = 0,
windings = [0, 0], // left, right winding
isOnPath = [null],
onPathWinding = 0,
length = curves.length,
roots = [],
abs = Math.abs;
// Horizontal curves may return wrong results, since the curves are
// monotonic in y direction and this is an indeterminate state.
if (horizontal) {
var yTop = -Infinity,
yBottom = Infinity,
yBefore = py - epsilon,
yAfter = py + epsilon;
// Find the closest top and bottom intercepts for the vertical line.
for (var i = 0; i < length; i++) {
var values = curves[i].values,
count = Curve.solveCubic(values, 0, px, roots, 0, 1);
for (var j = count - 1; j >= 0; j--) {
var y = Curve.getPoint(values, roots[j]).y;
if (y < yBefore && y > yTop) {
yTop = y;
} else if (y > yAfter && y < yBottom) {
yBottom = y;
vPrev;
var pathPrev = null;
var coord = horizontal ? 1 : 0;
for (var i = 0; i < length; i++) {
var curve = curves[i];
if (pathPrev !== curve.getPath()) {
if (isOnPath[0] != null) {
onPathWinding++;
isOnPath[0] = null;
}
vPrev = null;
var curvePrev = curve.getPrevious();
while (curvePrev && curvePrev != curve) {
var v2 = curvePrev.getValues();
if (v2[1 - coord] != v2[7 - coord]) {
vPrev = v2;
break;
}
curvePrev = curvePrev.getPrevious();
}
}
// Shift the point lying on the horizontal curves by half of the
// closest top and bottom intercepts.
yTop = (yTop + py) / 2;
yBottom = (yBottom + py) / 2;
if (yTop > -Infinity)
windLeft = getWinding(new Point(px, yTop), curves).winding;
if (yBottom < Infinity)
windRight = getWinding(new Point(px, yBottom), curves).winding;
} else {
var xBefore = px - epsilon,
xAfter = px + epsilon,
prevWinding,
prevXEnd,
// Separately count the windings for points on curves.
windLeftOnCurve = 0,
windRightOnCurve = 0,
isOnCurve = false;
for (var i = 0; i < length; i++) {
var curve = curves[i],
winding = curve.winding,
values = curve.values,
yStart = values[1],
yEnd = values[7];
// The first curve of a loop holds the last curve with non-zero
// winding. Retrieve and use it here (See _getMonoCurve()).
if (curve.last) {
// Get the end x coordinate and winding of the last
// non-horizontal curve, which will be the previous
// non-horizontal curve for the first curve in the loop.
prevWinding = curve.last.winding;
prevXEnd = curve.last.values[6];
// Reset the on curve flag for each loop.
isOnCurve = false;
}
// Since the curves are monotonic in y direction, we can just
// compare the endpoints of the curve to determine if the ray
// from query point along +-x direction will intersect the
// monotonic curve.
if (py >= yStart && py <= yEnd || py >= yEnd && py <= yStart) {
if (winding) {
// Calculate the x value for the ray's intersection.
var x = py === yStart ? values[0]
: py === yEnd ? values[6]
: Curve.solveCubic(values, 1, py, roots, 0, 1) === 1
? Curve.getPoint(values, roots[0]).x
: null;
if (x != null) {
// Test if the point is on the current mono-curve.
if (x >= xBefore && x <= xAfter) {
isOnCurve = true;
} else if (
// Count the intersection of the ray with the
// monotonic curve if the crossing is not the
// start of the curve, except if the winding
// changes...
(py !== yStart || winding !== prevWinding)
// ...and the point is not on the curve or on
// the horizontal connection between the last
// non-horizontal curve's end point and the
// current curve's start point.
&& !(py === yStart
&& (px - x) * (px - prevXEnd) < 0)) {
if (x < xBefore) {
windLeft += winding;
} else if (x > xAfter) {
windRight += winding;
}
}
}
// Update previous winding and end coordinate whenever
// the ray intersects a non-horizontal curve.
prevWinding = winding;
prevXEnd = values[6];
// Test if the point is on the horizontal curve.
} else if ((px - values[0]) * (px - values[6]) <= 0) {
isOnCurve = true;
}
}
// If we are at the end of a loop and the point was on a curve
// of the loop, we increment / decrement the on-curve winding
// numbers as if the point was inside the path.
if (isOnCurve && (i >= length - 1 || curves[i + 1].last)) {
windLeftOnCurve += 1;
windRightOnCurve -= 1;
}
if (!vPrev) {
vPrev = curve.getValues();
}
// Use the on-curve windings if no other intersections were found or
// if they canceled each other. On single paths this ensures that
// the overall winding is 1 if the point was on a monotonic curve.
if (windLeft === 0 && windRight === 0) {
windLeft = windLeftOnCurve;
windRight = windRightOnCurve;
// get mono curves
var pa = horizontal ? point.y : point.x;
var aBefore = pa - epsilon;
var aAfter = pa + epsilon;
var v = curve.getValues();
var monoCurves = (v[coord] < aBefore && v[2 + coord] < aBefore && v[4 + coord] < aBefore && v[6 + coord] < aBefore) ||
(v[coord] > aAfter && v[2 + coord] > aAfter && v[4 + coord] > aAfter && v[6 + coord] > aAfter)
? [v]
: Curve.splitToMonoCurves(v, coord);
for (var j = 0; j < monoCurves.length; j++) {
vPrev = addWindingContribution(monoCurves[j], vPrev, point.x, point.y, windings, isOnPath, coord);
}
pathPrev = curve.getPath();
}
if (isOnPath[0] != null) {
onPathWinding++;
}
var windLeft = windings[0] && (2 - Math.abs(windings[0]) % 2);
var windRight = windings[1] && (2 - Math.abs(windings[1]) % 2);
// Use the on-curve windings if no other intersections were found or
// if they canceled each other. On single paths this ensures that
// the overall winding is 1 if the point was on a monotonic curve.
if (windLeft === 0 && windRight === 0) {
windLeft = windRight = onPathWinding;
}
// Return both the calculated winding contribution, and also detect if
// we are on the contour of the area by comparing windLeft & windRight.
// This is required when handling unite operations, where a winding
// contribution of 2 is not part of the result unless it's the contour:
return {
winding: Math.max(abs(windLeft), abs(windRight)),
winding: Math.max(Math.abs(windLeft), Math.abs(windRight)),
contour: !windLeft ^ !windRight
};
}
function propagateWinding(segment, path1, path2, monoCurves, operator) {
// Here we try to determine the most likely winding number contribution
// for the curve-chain starting with this segment. Once we have enough
@ -459,8 +499,7 @@ PathItem.inject(new function() {
parent = path._parent,
t = curve.getTimeAt(length),
pt = curve.getPointAtTime(t),
hor = Math.abs(curve.getTangentAtTime(t).y)
< /*#=*/Numerical.TRIGONOMETRIC_EPSILON;
hor = Math.abs(curve.getTangentAtTime(t).normalize().y) < 0.5;
if (parent instanceof CompoundPath)
path = parent;
// While subtracting, we need to omit this curve if it is
@ -471,7 +510,7 @@ PathItem.inject(new function() {
path === path2 && !path1._getWinding(pt, hor)))
? getWinding(pt, monoCurves, hor)
: { winding: 0 };
break;
break;
}
length -= curveLength;
}
@ -688,7 +727,7 @@ PathItem.inject(new function() {
* @return {Number} the winding number
*/
_getWinding: function(point, horizontal) {
return getWinding(point, this._getMonoCurves(), horizontal).winding;
return getWinding(point, this._getCurves(), horizontal).winding;
},
/**
@ -928,101 +967,8 @@ Path.inject(/** @lends Path# */{
* which are monotonically decreasing or increasing in the y-direction.
* Used by getWinding().
*/
_getMonoCurves: function() {
var monoCurves = this._monoCurves,
last;
// Insert curve values into a cached array
function insertCurve(v) {
var y0 = v[1],
y1 = v[7],
// Look at the slope of the line between the mono-curve's anchor
// points with some tolerance to decide if it is horizontal.
winding = Math.abs((y0 - y1) / (v[0] - v[6]))
< /*#=*/Numerical.GEOMETRIC_EPSILON
? 0 // Horizontal
: y0 > y1
? -1 // Decreasing
: 1, // Increasing
curve = { values: v, winding: winding };
monoCurves.push(curve);
// Keep track of the last non-horizontal curve (with winding).
if (winding)
last = curve;
}
// Handle bezier curves. We need to chop them into smaller curves with
// defined orientation, by solving the derivative curve for y extrema.
function handleCurve(v) {
// Filter out curves of zero length.
// TODO: Do not filter this here.
if (Curve.getLength(v) === 0)
return;
var y0 = v[1],
y1 = v[3],
y2 = v[5],
y3 = v[7];
if (Curve.isStraight(v)
|| y0 >= y1 === y1 >= y2 && y1 >= y2 === y2 >= y3) {
// Straight curves and curves with end and control points sorted
// in y direction are guaranteed to be monotonic in y direction.
insertCurve(v);
} else {
// Split the curve at y extrema, to get bezier curves with clear
// orientation: Calculate the derivative and find its roots.
var a = 3 * (y1 - y2) - y0 + y3,
b = 2 * (y0 + y2) - 4 * y1,
c = y1 - y0,
tMin = /*#=*/Numerical.CURVETIME_EPSILON,
tMax = 1 - tMin,
roots = [],
// Keep then range to 0 .. 1 (excluding) in the search for y
// extrema.
n = Numerical.solveQuadratic(a, b, c, roots, tMin, tMax);
if (n === 0) {
insertCurve(v);
} else {
roots.sort();
var t = roots[0],
parts = Curve.subdivide(v, t);
insertCurve(parts[0]);
if (n > 1) {
// If there are two extrema, renormalize t to the range
// of the second range and split again.
t = (roots[1] - t) / (1 - t);
// Since we already processed parts[0], we can override
// the parts array with the new pair now.
parts = Curve.subdivide(parts[1], t);
insertCurve(parts[0]);
}
insertCurve(parts[1]);
}
}
}
if (!monoCurves) {
// Insert curves that are monotonic in y direction into cached array
monoCurves = this._monoCurves = [];
var curves = this.getCurves(),
segments = this._segments;
for (var i = 0, l = curves.length; i < l; i++)
handleCurve(curves[i].getValues());
// If the path is not closed, we need to join the end points with a
// straight line, just like how filling open paths works.
if (!this._closed && segments.length > 1) {
var p1 = segments[segments.length - 1]._point,
p2 = segments[0]._point,
p1x = p1._x, p1y = p1._y,
p2x = p2._x, p2y = p2._y;
handleCurve([p1x, p1y, p1x, p1y, p2x, p2y, p2x, p2y]);
}
if (monoCurves.length > 0) {
// Add information about the last curve with non-zero winding,
// as required in getWinding().
monoCurves[0].last = last;
}
}
return monoCurves;
_getCurves: function() {
return this.getCurves();
},
/**
@ -1037,28 +983,56 @@ Path.inject(/** @lends Path# */{
if (!this.contains(point)) {
// Since there is no guarantee that a poly-bezier path contains
// the center of its bounding rectangle, we shoot a ray in
// +x direction from the center and select a point between
// consecutive intersections of the ray.
var curves = this._getMonoCurves(),
roots = [],
// x direction and select a point between the first consecutive
// intersections of the ray on the left.
var curves = this.getCurves(),
y = point.y,
intercepts = [];
intercepts = [],
monoCurves = [];
// Collect values for all y-monotone curves that intersect the ray at y
for (var i = 0, l = curves.length; i < l; i++) {
var values = curves[i].values;
if (curves[i].winding === 1
&& y > values[1] && y <= values[7]
|| y >= values[7] && y < values[1]) {
var count = Curve.solveCubic(values, 1, y, roots, 0, 1);
for (var j = count - 1; j >= 0; j--) {
intercepts.push(Curve.getPoint(values, roots[j]).x);
var monoVals = Curve.splitToMonoCurves(curves[i].getValues(), 0);
for (var j = 0; j < monoVals.length; j++) {
var values = monoVals[j];
if (y >= values[1] && y <= values[7]
|| y >= values[7] && y <= values[1]) {
var winding = values[1] > values[7] ? 1 : values[1] < values[7] ? -1 : 0;
if (winding) {
monoCurves.push({values: values, winding: winding});
windingPrev = winding;
}
}
}
}
intercepts.sort(function(a, b) { return a - b; });
if (!monoCurves.length) {
// fallback in case no non-horizontal curves were found
return point;
}
var windingPrev = monoCurves[monoCurves.length - 1].winding;
for (var i = 0, l = monoCurves.length; i < l; i++) {
var v = monoCurves[i].values;
var winding = monoCurves[i].winding;
var roots = [];
var x = y === v[1] ? v[0]
: y === v[7] ? v[6]
: Curve.solveCubic(v, 1, y, roots, 0, 1) === 1
? Curve.getPoint(v, roots[0]).x
: (v[0] + v[6]) / 2;
//if (y != v[1] || winding != windingPrev)
intercepts.push(x);
windingPrev = winding;
}
intercepts.sort(function(a, b) {
return a - b;
});
point.x = (intercepts[0] + intercepts[1]) / 2;
}
return point;
}
});
CompoundPath.inject(/** @lends CompoundPath# */{
@ -1067,11 +1041,11 @@ CompoundPath.inject(/** @lends CompoundPath# */{
* are monotonically decreasing or increasing in the 'y' direction.
* Used by getWinding().
*/
_getMonoCurves: function() {
_getCurves: function() {
var children = this._children,
monoCurves = [];
curves = [];
for (var i = 0, l = children.length; i < l; i++)
monoCurves.push.apply(monoCurves, children[i]._getMonoCurves());
return monoCurves;
curves.push.apply(curves, children[i]._getCurves());
return curves;
}
});