mirror of
https://github.com/scratchfoundation/paper.js.git
synced 2025-01-03 19:45:44 -05:00
Use quality factor for better winding calculation and propagation
This commit is contained in:
parent
ed38634a80
commit
3c2588fdec
2 changed files with 94 additions and 97 deletions
|
@ -401,8 +401,8 @@ PathItem.inject(new function() {
|
||||||
*/
|
*/
|
||||||
function getWinding(point, curves, dir, closed, dontFlip) {
|
function getWinding(point, curves, dir, closed, dontFlip) {
|
||||||
var epsilon = /*#=*/Numerical.WINDING_EPSILON,
|
var epsilon = /*#=*/Numerical.WINDING_EPSILON,
|
||||||
// Determine the index of the abscissa and ordinate values in the
|
// Determine the index of the abscissa and ordinate values in the
|
||||||
// curve values arrays, based on the direction:
|
// curve values arrays, based on the direction:
|
||||||
ia = dir ? 1 : 0, // the abscissa index
|
ia = dir ? 1 : 0, // the abscissa index
|
||||||
io = dir ? 0 : 1, // the ordinate index
|
io = dir ? 0 : 1, // the ordinate index
|
||||||
pv = [point.x, point.y],
|
pv = [point.x, point.y],
|
||||||
|
@ -412,11 +412,8 @@ PathItem.inject(new function() {
|
||||||
paR = pa + epsilon,
|
paR = pa + epsilon,
|
||||||
windingL = 0,
|
windingL = 0,
|
||||||
windingR = 0,
|
windingR = 0,
|
||||||
pathWindingL = 0,
|
|
||||||
pathWindingR = 0,
|
|
||||||
onPath = false,
|
onPath = false,
|
||||||
onPathWinding = 0,
|
quality = 1,
|
||||||
onPathCount = 0,
|
|
||||||
roots = [],
|
roots = [],
|
||||||
vPrev,
|
vPrev,
|
||||||
vClose;
|
vClose;
|
||||||
|
@ -440,7 +437,7 @@ PathItem.inject(new function() {
|
||||||
// +-----+
|
// +-----+
|
||||||
// +----+ |
|
// +----+ |
|
||||||
// +-----+
|
// +-----+
|
||||||
if (a1 < paR && a3 > paL || a3 < paR && a1 > paL) {
|
if (a0 < paR && a3 > paL || a3 < paR && a0 > paL) {
|
||||||
onPath = true;
|
onPath = true;
|
||||||
}
|
}
|
||||||
// If curve does not change in ordinate direction, windings will
|
// If curve does not change in ordinate direction, windings will
|
||||||
|
@ -453,33 +450,30 @@ PathItem.inject(new function() {
|
||||||
: paL > max(a0, a1, a2, a3) || paR < min(a0, a1, a2, a3)
|
: paL > max(a0, a1, a2, a3) || paR < min(a0, a1, a2, a3)
|
||||||
? 0.5
|
? 0.5
|
||||||
: Curve.solveCubic(v, io, po, roots, 0, 1) === 1
|
: Curve.solveCubic(v, io, po, roots, 0, 1) === 1
|
||||||
? roots[0]
|
? roots[0]
|
||||||
: 0.5,
|
: 0.5,
|
||||||
a = t === 0 ? a0
|
a = t === 0 ? a0
|
||||||
: t === 1 ? a3
|
: t === 1 ? a3
|
||||||
: Curve.getPoint(v, t)[dir ? 'y' : 'x'],
|
: Curve.getPoint(v, t)[dir ? 'y' : 'x'],
|
||||||
winding = o0 > o3 ? 1 : -1,
|
winding = o0 > o3 ? 1 : -1,
|
||||||
windingPrev = vPrev[io] > vPrev[io + 6] ? 1 : -1,
|
windingPrev = vPrev[io] > vPrev[io + 6] ? 1 : -1,
|
||||||
a3Prev = vPrev[ia + 6];
|
a3Prev = vPrev[ia + 6];
|
||||||
|
if (a >= paL && a <= paR) onPath = true;
|
||||||
if (po !== o0) {
|
if (po !== o0) {
|
||||||
// Standard case, curve is not crossed at its starting point.
|
// Standard case, curve is not crossed at its starting point.
|
||||||
if (a < paL) {
|
if (a < paL) {
|
||||||
pathWindingL += winding;
|
windingL += winding;
|
||||||
} else if (a > paR) {
|
} else if (a > paR) {
|
||||||
pathWindingR += winding;
|
windingR += winding;
|
||||||
} else {
|
|
||||||
onPath = true;
|
|
||||||
pathWindingL += winding;
|
|
||||||
pathWindingR += winding;
|
|
||||||
}
|
}
|
||||||
} else if (winding !== windingPrev) {
|
} else if (winding !== windingPrev) {
|
||||||
// Curve is crossed at starting point and winding changes from
|
// Curve is crossed at starting point and winding changes from
|
||||||
// previous curve. Cancel the winding from previous curve.
|
// previous curve. Cancel the winding from previous curve.
|
||||||
if (a3Prev < paR) {
|
if (a3Prev < paR) {
|
||||||
pathWindingL += winding;
|
windingL += winding;
|
||||||
}
|
}
|
||||||
if (a3Prev > paL) {
|
if (a3Prev > paL) {
|
||||||
pathWindingR += winding;
|
windingR += winding;
|
||||||
}
|
}
|
||||||
} else if (a3Prev < paL && a > paL || a3Prev > paR && a < paR) {
|
} else if (a3Prev < paL && a > paL || a3Prev > paR && a < paR) {
|
||||||
// Point is on a horizontal curve between the previous non-
|
// Point is on a horizontal curve between the previous non-
|
||||||
|
@ -487,20 +481,34 @@ PathItem.inject(new function() {
|
||||||
onPath = true;
|
onPath = true;
|
||||||
if (a3Prev < paL) {
|
if (a3Prev < paL) {
|
||||||
// left winding was added before, now add right winding.
|
// left winding was added before, now add right winding.
|
||||||
pathWindingR += winding;
|
windingR += winding;
|
||||||
} else if (a3Prev > paR) {
|
} else if (a3Prev > paR) {
|
||||||
// right winding was added before, not add left winding.
|
// right winding was added before, now add left winding.
|
||||||
pathWindingL += winding;
|
windingL += winding;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Determine the quality of the winding calculation. Currently the
|
||||||
|
// quality is reduced with every crossing of the ray very close
|
||||||
|
// to the path. This means that if the point is on or near multiple
|
||||||
|
// curves, the quality becomes less than 0.5
|
||||||
|
//ToDo Set quality depending on distance
|
||||||
|
if (po !== o0) {
|
||||||
|
if (a > pa - 100 * epsilon && a < pa + 100 * epsilon) {
|
||||||
|
//quality *= Math.min(1, (100 * epsilon * Math.abs(a - pa) + 0.5));
|
||||||
|
quality /= 2;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//ToDo:
|
||||||
|
quality = 0;
|
||||||
|
}
|
||||||
vPrev = v;
|
vPrev = v;
|
||||||
// If we're on the curve, look at the tangent to decide whether to
|
// If we're on the curve, look at the tangent to decide whether to
|
||||||
// flip direction to better determine a reliable winding number:
|
// flip direction to better determine a reliable winding number:
|
||||||
// If the tangent is parallel to the direction, call getWinding()
|
// If the tangent is parallel to the direction, call getWinding()
|
||||||
// again with flipped direction and return that result instead.
|
// again with flipped direction and return that result instead.
|
||||||
return !dontFlip && a > paL && a < paR
|
return !dontFlip && a > paL && a < paR
|
||||||
&& Curve.getTangent(v, t)[dir ? 'x' : 'y'] === 0
|
&& Curve.getTangent(v, t)[dir ? 'x' : 'y'] === 0
|
||||||
&& getWinding(point, curves, dir ? 0 : 1, closed, true);
|
&& getWinding(point, curves, dir ? 0 : 1, closed, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCurve(v) {
|
function handleCurve(v) {
|
||||||
|
@ -516,11 +524,11 @@ PathItem.inject(new function() {
|
||||||
a1 = v[ia + 2],
|
a1 = v[ia + 2],
|
||||||
a2 = v[ia + 4],
|
a2 = v[ia + 4],
|
||||||
a3 = v[ia + 6],
|
a3 = v[ia + 6],
|
||||||
// Get monotone curves. If the curve is outside the point's
|
// Get monotone curves. If the curve is outside the point's
|
||||||
// abscissa, it can be treated as a monotone curve:
|
// abscissa, it can be treated as a monotone curve:
|
||||||
monoCurves = paL > max(a0, a1, a2, a3) ||
|
monoCurves = paL > max(a0, a1, a2, a3) ||
|
||||||
paR < min(a0, a1, a2, a3)
|
paR < min(a0, a1, a2, a3)
|
||||||
? [v] : Curve.getMonoCurves(v, dir),
|
? [v] : Curve.getMonoCurves(v, dir),
|
||||||
res;
|
res;
|
||||||
for (var i = 0, l = monoCurves.length; i < l; i++) {
|
for (var i = 0, l = monoCurves.length; i < l; i++) {
|
||||||
// Calling addWinding() my lead to direction flipping, in
|
// Calling addWinding() my lead to direction flipping, in
|
||||||
|
@ -548,9 +556,9 @@ PathItem.inject(new function() {
|
||||||
// into account, just like how closed paths behave.
|
// into account, just like how closed paths behave.
|
||||||
if (!path._closed) {
|
if (!path._closed) {
|
||||||
vClose = Curve.getValues(
|
vClose = Curve.getValues(
|
||||||
path.getLastCurve().getSegment2(),
|
path.getLastCurve().getSegment2(),
|
||||||
curve.getSegment1(),
|
curve.getSegment1(),
|
||||||
null, !closed);
|
null, !closed);
|
||||||
// This closing curve is a potential candidate for the last
|
// This closing curve is a potential candidate for the last
|
||||||
// non-horizontal curve.
|
// non-horizontal curve.
|
||||||
if (vClose[io] !== vClose[io + 6]) {
|
if (vClose[io] !== vClose[io + 6]) {
|
||||||
|
@ -586,31 +594,10 @@ PathItem.inject(new function() {
|
||||||
// it now to treat the path as closed:
|
// it now to treat the path as closed:
|
||||||
if (vClose && (res = handleCurve(vClose)))
|
if (vClose && (res = handleCurve(vClose)))
|
||||||
return res;
|
return res;
|
||||||
if (onPath && !pathWindingL && !pathWindingR) {
|
|
||||||
// If the point is on the path and the windings canceled
|
|
||||||
// each other, we treat the point as if it was inside the
|
|
||||||
// path. A point inside a path has a winding of [+1,-1]
|
|
||||||
// for clockwise and [-1,+1] for counter-clockwise paths.
|
|
||||||
// If the ray is cast in y direction (dir == 1), the
|
|
||||||
// windings always have opposite sign.
|
|
||||||
var add = path.isClockwise(closed) ^ dir ? 1 : -1;
|
|
||||||
windingL += add;
|
|
||||||
windingR -= add;
|
|
||||||
onPathWinding += add;
|
|
||||||
} else {
|
|
||||||
windingL += pathWindingL;
|
|
||||||
windingR += pathWindingR;
|
|
||||||
pathWindingL = pathWindingR = 0;
|
|
||||||
}
|
|
||||||
if (onPath)
|
|
||||||
onPathCount++;
|
|
||||||
onPath = false;
|
|
||||||
vClose = null;
|
vClose = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!windingL && !windingR) {
|
// If the winding one
|
||||||
windingL = windingR = onPathWinding;
|
|
||||||
}
|
|
||||||
windingL = windingL && (2 - abs(windingL) % 2);
|
windingL = windingL && (2 - abs(windingL) % 2);
|
||||||
windingR = windingR && (2 - abs(windingR) % 2);
|
windingR = windingR && (2 - abs(windingR) % 2);
|
||||||
// Return the calculated winding contribution and detect if we are
|
// Return the calculated winding contribution and detect if we are
|
||||||
|
@ -622,7 +609,8 @@ PathItem.inject(new function() {
|
||||||
windingL: windingL,
|
windingL: windingL,
|
||||||
windingR: windingR,
|
windingR: windingR,
|
||||||
onContour: !windingL ^ !windingR,
|
onContour: !windingL ^ !windingR,
|
||||||
onPathCount: onPathCount
|
onPath: onPath,
|
||||||
|
quality: quality
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -642,38 +630,47 @@ PathItem.inject(new function() {
|
||||||
totalLength += length;
|
totalLength += length;
|
||||||
segment = segment.getNext();
|
segment = segment.getNext();
|
||||||
} while (segment && !segment._intersection && segment !== start);
|
} while (segment && !segment._intersection && segment !== start);
|
||||||
// Sample the point at a middle of the chain to get its winding:
|
// Determine winding at three points in the chain. If a winding with
|
||||||
var length = totalLength / 2;
|
// sufficient quality is found, use it. Otherwise use the winding with
|
||||||
for (var j = 0, l = chain.length; j < l; j++) {
|
// the best quality.
|
||||||
var entry = chain[j],
|
var offsets = [0.48, 0.1, 0.9];
|
||||||
curveLength = entry.length;
|
for (var i = 0; (!winding || winding.quality < 0.5) && i < offsets.length; i++) {
|
||||||
if (length <= curveLength) {
|
var length = totalLength * offsets[i];
|
||||||
var curve = entry.curve,
|
for (var j = 0, l = chain.length; j < l; j++) {
|
||||||
path = curve._path,
|
var entry = chain[j],
|
||||||
parent = path._parent,
|
curveLength = entry.length;
|
||||||
t = curve.getTimeAt(length),
|
if (length <= curveLength) {
|
||||||
pt = curve.getPointAtTime(t),
|
var curve = entry.curve,
|
||||||
|
path = curve._path,
|
||||||
|
parent = path._parent,
|
||||||
|
t = curve.getTimeAt(length),
|
||||||
|
pt = curve.getPointAtTime(t),
|
||||||
// Determine the direction in which to check the winding
|
// Determine the direction in which to check the winding
|
||||||
// from the point (horizontal or vertical), based on the
|
// from the point (horizontal or vertical), based on the
|
||||||
// curve's direction at that point. If the tangent is less
|
// curve's direction at that point. If the tangent is less
|
||||||
// than 45°, cast the ray vertically, else horizontally.
|
// than 45°, cast the ray vertically, else horizontally.
|
||||||
dir = abs(curve.getTangentAtTime(t).normalize().y)
|
dir = abs(curve.getTangentAtTime(t).normalize().y)
|
||||||
< Math.SQRT1_2 ? 1 : 0;
|
< Math.SQRT1_2 ? 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 && (
|
var windingNew = !(operator.subtract && path2 && (
|
||||||
path === path1 &&
|
path === path1 &&
|
||||||
path2._getWinding(pt, dir, true).winding ||
|
path2._getWinding(pt, dir, true).winding ||
|
||||||
path === path2 &&
|
path === path2 &&
|
||||||
!path1._getWinding(pt, dir, true).winding))
|
!path1._getWinding(pt, dir, true).winding))
|
||||||
? getWinding(pt, curves, dir, true)
|
? getWinding(pt, curves, dir, true)
|
||||||
: { winding: 0 };
|
: {winding: 0};
|
||||||
break;
|
if (windingNew.winding != 0 &&
|
||||||
|
(!winding || winding.quality < windingNew.quality)) {
|
||||||
|
winding = windingNew;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
length -= curveLength;
|
||||||
}
|
}
|
||||||
length -= curveLength;
|
|
||||||
}
|
}
|
||||||
// Now assign the winding to the entire curve chain.
|
// Now assign the winding to the entire curve chain.
|
||||||
for (var j = chain.length - 1; j >= 0; j--) {
|
for (var j = chain.length - 1; j >= 0; j--) {
|
||||||
|
|
|
@ -256,26 +256,26 @@ var PathItem = Item.extend(/** @lends PathItem# */{
|
||||||
_contains: function(point) {
|
_contains: function(point) {
|
||||||
// NOTE: point is reverse transformed by _matrix, so we don't need to
|
// NOTE: point is reverse transformed by _matrix, so we don't need to
|
||||||
// apply the matrix here.
|
// apply the matrix here.
|
||||||
/*#*/ if (__options.nativeContains || !__options.booleanOperations) {
|
/*#*/ if (__options.nativeContains || !__options.booleanOperations) {
|
||||||
// To compare with native canvas approach:
|
// To compare with native canvas approach:
|
||||||
var ctx = CanvasProvider.getContext(1, 1);
|
var ctx = CanvasProvider.getContext(1, 1);
|
||||||
// Use dontFinish to tell _draw to only produce geometries for hit-test.
|
// Use dontFinish to tell _draw to only produce geometries for hit-test.
|
||||||
this._draw(ctx, new Base({ dontFinish: true }));
|
this._draw(ctx, new Base({ dontFinish: true }));
|
||||||
var res = ctx.isPointInPath(point.x, point.y, this.getFillRule());
|
var res = ctx.isPointInPath(point.x, point.y, this.getFillRule());
|
||||||
CanvasProvider.release(ctx);
|
CanvasProvider.release(ctx);
|
||||||
return res;
|
return res;
|
||||||
/*#*/ } else { // !__options.nativeContains && __options.booleanOperations
|
/*#*/ } else { // !__options.nativeContains && __options.booleanOperations
|
||||||
// Check the transformed point against the untransformed (internal)
|
// Check the transformed point against the untransformed (internal)
|
||||||
// handle bounds, which is the fastest rough bounding box to calculate
|
// handle bounds, which is the fastest rough bounding box to calculate
|
||||||
// for a quick check before calculating the actual winding.
|
// for a quick check before calculating the actual winding.
|
||||||
var winding = point.isInside(
|
var winding = point.isInside(
|
||||||
this.getBounds({ internal: true, handle: true }))
|
this.getBounds({ internal: true, handle: true }))
|
||||||
? this._getWinding(point)
|
? this._getWinding(point)
|
||||||
: {};
|
: {};
|
||||||
return !!(this.getFillRule() === 'evenodd'
|
return winding.onPath || !!(this.getFillRule() === 'evenodd'
|
||||||
? winding.windingL & 1 || winding.windingR & 1
|
? winding.windingL & 1 || winding.windingR & 1
|
||||||
: winding.winding);
|
: winding.winding);
|
||||||
/*#*/ } // !__options.nativeContains && __options.booleanOperations
|
/*#*/ } // !__options.nativeContains && __options.booleanOperations
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in a new issue