From 7dca6f4f344df566d3561138e78d38d45e3e2e12 Mon Sep 17 00:00:00 2001 From: Layla Ghaffari Date: Tue, 14 Apr 2026 23:09:36 -0700 Subject: [PATCH] Fix degenerate line-arc corner rounding Add guards in rounded_corner_segment_line_arc and rounded_corner_line_arc for three degenerate cases that crash during CurvilinearPolygon rounding: 1. Fillet center coincides with arc center (norm(C_f - O) < atol), producing NaN from the direction vector division 2. Tangent points collapse onto fillet center (fillet_r < atol), producing NaN from zero-length direction vectors 3. Fillet sweep angle below min_angle, producing arcs too small for GMSH to distinguish from a line All three return nothing (skip rounding, leave corner sharp). --- src/curvilinear.jl | 26 +++++++++++++++- src/solidmodels/render.jl | 34 ++++++++++++++++++--- test/test_line_arc_rounding.jl | 55 ++++++++++++++++++++++++++++++++++ 3 files changed, 110 insertions(+), 5 deletions(-) diff --git a/src/curvilinear.jl b/src/curvilinear.jl index ebb97aa0..a53cb741 100644 --- a/src/curvilinear.jl +++ b/src/curvilinear.jl @@ -919,16 +919,40 @@ function rounded_corner_line_arc( T_line = p_line + t_proj * v_line # Tangent point on arc: point on the arc in the direction of the fillet center - cf_dir = (C_f - O) / norm(C_f - O) + # When C_f ≈ O (fillet_r ≈ arc_r), the direction is undefined + # and the fillet geometry is degenerate — skip rounding this corner. + norm_cf_o = norm(C_f - O) + if norm_cf_o < atol + return (; points=[p_corner], T_arc=nothing) + end + cf_dir = (C_f - O) / norm_cf_o T_arc = O + R * cf_dir ## Fillet arc angles # The fillet arc must follow the polygon winding order: # arc_is_outgoing=true: polygon goes ...line → T_line → [fillet] → T_arc → arc... # arc_is_outgoing=false: polygon goes ...arc → T_arc → [fillet] → T_line → line... + + # When tangent points coincide with C_f (fillet_r < atol), + # the direction vectors are undefined — skip rounding this corner. + if norm(T_line - C_f) < atol || norm(T_arc - C_f) < atol + return (; points=[p_corner], T_arc=nothing) + end α_T_line = atan((T_line - C_f).y, (T_line - C_f).x) α_T_arc = atan((T_arc - C_f).y, (T_arc - C_f).x) + # When the fillet sweep angle is tiny, the arc is indistinguishable from + # a line — skip rounding this corner. + # TODO: instead of skipping, fall through to line-line rounding + # (rounded_corner) by treating the arc edge as a straight line. + # This requires restructuring the caller (to_polygons for CurvilinearPolygon). + dα = + arc_is_outgoing ? rem2pi(α_T_arc - α_T_line, RoundNearest) : + rem2pi(α_T_line - α_T_arc, RoundNearest) + if abs(dα) < min_angle + return (; points=[p_corner], T_arc=nothing) + end + fillet_pts = if arc_is_outgoing DeviceLayout.circular_arc([α_T_line, α_T_arc], r, atol, center=C_f) else diff --git a/src/solidmodels/render.jl b/src/solidmodels/render.jl index 8f9d8011..07b4fac7 100644 --- a/src/solidmodels/render.jl +++ b/src/solidmodels/render.jl @@ -539,7 +539,11 @@ function rounded_corner_segment_line_arc( T_line = p_line + t_proj * v_line # Tangent point on arc: point on arc in direction of fillet center - cf_dir = (C_f - O) / norm(C_f - O) + # When C_f ≈ O (fillet_r ≈ arc_r), the direction is undefined + # and the fillet geometry is degenerate — skip rounding this corner. + norm_cf_o = norm(C_f - O) + norm_cf_o < atol && return nothing + cf_dir = (C_f - O) / norm_cf_o T_arc_pt = O + R * cf_dir # Construct fillet Turn segment @@ -548,13 +552,26 @@ function rounded_corner_segment_line_arc( # arc_is_outgoing=false: ...arc → T_arc → [fillet] → T_line → line... start_pt, end_pt = arc_is_outgoing ? (T_line, T_arc_pt) : (T_arc_pt, T_line) - d_start = (start_pt - C_f) / norm(start_pt - C_f) - d_end = (end_pt - C_f) / norm(end_pt - C_f) + # When tangent points coincide with C_f (fillet_r < atol), + # the direction vectors are undefined — skip rounding this corner. + norm_start = norm(start_pt - C_f) + norm_end = norm(end_pt - C_f) + (norm_start < atol || norm_end < atol) && return nothing + d_start = (start_pt - C_f) / norm_start + d_end = (end_pt - C_f) / norm_end cross_val = d_start.x * d_end.y - d_start.y * d_end.x dot_val = d_start.x * d_end.x + d_start.y * d_end.y dα = atan(cross_val, dot_val) + # When the fillet sweep angle is tiny, the arc sagitta + # (r·(1 - cos(dα/2))) is sub-nanometer — GMSH can't distinguish it from + # a line and rejects it. Skip rounding this corner. + # TODO: instead of skipping, fall through to line-line rounding + # (rounded_corner_segment) by treating the arc edge as a straight line. + # This requires restructuring the caller (round_to_curvilinearpolygon). + abs(dα) < min_angle && return nothing + # Tangent direction at start: perpendicular to radius, rotated by sweep direction angle_start = atan(d_start.y, d_start.x) α0 = angle_start + sign(dα) * π / 2 @@ -1698,7 +1715,16 @@ function _add_curve!(endpoints, seg::Paths.Turn, k::OpenCascade, z; kwargs...) return k.add_circle_arc.(tags[1:(end - 1)], cen, tags[2:end], -1) end - return k.add_circle_arc(endpoints[1], cen, endpoints[2], -1) + try + return k.add_circle_arc(endpoints[1], cen, endpoints[2], -1) + catch e + # Arc too small or degenerate for GMSH — fall back to straight line. + # This typically happens with fillet arcs at nearly-tangent corners + # where the sagitta is below GMSH's internal tolerance. + @debug "addCircleArc failed, falling back to line" p0 = seg.p0 r = seg.r α = seg.α α0 = + seg.α0 + return k.add_line(endpoints[1], endpoints[2]) + end end # Exact *interpolating* cubic BSpline in OCC diff --git a/test/test_line_arc_rounding.jl b/test/test_line_arc_rounding.jl index 7e0b7ca0..5b939ab5 100644 --- a/test/test_line_arc_rounding.jl +++ b/test/test_line_arc_rounding.jl @@ -686,3 +686,58 @@ end @test horseshoe_sm isa CurvilinearPolygon @test length(horseshoe_sm.curves) > length(horseshoe.curves) end + +@testitem "Degenerate line-arc fillet" setup = [CommonTestSetup] begin + using LinearAlgebra + using DeviceLayout.SolidModels + + # CurvilinearPolygon with a nearly-tangent line-arc corner. + # When fillet_r ≈ arc_r, the fillet center C_f coincides with the arc + # center O, producing NaN from (C_f - O) / norm(C_f - O). + R = 150.0μm + δ = 0.002 # rad, just above min_angle (1e-3) + fillet_r = R * cos(δ) + + arc = Paths.Turn(90.0°, R; p0=Point(0.0μm, 0.0μm), α0=(rad2deg(δ))°) + arc_end = Paths.p1(arc) + top_y = max(arc_end.y, Paths.curvaturecenter(arc).y) + 200.0μm + poly = CurvilinearPolygon( + [ + Point(-500.0μm, 0.0μm), + Point(0.0μm, 0.0μm), + arc_end, + Point(arc_end.x, top_y), + Point(-500.0μm, top_y) + ], + [arc], + [2] + ) + + @test_nowarn to_polygons(poly, Rounded(fillet_r)) + + # Polygon with a tiny-angle Turn rendered into a SolidModel. + # GMSH rejects arcs with very small sweep angles; the fix falls back to a line. + # TODO: instead of skipping, fall through to line-line rounding + # (rounded_corner) by treating the arc edge as a straight line. + tiny_arc = Paths.Turn(2.0°, 175.0μm; p0=Point(0.0μm, 0.0μm), α0=0.0°) + tiny_end = Paths.p1(tiny_arc) + margin = 20.0μm + tiny_poly = CurvilinearPolygon( + [ + Point(-margin, -margin), + Point(0.0μm, -margin), + Point(0.0μm, 0.0μm), + tiny_end, + Point(tiny_end.x, tiny_end.y + margin), + Point(-margin, tiny_end.y + margin) + ], + [tiny_arc], + [3] + ) + + sm = SolidModel("tiny_arc_test"; overwrite=true) + SolidModels.gmsh.option.setNumber("General.Verbosity", 0) + cs_sm = CoordinateSystem("tiny_arc_sm", nm) + place!(cs_sm, tiny_poly, GDSMeta(0, 0)) + @test_nowarn render!(sm, cs_sm) +end