From e03c8cd198e41fa32663f32f843e147414a19695 Mon Sep 17 00:00:00 2001 From: iconexperience Date: Fri, 5 Feb 2016 12:45:33 +0100 Subject: [PATCH 1/3] Replace the "straight curves with zero-winding" test with a more comprehensive one containing more special cases. Also, this test requires that all points on a curve of a path must be counted as inside the path. --- test/tests/PathItem_Contains.js | 116 ++++++++++++++++---------------- 1 file changed, 59 insertions(+), 57 deletions(-) diff --git a/test/tests/PathItem_Contains.js b/test/tests/PathItem_Contains.js index f7788331..f633977b 100644 --- a/test/tests/PathItem_Contains.js +++ b/test/tests/PathItem_Contains.js @@ -216,67 +216,69 @@ test('Path#contains() (complex shape)', function() { test('Path#contains() (straight curves with zero-winding)', function() { - var pathPoints = [ - [140, 100], - [140, 10], - [100, 10], - [100, 20], - [120, 20], - [120, 40], - [100, 40], - [100, 60], - [200, 60], - [120, 60], - [120, 100], - [300, 100], - [50, 100] + var pointData = [ + [[250, 230], true, true, false, true], + [[200, 230], true, true, true, true], + [[200, 280], false, true, false, true], + [[190, 280], true, false, false, true], + [[175, 270], true, true, false, true], + [[175, 220], true, true, true, true], + [[175, 270], true, true, false, true], + [[160, 280], false, true, false, true], + [[150, 280], true, false, false, true], + [[150, 220], true, false, true, true], + [[150, 200], true, true, true, true], + [[100, 200], true, false, false, true], + [[100, 190], true, true, true, true], + [[50, 190], true, false, false, false], + [[100, 190], true, true, true, true], + [[100, 180], true, false, true, false], + [[150, 180], true, true, true, true], + [[150, 160], true, false, true, true], + [[150, 100], true, false, true, false], + [[160, 100], false, true, true, false], + [[175, 110], true, true, true, false], + [[175, 160], true, true, true, true], + [[175, 110], true, true, true, false], + [[190, 100], true, false, true, false], + [[200, 100], false, true, true, false], + [[200, 150], true, true, true, true], + [[250, 150], true, true, true, false], + [[270, 120], false, false, true, true], + [[270, 90], false, false, true, false], + [[270, 120], false, false, true, true], + [[290, 150], false, true, true, false], + [[290, 180], true, true, true, true], + [[340, 180], false, true, true, false], + [[340, 190], true, true, true, true], + [[390, 190], false, true, false, false], + [[340, 190], true, true, true, true], + [[340, 200], false, true, false, true], + [[290, 200], true, true, true, true], + [[290, 230], false, true, false, true], + [[270, 260], false, false, true, true], + [[270, 290], false, false, false, true], + [[270, 260], false, false, true, true], + [[250, 230], true, true, false, true] ]; - var path1 = new Path({ - segments: pathPoints, - closed: true, - fillRule: 'evenodd' - }); + var points = []; + for (var i = 0; i < pointData.length; i++) { + points.push(pointData[i][0]); + } + var path = new paper.Path({segments: points, closed: true}); + path.setWindingRule("evenodd"); - var hitPoints = [ - [[30,10], false], - [[110,10], true], - [[30,20], false], - [[110,20], true], - [[130,20], true], - [[170,20], false], - [[110,50], true], - [[30,60], false], - [[110,60], true], - [[130,60], true], - [[150,60], false], - [[230,60], false], - [[10,100], false], - [[60,100], false], - [[130,100], true], - [[170,100], false], - [[370,100], false] - ]; - - for (var i = 0; i < hitPoints.length; i++) { - var entry = hitPoints[i]; - testPoint(path1, new Point(entry[0]), entry[1]); + var offsetPoint = function(p, xOffs, yOffs) { + return new paper.Point(p.x + xOffs, p.y + yOffs); } - // Now test the x-y-reversed shape - - for (var i = 0; i < pathPoints.length; i++) { - pathPoints[i].reverse(); - } - - var path1 = new Path({ - segments: pathPoints, - closed: true, - fillRule: 'evenodd' - }); - - for (var i = 0; i < hitPoints.length; i++) { - var entry = hitPoints[i]; - testPoint(path1, new Point(entry[0].reverse()), entry[1]); + for (var i = 0; i < pointData.length; i++) { + var p = new paper.Point(points[i]); + testPoint(path, p, true); // point is a segment of the path, must be inside + testPoint(path, offsetPoint(p, 10, 0), pointData[i][1]); + testPoint(path, offsetPoint(p, -10, 0), pointData[i][2]); + testPoint(path, offsetPoint(p, 0, 10), pointData[i][3]); + testPoint(path, offsetPoint(p, 0, -10), pointData[i][4]); } }) From 5b31aee8bbc77491130bc4ef01ac73f83000a01c Mon Sep 17 00:00:00 2001 From: iconexperience Date: Fri, 5 Feb 2016 14:48:27 +0100 Subject: [PATCH 2/3] Change the implementation of getWinding() again, so we pass all tests. This new version does not count any intersections if the point is on a y-monotonic curve, but at the final calculation it sets the winding to at least 1 if the point is on a curve. --- src/path/PathItem.Boolean.js | 87 +++++++++++++++++++----------------- 1 file changed, 45 insertions(+), 42 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index c1e40c94..a1c48959 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -314,10 +314,10 @@ PathItem.inject(new function() { py = point.y, windLeft = 0, windRight = 0, - isOnCurve = false, length = curves.length, roots = [], - abs = Math.abs; + abs = Math.abs, + isOnCurve = false; // Horizontal curves may return wrong results, since the curves are // monotonic in y direction and this is an indeterminate state. if (horizontal) { @@ -357,12 +357,13 @@ PathItem.inject(new function() { 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()). + // 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 first curve in the loop. + // non-horizontal curve for the first curve in the loop. prevWinding = curve.last.winding; prevXEnd = curve.last.values[6]; } @@ -370,51 +371,53 @@ PathItem.inject(new function() { // compare the endpoints of the curve to determine if the // ray from query point along +-x direction will intersect // the monotone curve. - // Horizontal curves with winding == 0 will be completely - // ignored. - if (winding && (py >= yStart && py <= yEnd - || py >= yEnd && py <= yStart)) { - // 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 + 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) { - // If the point is between the curve's start point and - // the previous curve's end point, it must be on a - // horizontal curve in between. - var isOnHorizontal = - py === yStart && (px - x) * (px - prevXEnd) < 0; - // Test if the point is on a y-monotonic curve - if ((x >= xBefore && x <= xAfter) || isOnHorizontal) - isOnCurve = true; - // Count the intersection of the ray with the - // y-monotonic curve if: - // - the crossing is not the start or end of the curve - // - or the crossing is at the start of the curve and - // the winding did not change between curves - if (py != yEnd - && (py != yStart || winding === prevWinding)) { - if (x < xBefore) { - windLeft += winding; - } else if (x > xAfter) { - windRight += winding; + if (x != null) { + // determine if the point is on the horizontal + // connection between the last non-horizontal curve's + // end point and the current curve's start point + var isOnHorizontal = + (py === yStart && (px - x) * (px - prevXEnd) < 0); + // Test if the point is on the current monocurve + if (x >= xBefore && x <= xAfter) { + isOnCurve = true; + // 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. + // - and the point is not on the curve or on the + // horizontal connection between the previous + // curve and the current curve + } else if ((py != yStart || winding !== prevWinding) + && !isOnHorizontal) { + 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; } - // Update previous winding and end coordinate whenever - // the ray intersects a non-horizontal curve. - prevWinding = winding; - prevXEnd = values[6]; } } } - // If the point is on a monotonic curve, ensure that the winding is odd - // by applying bit-wise or `| 1` to the bigger absolute winding number: - // If winding is an even number and the point is on a curve, this will - // add up to the next uneven number, otherwise it remains the same. - return Math.max(abs(windLeft), abs(windRight)) | (isOnCurve ? 1 : 0 ); + // If the point was on a monocurve, we are on the path by definition. + // In this case ensure that the winding is at least 1. + return Math.max(abs(windLeft), abs(windRight), isOnCurve ? 1 : 0); } function propagateWinding(segment, path1, path2, monoCurves, operator) { From 55909b8bd5d267cdb0338277ba32094d2b0d4544 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Fri, 5 Feb 2016 19:43:48 +0100 Subject: [PATCH 3/3] Some code cleanup for winding-fix. --- src/path/PathItem.Boolean.js | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index a1c48959..343415f2 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -380,23 +380,21 @@ PathItem.inject(new function() { ? Curve.getPoint(values, roots[0]).x : null; if (x != null) { - // determine if the point is on the horizontal - // connection between the last non-horizontal curve's - // end point and the current curve's start point - var isOnHorizontal = - (py === yStart && (px - x) * (px - prevXEnd) < 0); - // Test if the point is on the current monocurve + // Test if the point is on the current mono-curve. if (x >= xBefore && x <= xAfter) { isOnCurve = true; - // 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. - // - and the point is not on the curve or on the - // horizontal connection between the previous - // curve and the current curve - } else if ((py != yStart || winding !== prevWinding) - && !isOnHorizontal) { + } 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) { @@ -415,8 +413,8 @@ PathItem.inject(new function() { } } } - // If the point was on a monocurve, we are on the path by definition. - // In this case ensure that the winding is at least 1. + // If the point was on a monotonic curve, we are on the path by + // definition. In this case ensure that the winding is at least 1. return Math.max(abs(windLeft), abs(windRight), isOnCurve ? 1 : 0); }