From bc65338979128d1d53d0024905a2f89ccca29781 Mon Sep 17 00:00:00 2001 From: Gregory Peairs Date: Tue, 24 Mar 2026 12:47:04 +0100 Subject: [PATCH 1/4] Use preferred coordinate type for Path except when explicitly provided --- src/paths/paths.jl | 53 ++++++++++++++++++++++++++++++--------------- test/test_entity.jl | 7 +++++- test/tests.jl | 40 ++++++++++++++-------------------- 3 files changed, 58 insertions(+), 42 deletions(-) diff --git a/src/paths/paths.jl b/src/paths/paths.jl index 5a61582b..44720c14 100644 --- a/src/paths/paths.jl +++ b/src/paths/paths.jl @@ -204,7 +204,12 @@ mutable struct Node{T} <: GeometryEntity{T} end convert(::Type{Node{T}}, n::Node{T}) where {T} = n function convert(::Type{Node{T}}, n::Node{S}) where {S, T} - return Node{T}(convert(Segment{T}, n.seg), n.sty) + # Breaks linked list but can at least have dummies on either side + # to preserve info about whether the node started/ended a path + cur = Node{T}(convert(Segment{T}, n.seg), n.sty) + n.prev !== n && (cur.prev = Node{T}(convert(Segment{T}, n.prev.seg), n.prev.sty)) + n.next !== n && (cur.next = Node{T}(convert(Segment{T}, n.next.seg), n.next.sty)) + return cur end convert(::Type{GeometryEntity{T}}, n::Node) where {T} = convert(Node{T}, n) @@ -362,14 +367,19 @@ end Type for abstracting an arbitrary styled path in the plane. Iterating returns [`Paths.Node`](@ref) objects. -Convenience constructors for `Path{T}` object: +The most common convenience constructors: - Path{T}(p0::Point=zero(Point{T}), α0::typeof(1.0°)=0.0°, metadata::Meta=UNDEF_META) - Path{T}(name::String, p0::Point{T}=zero(Point{T}), α0=0.0°, meta::Meta=UNDEF_META, nodes::Vector{Node{T}}=Node{T}[]) - Path(p0::Point=zero(Point{typeof(1.0UPREFERRED)}); α0=0.0, name=uniquename("path"), metadata=UNDEF_META) - Path(p0x::Coordinate, p0y::Coordinate; α0=0.0, name=uniquename("path"), metadata=UNDEF_META) - Path(u::CoordinateUnits; α0=0.0, name=uniquename("path"), metadata=UNDEF_META) + Path(p0::Point=zero(Point{typeof(1.0UPREFERRED)}); α0=0.0°, name=uniquename("path"), metadata=UNDEF_META) + Path(p0x::Coordinate, p0y::Coordinate; α0=0.0°, name=uniquename("path"), metadata=UNDEF_META) + +Paths constructed via those constructors with `p0` in `Length` units will have coordinate type +`typeof(1.0UPREFERRED)`. +For explicit control over the coordinate type, use one of the other constructors: + + Path{T}(p0::Point=zero(Point{T}), α0=0.0°, metadata::Meta=UNDEF_META) + Path{T}(name::String, p0::Point=zero(Point{T}), α0=0.0°, metadata::Meta=UNDEF_META, nodes::Vector{Node{T}}=Node{T}[]) Path(v::Vector{Node{T}}; name=uniquename("path"), metadata=UNDEF_META) where {T} + Path(u::CoordinateUnits, p0=zero(Point{typeof(1.0u)}); α0=0.0°, name=uniquename("path"), metadata=UNDEF_META) """ mutable struct Path{T} <: AbstractComponent{T} name::String @@ -381,7 +391,7 @@ mutable struct Path{T} <: AbstractComponent{T} Path{T}( name::String, - p0::Point{T}=zero(Point{T}), + p0::Point=zero(Point{T}), α0=0.0°, meta::Meta=UNDEF_META, nodes::Vector{Node{T}}=Node{T}[] @@ -477,26 +487,35 @@ function show(io::IO, x::Node) return print(io, "$(segment(x)) styled as $(style(x))") end -Path{T}(p0::Point{T}=zero(Point{T}), α0=0.0°, meta::Meta=UNDEF_META) where {T} = +Path{T}(p0::Point=zero(Point{T}), α0=0.0°, meta::Meta=UNDEF_META) where {T} = Path{T}(uniquename("path"), p0, α0, meta, Node{T}[]) +# Return "preferred" path length coordinate type +# for consistency with non-explicit unit choice +_to_preferred(::Length) = typeof(1.0UPREFERRED) +_to_preferred(T) = Float64 + function Path( p0::Point{T}=zero(Point{typeof(1.0UPREFERRED)}); α0=0.0°, name=uniquename("path"), metadata=UNDEF_META ) where {T} - return Path{float(T)}(name, float.(p0), α0, metadata) + S = _to_preferred(oneunit(T)) + return Path{S}(name, p0, α0, metadata) end -Path(p0x::T, p0y::T; kwargs...) where {T <: Coordinate} = - Path(Point{float(T)}(p0x, p0y); kwargs...) +Path(p0x::Coordinate, p0y::Coordinate; kwargs...) = Path(Point(p0x, p0y); kwargs...) -Path(p0x::S, p0y::T; kwargs...) where {S <: Coordinate, T <: Coordinate} = - Path(promote(p0x, p0y)...; kwargs...) - -function Path(u::DeviceLayout.CoordinateUnits; kwargs...) - return Path(Point(0.0u, 0.0u); kwargs...) +function Path( + u::DeviceLayout.CoordinateUnits, + p0=zero(Point{typeof(1.0u)}); + α0=0.0°, + name=uniquename("path"), + metadata=UNDEF_META +) + T = typeof(1.0u) + return Path{T}(name, p0, α0, metadata) end function Path(v::Vector{Node{T}}; name=uniquename("path"), metadata=UNDEF_META) where {T} isempty(v) && return Path{T}(zero(Point{T}), 0.0°, v) diff --git a/test/test_entity.jl b/test/test_entity.jl index 17a6baeb..e83156f0 100644 --- a/test/test_entity.jl +++ b/test/test_entity.jl @@ -111,7 +111,12 @@ @test Paths.style(elements(cs)[3]).length == 100μm # Make sure taper got reconciled flathalo = halo(elements(cs), 2μm) @test eltype(flathalo) <: Paths.Node - @test all(Paths.style.(elements(halopath)) .== Paths.style.(flathalo)) + @show Paths.style.(elements(halopath)) + @show Paths.style.(flathalo) + @test all( + Paths.width.(Paths.style.(elements(halopath)), 0nm) .≈ + Paths.width.(Paths.style.(flathalo), 0nm) + ) @test Paths.style(flathalo[4]).length == 100μm # Issue: Corner transformation diff --git a/test/tests.jl b/test/tests.jl index ec4e09b4..2379e22d 100644 --- a/test/tests.jl +++ b/test/tests.jl @@ -511,32 +511,24 @@ end @test typeof(Path(Point(0, 0))) == Path{Float64} @test α0(Path()) == 0.0° == 0.0 == 0.0rad - @test typeof(Path(0.0μm, 0.0μm)) == Path{typeof(1.0μm)} - @test typeof(Path(0.0μm2μm, 0.0μm2μm)) == Path{typeof(1.0μm2μm)} - @test typeof(Path(0.0μm2μm, 0.0nm2μm)) == Path{typeof(1.0μm2μm)} - @test typeof(Path(0.0μm2nm, 0.0μm2nm)) == Path{typeof(1.0μm2nm)} - @test typeof(Path(0.0μm2nm, 0.0nm2nm)) == Path{typeof(1.0nm2nm)} - @test typeof(Path(0μm, 0μm)) == Path{typeof(1.0μm)} - @test typeof(Path(0μm2μm, 0μm2μm)) == Path{typeof(1.0μm2μm)} - @test typeof(Path(0μm2μm, 0nm2μm)) == Path{typeof(1.0μm2μm)} - @test typeof(Path(0μm2nm, 0μm2nm)) == Path{typeof(1.0μm2nm)} - @test typeof(Path(0μm2nm, 0nm2nm)) == Path{typeof(1.0nm2nm)} - - @test typeof(Path(Point(0.0μm, 0.0μm))) == Path{typeof(1.0μm)} - @test typeof(Path(Point(0.0μm2μm, 0.0μm2μm))) == Path{typeof(1.0μm2μm)} - @test typeof(Path(Point(0.0μm2μm, 0.0nm2μm))) == Path{typeof(1.0μm2μm)} - @test typeof(Path(Point(0.0μm2nm, 0.0μm2nm))) == Path{typeof(1.0μm2nm)} - @test typeof(Path(Point(0.0μm2nm, 0.0nm2nm))) == Path{typeof(1.0nm2nm)} - @test typeof(Path(Point(0μm, 0μm))) == Path{typeof(1.0μm)} - @test typeof(Path(Point(0μm2μm, 0μm2μm))) == Path{typeof(1.0μm2μm)} - @test typeof(Path(Point(0μm2μm, 0nm2μm))) == Path{typeof(1.0μm2μm)} - @test typeof(Path(Point(0μm2nm, 0μm2nm))) == Path{typeof(1.0μm2nm)} - @test typeof(Path(Point(0μm2nm, 0nm2nm))) == Path{typeof(1.0nm2nm)} + # FreeUnits (plain Unitful) normalize to preferred type (#93) + up = DeviceLayout.UPREFERRED + @test typeof(Path(0.0μm, 0.0μm)) == Path{typeof(1.0up)} + @test typeof(Path(0.0mm, 0.0mm)) == Path{typeof(1.0up)} + # ContextUnits preserve their preferred context + @test typeof(Path(0.0μm2μm, 0.0μm2μm)) == Path{typeof(1.0up)} + @test typeof(Path(0.0μm2μm, 0.0nm2μm)) == Path{typeof(1.0up)} + # Unit constructor preserves explicit unit choice (matches Cell("name", μm)) + @test typeof(Path(μm)) == Path{typeof(1.0μm)} + @test typeof(Path(μm2nm, Point(0.0nm2nm, 0.0nm2nm))) == Path{typeof(1.0μm2nm)} + @test typeof(Path(μm2nm, Point(0, 0)μm2μm)) == Path{typeof(1.0μm2nm)} + @test typeof(Path(μm, Point(0, 0)μm2μm)) == Path{typeof(1.0μm)} @test α0(Path(Point(0.0μm, 0.0μm); α0=90°)) == 90.0° == π * rad / 2 == π / 2 @test α0(Path(Point(0.0μm, 0.0μm); α0=π / 2)) == 90.0° == π * rad / 2 == π / 2 - @test typeof(Path(μm)) == Path{typeof(1.0μm)} + # Coordinate values preserved after normalization + @test getx(Path(1.0mm, 2.0mm).p0) ≈ 1.0mm @test_throws DimensionError Path(0, 0μm) @test_throws DimensionError Path(0nm, 0) @@ -684,8 +676,8 @@ end splice!(pa, 1, split(pa[1], π / 4 * 10μm)) splice!(pa, 3, split(pa[3], π / 4 * 10μm)) - @test (α1(pa[2].seg) == π / 2) && (α1(pa[1].seg) == α0(pa[2].seg)) - @test (α1(pa[4].seg) == 0) && (α1(pa[3].seg) == α0(pa[4].seg)) + @test (α1(pa[2].seg) ≈ π / 2) && (α1(pa[1].seg) ≈ α0(pa[2].seg)) + @test (α1(pa[4].seg) ≈ 0) && (α1(pa[3].seg) ≈ α0(pa[4].seg)) # Splitting bsplines near endpoints (MR !198) pa = Path(0μm, 0μm) From f22439f28474fb145300efae2c70289646925ad9 Mon Sep 17 00:00:00 2001 From: Greg Peairs Date: Wed, 25 Mar 2026 13:07:35 +0100 Subject: [PATCH 2/4] Update test/test_entity.jl --- test/test_entity.jl | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/test_entity.jl b/test/test_entity.jl index e83156f0..c9220d77 100644 --- a/test/test_entity.jl +++ b/test/test_entity.jl @@ -111,8 +111,6 @@ @test Paths.style(elements(cs)[3]).length == 100μm # Make sure taper got reconciled flathalo = halo(elements(cs), 2μm) @test eltype(flathalo) <: Paths.Node - @show Paths.style.(elements(halopath)) - @show Paths.style.(flathalo) @test all( Paths.width.(Paths.style.(elements(halopath)), 0nm) .≈ Paths.width.(Paths.style.(flathalo), 0nm) From 88e185595248def6711944141ad294c3116e2dd7 Mon Sep 17 00:00:00 2001 From: Gregory Peairs Date: Fri, 27 Mar 2026 14:09:38 +0100 Subject: [PATCH 3/4] Handle PreferNoUnits in Path coordinate type _to_preferred --- src/paths/paths.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/paths/paths.jl b/src/paths/paths.jl index 44720c14..62a6cfbd 100644 --- a/src/paths/paths.jl +++ b/src/paths/paths.jl @@ -492,8 +492,8 @@ Path{T}(p0::Point=zero(Point{T}), α0=0.0°, meta::Meta=UNDEF_META) where {T} = # Return "preferred" path length coordinate type # for consistency with non-explicit unit choice -_to_preferred(::Length) = typeof(1.0UPREFERRED) -_to_preferred(T) = Float64 +_to_preferred(x::Length) = 1.0UPREFERRED isa Length ? typeof(1.0UPREFERRED) : typeof(x) +_to_preferred(_) = Float64 function Path( p0::Point{T}=zero(Point{typeof(1.0UPREFERRED)}); From 67ccbc61423f66d7509a05b8b7cc40970e1ed8a2 Mon Sep 17 00:00:00 2001 From: Gregory Peairs Date: Tue, 7 Apr 2026 17:10:04 +0200 Subject: [PATCH 4/4] Dummy neighbors link back to converted node --- src/paths/paths.jl | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/paths/paths.jl b/src/paths/paths.jl index 62a6cfbd..df8b6a91 100644 --- a/src/paths/paths.jl +++ b/src/paths/paths.jl @@ -207,8 +207,14 @@ function convert(::Type{Node{T}}, n::Node{S}) where {S, T} # Breaks linked list but can at least have dummies on either side # to preserve info about whether the node started/ended a path cur = Node{T}(convert(Segment{T}, n.seg), n.sty) - n.prev !== n && (cur.prev = Node{T}(convert(Segment{T}, n.prev.seg), n.prev.sty)) - n.next !== n && (cur.next = Node{T}(convert(Segment{T}, n.next.seg), n.next.sty)) + if n.prev !== n + cur.prev = Node{T}(convert(Segment{T}, n.prev.seg), n.prev.sty) + cur.prev.next = cur + end + if n.next !== n + cur.next = Node{T}(convert(Segment{T}, n.next.seg), n.next.sty) + cur.next.prev = cur + end return cur end convert(::Type{GeometryEntity{T}}, n::Node) where {T} = convert(Node{T}, n) @@ -492,8 +498,9 @@ Path{T}(p0::Point=zero(Point{T}), α0=0.0°, meta::Meta=UNDEF_META) where {T} = # Return "preferred" path length coordinate type # for consistency with non-explicit unit choice -_to_preferred(x::Length) = 1.0UPREFERRED isa Length ? typeof(1.0UPREFERRED) : typeof(x) -_to_preferred(_) = Float64 +_preferred_coordinatetype(x::Length) = + 1.0UPREFERRED isa Length ? typeof(1.0UPREFERRED) : typeof(x) +_preferred_coordinatetype(::Coordinate) = Float64 function Path( p0::Point{T}=zero(Point{typeof(1.0UPREFERRED)}); @@ -501,7 +508,7 @@ function Path( name=uniquename("path"), metadata=UNDEF_META ) where {T} - S = _to_preferred(oneunit(T)) + S = _preferred_coordinatetype(oneunit(T)) return Path{S}(name, p0, α0, metadata) end