Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion src/curvilinear.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 30 additions & 4 deletions src/solidmodels/render.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this TODO needed? If I understand the geometry right, this should also be skipped for being below min_angle in line-line rounding (at least approximately).

# (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
Expand Down Expand Up @@ -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
Comment on lines +1720 to +1727
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I worry about hiding real changes to geometry under a @debug message. I'd suggest making sure it's not some other error:

catch e
    if contains(string(e), <substring of expected error message text>)
        @debug ...
        return k.add_line(endpoints[1], endpoints[2])
    end
    rethrow()
end

end

# Exact *interpolating* cubic BSpline in OCC
Expand Down
55 changes: 55 additions & 0 deletions test/test_line_arc_rounding.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading