Change the way winding contributions are propagated

The new approach preserves segment sequence.

Relates to #777
This commit is contained in:
Jürg Lehni 2015-09-13 13:06:01 +02:00
parent 19c9a0e722
commit d84a84c67f
3 changed files with 96 additions and 85 deletions

View file

@ -619,7 +619,7 @@ statics: {
} else if (sy === -1) {
ty = tx;
}
// Use average if we're within epsilon
// Use average if we're within curve-time epsilon
if (abs(tx - ty) < /*#=*/Numerical.CURVETIME_EPSILON)
return (tx + ty) * 0.5;
}
@ -1333,6 +1333,12 @@ new function() { // Scope for intersection using bezier fat-line clipping
t1 = res[0];
t2 = res[1];
}
/*
var d1 = p1 ? p1.getDistance(Curve.getPoint(v1, t1)) : 0,
d2 = p2 ? p2.getDistance(Curve.getPoint(v2, t2)) : 0;
if (!Numerical.isZero(d1) || !Numerical.isZero(d2))
debugger;
*/
locations.push(
new CurveLocation(c1, t1, p1 || Curve.getPoint(v1, t1),
null, overlap,

View file

@ -70,6 +70,7 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{
// TODO: Remove this once debug logging is removed.
_intersection._other = true;
}
this._segment = null; // To be determined, see #getSegment()
// Also store references to segment1 and segment2, in case path
// splitting / dividing is going to happen, in which case the segments
// can be used to determine the new curves, see #getCurve(true)

View file

@ -96,11 +96,9 @@ PathItem.inject(new function() {
// console.timeEnd('intersection');
splitPath(Curve._filterIntersections(locations, true));
var chain = [],
segments = [],
var segments = [],
// Aggregate of all curves in both operands, monotonic in y
monoCurves = [],
epsilon = /*#=*/Numerical.GEOMETRIC_EPSILON;
monoCurves = [];
function collect(paths) {
for (var i = 0, l = paths.length; i < l; i++) {
@ -116,91 +114,23 @@ PathItem.inject(new function() {
collect(_path2._children || [_path2]);
// Propagate the winding contribution. Winding contribution of curves
// does not change between two intersections.
// First, sort all segments with an intersection to the beginning.
segments.sort(function(a, b) {
var _a = a._intersection,
_b = b._intersection;
return !_a && !_b || _a && _b ? 0 : _a ? -1 : 1;
});
// First, propagate winding contributions for curve chains starting in
// all intersections:
for (var i = 0, l = locations.length; i < l; i++) {
propagateWinding(locations[i]._segment, _path1, _path2, monoCurves,
operation);
}
// Now process the segments that are not part of any intersecting chains
for (var i = 0, l = segments.length; i < l; i++) {
var segment = segments[i];
if (segment._winding != null)
continue;
// Here we try to determine the most probable winding number
// contribution for this curve-chain. Once we have enough confidence
// in the winding contribution, we can propagate it until the
// intersection or end of a curve chain.
chain.length = 0;
var startSeg = segment,
totalLength = 0,
windingSum = 0;
do {
var length = segment.getCurve().getLength();
chain.push({ segment: segment, length: length });
totalLength += length;
segment = segment.getNext();
} while (segment && !segment._intersection && segment !== startSeg);
// Calculate the average winding among three evenly distributed
// points along this curve chain as a representative winding number.
// This selection gives a better chance of returning a correct
// winding than equally dividing the curve chain, with the same
// (amortised) time.
for (var j = 0; j < 3; j++) {
// Try the points at 1/4, 2/4 and 3/4 of the total length:
var length = totalLength * (j + 1) / 4;
for (var k = 0, m = chain.length; k < m; k++) {
var node = chain[k],
curveLength = node.length;
if (length <= curveLength) {
// If the selected location on the curve falls onto its
// beginning or end, use the curve's center instead.
if (length < epsilon || curveLength - length < epsilon)
length = curveLength / 2;
var curve = node.segment.getCurve(),
pt = curve.getPointAt(length),
hor = isHorizontal(curve),
path = getMainPath(curve);
// While subtracting, we need to omit this curve if this
// curve is contributing to the second operand and is
// outside the first operand.
windingSum += operation === 'subtract' && _path2
&& (path === _path1 && _path2._getWinding(pt, hor)
|| path === _path2 && !_path1._getWinding(pt, hor))
? 0
: getWinding(pt, monoCurves, hor);
break;
}
length -= curveLength;
}
}
// Assign the average winding to the entire curve chain.
var winding = Math.round(windingSum / 3);
for (var j = chain.length - 1; j >= 0; j--) {
var seg = chain[j].segment,
inter = seg._intersection,
wind = winding;
// We need to handle the edge cases of overlapping curves
// differently based on the type of operation, and adjust the
// winding number accordingly:
if (inter && inter._overlap) {
switch (operation) {
case 'unite':
if (wind === 1)
wind = 2;
break;
case 'intersect':
if (wind === 2)
wind = 1;
break;
}
}
seg._winding = wind;
if (segment._winding == null) {
propagateWinding(segment, _path1, _path2, monoCurves,
operation);
}
}
// Trace closed contours and insert them into the result.
var result = new CompoundPath(Item.NO_INSERT);
result.addChildren(tracePaths(segments, monoCurves, operation, !_path2),
true);
result.addChildren(tracePaths(segments, monoCurves, operation), true);
// See if the CompoundPath can be reduced to just a simple Path.
result = result.reduce();
// Insert the resulting path above whichever of the two paths appear
@ -427,6 +357,80 @@ PathItem.inject(new function() {
return Math.max(abs(windLeft), abs(windRight));
}
function propagateWinding(segment, path1, path2, monoCurves, operation) {
// Here we try to determine the most probable 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 next intersection or end of a curve chain.
var epsilon = /*#=*/Numerical.GEOMETRIC_EPSILON;
chain = [],
startSeg = segment,
totalLength = 0,
windingSum = 0;
do {
var length = segment.getCurve().getLength();
chain.push({ segment: segment, length: length });
totalLength += length;
segment = segment.getNext();
} while (segment && !segment._intersection && segment !== startSeg);
// Calculate the average winding among three evenly distributed
// points along this curve chain as a representative winding number.
// This selection gives a better chance of returning a correct
// winding than equally dividing the curve chain, with the same
// (amortised) time.
for (var i = 0; i < 3; i++) {
// Try the points at 1/4, 2/4 and 3/4 of the total length:
var length = totalLength * (i + 1) / 4;
for (var k = 0, m = chain.length; k < m; k++) {
var node = chain[k],
curveLength = node.length;
if (length <= curveLength) {
// If the selected location on the curve falls onto its
// beginning or end, use the curve's center instead.
if (length < epsilon || curveLength - length < epsilon)
length = curveLength / 2;
var curve = node.segment.getCurve(),
pt = curve.getPointAt(length),
hor = isHorizontal(curve),
path = getMainPath(curve);
// While subtracting, we need to omit this curve if this
// curve is contributing to the second operand and is
// outside the first operand.
windingSum += operation === 'subtract' && path2
&& (path === path1 && path2._getWinding(pt, hor)
|| path === path2 && !path1._getWinding(pt, hor))
? 0
: getWinding(pt, monoCurves, hor);
break;
}
length -= curveLength;
}
}
// Assign the average winding to the entire curve chain.
var winding = Math.round(windingSum / 3);
for (var j = chain.length - 1; j >= 0; j--) {
var seg = chain[j].segment,
inter = seg._intersection,
wind = winding;
// We need to handle the edge cases of overlapping curves
// differently based on the type of operation, and adjust the
// winding number accordingly:
if (inter && inter._overlap) {
switch (operation) {
case 'unite':
if (wind === 1)
wind = 2;
break;
case 'intersect':
if (wind === 2)
wind = 1;
break;
}
}
seg._winding = wind;
}
}
var segmentOffset = {};
var pathIndices = {};
var pathIndex = 0;
@ -523,7 +527,7 @@ PathItem.inject(new function() {
for (var i = 0, seg, startSeg, l = segments.length; i < l; i++) {
seg = startSeg = segments[i];
if (seg._visited || !operator(seg._winding)) {
drawSegment(seg, 'ignore', i, 'red');
drawSegment(seg, seg._visited ? 'visited' : 'ignore', i, 'red');
continue;
}
var path = new Path(Item.NO_INSERT),