Skip to content

Commit 3dd2aad

Browse files
committed
add polyline utilities
1 parent 9e8a121 commit 3dd2aad

File tree

1 file changed

+263
-0
lines changed

1 file changed

+263
-0
lines changed
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
import numpy as np
2+
3+
4+
def _find_circle_center_and_tangent_points(a, b, c, r, max_ratio=1):
5+
"""
6+
Find the center of a circle and its tangent points with given vertices and radius.
7+
8+
Parameters
9+
----------
10+
a, b, c : array-like
11+
Vertices of a triangle (b is the middle vertex) with shape (2,) or (3,).
12+
r : float
13+
Radius of the circle.
14+
max_ratio : float, optional, default: 0.5
15+
Maximum allowed ratio of the distance to the tangent point relative to the length of the triangle sides.
16+
17+
Returns
18+
-------
19+
tuple
20+
Circle center, tangent point a, tangent point b as NumPy arrays.
21+
"""
22+
# Calculate the unit vectors along AB and BC
23+
norm_ab = np.linalg.norm(a - b)
24+
norm_bc = np.linalg.norm(c - b)
25+
ba_unit = (a - b) / norm_ab
26+
bc_unit = (c - b) / norm_bc
27+
28+
dot_babc = np.dot(ba_unit, bc_unit)
29+
if dot_babc == -1: # angle is 180°
30+
return None
31+
theta = np.arccos(dot_babc) / 2
32+
tan_theta = np.tan(theta)
33+
d = r / tan_theta
34+
if d > norm_bc * max_ratio or d > norm_ab * max_ratio:
35+
rold, dold = r, d
36+
print("r, d, norm_ab, norm_bc: ", r, d, norm_ab, norm_bc)
37+
d = min(norm_bc * max_ratio, norm_ab * max_ratio)
38+
r = d * tan_theta if theta > 0 else 0
39+
# warnings.warn(f"Radius {rold:.4g} is too big and has been reduced to {r:.4g}")
40+
ta = b + ba_unit * d
41+
tb = b + bc_unit * d
42+
43+
rl = (d**2 + r**2) ** 0.5
44+
bisector = ba_unit + bc_unit
45+
unit_bisector = bisector / np.linalg.norm(bisector)
46+
circle_center = b + unit_bisector * rl
47+
48+
return circle_center, ta, tb
49+
50+
51+
def _interpolate_circle(center, start, end, n_points):
52+
"""
53+
Interpolate points along a circle arc between two points.
54+
55+
Parameters
56+
----------
57+
center : array-like
58+
Center of the circle with shape (2,) or (3,).
59+
start, end : array-like
60+
Start and end points of the arc with shape (2,) or (3,).
61+
n_points : int
62+
Number of points to interpolate.
63+
64+
Returns
65+
-------
66+
list
67+
List of NumPy arrays representing the interpolated points.
68+
"""
69+
angle_diff = np.arccos(
70+
np.dot(start - center, end - center)
71+
/ (np.linalg.norm(start - center) * np.linalg.norm(end - center))
72+
)
73+
angles = np.linspace(0, angle_diff, n_points)
74+
v = start - center
75+
w = np.cross(v, end - start)
76+
w /= np.linalg.norm(w)
77+
circle_points = [
78+
center + np.cos(angle) * v + np.sin(angle) * np.cross(w, v) for angle in angles
79+
]
80+
return circle_points
81+
82+
83+
def _create_fillet_segment(a, b, c, r, N):
84+
"""
85+
Create a fillet segment with a given radius between three vertices.
86+
87+
Parameters
88+
----------
89+
a, b, c : array-like
90+
Vertices of a triangle (b is the middle vertex) with shape (2,) or (3,).
91+
r : float
92+
Radius of the fillet.
93+
N : int
94+
Number of points to interpolate along the fillet.
95+
96+
Returns
97+
-------
98+
list
99+
List of NumPy arrays representing the fillet points.
100+
"""
101+
res = _find_circle_center_and_tangent_points(a, b, c, r)
102+
if res is None:
103+
return [b]
104+
circle_center, ta, tb = res
105+
return _interpolate_circle(circle_center, ta, tb, N)
106+
107+
108+
def create_polyline_fillet(polyline, max_radius, N):
109+
"""
110+
Create a filleted polyline with specified maximum radius and number of points.
111+
112+
Parameters
113+
----------
114+
polyline : list or array-like
115+
List or array of vertices forming the polyline with shape (N, 2) or (N, 3).
116+
max_radius : float
117+
Maximum radius of the fillet.
118+
N : int
119+
Number of points to interpolate along the fillet.
120+
121+
Returns
122+
-------
123+
numpy.ndarray
124+
Array of filleted points with shape (M, 2) or (M, 3), where M depends on the number of
125+
filleted segments.
126+
"""
127+
points = np.array(polyline)
128+
radius = max_radius
129+
if radius == 0 or N == 0:
130+
return points
131+
132+
closed = np.allclose(points[0], points[-1])
133+
if closed:
134+
points = np.append(points, points[1:2], axis=0)
135+
filleted_points = [points[0]]
136+
n = len(points)
137+
for i in range(1, n - 1):
138+
a, b, c = (
139+
filleted_points[-1],
140+
points[i],
141+
points[i + 1],
142+
)
143+
if closed and i == n - 2:
144+
c = filleted_points[1]
145+
try:
146+
filleted_points.extend(_create_fillet_segment(a, b, c, radius, N))
147+
except ValueError:
148+
raise ValueError(f"The radius {radius} on position vertex {i} is too large")
149+
if closed:
150+
filleted_points[0] = filleted_points[-1]
151+
else:
152+
filleted_points = np.append(filleted_points, points[-1:], axis=0)
153+
return np.array(filleted_points)
154+
155+
156+
def _bisectors(polyline):
157+
"""
158+
Calculate and normalize bisectors of the segments in a polyline.
159+
160+
Parameters
161+
----------
162+
polyline : numpy.ndarray
163+
A 2D array of shape (N, 3) representing N vertices of a polyline in 3D space.
164+
165+
Returns
166+
-------
167+
bisectors_normalized : numpy.ndarray
168+
A 2D array of shape (N-2, 3) representing normalized bisectors for each pair of consecutive
169+
segments in the polyline.
170+
"""
171+
# Calculate the segment vectors
172+
segment_vectors = np.diff(polyline, axis=0)
173+
174+
# Normalize the segment vectors
175+
normalized_vectors = (
176+
segment_vectors / np.linalg.norm(segment_vectors, axis=1)[:, np.newaxis]
177+
)
178+
179+
# Calculate the bisectors by adding normalized adjacent vectors
180+
bisectors = normalized_vectors[:-1] + normalized_vectors[1:]
181+
182+
# Normalize the bisectors
183+
bisectors_normalized = bisectors / np.linalg.norm(bisectors, axis=1)[:, np.newaxis]
184+
185+
return bisectors_normalized
186+
187+
188+
def _line_plane_intersection(plane_point, plane_normal, line_points, line_directions):
189+
"""
190+
Find the intersection points of multiple lines and a plane.
191+
192+
Parameters
193+
----------
194+
plane_point : numpy.ndarray
195+
A 1D array of shape (3,) representing a point on the plane.
196+
plane_normal : numpy.ndarray
197+
A 1D array of shape (3,) representing the normal vector of the plane.
198+
line_points : numpy.ndarray
199+
A 2D array of shape (N, 3) representing N points on the lines.
200+
line_directions : numpy.ndarray
201+
A 2D array of shape (N, 3) representing the direction vectors of the lines.
202+
203+
Returns
204+
-------
205+
intersection_points : numpy.ndarray
206+
A 2D array of shape (N, 3) representing the intersection points of the lines and the plane.
207+
"""
208+
# Calculate the plane equation coefficients A, B, C, and D
209+
A, B, C = plane_normal
210+
x0, y0, z0 = plane_point
211+
D = -np.dot(plane_normal, plane_point)
212+
213+
# Calculate the parameter t
214+
t = -(A * line_points[:, 0] + B * line_points[:, 1] + C * line_points[:, 2] + D) / (
215+
A * line_directions[:, 0]
216+
+ B * line_directions[:, 1]
217+
+ C * line_directions[:, 2]
218+
)
219+
220+
# Find the intersection points by plugging t back into the parametric line equation
221+
intersection_points = line_points + np.expand_dims(t, axis=-1) * line_directions
222+
223+
return intersection_points
224+
225+
226+
def move_grid_along_polyline(verts, grid):
227+
"""
228+
Move a grid along a polyline, defined by the vertices.
229+
230+
Parameters
231+
----------
232+
verts : np.ndarray, shape (n, d)
233+
Array of polyline vertices, where n is the number of vertices and d is the dimension.
234+
grid : np.ndarray, shape (m, d)
235+
Array of grid points to move along the polyline, where m is the number of points.
236+
237+
Returns
238+
-------
239+
np.ndarray, shape (m, n, d)
240+
Array of moved grid points along the polyline, with the same dimensions as the input grid.
241+
"""
242+
grid = grid.copy()
243+
pts = [grid]
244+
normals = _bisectors(verts)
245+
closed = np.allclose(verts[0], verts[-1])
246+
if closed:
247+
v_ext = np.concatenate([verts[-2:], verts[1:2]])
248+
last_normal = _bisectors(v_ext)
249+
else:
250+
last_normal = [verts[-1] - verts[-2]]
251+
normals = np.concatenate([normals, last_normal])
252+
for i in range(len(verts) - 1):
253+
plane_point = verts[i + 1]
254+
plane_normal = normals[i]
255+
line_points = pts[-1]
256+
line_directions = np.tile(verts[i + 1] - verts[i], (line_points.shape[0], 1))
257+
pts1 = _line_plane_intersection(
258+
plane_point, plane_normal, line_points, line_directions
259+
)
260+
pts.append(pts1)
261+
if closed:
262+
pts[0] = pts[-1]
263+
return np.array(pts).swapaxes(0, 1)

0 commit comments

Comments
 (0)