Compare commits
4 Commits
c3534beb3f
...
858ef4a114
Author | SHA1 | Date | |
---|---|---|---|
858ef4a114 | |||
b27b1d93d8 | |||
c74573e7dd | |||
0e34242ba5 |
@ -21,7 +21,7 @@ def ell(
|
||||
*,
|
||||
spacing: float | ArrayLike | None = None,
|
||||
set_rotation: float | None = None,
|
||||
) -> dict[str, float]:
|
||||
) -> dict[str, numpy.float64]:
|
||||
"""
|
||||
Calculate extension for each port in order to build a 90-degree bend with the provided
|
||||
channel spacing:
|
||||
|
@ -233,7 +233,7 @@ class Arc(Shape):
|
||||
r0, r1 = self.radii
|
||||
|
||||
# Convert from polar angle to ellipse parameter (for [rx*cos(t), ry*sin(t)] representation)
|
||||
a_ranges = cast(tuple[tuple[float, float], tuple[float, float]], self._angles_to_parameters())
|
||||
a_ranges = cast(_array2x2_t, self._angles_to_parameters())
|
||||
|
||||
# Approximate perimeter via numerical integration
|
||||
|
||||
@ -246,13 +246,13 @@ class Arc(Shape):
|
||||
|
||||
def get_arclens(n_pts: int, a0: float, a1: float, dr: float) -> tuple[NDArray[numpy.float64], NDArray[numpy.float64]]:
|
||||
""" Get `n_pts` arclengths """
|
||||
t, dt = numpy.linspace(a0, a1, n_pts, retstep=True) # NOTE: could probably use an adaptive number of points
|
||||
r0sin = (r0 + dr) * numpy.sin(t)
|
||||
r1cos = (r1 + dr) * numpy.cos(t)
|
||||
tt, dt = numpy.linspace(a0, a1, n_pts, retstep=True) # NOTE: could probably use an adaptive number of points
|
||||
r0sin = (r0 + dr) * numpy.sin(tt)
|
||||
r1cos = (r1 + dr) * numpy.cos(tt)
|
||||
arc_dl = numpy.sqrt(r0sin * r0sin + r1cos * r1cos)
|
||||
#arc_lengths = numpy.diff(t) * (arc_dl[1:] + arc_dl[:-1]) / 2
|
||||
#arc_lengths = numpy.diff(tt) * (arc_dl[1:] + arc_dl[:-1]) / 2
|
||||
arc_lengths = (arc_dl[1:] + arc_dl[:-1]) * numpy.abs(dt) / 2
|
||||
return arc_lengths, t
|
||||
return arc_lengths, tt
|
||||
|
||||
wh = self.width / 2.0
|
||||
if num_vertices is not None:
|
||||
@ -286,6 +286,7 @@ class Arc(Shape):
|
||||
thetas = thetas[::-1]
|
||||
return thetas
|
||||
|
||||
thetas_inner: NDArray[numpy.float64]
|
||||
if wh in (r0, r1):
|
||||
thetas_inner = numpy.zeros(1) # Don't generate multiple vertices if we're at the origin
|
||||
else:
|
||||
@ -320,11 +321,11 @@ class Arc(Shape):
|
||||
|
||||
If the extrema are innaccessible due to arc constraints, check the arc endpoints instead.
|
||||
"""
|
||||
a_ranges = self._angles_to_parameters()
|
||||
a_ranges = cast(_array2x2_t, self._angles_to_parameters())
|
||||
|
||||
mins = []
|
||||
maxs = []
|
||||
for a, sgn in zip(a_ranges, (-1, +1), strict=True):
|
||||
for aa, sgn in zip(a_ranges, (-1, +1), strict=True):
|
||||
wh = sgn * self.width / 2
|
||||
rx = self.radius_x + wh
|
||||
ry = self.radius_y + wh
|
||||
@ -335,13 +336,13 @@ class Arc(Shape):
|
||||
maxs.append([0, 0])
|
||||
continue
|
||||
|
||||
a0, a1 = a
|
||||
a0, a1 = aa
|
||||
a0_offset = a0 - (a0 % (2 * pi))
|
||||
|
||||
sin_r = numpy.sin(self.rotation)
|
||||
cos_r = numpy.cos(self.rotation)
|
||||
sin_a = numpy.sin(a)
|
||||
cos_a = numpy.cos(a)
|
||||
sin_a = numpy.sin(aa)
|
||||
cos_a = numpy.cos(aa)
|
||||
|
||||
# Cutoff angles
|
||||
xpt = (-self.rotation) % (2 * pi) + a0_offset
|
||||
@ -431,19 +432,19 @@ class Arc(Shape):
|
||||
[[x2, y2], [x3, y3]]], would create this arc from its corresponding ellipse.
|
||||
```
|
||||
"""
|
||||
a_ranges = self._angles_to_parameters()
|
||||
a_ranges = cast(_array2x2_t, self._angles_to_parameters())
|
||||
|
||||
mins = []
|
||||
maxs = []
|
||||
for a, sgn in zip(a_ranges, (-1, +1), strict=True):
|
||||
for aa, sgn in zip(a_ranges, (-1, +1), strict=True):
|
||||
wh = sgn * self.width / 2
|
||||
rx = self.radius_x + wh
|
||||
ry = self.radius_y + wh
|
||||
|
||||
sin_r = numpy.sin(self.rotation)
|
||||
cos_r = numpy.cos(self.rotation)
|
||||
sin_a = numpy.sin(a)
|
||||
cos_a = numpy.cos(a)
|
||||
sin_a = numpy.sin(aa)
|
||||
cos_a = numpy.cos(aa)
|
||||
|
||||
# arc endpoints
|
||||
xn, xp = sorted(rx * cos_r * cos_a - ry * sin_r * sin_a)
|
||||
@ -461,21 +462,23 @@ class Arc(Shape):
|
||||
"Eccentric anomaly" parameter ranges for the inner and outer edges, in the form
|
||||
`[[a_min_inner, a_max_inner], [a_min_outer, a_max_outer]]`
|
||||
"""
|
||||
a = []
|
||||
aa = []
|
||||
for sgn in (-1, +1):
|
||||
wh = sgn * self.width / 2.0
|
||||
rx = self.radius_x + wh
|
||||
ry = self.radius_y + wh
|
||||
|
||||
a0, a1 = (numpy.arctan2(rx * numpy.sin(a), ry * numpy.cos(a)) for a in self.angles)
|
||||
a0, a1 = (numpy.arctan2(rx * numpy.sin(ai), ry * numpy.cos(ai)) for ai in self.angles)
|
||||
sign = numpy.sign(self.angles[1] - self.angles[0])
|
||||
if sign != numpy.sign(a1 - a0):
|
||||
a1 += sign * 2 * pi
|
||||
|
||||
a.append((a0, a1))
|
||||
return numpy.array(a, dtype=float)
|
||||
aa.append((a0, a1))
|
||||
return numpy.array(aa, dtype=float)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
angles = f' a°{numpy.rad2deg(self.angles)}'
|
||||
rotation = f' r°{numpy.rad2deg(self.rotation):g}' if self.rotation != 0 else ''
|
||||
return f'<Arc o{self.offset} r{self.radii}{angles} w{self.width:g}{rotation}>'
|
||||
|
||||
_array2x2_t = tuple[tuple[float, float], tuple[float, float]]
|
||||
|
@ -271,7 +271,7 @@ class Path(Shape):
|
||||
# TODO: Path.travel() needs testing
|
||||
direction = numpy.array([1, 0])
|
||||
|
||||
verts = [numpy.zeros(2)]
|
||||
verts: list[NDArray[numpy.float64]] = [numpy.zeros(2)]
|
||||
for angle, distance in travel_pairs:
|
||||
direction = numpy.dot(rotation_matrix_2d(angle), direction.T).T
|
||||
verts.append(verts[-1] + direction * distance)
|
||||
|
@ -22,9 +22,10 @@ def bezier(
|
||||
Returns:
|
||||
`[[x0, y0], [x1, y1], ...]` corresponding to `[tt0, tt1, ...]`
|
||||
"""
|
||||
nodes = numpy.asarray(nodes)
|
||||
tt = numpy.asarray(tt)
|
||||
nn = nodes.shape[0]
|
||||
if weights is None:
|
||||
weights = numpy.ones(nn)
|
||||
weights = numpy.ones(nn) if weights is None else numpy.asarray(weights)
|
||||
|
||||
t_half0 = tt <= 0.5
|
||||
umul = tt / (1 - tt)
|
||||
@ -43,7 +44,10 @@ def bezier(
|
||||
|
||||
|
||||
|
||||
def euler_bend(switchover_angle: float) -> NDArray[numpy.float64]:
|
||||
def euler_bend(
|
||||
switchover_angle: float,
|
||||
num_points: int = 200,
|
||||
) -> NDArray[numpy.float64]:
|
||||
"""
|
||||
Generate a 90 degree Euler bend (AKA Clothoid bend or Cornu spiral).
|
||||
|
||||
@ -51,42 +55,44 @@ def euler_bend(switchover_angle: float) -> NDArray[numpy.float64]:
|
||||
switchover_angle: After this angle, the bend will transition into a circular arc
|
||||
(and transition back to an Euler spiral on the far side). If this is set to
|
||||
`>= pi / 4`, no circular arc will be added.
|
||||
num_points: Number of points in the curve
|
||||
|
||||
Returns:
|
||||
`[[x0, y0], ...]` for the curve
|
||||
"""
|
||||
# Switchover angle
|
||||
# AKA: Clothoid bend, Cornu spiral
|
||||
theta_max = numpy.sqrt(2 * switchover_angle)
|
||||
ll_max = numpy.sqrt(2 * switchover_angle) # total length of (one) spiral portion
|
||||
ll_tot = 2 * ll_max + (pi / 2 - 2 * switchover_angle)
|
||||
num_points_spiral = numpy.floor(ll_max / ll_tot * num_points).astype(int)
|
||||
num_points_arc = num_points - 2 * num_points_spiral
|
||||
|
||||
def gen_curve(theta_max: float):
|
||||
def gen_spiral(ll_max: float):
|
||||
xx = []
|
||||
yy = []
|
||||
for theta in numpy.linspace(0, theta_max, 100):
|
||||
qq = numpy.linspace(0, theta, 1000)
|
||||
for ll in numpy.linspace(0, ll_max, num_points_spiral):
|
||||
qq = numpy.linspace(0, ll, 1000) # integrate to current arclength
|
||||
xx.append(numpy.trapz( numpy.cos(qq * qq / 2), qq))
|
||||
yy.append(numpy.trapz(-numpy.sin(qq * qq / 2), qq))
|
||||
xy_part = numpy.stack((xx, yy), axis=1)
|
||||
return xy_part
|
||||
|
||||
xy_part = gen_curve(theta_max)
|
||||
xy_parts = [xy_part]
|
||||
xy_spiral = gen_spiral(ll_max)
|
||||
xy_parts = [xy_spiral]
|
||||
|
||||
if switchover_angle < pi / 4:
|
||||
# Build a circular segment to join the two euler portions
|
||||
rmin = 1.0 / theta_max
|
||||
rmin = 1.0 / ll_max
|
||||
half_angle = pi / 4 - switchover_angle
|
||||
qq = numpy.linspace(half_angle * 2, 0, 10) + switchover_angle
|
||||
qq = numpy.linspace(half_angle * 2, 0, num_points_arc + 1) + switchover_angle
|
||||
xc = rmin * numpy.cos(qq)
|
||||
yc = rmin * numpy.sin(qq) + xy_part[-1, 1]
|
||||
xc += xy_part[-1, 0] - xc[0]
|
||||
yc += xy_part[-1, 1] - yc[0]
|
||||
xy_parts.append(numpy.stack((xc, yc), axis=1))
|
||||
yc = rmin * numpy.sin(qq) + xy_spiral[-1, 1]
|
||||
xc += xy_spiral[-1, 0] - xc[0]
|
||||
yc += xy_spiral[-1, 1] - yc[0]
|
||||
xy_parts.append(numpy.stack((xc[1:], yc[1:]), axis=1))
|
||||
|
||||
endpoint_xy = xy_parts[-1][-1, :]
|
||||
second_curve = xy_part[::-1, ::-1] + endpoint_xy - xy_part[-1, ::-1]
|
||||
second_spiral = xy_spiral[::-1, ::-1] + endpoint_xy - xy_spiral[-1, ::-1]
|
||||
|
||||
xy_parts.append(second_curve)
|
||||
xy_parts.append(second_spiral)
|
||||
xy = numpy.concatenate(xy_parts)
|
||||
|
||||
# Remove any 2x-duplicate points
|
||||
|
Loading…
x
Reference in New Issue
Block a user