Skip to content
Merged
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
3 changes: 3 additions & 0 deletions scripts/format.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Usage: julia format.jl <action>
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
Expand Down
64 changes: 30 additions & 34 deletions src/curvilinear.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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])

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -542,26 +548,22 @@ 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)
prev_i = mod1(i - 1, n)

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)
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down
18 changes: 5 additions & 13 deletions src/solidmodels/render.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion test/test_line_arc_rounding.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
23 changes: 19 additions & 4 deletions test/test_shapes.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
Loading