Skip to content
This repository was archived by the owner on Aug 9, 2024. It is now read-only.

Commit 610e1c2

Browse files
committed
Fix for Path.smooth not fiding _hobby (#182)
1 parent 91e06e8 commit 610e1c2

File tree

3 files changed

+252
-213
lines changed

3 files changed

+252
-213
lines changed

gdspy/hobby.py

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
######################################################################
2+
# #
3+
# Copyright 2009 Lucas Heitzmann Gabrielli. #
4+
# This file is part of gdspy, distributed under the terms of the #
5+
# Boost Software License - Version 1.0. See the accompanying #
6+
# LICENSE file or <http://www.boost.org/LICENSE_1_0.txt> #
7+
# #
8+
######################################################################
9+
10+
from __future__ import division
11+
from __future__ import unicode_literals
12+
from __future__ import print_function
13+
from __future__ import absolute_import
14+
15+
import sys
16+
17+
if sys.version_info.major < 3:
18+
from builtins import zip
19+
from builtins import open
20+
from builtins import int
21+
from builtins import round
22+
from builtins import range
23+
from builtins import super
24+
25+
from future import standard_library
26+
27+
standard_library.install_aliases()
28+
else:
29+
# Python 3 doesn't have basestring, as unicode is type string
30+
# Python 2 doesn't equate unicode to string, but both are basestring
31+
# Now isinstance(s, basestring) will be True for any python version
32+
basestring = str
33+
34+
import numpy
35+
36+
37+
def _hobby(points, angles=None, curl_start=1, curl_end=1, t_in=1, t_out=1, cycle=False):
38+
"""
39+
Calculate control points for a smooth interpolating curve.
40+
41+
Uses the Hobby algorithm [1]_ to calculate a smooth interpolating
42+
curve made of cubic Bezier segments between each pair of points.
43+
44+
Parameters
45+
----------
46+
points : Numpy array[N, 2]
47+
Vertices in the interpolating curve.
48+
angles : array-like[N] or None
49+
Tangent angles at each point (in *radians*). Any angles defined
50+
as None are automatically calculated.
51+
curl_start : number
52+
Ratio between the mock curvatures at the first point and at its
53+
neighbor. A value of 1 renders the first segment a good
54+
approximation for a circular arc. A value of 0 will better
55+
approximate a straight segment. It has no effect for closed
56+
curves or when an angle is defined for the first point.
57+
curl_end : number
58+
Ratio between the mock curvatures at the last point and at its
59+
neighbor. It has no effect for closed curves or when an angle
60+
is defined for the last point.
61+
t_in : number or array-like[N]
62+
Tension parameter when arriving at each point. One value per
63+
point or a single value used for all points.
64+
t_out : number or array-like[N]
65+
Tension parameter when leaving each point. One value per point
66+
or a single value used for all points.
67+
cycle : bool
68+
If True, calculates control points for a closed curve, with
69+
an additional segment connecting the first and last points.
70+
71+
Returns
72+
-------
73+
out : 2-tuple of Numpy array[M, 2]
74+
Pair of control points for each segment in the interpolating
75+
curve. For a closed curve (`cycle` True), M = N. For an open
76+
curve (`cycle` False), M = N - 1.
77+
78+
References
79+
----------
80+
.. [1] Hobby, J.D. *Discrete Comput. Geom.* (1986) 1: 123.
81+
`DOI: 10.1007/BF02187690 <https://doi.org/10.1007/BF02187690>`_
82+
"""
83+
z = points[:, 0] + 1j * points[:, 1]
84+
n = z.size
85+
if numpy.isscalar(t_in):
86+
t_in = t_in * numpy.ones(n)
87+
else:
88+
t_in = numpy.array(t_in)
89+
if numpy.isscalar(t_out):
90+
t_out = t_out * numpy.ones(n)
91+
else:
92+
t_out = numpy.array(t_out)
93+
if angles is None:
94+
angles = [None] * n
95+
rotate = 0
96+
if cycle and any(a is not None for a in angles):
97+
while angles[rotate] is None:
98+
rotate += 1
99+
angles = [angles[(rotate + j) % n] for j in range(n + 1)]
100+
z = numpy.hstack((numpy.roll(z, -rotate), z[rotate : rotate + 1]))
101+
t_in = numpy.hstack((numpy.roll(t_in, -rotate), t_in[rotate : rotate + 1]))
102+
t_out = numpy.hstack((numpy.roll(t_out, -rotate), t_out[rotate : rotate + 1]))
103+
cycle = False
104+
if cycle:
105+
# Closed curve
106+
v = numpy.roll(z, -1) - z
107+
d = numpy.abs(v)
108+
delta = numpy.angle(v)
109+
psi = (delta - numpy.roll(delta, 1) + numpy.pi) % (2 * numpy.pi) - numpy.pi
110+
coef = numpy.zeros(2 * n)
111+
coef[:n] = -psi
112+
m = numpy.zeros((2 * n, 2 * n))
113+
i = numpy.arange(n)
114+
i1 = (i + 1) % n
115+
i2 = (i + 2) % n
116+
ni = n + i
117+
m[i, i] = 1
118+
m[i, n + (i - 1) % n] = 1
119+
# A_i
120+
m[ni, i] = d[i1] * t_in[i2] * t_in[i1] ** 2
121+
# B_{i+1}
122+
m[ni, i1] = -d[i] * t_out[i] * t_out[i1] ** 2 * (1 - 3 * t_in[i2])
123+
# C_{i+1}
124+
m[ni, ni] = d[i1] * t_in[i2] * t_in[i1] ** 2 * (1 - 3 * t_out[i])
125+
# D_{i+2}
126+
m[ni, n + i1] = -d[i] * t_out[i] * t_out[i1] ** 2
127+
sol = numpy.linalg.solve(m, coef)
128+
theta = sol[:n]
129+
phi = sol[n:]
130+
w = numpy.exp(1j * (theta + delta))
131+
a = 2 ** 0.5
132+
b = 1.0 / 16
133+
c = (3 - 5 ** 0.5) / 2
134+
sintheta = numpy.sin(theta)
135+
costheta = numpy.cos(theta)
136+
sinphi = numpy.sin(phi)
137+
cosphi = numpy.cos(phi)
138+
alpha = (
139+
a * (sintheta - b * sinphi) * (sinphi - b * sintheta) * (costheta - cosphi)
140+
)
141+
cta = z + w * d * ((2 + alpha) / (1 + (1 - c) * costheta + c * cosphi)) / (
142+
3 * t_out
143+
)
144+
ctb = numpy.roll(z, -1) - numpy.roll(w, -1) * d * (
145+
(2 - alpha) / (1 + (1 - c) * cosphi + c * costheta)
146+
) / (3 * numpy.roll(t_in, -1))
147+
else:
148+
# Open curve(s)
149+
n = z.size - 1
150+
v = z[1:] - z[:-1]
151+
d = numpy.abs(v)
152+
delta = numpy.angle(v)
153+
psi = (delta[1:] - delta[:-1] + numpy.pi) % (2 * numpy.pi) - numpy.pi
154+
theta = numpy.empty(n)
155+
phi = numpy.empty(n)
156+
i = 0
157+
if angles[0] is not None:
158+
theta[0] = angles[0] - delta[0]
159+
while i < n:
160+
j = i + 1
161+
while j < n + 1 and angles[j] is None:
162+
j += 1
163+
if j == n + 1:
164+
j -= 1
165+
else:
166+
phi[j - 1] = delta[j - 1] - angles[j]
167+
if j < n:
168+
theta[j] = angles[j] - delta[j]
169+
# Solve open curve z_i, ..., z_j
170+
nn = j - i
171+
coef = numpy.zeros(2 * nn)
172+
coef[1:nn] = -psi[i : j - 1]
173+
m = numpy.zeros((2 * nn, 2 * nn))
174+
if nn > 1:
175+
ii = numpy.arange(nn - 1) # [0 .. nn-2]
176+
i0 = i + ii # [i .. j-1]
177+
i1 = 1 + i0 # [i+1 .. j]
178+
i2 = 2 + i0 # [i+2 .. j+1]
179+
ni = nn + ii # [nn .. 2*nn-2]
180+
ii1 = 1 + ii # [1 .. nn-1]
181+
m[ii1, ii1] = 1
182+
m[ii1, ni] = 1
183+
# A_ii
184+
m[ni, ii] = d[i1] * t_in[i2] * t_in[i1] ** 2
185+
# B_{ii+1}
186+
m[ni, ii1] = -d[i0] * t_out[i0] * t_out[i1] ** 2 * (1 - 3 * t_in[i2])
187+
# C_{ii+1}
188+
m[ni, ni] = d[i1] * t_in[i2] * t_in[i1] ** 2 * (1 - 3 * t_out[i0])
189+
# D_{ii+2}
190+
m[ni, ni + 1] = -d[i0] * t_out[i0] * t_out[i1] ** 2
191+
if angles[i] is None:
192+
to3 = t_out[0] ** 3
193+
cti3 = curl_start * t_in[1] ** 3
194+
# B_0
195+
m[0, 0] = to3 * (1 - 3 * t_in[1]) - cti3
196+
# D_1
197+
m[0, nn] = to3 - cti3 * (1 - 3 * t_out[0])
198+
else:
199+
coef[0] = theta[i]
200+
m[0, 0] = 1
201+
m[0, nn] = 0
202+
if angles[j] is None:
203+
ti3 = t_in[n] ** 3
204+
cto3 = curl_end * t_out[n - 1] ** 3
205+
# A_{nn-1}
206+
m[2 * nn - 1, nn - 1] = ti3 - cto3 * (1 - 3 * t_in[n])
207+
# C_nn
208+
m[2 * nn - 1, 2 * nn - 1] = ti3 * (1 - 3 * t_out[n - 1]) - cto3
209+
else:
210+
coef[2 * nn - 1] = phi[j - 1]
211+
m[2 * nn - 1, nn - 1] = 0
212+
m[2 * nn - 1, 2 * nn - 1] = 1
213+
if nn > 1 or angles[i] is None or angles[j] is None:
214+
# print("range:", i, j)
215+
# print("A =", m)
216+
# print("b =", coef)
217+
sol = numpy.linalg.solve(m, coef)
218+
# print("x =", sol)
219+
theta[i:j] = sol[:nn]
220+
phi[i:j] = sol[nn:]
221+
i = j
222+
w = numpy.hstack(
223+
(numpy.exp(1j * (delta + theta)), numpy.exp(1j * (delta[-1:] - phi[-1:])))
224+
)
225+
a = 2 ** 0.5
226+
b = 1.0 / 16
227+
c = (3 - 5 ** 0.5) / 2
228+
sintheta = numpy.sin(theta)
229+
costheta = numpy.cos(theta)
230+
sinphi = numpy.sin(phi)
231+
cosphi = numpy.cos(phi)
232+
alpha = (
233+
a * (sintheta - b * sinphi) * (sinphi - b * sintheta) * (costheta - cosphi)
234+
)
235+
cta = z[:-1] + w[:-1] * d * (
236+
(2 + alpha) / (1 + (1 - c) * costheta + c * cosphi)
237+
) / (3 * t_out[:-1])
238+
ctb = z[1:] - w[1:] * d * (
239+
(2 - alpha) / (1 + (1 - c) * cosphi + c * costheta)
240+
) / (3 * t_in[1:])
241+
if rotate > 0:
242+
cta = numpy.roll(cta, rotate)
243+
ctb = numpy.roll(ctb, rotate)
244+
return (
245+
numpy.vstack((cta.real, cta.imag)).transpose(),
246+
numpy.vstack((ctb.real, ctb.imag)).transpose(),
247+
)
248+
249+
250+

0 commit comments

Comments
 (0)