-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
path.unite() fails to resolve intersections #899
Comments
Uh oh, here we go again : ) Looks like it does find all the intersections... Next I'm looking at |
Ok, so this intersections get wrongly filtered out as not a crossing: The problem is in Or better, the way it gets used in |
But even when I I deactivate this check, (which doesn't seem to add any issues btw), it still gets wrongly flagged as not a crossing by the code further down. The reason being all the tangents pointing in the same direction. This is where this comment here becomes important: https://github.com/paperjs/paper.js/blob/develop/src/path/CurveLocation.js#L476
The question then also is: Is there a better way to do this? E.g. use a solver to find the first location where the slope changes enough? |
Isn't this exactly what Cary Clark talks about here? https://www.youtube.com/watch?v=OmfliNQsk88&feature=youtu.be&t=1372 |
Yes it appears to be. And it's quite interesting... It sounds like he's calculating the winding contributions differently to what we're doing, based on just the found intersections and the curves going through it, while we're calling |
Oh but after he discussed about propagation, which is what we are doing. I should listen to this properly again. |
Unfortunately his solution for ordering the curves is for quadratic curves, and he says at the end: "There may be a better one [solution]". So we cannot use this. |
Yeah we shouldn't switch now. = ) I also don't feel that our approach is too slow, so that's fine. |
But if the two things would happen together (finding intersections and calculating winding contributions) we could probably use the winding numbers to figure out the answer. I think what @kuribas is doing here is smarter in that respect and would help with that: #761 (comment) But we're too far down our current road probably. |
Uuuh, I do not want to think about this now :) Just for your information, I found this issue only because I made a mistake when playing with offsetting, which accidentially caused a loop. So this edge case should have never happened (but it did). |
hehe, as they always do! So here is the code mentioned above: https://github.com/adamwulf/vectorboolean/blob/master/VectorBoolean/FBContourOverlap.m#L287 I think we can use the |
Ha, interesting commit message here. It looks like we're not the only ones out there : ) adamwulf/vectorboolean@582e65d |
I am not sure if I fully understand this. So far I tried to stay away from that part of the code. But is the use of |
Well, it's simple: In this case, both involved curves have collinear tangents at the intersection. The intersection happens to be in existing segments, and the segments are smooth, meaning their in- & out-handles are collinear, too. So I was wrong about
The only solution that I can think of right now is the same as in I was wondering if there could be a better solution, at the same time, this happens so rarely that I guess I shouldn't care? |
I initially forgot the example in my previous post. Wouldn't this be better than slowly moving away from the point? |
Oh wow, yes! Let me look into this a bit! : ) |
Hmm not really... There is no distinction between this sketch and that sketch in terms of curvature values. |
I wouldn't expect there to be a difference, because in your sketches you only look at the second curve (the one to the right), which is the same. Look at this example. To be honest, it surprises me a bit that the curvature is different at the same point, depending on which curve you look at. |
Yes, you're of course right! I shall give this a go, good thinking. And I guess that's just how curvature works? |
But what about this situation? They both change signs, and they are actually crossing. Does the amount of curvature then decide? Can we be certain? |
Doesn't the curvature indicate how fast the curve is crawling away from the tangent and to which direction? I think it should work in your example, but I cannot give you the correct algorithm at the moment. |
Yea I was thinking the same. It's one for another day, with a fresher head : ) But definitely solvable. |
As a first step I have created This little sketch that shows the relation of the curvature at t=0 and the direction in which two curves with identical tangent separate. The curve with the larger (signed) curvature will always curl away to the right, from the curve with the smaller curvature value. In the example below, the green curve has the larger curvature value. |
Yes, because: |
But you're aware that you're comparing signed curvatures, yes? |
You are correct, the console output is wrong. It should read "red/green curve is to the right". I updated the sketch in my comment above. |
Yep, using |
It would have been clever if I knew why it solved a few edge cases. I guess I had a hunch. But now we know! : ) |
Here are some metrics for fat line clipping on this issue.
At least for this intersection the fear that we will end up with a huge call tree is unfounded. That's a great relief. |
Another thing that I noticed is that there seem to be many cases that reach the maximum recursion number without any curve splitting at all. These cases do not yield in an intersection and occur if two curves are connected tangentially at an end point, like in this example: These curve pairs do not seem to be a problem, but it would certainly be nice to recognize them earlier so we do not have to go to the maximum number of recusions if there is no intersection anyway. Let's keep this in mind as a potential performance enhancement. |
Here are some more metrics. I ran my large test case and measured the number of calls to
Overall this is another great relief. The call tree does not grow as much as I had suspected. |
Very interesting! Regarding #899 (comment): The handle bounds of these curves touch merely. If we exclude touch, which we can because we handle start- and end-points separately, we should get rid of all of these right away, correct? |
Correct, but how do you exclude touch in fat line clipping while ensuring that any other intersections will be found? That is the big question. If we find an easy way it seems like we can also raise the maximum recursions value to 32. I just ran another test with 1,000,000 curve pairs and maximum recursions set to 32. The number of calls to The problem with raising the max recursions value is that there are many cases of tangentially touching curves (just think of an ellipse). Since these cases always reach maximum recursion without finding an intersection, raising the number of recursion directly affects performance. |
Hmm but that suggestion would kill this again: #878 (comment) |
Another option would be to wait a certain number of recursions (until the curves are almost stright) and then check if the handle bounds touch only in one point. I think this can be done after just a few recursions. |
I just switched the code linked above to compare against |
Sorry, I do not understand :) |
Let me just test it : ) |
Something like this, I was thinking: var epsilon = /*#=*/Numerical.GEOMETRIC_EPSILON,
boundsEpsilon = 0,
c1p1x = v1[0], c1p1y = v1[1],
c1p2x = v1[6], c1p2y = v1[7],
c2p1x = v2[0], c2p1y = v2[1],
c2p2x = v2[6], c2p2y = v2[7],
// 's' stands for scaled handles...
c1s1x = (3 * v1[2] + c1p1x) / 4,
c1s1y = (3 * v1[3] + c1p1y) / 4,
c1s2x = (3 * v1[4] + c1p2x) / 4,
c1s2y = (3 * v1[5] + c1p2y) / 4,
c2s1x = (3 * v2[2] + c2p1x) / 4,
c2s1y = (3 * v2[3] + c2p1y) / 4,
c2s2x = (3 * v2[4] + c2p2x) / 4,
c2s2y = (3 * v2[5] + c2p2y) / 4,
min = Math.min,
max = Math.max,
c1x1 = min(c1p1x, c1s1x, c1s2x, c1p2x),
c1x2 = max(c1p1x, c1s1x, c1s2x, c1p2x),
c2x1 = min(c2p1x, c2s1x, c2s2x, c2p2x),
c2x2 = max(c2p1x, c2s1x, c2s2x, c2p2x),
c1y1 = min(c1p1y, c1s1y, c1s2y, c1p2y),
c1y2 = max(c1p1y, c1s1y, c1s2y, c1p2y),
c2y1 = min(c2p1y, c2s1y, c2s2y, c2p2y),
c2y2 = max(c2p1y, c2s1y, c2s2y, c2p2y);
if (c1x1 === c1x2 || c1y1 === c1y2 || c2x1 === c2x2 || c2y1 === c2y2)
boundsEpsilon = epsilon;
if (!( c1x2 + boundsEpsilon > c2x1 &&
c1x1 - boundsEpsilon < c2x2 &&
c1y2 + boundsEpsilon > c2y1 &&
c1y1 - boundsEpsilon < c2y2))
return locations; |
Oh this doesn't work for edge cases yet (e.g. #568 (comment)), because this is only the bounds check to be used before calling |
This works for me: _getIntersections: function(v1, v2, c1, c2, locations, param) {
if (!v2) {
// If v2 is not provided, search for a self-intersection on v1.
return Curve._getSelfIntersection(v1, c1, locations, param);
}
// Avoid checking curves if completely out of control bounds. As
// a little optimization, we can scale the handles with 0.75
// before calculating the control bounds and still be sure that
// the curve is fully contained.
var epsilon = /*#=*/Numerical.GEOMETRIC_EPSILON,
c1p1x = v1[0], c1p1y = v1[1],
c1p2x = v1[6], c1p2y = v1[7],
c2p1x = v2[0], c2p1y = v2[1],
c2p2x = v2[6], c2p2y = v2[7],
// 's' stands for scaled handles...
c1s1x = (3 * v1[2] + c1p1x) / 4,
c1s1y = (3 * v1[3] + c1p1y) / 4,
c1s2x = (3 * v1[4] + c1p2x) / 4,
c1s2y = (3 * v1[5] + c1p2y) / 4,
c2s1x = (3 * v2[2] + c2p1x) / 4,
c2s1y = (3 * v2[3] + c2p1y) / 4,
c2s2x = (3 * v2[4] + c2p2x) / 4,
c2s2y = (3 * v2[5] + c2p2y) / 4,
min = Math.min,
max = Math.max,
c1x1 = min(c1p1x, c1s1x, c1s2x, c1p2x),
c1x2 = max(c1p1x, c1s1x, c1s2x, c1p2x),
c2x1 = min(c2p1x, c2s1x, c2s2x, c2p2x),
c2x2 = max(c2p1x, c2s1x, c2s2x, c2p2x),
c1y1 = min(c1p1y, c1s1y, c1s2y, c1p2y),
c1y2 = max(c1p1y, c1s1y, c1s2y, c1p2y),
c2y1 = min(c2p1y, c2s1y, c2s2y, c2p2y),
c2y2 = max(c2p1y, c2s1y, c2s2y, c2p2y);
function checkBounds(epsilon) {
return c1x2 + epsilon > c2x1 && c1x1 - epsilon < c2x2 &&
c1y2 + epsilon > c2y1 && c1y1 - epsilon < c2y2;
}
if (!checkBounds(epsilon))
return locations;
// Now detect and handle overlaps:
var overlaps = Curve.getOverlaps(v1, v2);
if (overlaps) {
for (var i = 0; i < 2; i++) {
var overlap = overlaps[i];
addLocation(locations, param,
v1, c1, overlap[0], null,
v2, c2, overlap[1], null, true);
}
return locations;
}
var straight1 = Curve.isStraight(v1),
straight2 = Curve.isStraight(v2),
straight = straight1 && straight2,
before = locations.length;
// If both bounding boxes have with and height, perform a stricter
// bounds check, excluding merely touching at their borders.
if ((c1x1 === c1x2 || c1y1 === c1y2 ||
c2x1 === c2x2 || c2y1 === c2y2) || checkBounds(0)) {
// Determine the correct intersection method based on whether
// one or curves are straight lines:
(straight
? addLineIntersection
: straight1 || straight2
? addCurveLineIntersections
: addCurveIntersections)(
v1, v2, c1, c2, locations, param,
// Define the defaults for these parameters of
// addCurveIntersections():
// tMin, tMax, uMin, uMax, reverse, recursion
0, 1, 0, 1, 0, 0);
}
... |
The crucial part being: // If both bounding boxes have with and height, perform a stricter
// bounds check, excluding merely touching at their borders.
if ((c1x1 === c1x2 || c1y1 === c1y2 ||
c2x1 === c2x2 || c2y1 === c2y2) || checkBounds(0)) {
// Determine the correct intersection method based on whether
// one or curves are straight lines: The question is: Are these the right conditions for when to not perform this additional, more strict check? |
So if you have two vertical lines with a distance of 1e-15, no intersections will be found? |
In that situation, the same intersections as currently are found, because the first call to |
Yeah you're right : / |
And for some strange reason that I don't quite comprehend it has lead to slowdowns in the |
I think I found a very nice way to solve this. If we change the code in } else if (tDiff > 0) { // Iterate
addCurveIntersections(v2, v1Clip, c2, c1, locations, param,
uMin, uMax, tMinNew, tMaxNew, !reverse, recursion);
} else {
// Curve 1 has converged to a point. Since we cannot construct a
// fat-line from a point, we dismiss this clipping so we can
// continue clipping curve 2.
addCurveIntersections(v2, v1, c2, c1, locations, param,
uMin, uMax, tMin, tMax, !reverse, recursion);
} to } else { // Iterate
addCurveIntersections(v2, v1Clip, c2, c1, locations, param,
uMin, uMax, tMinNew, tMaxNew, !reverse, recursion);
} everything seems to work. In my big test case this reduces the calls to Could you check how this affects performance? |
Wowowow! Will check right now : ) |
Very nice. In the particular case in |
Some nice code simplifications resulted from this again. |
If I now increase maximum recursion to 32, the number of calls rises marginally from 1013953 to 1019560, but one new glitch (7751) appears in my test case. This was probably hidden before by a missed intersection. I can investige this later, maybe tonight. So fat line clipping seems to be very robust, with a clean code. The two remaining minor issues that I see are
|
I have started to investigate the curve pairs that create many calls to Initially I found this curve pair which creates 672 calls to So it seems like the problematic curve pairs are the ones where one curve is almost identical to part of the other curve. Therefore I decided to generate these curves artifically. I take one curve and create another curve that is exactly part of the first curve. Then I move one end point of the second curve just slightly, so that var crv1 = CubicBezierUtils.createRandomCurve(100);
var crv2 = crv1.clone().divide(Math.random(), true);
crv2.segment2.point.x += 1.0001 * Numerical.GEOMETRIC_EPSILON;
crv2.segment2.point.y += 1.0001 * Numerical.GEOMETRIC_EPSILON; If you call Another thing that I found is that the number of calls to Now this sounds quite grim, but let's keep in mind that these curve pairs should be the worst of the worst cases that we can encounter, and I have never seen any similar pair in a real case scenario. But I want to look further into these edge cases and see if there is a way to recognize and handle them in a better way. |
Interesting indeed! I think this deserves a new issue. Could you open one with the above content copied to it? Does this mean that |
In this example calling
unite()
on a path fails to resolve the self intersections of the path. The result is a path that still has self intersections.The text was updated successfully, but these errors were encountered: