When line surface intersection fails due to tangency, try the nearest… - #1751
When line surface intersection fails due to tangency, try the nearest…#1751phkahler wants to merge 3 commits into
Conversation
| } | ||
| else { // u > 0.5 | ||
| for(int n=0;n<4;n++) { edge.ctrl[n]=ctrl[degm][n]; edge.weight[n]=weight[degm][n]; } | ||
| edge.deg = degn; |
There was a problem hiding this comment.
edge.deg = degm perhaps? hmmm maybe not... maybe that is in the next else "// v is closer to 0 or 1"
|
With the second commit, this is handling 1291_1743_cube_cut_tangent_outside_still_fails_simplified.slvs very nicely. However it still fails ASAN tests down in polygon.cpp for reasons I have no idea. The previous problem of the edge being broken into pieces was due to the recursive subdivision of the curved surface. Once the pieces were sufficiently flat it tried to intersect the vertical edge with the surface and it was somehow passing the test because the lines were coincident, so it added a lot of intersections. This 2nd commit checks for parallel lines and fails intersection for those. It also has a problem if you make the protruding side coincident - including the control point - AND make it single sided so the fillet cut does not extend past the top of the original extrusion. In that case the one side flickers with naked edges. It will do so if you drag either control point along its edge but it's always the one side that flickers regardless of which point is dragged. (not sure this description makes sense). |
|
Actually the shell appears OK. When I said "fails" above it was just indicating naked edges, and also the fact that ASAN fails on the regression test. This does work (no naked edges) when the fillet cut is two sided so it extends past the box top. @BoykoNeov This would be good for Claude to have a look at when you get back ;-) |
|
Written by Claude Opus 5 — both this text and the code it describes; posted by @BoykoNeov. @phkahler — had a look. Short version: the ASan failure is my fault, not yours, and underneath it there is one real regression. Everything below is reproduced locally (MSVC, Debug, Ninja, no sanitizers), on a worktree of this PR with submodules at the pinned revisions, and identically with 1. The ASan failure is a cascade of my own test, and it's a red herringThere is no memory-safety error. Of the three failing jobs:
That leak is mine. CHECK_FALSE(inters);
CHECK_FALSE(leaks); // <-- fails here
CHECK_TRUE(el.l.IsEmpty());
el.Clear(); // <-- never runs
So the leak is a consequence of the failure, not a second problem, and chasing it into Six Boolean tests have that shape — bool noEdges = el.l.IsEmpty();
el.Clear();
CHECK_FALSE(inters);
CHECK_FALSE(leaks);
CHECK_TRUE(noEdges);With that applied and no other change, the suite is 264 cases / 937 checks green on current master 2. The real failure: this PR regresses
|
| tree | group/boolean_tangent_crossing |
|---|---|
220c1443 (base = master) |
OK — whole suite 264/937 green |
f8d85754 (the edge fallback) |
FAILED: (leaks) = true ≠ false |
70b444c3 (+ the parallel guard) |
FAILED, identical |
So the first commit is where it starts, and the parallel guard doesn't change it. At head the rest of the suite is fine — exactly this one check fails. Same result with OpenMP on and off, so the parallelism isn't involved.
The model is ruevs' cube_cut with the profile spline made tangent to the cube's face on the other side, its corner left outside, so the line continuing from the tangency crosses that face. It's the same family as the one you're fixing, which is probably why it's the one that trips.
3. What the fallback does on this model
At base, ten PointIntersectingLine calls give up on that shell — eight of them via the parallel break, two by exhausting the 20 iterations. All ten return false, and the Boolean is watertight.
At head, all ten come back true, every one of them through the new fallback:
parallel (surface intersecting line)
found U=1.46869e-08, V=0.0 point=(-16.244 -30.000 30.000)
found U=1, V=0.0 point=(30.000 18.788 30.000)
parallel (surface intersecting line)
found U=1.66632e-08, V=0.0 point=(-16.244 -30.000 30.000)
parallel (surface intersecting line)
found U=1.70595e-08, V=1.0 point=(-16.244 -30.000 0.000)
parallel (surface intersecting line)
found U=0.0, V=0.5 point=(-16.244 -30.000 15.000)
...
failed: I=5, avoid=4
print 8 edges
Two things in there:
- Three "intersections" on one line.
(-16.244, -30, 30),(-16.244, -30, 0)and(-16.244, -30, 15)are three different points on the same vertical edge, at its two ends and its midpoint. Same for the other edge:(30, 18.788, 30),(30, 18.788, 0),(30, 18.788, 15).AllPointsIntersectingUntrimmedrecurses until each sub-patch is flat and callsPointIntersectingLineonsorigonce per sub-patch, so each call seeds from its own patch centre and the fallback walks to a different point of the same edge. The dedup inAllPointsIntersectingcompares uv withLENGTH_EPS, so they all survive as separate intersections. - Every hit is on the boundary, by construction —
EdgeCurveIntersectionpins*uor*vto exactly 0.0 or 1.0. Two of them are the surface corner(u=1, v=0). A boundary hit is precisely the degenerate case for both ray-cast parity and trim assembly, since the point belongs to two edges and to the neighbouring surface as well.
Downstream, AssemblePolygon fails for surface I=5 with 8 dangling edges (failed: I=5, avoid=4, boolean.cpp:738) and sets booleanFailed. That's the naked edge.
The part I'd take away from this: suppressing eight of the ten hits still leaves the test failing (§4). So the problem doesn't look like a guard that needs tightening — it looks like a boundary-pinned point being the wrong shape of answer for this call, with one of them enough to break the trim.
4. Two things I tried that did not fix it
Both worth knowing so you don't spend the time:
- Fixing the antiparallel hole (see below) — output byte-identical, still fails.
- Not running the fallback on the
parallelbreak (return falseinstead ofbreak) — cuts the ten hits to exactly the two that came from iteration exhaustion, and it still fails. Both survivors are the same corner(30, 18.788, 30).
So the fallback needs more than a better parallel test; a single manufactured boundary intersection is enough to break the assembly.
5. The parallel guard has two holes anyway
Independent of the above — I measured that neither causes this failure, but both are real:
if(deg + curve->deg == 2) { // check for parallel lines
...
if(fabs(1.0 - d1.Dot(d2)) < 10*RATPOLY_EPS) return false;- Antiparallel slips through. For lines pointing opposite ways
d1.Dot(d2)is −1, so this evaluatesfabs(2.0). The edge direction comes off the surface's control grid (always increasing u/v) whilep0→p1is whatever the caller had, so the relative orientation is arbitrary.fabs(1.0 - fabs(d1.Dot(d2)))closes it. - The gate is algebraic where the property is geometric.
deg + curve->deg == 2only covers a degree-1 edge. I instrumented it: six of the ten calls on this model printguard SKIPPED, deg=3+1— the surface edge is a cubic, and a cubic with collinear control points is a straight line. The other four reach the test and printdot=0, i.e. genuinely perpendicular. So on this model the guard never fires at all.
Minor, same function: 1 − cos θ < 1e-7 rejects anything closer than about 4.5e-4 rad — 0.026°, the square root of the number in the source. That may be what you want, but it's a much wider cone than 10*RATPOLY_EPS reads like. (If you switch to a cross-product form, |d1 × d2| = |sin θ| is linear in the angle, so the same 1e-7 would be ~4500× narrower — pick the threshold deliberately rather than reusing the constant.)
And one correction to something I nearly told you: the Vector::WithMagnitude(1) of zero vector! spam next to this test in the CI log is pre-existing — six of them on master in this same test. Not yours.
6. Offer
I have the worktree and build set up, so I can turn any patch around quickly — say the word and I'll run it against the full suite and report back. Two directions that look worth trying, if you want a second pair of hands on them: rejecting a fallback hit unless it's a genuine transversal crossing (the returned point currently isn't checked against the query segment at all), or handling the tangent-at-edge case where the degeneracy is actually understood, in the ray caster rather than in PointIntersectingLine.
Either way, the approach is still the right shape to me — fixing it in ratpoly.cpp is more general than splitting curves after the fact, and it's orthogonal to BoykoNeov#6 rather than competing with it.
CHECK_FALSE expands to a check followed by a `return`, so a failing check returns from the test case and skips the el.Clear() two lines below it. The SEdgeList's buffer then leaks, and LeakSanitizer reports it with a stack in polygon.cpp -- which names the allocation site rather than the check that actually failed. On PR #1751 that report drew the attention, and the single failing check under it went unexamined for a while. Read IsEmpty() into a local and free the list before any of the three checks, so every check is reachable on every path and none of them can leak. The values checked are unchanged. Note that the old order leaked on a failing CHECK_FALSE(inters) too, not only on the leaks check. translate_asy already frees before it checks and needs no change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
@claude and @BoykoNeov First one question for @claude. What does "A boundary hit is precisely the degenerate case for both ray-cast parity and trim assembly, since the point belongs to two edges and to the neighbouring surface as well." mean? How is that a degenerate case? By the way a fillet surface is made the intersection is exactly on the boundary. The fix here just allows it to be found.
Anything else you can think of that might get this working with no regressions - it does fix both 1291_1743_cube_cut_tangent_outside... and cube_cut_2, so it has quite a bit going for it. @claude please give this approach your best shot (best 5 even!) you really understand the nuance of this code sometimes better than me. |
First, your question — you're right, and I'm withdrawing two claims
You're right and I can't defend what I wrote. "A boundary hit is precisely the degenerate case for both ray-cast parity and trim assembly" was too strong. Your fillet is the counterexample: the intersection is on the boundary by construction, and finding it is the whole point of the change. Being on a boundary is not the defect. What I actually had evidence for was much narrower — on these models, the hits that broke things happened to land at endpoints. I generalised that into a statement about boundaries in general, which the evidence didn't support. I've since found what the defect really is, and it isn't where the hit lands. Second correction from that same comment, which I found while checking the first: I said the uv dedup lets three hits on one edge survive into the trim. That's wrong. Those three were three separate ray casts, and the dedup only runs within a single call, so it never had the opportunity to merge them and isn't at fault. I shouldn't have made the claim. Where each of your five stands
Both commits sit directly on 1. The parallel guard (your #2)The test had two holes. It misses antiparallel. When the two directions point opposite ways And it gates on degree where the property is geometric. One test closes both: the magnitude of the cross product of the two unit directions is the sine of the angle between them, which is zero for parallel and antiparallel alike. It's also the quantity that actually matters, since This changes nothing I can measure, and I'd rather say so than claim a fix. Meshes from 2. The regression, which is the part that matters
Eight orders of magnitude between the two populations, so the threshold isn't a delicate one. This is the same degeneracy your guard was written for. You had it right in the thread — "it was somehow passing the test because the lines were coincident." Your guard misses these because it asks whether a curve is straight globally, and here the curves are genuinely curved cubics that happen to run tangent to the line at the point where they meet it. Local, not global. Why one manufactured point wrecks the model. What comes back isn't an intersection, it's the centre of whatever sub-patch
So on #3: the endpoint filter isn't missing, it's being beaten. Tightening it means picking a tolerance larger than the error of a point that shouldn't exist, which I don't think is winnable. Better not to manufacture the point. The fix is to require, after
3. A separate bug I hit on the way, worth its own issueThe zero-length trim edge in step 3 above is fatal to If I remove those edges by hand the Boolean assembles, but the mesh still leaks — the same vertex ends up at two positions 4e-6 mm apart and won't weld. So culling is a band-aid; the real fix is upstream, which is commit 2. But the assembler's inability to survive a degenerate edge seems worth hardening regardless. 4. The limit of my fix, and a question back to youI've overstated things once on this thread already, so plainly: this shows the new rule breaks none of the models I have. It doesn't show no model needs a tangential hit. The one shape it rejects is a boundary curve running tangent to the line being cast, at a point where the touch genuinely has to be found. And that is uncomfortably close to your own motivating case — so, concretely: on your fillet, does the boundary curve run tangent to the cast line at the intersection, or does it cross it? If it runs tangent, commit 2 is wrong for you and I'd want the model to work against. If it crosses — the surface is tangent but the boundary curve isn't — then the two cases are cleanly separable and I think this holds. That's the one thing I can't answer from here, and it decides whether commit 2 is right. 5. Your #5, and an offerThe exact-curve refinement step is untouched. I wanted the regression understood before adding another change on top, and the same question above governs it — if a tangential touch is sometimes legitimate, the criterion for both places needs to be different from the one I've used. Standing offer from before still holds: send me any patch and I'll run it against the full suite, both OpenMP settings, plus your three models, and report whatever it says. Written by Claude Opus 5 — both this text and the code it describes; posted by @BoykoNeov. |
|
@ruevs With the 3rd commit this PR now fixes 1291_1743_cuve_cut_tangent_out... as well as cube_cut_2 and it not longer causes a regression with 1291_cube_cut_tangnet_out... So this fixes the same sketches as BoykoNeov # 6 without doing all the extra work. Instead a fallback is used where line/surface intersection runs into tangency issues. I'd prefer to merge this one and not that one. The other method is interesting (if a bit hackish) so I wonder how to keep it around for reference, or to test with other issues. @claude no further work for you on this one, but I wanted to say your fix for the parallel test is broken. You used the cross product of two vectors d1 and d2, but neither of them was initialized. This was probably confusion about code from my prior version of the test assigning to those vectors, but that code was removed. Also, that test was just an early exit and didn't need to create a whole function checking for straight curve (colinear points). Your analysis was pretty good though and helped to find a proper fix. p.s. @ruevs I checked my entire collection of sketches and found no regressions. |
… edge of the surface in a curve-curve intersection test.
…ork with anti-parallel lines by using cross product instead of dot product. Also add a second test - if the intersection is found, verify that the curves are not parallel at/near the intersection point, as that will likely cause problems. This fixes a regression where test/group/boolean_tangent_crossing failed after the addition of this function.
42b0811 to
e7330a7
Compare
There was a problem hiding this comment.
@phkahler I rebased on master.
For all the NURBS test models I tried (87 of them), this works just as good as BoykoNeov#6 / #1746
Your conversation with Claude was rather interesting and it lead to yet another test case: #1743 (comment)
it fails the same with this and BoykoNeov#6 / #1746.
However I think this is better (more general) that Calude's solution and can be merged after fixing the few comments below (and squashing into one commit - despite the new test case above.
I'll be away from my development machine for for the next 10 days starting tomorrow, so I'll not be able to test/debug further.
P.S. You said:
[Claude] You used the cross product of two vectors d1 and d2, but neither of them was initialized
for Claude's work based on this PR here. I did not have time to pull it and test it, but d1 and d2 are initialized by the new SBezier::IsLine function.
| } | ||
| } | ||
| // we will not tail call here because of one more check afterward. | ||
| if(!PointOnThisAndCurve(curve, p)) { |
There was a problem hiding this comment.
The only other place where SBezier::PointOnThisAndCurve is called is here:
Line 293 in 1b7bc42
@phkahler please think whether PointOnNonparallelCurve should not be called there as well. And if so just remove PointOnNonparallelCurve entirely and put the new logic into PointOnThisAndCurve directly.
By the way I opened all 87 NURBS test models with a break point on the above line in curve.cpp and it did not trigger... I did not spend time to follow the call graph and figure out what kind of model will depend on that code....
There was a problem hiding this comment.
@ruevs I deliberately added PointOnNonparallelCurve as a sort of wrapper to PointOnThisAndCurve because I wasn't sure the extra should be in there. The other instance looks more like a 2D use of it, but I'm not sure so I don't want to merge them now. I wanted to ensure no side effects or regressions. My goal was to test the curve that forms the edge of a surface knowing ahead of time that they are tangent at intersection - the specific case we're dealing with in these 3 or 4 sketches.
| Vector d1 = ctrl[1].Minus(ctrl[0]).WithMagnitude(1.0); | ||
| Vector d2 = curve->ctrl[1].Minus(curve->ctrl[0]).WithMagnitude(1.0); | ||
| // I'm not sure what the angle tollerance should be here. | ||
| if(d1.Cross(d2).Magnitude() < LENGTH_EPS) { |
There was a problem hiding this comment.
if(d1.Cross(d2).MagSquared() < LENGTH_EPS * LENGTH_EPS)
We do not like sqrt - when it can be avoided ;-)
Or the two WithMagnitude(1.0); above... are they needed? Can they be "combined"? It's too late and I cant think...
There was a problem hiding this comment.
No because the length of the cross product is len(d1) * len(d2) * sin(theta). You'd need to get to sin^2 in order to use LENGTH_EPS^2. Clarity is good. I'm not too concerned about a couple square roots in an obscure fall-back code path ;-)
|
Written by Claude Opus 5, posted by @BoykoNeov. Agreed — merge this one and not #6. Your version reaches the same models through Where the fallback can and can't reachThe two fixes attack the defect from opposite ends. Mine splits a curve wherever It only runs when the iteration fails. It tries one of the four patch boundaries. The choice comes from The tolerance you weren't sure about
On The degree gate, in the path this code creates
One correction
They were — The offer to run any patch against the full suite still stands. |
Ha! Claude and I had the same idea this morning. Reading Claude's check [here](https://github.com/BoykoNeov/solvespace/commits/pr1751-fallback-fixes/](https://github.com/BoykoNeov/solvespace/commit/62d92299a594faa950e50d9465fbfa7f019a9104#diff-eab1902864a572597d5255f33a6675454c1826fa88b5468dbf4b769c53127f22R181-R182) So I tried to create a model that breaks "@phkahler" but not "Claude", here it is: 1291_1743_cube_cut_tangent_outside_still_fails_simplified_StraightDegree2Bezier.zip It is a model with a straight degree 2 bezier (all control points lie on a line) that is coincident with one of the edges of the solid being cut. Unfortunately it does not behave differently, it fails the same way for both versions (@phkahler and Claude).
Interestingly it fails in a different way in the 3.2 release:
I'm out of time for now - in the next 10 days I'll only look at code and comments - no compiling, testing, debugging. |
Both this and BoykoNeov#6 / #1746 behave worse than the 3.2 release for this model see here: #1743 (comment). |
|
@ruevs I want to merge this one if you're OK with that. Then we can get over to those solver fixes. |


… edge of the surface in a curve-curve intersection test.
This is my attempt to fix #1743. It does fix the failing edge/surface in the file 1291_1743_cube_cut....
The problem is that it breaks the opposite face. It seem to be splitting the crucial edge on that face into abou 10 segments for reasons I don't understand. A previous version of this also failed ASAN for reasons I don't understand - let's see if this one does too.
IMHO this approach should be better than the other fixes because it addresses the problem at the source in ratpoly.cpp when an edge/surface intersection fails due to being tangent we try something else to get the intersection.