blob: dafa294f8309b3542cbd89bd9f03a6aac90fb09e [file] [log] [blame]
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -08001import numpy
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -08002
3# joint_center in x-y space.
4joint_center = (-0.299, 0.299)
5
6# Joint distances (l1 = "proximal", l2 = "distal")
7l1 = 46.25 * 0.0254
8l2 = 43.75 * 0.0254
9
10max_dist = 0.01
11max_dist_theta = numpy.pi / 64
12xy_end_circle_size = 0.01
13theta_end_circle_size = 0.07
14
15
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -080016# Convert from x-y coordinates to theta coordinates.
17# orientation is a bool. This orientation is circular_index mod 2.
18# where circular_index is the circular index, or the position in the
19# "hyperextension" zones. "cross_point" allows shifting the place where
20# it rounds the result so that it draws nicer (no other functional differences).
21def to_theta(pt, circular_index, cross_point=-numpy.pi):
22 orient = (circular_index % 2) == 0
23 x = pt[0]
24 y = pt[1]
25 x -= joint_center[0]
26 y -= joint_center[1]
27 l3 = numpy.hypot(x, y)
28 t3 = numpy.arctan2(y, x)
29 theta1 = numpy.arccos((l1**2 + l3**2 - l2**2) / (2 * l1 * l3))
30
31 if orient:
32 theta1 = -theta1
33 theta1 += t3
34 theta1 = (theta1 - cross_point) % (2 * numpy.pi) + cross_point
35 theta2 = numpy.arctan2(y - l1 * numpy.sin(theta1),
36 x - l1 * numpy.cos(theta1))
37 return numpy.array((theta1, theta2))
38
39
40# Simple trig to go back from theta1, theta2 to x-y
41def to_xy(theta1, theta2):
42 x = numpy.cos(theta1) * l1 + numpy.cos(theta2) * l2 + joint_center[0]
43 y = numpy.sin(theta1) * l1 + numpy.sin(theta2) * l2 + joint_center[1]
44 orient = ((theta2 - theta1) % (2.0 * numpy.pi)) < numpy.pi
45 return (x, y, orient)
46
47
48def get_circular_index(theta):
49 return int(numpy.floor((theta[1] - theta[0]) / numpy.pi))
50
51
52def get_xy(theta):
53 theta1 = theta[0]
54 theta2 = theta[1]
55 x = numpy.cos(theta1) * l1 + numpy.cos(theta2) * l2 + joint_center[0]
56 y = numpy.sin(theta1) * l1 + numpy.sin(theta2) * l2 + joint_center[1]
57 return numpy.array((x, y))
58
59
60# Subdivide in theta space.
61def subdivide_theta(lines):
62 out = []
63 last_pt = lines[0]
64 out.append(last_pt)
65 for n_pt in lines[1:]:
66 for pt in subdivide(last_pt, n_pt, max_dist_theta):
67 out.append(pt)
68 last_pt = n_pt
69
70 return out
71
72
73# subdivide in xy space.
74def subdivide_xy(lines, max_dist=max_dist):
75 out = []
76 last_pt = lines[0]
77 out.append(last_pt)
78 for n_pt in lines[1:]:
79 for pt in subdivide(last_pt, n_pt, max_dist):
80 out.append(pt)
81 last_pt = n_pt
82
83 return out
84
85
86def to_theta_with_ci(pt, circular_index):
87 return to_theta_with_circular_index(pt[0], pt[1], circular_index)
88
89
90# to_theta, but distinguishes between
91def to_theta_with_circular_index(x, y, circular_index):
92 theta1, theta2 = to_theta((x, y), circular_index)
93 n_circular_index = int(numpy.floor((theta2 - theta1) / numpy.pi))
94 theta2 = theta2 + ((circular_index - n_circular_index)) * numpy.pi
95 return numpy.array((theta1, theta2))
96
97
98# alpha is in [0, 1] and is the weight to merge a and b.
99def alpha_blend(a, b, alpha):
100 """Blends a and b.
101
102 Args:
103 alpha: double, Ratio. Needs to be in [0, 1] and is the weight to blend a
104 and b.
105 """
106 return b * alpha + (1.0 - alpha) * a
107
108
109def normalize(v):
110 """Normalize a vector while handling 0 length vectors."""
111 norm = numpy.linalg.norm(v)
112 if norm == 0:
113 return v
114 return v / norm
115
116
117# CI is circular index and allows selecting between all the stats that map
118# to the same x-y state (by giving them an integer index).
119# This will compute approximate first and second derivatives with respect
120# to path length.
121def to_theta_with_circular_index_and_derivs(x, y, dx, dy,
122 circular_index_select):
123 a = to_theta_with_circular_index(x, y, circular_index_select)
124 b = to_theta_with_circular_index(x + dx * 0.0001, y + dy * 0.0001,
125 circular_index_select)
126 c = to_theta_with_circular_index(x - dx * 0.0001, y - dy * 0.0001,
127 circular_index_select)
128 d1 = normalize(b - a)
129 d2 = normalize(c - a)
130 accel = (d1 + d2) / numpy.linalg.norm(a - b)
131 return (a[0], a[1], d1[0], d1[1], accel[0], accel[1])
132
133
134def to_theta_with_ci_and_derivs(p_prev, p, p_next, c_i_select):
135 a = to_theta(p, c_i_select)
136 b = to_theta(p_next, c_i_select)
137 c = to_theta(p_prev, c_i_select)
138 d1 = normalize(b - a)
139 d2 = normalize(c - a)
140 accel = (d1 + d2) / numpy.linalg.norm(a - b)
141 return (a[0], a[1], d1[0], d1[1], accel[0], accel[1])
142
143
144# Generic subdivision algorithm.
145def subdivide(p1, p2, max_dist):
146 dx = p2[0] - p1[0]
147 dy = p2[1] - p1[1]
148 dist = numpy.sqrt(dx**2 + dy**2)
149 n = int(numpy.ceil(dist / max_dist))
150 return [(alpha_blend(p1[0], p2[0],
151 float(i) / n), alpha_blend(p1[1], p2[1],
152 float(i) / n))
153 for i in range(1, n + 1)]
154
155
156# convert from an xy space loop into a theta loop.
157# All segements are expected go from one "hyper-extension" boundary
158# to another, thus we must go backwards over the "loop" to get a loop in
159# x-y space.
160def to_theta_loop(lines, cross_point=-numpy.pi):
161 out = []
162 last_pt = lines[0]
163 for n_pt in lines[1:]:
164 for pt in subdivide(last_pt, n_pt, max_dist):
165 out.append(to_theta(pt, 0, cross_point))
166 last_pt = n_pt
167 for n_pt in reversed(lines[:-1]):
168 for pt in subdivide(last_pt, n_pt, max_dist):
169 out.append(to_theta(pt, 1, cross_point))
170 last_pt = n_pt
171 return out
172
173
174# Convert a loop (list of line segments) into
175# The name incorrectly suggests that it is cyclic.
176def back_to_xy_loop(lines):
177 out = []
178 last_pt = lines[0]
179 out.append(to_xy(last_pt[0], last_pt[1]))
180 for n_pt in lines[1:]:
181 for pt in subdivide(last_pt, n_pt, max_dist_theta):
182 out.append(to_xy(pt[0], pt[1]))
183 last_pt = n_pt
184
185 return out
186
187
188def spline_eval(start, control1, control2, end, alpha):
189 a = alpha_blend(start, control1, alpha)
190 b = alpha_blend(control1, control2, alpha)
191 c = alpha_blend(control2, end, alpha)
192 return alpha_blend(alpha_blend(a, b, alpha), alpha_blend(b, c, alpha),
193 alpha)
194
195
196def subdivide_spline(start, control1, control2, end):
197 # TODO: pick N based on spline parameters? or otherwise change it to be more evenly spaced?
198 n = 100
199 for i in range(0, n + 1):
200 yield i / float(n)
201
202
203def get_derivs(t_prev, t, t_next):
204 c, a, b = t_prev, t, t_next
205 d1 = normalize(b - a)
206 d2 = normalize(c - a)
207 accel = (d1 + d2) / numpy.linalg.norm(a - b)
208 return (a[0], a[1], d1[0], d1[1], accel[0], accel[1])
209
210
Maxwell Henderson83cf6d62023-02-10 20:29:26 -0800211# Draw a list of lines to a cairo context.
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -0800212def draw_lines(cr, lines):
213 cr.move_to(lines[0][0], lines[0][1])
214 for pt in lines[1:]:
215 cr.line_to(pt[0], pt[1])
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -0800216
217
218# Segment in angle space.
219class AngleSegment:
220
221 def __init__(self, start, end, name=None, alpha_unitizer=None, vmax=None):
222 """Creates an angle segment.
223
224 Args:
225 start: (double, double), The start of the segment in theta1, theta2
226 coordinates in radians
227 end: (double, double), The end of the segment in theta1, theta2
228 coordinates in radians
229 """
230 self.start = start
231 self.end = end
232 self.name = name
233 self.alpha_unitizer = alpha_unitizer
234 self.vmax = vmax
235
236 def __repr__(self):
237 return "AngleSegment(%s, %s)" % (repr(self.start), repr(self.end))
238
239 def DrawTo(self, cr, theta_version):
240 if theta_version:
241 cr.move_to(self.start[0], self.start[1] + theta_end_circle_size)
242 cr.arc(self.start[0], self.start[1], theta_end_circle_size, 0,
243 2.0 * numpy.pi)
244 cr.move_to(self.end[0], self.end[1] + theta_end_circle_size)
245 cr.arc(self.end[0], self.end[1], theta_end_circle_size, 0,
246 2.0 * numpy.pi)
247 cr.move_to(self.start[0], self.start[1])
248 cr.line_to(self.end[0], self.end[1])
249 else:
250 start_xy = to_xy(self.start[0], self.start[1])
251 end_xy = to_xy(self.end[0], self.end[1])
252 draw_lines(cr, back_to_xy_loop([self.start, self.end]))
253 cr.move_to(start_xy[0] + xy_end_circle_size, start_xy[1])
254 cr.arc(start_xy[0], start_xy[1], xy_end_circle_size, 0,
255 2.0 * numpy.pi)
256 cr.move_to(end_xy[0] + xy_end_circle_size, end_xy[1])
257 cr.arc(end_xy[0], end_xy[1], xy_end_circle_size, 0, 2.0 * numpy.pi)
258
259 def ToThetaPoints(self):
260 dx = self.end[0] - self.start[0]
261 dy = self.end[1] - self.start[1]
262 mag = numpy.hypot(dx, dy)
263 dx /= mag
264 dy /= mag
265
266 return [(self.start[0], self.start[1], dx, dy, 0.0, 0.0),
267 (self.end[0], self.end[1], dx, dy, 0.0, 0.0)]
268
269
270class XYSegment:
271 """Straight line in XY space."""
272
273 def __init__(self, start, end, name=None, alpha_unitizer=None, vmax=None):
274 """Creates an XY segment.
275
276 Args:
277 start: (double, double), The start of the segment in theta1, theta2
278 coordinates in radians
279 end: (double, double), The end of the segment in theta1, theta2
280 coordinates in radians
281 """
282 self.start = start
283 self.end = end
284 self.name = name
285 self.alpha_unitizer = alpha_unitizer
286 self.vmax = vmax
287
288 def __repr__(self):
289 return "XYSegment(%s, %s)" % (repr(self.start), repr(self.end))
290
291 def DrawTo(self, cr, theta_version):
292 if theta_version:
293 theta1, theta2 = self.start
294 circular_index_select = int(
295 numpy.floor((self.start[1] - self.start[0]) / numpy.pi))
296 start = get_xy(self.start)
297 end = get_xy(self.end)
298
299 ln = [(start[0], start[1]), (end[0], end[1])]
300 draw_lines(cr, [
301 to_theta_with_circular_index(x, y, circular_index_select)
302 for x, y in subdivide_xy(ln)
303 ])
304 cr.move_to(self.start[0] + theta_end_circle_size, self.start[1])
305 cr.arc(self.start[0], self.start[1], theta_end_circle_size, 0,
306 2.0 * numpy.pi)
307 cr.move_to(self.end[0] + theta_end_circle_size, self.end[1])
308 cr.arc(self.end[0], self.end[1], theta_end_circle_size, 0,
309 2.0 * numpy.pi)
310 else:
311 start = get_xy(self.start)
312 end = get_xy(self.end)
313 cr.move_to(start[0], start[1])
314 cr.line_to(end[0], end[1])
315 cr.move_to(start[0] + xy_end_circle_size, start[1])
316 cr.arc(start[0], start[1], xy_end_circle_size, 0, 2.0 * numpy.pi)
317 cr.move_to(end[0] + xy_end_circle_size, end[1])
318 cr.arc(end[0], end[1], xy_end_circle_size, 0, 2.0 * numpy.pi)
319
320 def ToThetaPoints(self):
321 """ Converts to points in theta space via to_theta_with_circular_index_and_derivs"""
322 theta1, theta2 = self.start
323 circular_index_select = int(
324 numpy.floor((self.start[1] - self.start[0]) / numpy.pi))
325 start = get_xy(self.start)
326 end = get_xy(self.end)
327
328 ln = [(start[0], start[1]), (end[0], end[1])]
329
330 dx = end[0] - start[0]
331 dy = end[1] - start[1]
332 mag = numpy.hypot(dx, dy)
333 dx /= mag
334 dy /= mag
335
336 return [
337 to_theta_with_circular_index_and_derivs(x, y, dx, dy,
338 circular_index_select)
339 for x, y in subdivide_xy(ln, 0.01)
340 ]
341
342
343class SplineSegment:
344
345 def __init__(self,
346 start,
347 control1,
348 control2,
349 end,
350 name=None,
351 alpha_unitizer=None,
352 vmax=None):
353 self.start = start
354 self.control1 = control1
355 self.control2 = control2
356 self.end = end
357 self.name = name
358 self.alpha_unitizer = alpha_unitizer
359 self.vmax = vmax
360
361 def __repr__(self):
362 return "SplineSegment(%s, %s, %s, %s)" % (repr(
363 self.start), repr(self.control1), repr(
364 self.control2), repr(self.end))
365
366 def DrawTo(self, cr, theta_version):
367 if theta_version:
368 c_i_select = get_circular_index(self.start)
369 start = get_xy(self.start)
370 control1 = get_xy(self.control1)
371 control2 = get_xy(self.control2)
372 end = get_xy(self.end)
373
374 draw_lines(cr, [
375 to_theta(spline_eval(start, control1, control2, end, alpha),
376 c_i_select)
377 for alpha in subdivide_spline(start, control1, control2, end)
378 ])
379 cr.move_to(self.start[0] + theta_end_circle_size, self.start[1])
380 cr.arc(self.start[0], self.start[1], theta_end_circle_size, 0,
381 2.0 * numpy.pi)
382 cr.move_to(self.end[0] + theta_end_circle_size, self.end[1])
383 cr.arc(self.end[0], self.end[1], theta_end_circle_size, 0,
384 2.0 * numpy.pi)
385 else:
386 start = get_xy(self.start)
387 control1 = get_xy(self.control1)
388 control2 = get_xy(self.control2)
389 end = get_xy(self.end)
390
391 draw_lines(cr, [
392 spline_eval(start, control1, control2, end, alpha)
393 for alpha in subdivide_spline(start, control1, control2, end)
394 ])
395
396 cr.move_to(start[0] + xy_end_circle_size, start[1])
397 cr.arc(start[0], start[1], xy_end_circle_size, 0, 2.0 * numpy.pi)
398 cr.move_to(end[0] + xy_end_circle_size, end[1])
399 cr.arc(end[0], end[1], xy_end_circle_size, 0, 2.0 * numpy.pi)
400
401 def ToThetaPoints(self):
402 t1, t2 = self.start
403 c_i_select = get_circular_index(self.start)
404 start = get_xy(self.start)
405 control1 = get_xy(self.control1)
406 control2 = get_xy(self.control2)
407 end = get_xy(self.end)
408
409 return [
410 to_theta_with_ci_and_derivs(
411 spline_eval(start, control1, control2, end, alpha - 0.00001),
412 spline_eval(start, control1, control2, end, alpha),
413 spline_eval(start, control1, control2, end, alpha + 0.00001),
414 c_i_select)
415 for alpha in subdivide_spline(start, control1, control2, end)
416 ]
417
418
419class ThetaSplineSegment:
420
421 def __init__(self,
422 start,
423 control1,
424 control2,
425 end,
426 name=None,
427 alpha_unitizer=None,
428 vmax=None):
429 self.start = start
430 self.control1 = control1
431 self.control2 = control2
432 self.end = end
433 self.name = name
434 self.alpha_unitizer = alpha_unitizer
435 self.vmax = vmax
436
437 def __repr__(self):
438 return "ThetaSplineSegment(%s, %s, &s, %s)" % (repr(
439 self.start), repr(self.control1), repr(
440 self.control2), repr(self.end))
441
442 def DrawTo(self, cr, theta_version):
443 if (theta_version):
444 draw_lines(cr, [
445 spline_eval(self.start, self.control1, self.control2, self.end,
446 alpha)
447 for alpha in subdivide_spline(self.start, self.control1,
448 self.control2, self.end)
449 ])
450 else:
451 start = get_xy(self.start)
452 end = get_xy(self.end)
453
454 draw_lines(cr, [
455 get_xy(
456 spline_eval(self.start, self.control1, self.control2,
457 self.end, alpha))
458 for alpha in subdivide_spline(self.start, self.control1,
459 self.control2, self.end)
460 ])
461
462 cr.move_to(start[0] + xy_end_circle_size, start[1])
463 cr.arc(start[0], start[1], xy_end_circle_size, 0, 2.0 * numpy.pi)
464 cr.move_to(end[0] + xy_end_circle_size, end[1])
465 cr.arc(end[0], end[1], xy_end_circle_size, 0, 2.0 * numpy.pi)
466
467 def ToThetaPoints(self):
468 return [
469 get_derivs(
470 spline_eval(self.start, self.control1, self.control2, self.end,
471 alpha - 0.00001),
472 spline_eval(self.start, self.control1, self.control2, self.end,
473 alpha),
474 spline_eval(self.start, self.control1, self.control2, self.end,
475 alpha + 0.00001))
476 for alpha in subdivide_spline(self.start, self.control1,
477 self.control2, self.end)
478 ]
479
480
481def expand_points(points, max_distance):
482 """Expands a list of points to be at most max_distance apart
483
484 Generates the paths to connect the new points to the closest input points,
485 and the paths connecting the points.
486
487 Args:
488 points, list of tuple of point, name, The points to start with and fill
489 in.
490 max_distance, float, The max distance between two points when expanding
491 the graph.
492
493 Return:
494 points, edges
495 """
496 result_points = [points[0]]
497 result_paths = []
498 for point, name in points[1:]:
499 previous_point = result_points[-1][0]
500 previous_point_xy = get_xy(previous_point)
501 circular_index = get_circular_index(previous_point)
502
503 point_xy = get_xy(point)
504 norm = numpy.linalg.norm(point_xy - previous_point_xy)
505 num_points = int(numpy.ceil(norm / max_distance))
506 last_iteration_point = previous_point
507 for subindex in range(1, num_points):
508 subpoint = to_theta(alpha_blend(previous_point_xy, point_xy,
509 float(subindex) / num_points),
510 circular_index=circular_index)
511 result_points.append(
512 (subpoint, '%s%dof%d' % (name, subindex, num_points)))
513 result_paths.append(
514 XYSegment(last_iteration_point, subpoint, vmax=6.0))
515 if (last_iteration_point != previous_point).any():
516 result_paths.append(XYSegment(previous_point, subpoint))
517 if subindex == num_points - 1:
518 result_paths.append(XYSegment(subpoint, point, vmax=6.0))
519 else:
520 result_paths.append(XYSegment(subpoint, point))
521 last_iteration_point = subpoint
522 result_points.append((point, name))
523
524 return result_points, result_paths