diff --git a/scripts/format.jl b/scripts/format.jl index d364a292f..778394227 100644 --- a/scripts/format.jl +++ b/scripts/format.jl @@ -1,5 +1,8 @@ # Usage: julia format.jl using Pkg +# Use fresh temp env to ensure that JuliaFormatter version run locally +# is the same as that being run by CI pipeline +Pkg.activate(; temp=true) Pkg.add(name="JuliaFormatter", version="1") using JuliaFormatter # Directories to format (recursive); paths relative to repo root diff --git a/src/curvilinear.jl b/src/curvilinear.jl index ebb97aa02..f8c9d0bab 100644 --- a/src/curvilinear.jl +++ b/src/curvilinear.jl @@ -52,9 +52,16 @@ struct CurvilinearPolygon{T} <: GeometryEntity{T} p::Vector{Point{T}} curves::Vector{<:Paths.Segment} # Only need to store non-line-segment curves curve_start_idx::Vector{Int} # And the indices at which they start - # A negative start idx like -3 means that the corresponding curve - # between p[3] and p[4] is actually parameterized from p[4] to p[3] + # Backward-parameterized curves (negative start idx) are normalized in the constructor: + # the segment is reversed and the index flipped positive. function CurvilinearPolygon{T}(p, c, csi) where {T} # Make sure you don't have zero-length curves + # Normalize backward-parameterized curves: reverse segment, flip index positive. + for i in eachindex(csi) + if csi[i] < 0 + c[i] = reverse(c[i]) + csi[i] = -csi[i] + end + end # Don't treat duplicates in any different fashion -> view as user error # Some endpoint pairs may be identical; delete the duplicates # Maybe inefficient but least confusing to iterate to find them and then delete @@ -100,24 +107,24 @@ function to_polygons( 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)]) + append!(p, e.p[i:csi]) # Discretize segment using tolerance-based adaptive grid. - wrapped_i = mod1(abs(csi) + 1, length(e.p)) + wrapped_i = mod1(csi + 1, length(e.p)) pp = DeviceLayout.discretize_curve(c, atol) # Remove the calculated points corresponding to start and end. - term_p = csi < 0 ? popfirst!(pp) : pop!(pp) - init_p = csi < 0 ? pop!(pp) : popfirst!(pp) + term_p = pop!(pp) + init_p = popfirst!(pp) # Add interior points and bump counter. - append!(p, csi < 0 ? reverse(pp) : pp) - i = abs(csi) + 1 + append!(p, pp) + i = csi + 1 # Ensure that the calculated start and end points match the non-calculated points. @assert !isapprox(init_p, term_p; atol=1e-3 * DeviceLayout.onenanometer(T)) "Curve $idx must have non-zero length!" - @assert isapprox(term_p, e.p[wrapped_i]; atol=1e-3 * DeviceLayout.onenanometer(T)) "Curve $idx must $(csi < 0 ? "start" : "end") at point $(wrapped_i)!" - @assert isapprox(init_p, e.p[abs(csi)]; atol=1e-3 * DeviceLayout.onenanometer(T)) "Curve $idx must $(csi < 0 ? "end" : "start") at point $(abs(csi))!" + @assert isapprox(term_p, e.p[wrapped_i]; atol=1e-3 * DeviceLayout.onenanometer(T)) "Curve $idx must end at point $(wrapped_i)!" + @assert isapprox(init_p, e.p[csi]; atol=1e-3 * DeviceLayout.onenanometer(T)) "Curve $idx must start at point $(csi)!" end append!(p, e.p[i:end]) @@ -463,8 +470,7 @@ end # Only indices that don't start or end a curve are available for rounding. # cornerindices(p::CurvilinearPolygon, s::GeometryEntityStyle) = cornerindices(p, p0(s)) function cornerindices(p::CurvilinearPolygon{T}) where {T} - curve_bound_ind = - vcat((x -> [abs(x), (abs(x) % length(p.p)) + 1]).(p.curve_start_idx)...) + curve_bound_ind = vcat((x -> [x, (x % length(p.p)) + 1]).(p.curve_start_idx)...) valid_ind = setdiff(1:length(p.p), curve_bound_ind) return valid_ind end @@ -542,9 +548,9 @@ Returns a NamedTuple `(incoming=..., outgoing=...)` where each field is either `:straight` or the `Paths.Segment` (e.g., `Paths.Turn`) for that edge. - **Outgoing edge** (from `p[i]` to `p[i+1]`): curved if any - `abs(curve_start_idx[k]) == i` + `curve_start_idx[k] == i` - **Incoming edge** (from `p[i-1]` to `p[i]`): curved if any - `abs(curve_start_idx[k]) == mod1(i-1, n)` + `curve_start_idx[k] == mod1(i-1, n)` """ function edge_type_at_vertex(p::CurvilinearPolygon, i::Int) n = length(p.p) @@ -552,16 +558,12 @@ function edge_type_at_vertex(p::CurvilinearPolygon, i::Int) incoming = :straight outgoing = :straight - # k = index into curves array, csi = vertex index where curve k starts. - # Negative csi means the curve is parameterized in reverse; use reverse(curve) - # so p0/p1 match the vertex order expected by downstream rounding code. for (k, csi) in enumerate(p.curve_start_idx) - curve = csi < 0 ? reverse(p.curves[k]) : p.curves[k] - if abs(csi) == prev_i - incoming = curve + if csi == prev_i + incoming = p.curves[k] end - if abs(csi) == i - outgoing = curve + if csi == i + outgoing = p.curves[k] end end return (; incoming=incoming, outgoing=outgoing) @@ -596,7 +598,7 @@ function to_polygons( la_corners = Set(line_arc_cornerindices(ent, sty)) # Precompute vertex for curve index lookup - vertex_to_curve = Dict(abs(csi) => k for (k, csi) in enumerate(ent.curve_start_idx)) + vertex_to_curve = Dict(csi => k for (k, csi) in enumerate(ent.curve_start_idx)) # Round corners, tracking which original vertex maps to which output range. # Also track T_arc for each line-arc corner so we can trim the curves. @@ -696,19 +698,13 @@ function to_polygons( csi = ent.curve_start_idx[curve_k] arc_len = pathlength(c) - # Determine trim parameters: find t values for trim points on the arc. - # For negative csi, the curve is parameterized in reverse, so trim_start - # (at the forward-start vertex) maps to a high t value and trim_end to a - # low t value. Swap them so t_start < t_end for correct discretization. t_start = zero(S) t_end = arc_len - ts_dict = csi < 0 ? trim_end : trim_start - te_dict = csi < 0 ? trim_start : trim_end - if haskey(ts_dict, curve_k) - t_start = Paths.pathlength_nearest(c, ts_dict[curve_k]) + if haskey(trim_start, curve_k) + t_start = Paths.pathlength_nearest(c, trim_start[curve_k]) end - if haskey(te_dict, curve_k) - t_end = Paths.pathlength_nearest(c, te_dict[curve_k]) + if haskey(trim_end, curve_k) + t_end = Paths.pathlength_nearest(c, trim_end[curve_k]) end # Discretize the trimmed portion of the curve. @@ -725,7 +721,7 @@ function to_polygons( ) # Remove endpoints (already present as fillet tangent points) inner = grid[(begin + 1):(end - 1)] .* l - pp = c.(csi < 0 ? reverse(inner) : inner) + pp = c.(inner) append!(final_points, pp) end end diff --git a/src/solidmodels/render.jl b/src/solidmodels/render.jl index 8f9d8011f..6aa734f1b 100644 --- a/src/solidmodels/render.jl +++ b/src/solidmodels/render.jl @@ -296,7 +296,7 @@ function round_to_curvilinearpolygon( push!(new_points, Paths.p1(result.fillet)) # Record trim for the original arc arc_start_vtx = arc_is_outgoing ? i : mod1(i - 1, len) - curve_k = findfirst(csi -> abs(csi) == arc_start_vtx, pol.curve_start_idx) + curve_k = findfirst(isequal(arc_start_vtx), pol.curve_start_idx) if !isnothing(curve_k) if arc_is_outgoing trim_start_pts[curve_k] = result.T_arc @@ -366,7 +366,7 @@ function round_to_curvilinearpolygon( # Sort curves by start index so to_polygons can iterate in vertex order if length(new_curve_start_idx) > 1 - perm = sortperm(new_curve_start_idx, by=abs) + perm = sortperm(new_curve_start_idx) new_curves = new_curves[perm] new_curve_start_idx = new_curve_start_idx[perm] end @@ -1652,17 +1652,9 @@ function _add_loop!( endpoint_pairs = zip(pts, circshift(pts, -1)) curves = map(enumerate(endpoint_pairs)) do (i, endpoints) curve_idx = findfirst(isequal(i), cl.curve_start_idx) - if isnothing(curve_idx) # not found - # see if the negative of the index is in there - curve_idx = findfirst(isequal(-i), cl.curve_start_idx) - if isnothing(curve_idx) # nope, just a line - return k.add_line(endpoints[1], endpoints[2]) - else # negative index => reverse endpoints - c = _add_curve!(reverse(endpoints), cl.curves[curve_idx], k, z; atol) - !isempty(size(c)) && return reverse(c) - return c - end - else # add the curve whose start index is i + if isnothing(curve_idx) + return k.add_line(endpoints[1], endpoints[2]) + else return _add_curve!(endpoints, cl.curves[curve_idx], k, z; atol) end end diff --git a/test/test_line_arc_rounding.jl b/test/test_line_arc_rounding.jl index 7e0b7ca00..6c805780f 100644 --- a/test/test_line_arc_rounding.jl +++ b/test/test_line_arc_rounding.jl @@ -447,7 +447,7 @@ [neg_arc], [-3] # negative: curve between p[3] and p[4], parameterized from p[4] to p[3] ) - @test neg_cp.curve_start_idx[1] < 0 + @test neg_cp.curve_start_idx[1] > 0 # constructor normalizes to positive # Plain rendering must work (catches invalid vertex/curve mismatch) neg_plain = to_polygons(neg_cp) diff --git a/test/test_shapes.jl b/test/test_shapes.jl index 6840e0036..7d2ebdc76 100644 --- a/test/test_shapes.jl +++ b/test/test_shapes.jl @@ -429,6 +429,9 @@ end c = Cell("main", nm) @test_nowarn render!(c, cs) + # Constructor normalizes negative curve_start_idx to positive via reverse() + @test cp.curve_start_idx[1] == 2 + # 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) @@ -438,10 +441,22 @@ end c = Cell("main", nm) @test_nowarn render!(c, cs) - # 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 + # Transformed forward and reverse should give identical geometry. + # (Constructor normalization makes them internally equivalent.) + cpt_pts = points(to_polygons(cpt)) + tcp_pts = points(to_polygons(t(cp))) + @test length(cpt_pts) == length(tcp_pts) + @test all(isapprox.(cpt_pts, tcp_pts; atol=0.01nm)) + + # Negative csi must be normalized before duplicate-point cleanup, otherwise + # -3 ∉ [3] and the curve between duplicate points survives deletion. + dup_pts = + [Point(0.0μm, 0.0μm), Point(1.0μm, 0.0μm), Point(0.5μm, 1.0μm), Point(0.5μm, 1.0μm)] + dup_seg = Paths.Turn(90°, 1.0μm, α0=0°, p0=dup_pts[4]) + dup_cp = CurvilinearPolygon(dup_pts, [dup_seg], [-3]) + @test length(dup_cp.p) == 3 + @test isempty(dup_cp.curves) + @test isempty(dup_cp.curve_start_idx) # Convert a SimpleTrace to a CurvilinearRegion pa = Path(0nm, 0nm)