Skip to content

Commit a61a010

Browse files
authored
fix: Account for splits in BSpline conversion and reflection (#87)
* fix: Account for splits in BSpline conversion and reflection * Broadcast assignment instead of unsafe map in BSpline transforms
1 parent 791b221 commit a61a010

File tree

4 files changed

+62
-14
lines changed

4 files changed

+62
-14
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ The format of this changelog is based on
44
[Keep a Changelog](https://keepachangelog.com/), and this project adheres to
55
[Semantic Versioning](https://semver.org/).
66

7+
## Upcoming
8+
9+
- Fixed incorrect conversion and reflection of split BSplines
10+
- Added FAQ entry about MeshSized/OptionalEntity styling on Paths
11+
712
## 1.4.2 (2025-07-16)
813

914
- Removed invalid keyword constructor without type parameters for `@compdef`-ed components with type parameters, so it can be overridden without warnings

src/paths/paths.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import DeviceLayout:
4949
Meta,
5050
PointHook,
5151
Polygons,
52+
Reflection,
5253
Rotation,
5354
ScaledIsometry,
5455
StructureReference,

src/paths/segments/bspline.jl

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ Translate the interpolated segment so its initial point is `p`.
8787
function setp0!(b::BSpline, p::Point)
8888
# Adjust interpolation points
8989
translate = Translation(p - p0(b))
90-
map!(translate, b.p, b.p)
90+
b.p .= translate.(b.p)
9191

9292
return _update_interpolation!(b)
9393
end
@@ -101,7 +101,7 @@ setα0!(b::BSpline, α0′) = begin
101101
# Adjust interpolation points
102102
rotate = Rotation(α0′ - α0(b))
103103
rotate_interp = Translation(p0(b)) rotate Translation(-p0(b))
104-
map!(rotate_interp, b.p, b.p)
104+
b.p .= rotate_interp.(b.p)
105105

106106
# Adjust tangents
107107
b.t0 = rotate(b.t0)
@@ -121,19 +121,16 @@ Change the "handedness" of `b` by reflecting across the tangent at the start poi
121121
"""
122122
function change_handedness!(b::BSpline)
123123
# Perform reflection of points across line
124-
axis_dir = b.t0 / norm(b.t0)
125-
dx = axis_dir.x
126-
dy = axis_dir.y
127-
reflection = LinearMap(StaticArrays.@SMatrix [(dx^2-dy^2) 2dx*dy; 2dx*dy (dy^2-dx^2)])
128-
reflect_interp = Translation(p0(b)) reflection Translation(-p0(b))
129-
map!(reflect_interp, b.p, b.p)
130-
124+
axis_dir = Point(cos(α0(b)), sin(α0(b)))
125+
refl = Reflection(axis_dir)
131126
# Adjust tangents
132-
t1_reflected = Rotation(-(α1(b) - α0(b)))(axis_dir) * norm(b.t1)
133-
b.t1 = t1_reflected
127+
b.t0 = refl(b.t0)
128+
b.t1 = refl(b.t1)
129+
b.p = Reflection(axis_dir; through_pt=p0(b)).(b.p)
134130

135131
# Effective final angle at 0 and 1
136-
b.α1 = α0(b) - (α1(b) - α0(b))
132+
b.α0 = rotated_direction(b.α0, refl)
133+
b.α1 = rotated_direction(b.α1, refl)
137134

138135
return _update_interpolation!(b)
139136
end
@@ -163,8 +160,21 @@ function _update_interpolation!(b::BSpline)
163160
end
164161

165162
convert(::Type{BSpline{T}}, x::BSpline{T}) where {T} = x
166-
convert(::Type{BSpline{T}}, x::BSpline{S}) where {T, S} =
167-
BSpline(convert.(Point{T}, x.p), convert(Point{T}, x.t0), convert(Point{T}, x.t1))
163+
function convert(::Type{BSpline{T}}, b::BSpline{S}) where {T, S}
164+
# Use true t range for interpolations defined by points that have been scaled out of [0,1]
165+
tmin = b.r.ranges[1][1]
166+
tmax = b.r.ranges[1][end]
167+
p = convert.(Point{T}, b.p)
168+
t0 = convert(Point{T}, b.t0)
169+
t1 = convert(Point{T}, b.t1)
170+
p0 = convert(Point{T}, b.p0)
171+
p1 = convert(Point{T}, b.p1)
172+
r = Interpolations.scale(
173+
interpolate(p, Interpolations.BSpline(Cubic(NeumannBC(t0, t1)))),
174+
range(tmin, stop=tmax, length=length(p))
175+
)
176+
return BSpline(p, t0, t1, r, p0, p1, b.α0, b.α1)
177+
end
168178
convert(::Type{Segment{T}}, x::BSpline) where {T} = convert(BSpline{T}, x)
169179
copy(b::BSpline) = BSpline(copy(b.p), b.t0, b.t1, b.r, b.p0, b.p1, b.α0, b.α1)
170180

test/test_bspline.jl

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,11 @@
7676
@test pathlength(a1) 500μm rtol = 1e-9
7777
@test pathlength(a2) pathlength(b) - 500μm rtol = 1e-9
7878

79+
# Convert after split
80+
a2_conv = convert(Paths.BSpline{typeof(1.0mm)}, a2)
81+
@test p0(a2_conv) p0(a2)
82+
@test p1(a2_conv) p1(a2)
83+
7984
# Splice
8085
b4 = Paths.BSpline(
8186
[Point(-100, -200), Point(200, 200), Point(100, -300), Point(500, 0)],
@@ -100,6 +105,33 @@
100105
# Check an arbitrary point to make sure we have just rotated and translated a curve segment
101106
@test (pa2[1].seg)(Paths.t_to_arclength(pa2[1].seg, 0.2))
102107
splice_transform(b4.r(tsplit2 + (1 - tsplit2) * 0.2)) atol = 1e-9
108+
109+
# Also check reflection after splitting
110+
# And splitting with non-preferred unit
111+
pa3 = Path(10.0μm, 12.0μm; metadata=GDSMeta())
112+
turn!(pa3, 60°, 100μm, Paths.Trace(10μm))
113+
bspline!(pa3, [Point(200μm, 200μm)], 30°; endpoints_speed=200μm)
114+
splice!(pa3, 2, split(pa3[2], 100μm))
115+
cs = CoordinateSystem("test")
116+
addref!(cs, pa3, Point(-20, -20)μm, rot=45°, xrefl=true)
117+
tr = transformation(refs(cs)[1])
118+
csflat = flatten(cs)
119+
# Endpoints
120+
@test p0(elements(csflat)[2].seg) tr(p0(pa3[2].seg))
121+
@test p1(elements(csflat)[2].seg) tr(p1(pa3[2].seg))
122+
@test p0(elements(csflat)[3].seg) tr(p1(pa3[2].seg))
123+
@test p1(elements(csflat)[3].seg) tr(p1(pa3[3].seg))
124+
@test α0(elements(csflat)[2].seg) rotated_direction(α0(pa3[2].seg), tr)
125+
@test α1(elements(csflat)[2].seg) rotated_direction(α1(pa3[2].seg), tr)
126+
@test α0(elements(csflat)[3].seg) rotated_direction(α1(pa3[2].seg), tr)
127+
@test α1(elements(csflat)[3].seg) rotated_direction(α1(pa3[3].seg), tr)
128+
# Arbitrary points
129+
@test elements(csflat)[2].seg(50μm) tr(pa3[2].seg(50μm))
130+
@test elements(csflat)[3].seg(50μm) tr(pa3[3].seg(50μm))
131+
@test direction(elements(csflat)[2].seg, 50μm)
132+
rotated_direction(direction(pa3[2].seg, 50μm), tr)
133+
@test direction(elements(csflat)[3].seg, 50μm)
134+
rotated_direction(direction(pa3[3].seg, 50μm), tr)
103135
end
104136

105137
@testset "BSpline approximation" begin

0 commit comments

Comments
 (0)