def make_venn3_region_patch(region): ''' Given a venn3 region (as returned from compute_venn3_regions) produces a Patch object, depicting the region as a curve. >>> centers, radii = solve_venn3_circles((1, 1, 1, 1, 1, 1, 1)) >>> regions = compute_venn3_regions(centers, radii) >>> patches = [make_venn3_region_patch(r) for r in regions] ''' if region is None or len(region[0]) == 0: return None if region[0] == "CIRCLE": return Circle(region[1][0], region[1][1]) pts, arcs, label_pos = region path = [pts[0]] for i in range(len(pts)): j = (i + 1) % len(pts) (center, radius, direction) = arcs[i] fromangle = vector_angle_in_degrees(pts[i] - center) toangle = vector_angle_in_degrees(pts[j] - center) if direction: vertices = Path.arc(fromangle, toangle).vertices else: vertices = Path.arc(toangle, fromangle).vertices vertices = vertices[np.arange(len(vertices) - 1, -1, -1)] vertices = vertices * radius + center path = path + list(vertices[1:]) codes = [1] + [4] * (len(path) - 1) return PathPatch(Path(path, codes))
def curved_arrow_double(theta1, theta2, radius, width_outer, width_inner, origin=(0,0), rel_head_width=1.5, f_abs_head_len=None, r_abs_head_len=None, rel_head_len=0.1, reverse=False): """Construct the paths a double-sided reversible curved arrow. Returns the paths for both the outer and inner arrows. Radius is the distance from the origin to the inside of the outer arrow""" if not reverse: angle_tip_out = math.radians(theta1) angle_tip_in = math.radians(theta2) # set the angle swept by the arrowhead if f_abs_head_len is None: # compute arrow head length (angle swept) as a fraction of total length f_angle_offset = math.radians((theta2-theta1) * rel_head_len) else: f_angle_offset = f_abs_head_len # set the angle swept by the arrowhead if r_abs_head_len is None: # compute arrow head length (angle swept) as a fraction of total length r_angle_offset = math.radians((theta2 - theta1) * rel_head_len) else: r_angle_offset = r_abs_head_len # Define the radii of the inside and outside of the head and tail head_out_width = width_outer * (rel_head_width + 1) head_in_width = width_inner * (rel_head_width + 1) tail_out_radius = radius + width_outer tail_in_radius = radius - width_inner head_out_in_xy, arrowtip_out_xy, head_out_out_xy = get_isosceles_arrowhead(radius, angle_tip_out, angle_tip_out + f_angle_offset, head_out_width) head_in_in_xy, arrowtip_in_xy, head_in_out_xy = get_isosceles_arrowhead(radius, angle_tip_in, angle_tip_in - r_angle_offset, head_in_width) int_outer, ix_pts_outer = get_intersect_segment_circle(head_out_in_xy, head_out_out_xy, tail_out_radius) int_inner, ix_pts_inner = get_intersect_segment_circle(head_in_in_xy, head_in_out_xy, tail_in_radius) if int_outer: start_outer_arc = math.degrees(cart2pol(*ix_pts_outer[0])[1]) else: start_outer_arc = theta1 + math.degrees(f_angle_offset) if int_inner: end_inner_arc = math.degrees(cart2pol(*ix_pts_inner[0])[1]) else: end_inner_arc = theta2 - math.degrees(r_angle_offset) outer_arc = scale_arc(Path.arc(start_outer_arc, theta2), tail_out_radius) middle_arc = scale_arc(path_arc_cw(theta2, theta1), radius) inner_arc = scale_arc(Path.arc(theta1, end_inner_arc), tail_in_radius) outer_arrowhead = join_points([head_out_out_xy]) inner_arrowhead = join_points([head_in_in_xy]) outer_path = shift_path_by_vec(concatenate_paths([outer_arc, middle_arc, outer_arrowhead]), np.array(origin)) inner_path = shift_path_by_vec(concatenate_paths([middle_arc, inner_arc, inner_arrowhead]), np.array(origin)) return outer_path, inner_path else: pass
def curved_arrow_single(theta1, theta2, radius, width, origin=(0,0), rel_head_width=1.5, rel_head_len=0.1, abs_head_len=None, reverse=False): """Construct the path for an irreversible curved arrow""" # set the angle swept by the arrowhead if abs_head_len is None: # compute arrow head length (angle swept) as a fraction of total length f_angle_offset = math.radians((theta2 - theta1) * rel_head_len) else: f_angle_offset = abs_head_len # Define the radii of the inside and outside of the head and tail head_width = width * rel_head_width tail_out_radius = radius + width / 2.0 tail_in_radius = radius - width / 2.0 if not reverse: theta_tip = theta1 theta_tail = theta2 else: theta_tip = theta2 theta_tail = theta1 f_angle_offset = -f_angle_offset # head_in_point, arrowhead_point, head_out_point = get_perp_arrowhead(radius, theta_tip, f_angle_offset, width, rel_head_width) head_in_point, arrowhead_point, head_out_point = get_isosceles_arrowhead(radius, math.radians(theta_tip), math.radians(theta_tip) + f_angle_offset, head_width) int_outer, ix_pts_outer = get_intersect_segment_circle(head_in_point, head_out_point, tail_out_radius) int_inner, ix_pts_inner = get_intersect_segment_circle(head_in_point, head_out_point, tail_in_radius) if int_outer: start = math.degrees(cart2pol(*ix_pts_outer[0])[1]) else: start = theta_tip + math.degrees(f_angle_offset) # make head wider if it doesn't intersect both sides of tail # return curved_arrow_single(theta1, theta2, radius, width, origin, rel_head_width + 0.1, rel_head_len, # abs_head_len, reverse) if int_inner: end = math.degrees(cart2pol(*ix_pts_inner[0])[1]) else: end = theta_tip + math.degrees(f_angle_offset) # make head wider if it doesn't intersect both sides of tail # return curved_arrow_single(theta1, theta2, radius, width, origin, rel_head_width + 0.1, rel_head_len, # abs_head_len, reverse) if not reverse: outer_arc = scale_arc(Path.arc(start, theta_tail), tail_out_radius) inner_arc = scale_arc(path_arc_cw(theta_tail, end), tail_in_radius) else: outer_arc = scale_arc(path_arc_cw(start, theta_tail), tail_out_radius) inner_arc = scale_arc(Path.arc(theta_tail, end), tail_in_radius) arrowhead = join_points([head_in_point, arrowhead_point, head_out_point]) return shift_path_by_vec(concatenate_paths([outer_arc, inner_arc, arrowhead]), np.array(origin))
def to_mpl_path(self): """ Converts the Region into a matplotlib path """ codes = [] verts = [] for seg in self.segments: codes.append(MPLPath.MOVETO) verts.append((seg["x0"], seg["y0"])) if seg["t"] == "P": continue if seg["t"] == "L": codes.append(MPLPath.LINETO) verts.append((seg["x1"], seg["y1"])) elif seg["t"] == "A": x0, y0, x1, y1 = seg["x0"], seg["y0"], seg["x1"], seg["y1"] xc, yc = seg["xc"], seg["yc"] r = np.sqrt((x0 - xc)**2 + (y0 - yc)**2) t0 = np.arctan2((y0 - yc), (x0 - xc)) t1 = np.arctan2((y1 - yc), (x1 - xc)) t0 = np.rad2deg(t0) t1 = np.rad2deg(t1) if t0 < 0: t0 = 360 + t0 if t1 < 0: t1 = 360 + t1 if t1 - t0 > 180: t1 -= 360 if t0 <= t1: arc = MPLPath.arc(t0, t1) if t0 > t1: arc = MPLPath.arc(t1, t0) arccodes = list(arc.codes[:]) arcverts = list(arc.vertices[:]) if t0 > t1: arcverts = arcverts[::-1] arccodes = arccodes[::-1] arccodes[0], arccodes[-1] = arccodes[-1], arccodes[0] arcverts = [v * r + np.array([xc, yc]) for v in arcverts] codes += arccodes verts += arcverts if self.fill: codes.append(MPLPath.CLOSEPOLY) verts.append((0, 0)) c0, v0 = codes[0], verts[0] verts = [ v for i, v in enumerate(verts) if codes[i] != MPLPath.MOVETO ] codes = [c for c in codes if c != MPLPath.MOVETO] verts.insert(0, v0) codes.insert(0, c0) return MPLPath(verts, codes)
def convert_boundery_to_path(points): vertices = list() codes = list() for segment in points: ptype, start, end = segment[:3] if ptype == "straight": #ray line intersection with infinite ray from (p_x,p_y) to (inf,p_y) of first point of A v, c = next(Path((start, end)).iter_segments()) vertices.append((v[0], v[1])) codes.append(c) elif ptype == "arc": #ray arc intersection with infinite ray from (p_x,p_y) to (p_x+1,p_y) of first point of A rel, ccw, arc = segment[3:] trans = Affine2D().scale(rel.magnitude).translate( arc.center.x, arc.center.y) path = Path.arc( arc.start_angle, arc.end_angle ) # if ccw else Path.arc(arc.end_angle, arc.start_angle,) path = path.transformed(trans) for v, c in path.iter_segments(curves=False): vertices.append((v[0], v[1])) codes.append(c) else: logging.warning( "unknown boundary type found (convert_boundery_to_path)") codes = [1] + [2] * (len(vertices) - 1) ret = Path(vertices, codes=codes, closed=True) #.cleaned(simplify=True)# return ret
def test_full_arc(offset): low = offset high = 360 + offset path = Path.arc(low, high) mins = np.min(path.vertices, axis=0) maxs = np.max(path.vertices, axis=0) np.testing.assert_allclose(mins, -1) assert np.allclose(maxs, 1)
def test_full_arc(offset): low = offset high = 360 + offset path = Path.arc(low, high) mins = np.min(path.vertices, axis=0) maxs = np.max(path.vertices, axis=0) np.testing.assert_allclose(mins, -1) np.testing.assert_allclose(maxs, 1)
def filled_circular_arc(theta1, theta2, radius, width, origin=(0,0)): """Construct the path for a circular arc""" # Define the radii of the inside and outside of the arc out_radius = radius + width / 2.0 in_radius = radius - width / 2.0 outer_arc = scale_arc(Path.arc(theta1, theta2), out_radius) inner_arc = scale_arc(path_arc_cw(theta2, theta1), in_radius) return shift_path_by_vec(concatenate_paths([outer_arc, inner_arc]), np.array(origin))
def _recompute_path(self): # Form the outer ring arc = Path.arc(theta1=0.0, theta2=360.0) # Draw the outer unit circle followed by a reversed and scaled inner circle v1 = arc.vertices v2 = arc.vertices[::-1] * float(1.0 - self.thick) v = np.vstack([v1, v2, v1[0, :], (0, 0)]) c = np.hstack([arc.codes, arc.codes, Path.MOVETO, Path.CLOSEPOLY]) c[len(arc.codes)] = Path.MOVETO # Final shape acheieved through axis transformation. See _recompute_transform self._path = Path(v, c)
def bezier_path(self): """ Return ``self`` as a Bezier path. This is needed to concatenate arcs, in order to create hyperbolic polygons. EXAMPLES:: sage: from sage.plot.arc import Arc sage: op = {'alpha':1,'thickness':1,'rgbcolor':'blue','zorder':0, ....: 'linestyle':'--'} sage: Arc(2,3,2.2,2.2,0,2,3,op).bezier_path() Graphics object consisting of 1 graphics primitive sage: a = arc((0,0),2,1,0,(pi/5,pi/2+pi/12), linestyle="--", color="red") sage: b = a[0].bezier_path() sage: b[0] Bezier path from (1.133..., 0.8237...) to (-0.2655..., 0.9911...) """ from sage.plot.bezier_path import BezierPath from sage.plot.graphics import Graphics from matplotlib.path import Path import numpy as np ma = self._matplotlib_arc() def theta_stretch(theta, scale): theta = np.deg2rad(theta) x = np.cos(theta) y = np.sin(theta) return np.rad2deg(np.arctan2(scale * y, x)) theta1 = theta_stretch(ma.theta1, ma.width / ma.height) theta2 = theta_stretch(ma.theta2, ma.width / ma.height) pa = ma pa._path = Path.arc(theta1, theta2) transform = pa.get_transform().get_matrix() cA, cC, cE = transform[0] cB, cD, cF = transform[1] points = [] for u in pa._path.vertices: x, y = list(u) points += [(cA * x + cC * y + cE, cB * x + cD * y + cF)] cutlist = [points[0:4]] N = 4 while N < len(points): cutlist += [points[N:N + 3]] N += 3 g = Graphics() opt = self.options() opt['fill'] = False g.add_primitive(BezierPath(cutlist, opt)) return g
def bezier_path(self): """ Return ``self`` as a Bezier path. This is needed to concatenate arcs, in order to create hyperbolic polygons. EXAMPLES:: sage: from sage.plot.arc import Arc sage: op = {'alpha':1,'thickness':1,'rgbcolor':'blue','zorder':0, ....: 'linestyle':'--'} sage: Arc(2,3,2.2,2.2,0,2,3,op).bezier_path() Graphics object consisting of 1 graphics primitive sage: a = arc((0,0),2,1,0,(pi/5,pi/2+pi/12), linestyle="--", color="red") sage: b = a[0].bezier_path() sage: b[0] Bezier path from (1.133..., 0.8237...) to (-0.2655..., 0.9911...) """ from sage.plot.bezier_path import BezierPath from sage.plot.graphics import Graphics from matplotlib.path import Path import numpy as np ma = self._matplotlib_arc() def theta_stretch(theta, scale): theta = np.deg2rad(theta) x = np.cos(theta) y = np.sin(theta) return np.rad2deg(np.arctan2(scale * y, x)) theta1 = theta_stretch(ma.theta1, ma.width / ma.height) theta2 = theta_stretch(ma.theta2, ma.width / ma.height) pa = ma pa._path = Path.arc(theta1, theta2) transform = pa.get_transform().get_matrix() cA, cC, cE = transform[0] cB, cD, cF = transform[1] points = [] for u in pa._path.vertices: x, y = list(u) points += [(cA * x + cC * y + cE, cB * x + cD * y + cF)] cutlist = [points[0: 4]] N = 4 while N < len(points): cutlist += [points[N: N + 3]] N += 3 g = Graphics() opt = self.options() opt['fill'] = False g.add_primitive(BezierPath(cutlist, opt)) return g
def __init__(self, center=(0, 0), r1=0, r2=None, theta1=0, theta2=360, **kwargs): """ Draw a ring centered at *x*, *y* center with inner radius *r1* and outer radius *r2* that sweeps *theta1* to *theta2* (in degrees). Valid kwargs are: %(Patch)s """ patches.Patch.__init__(self, **kwargs) self.center = center self.r1, self.r2 = r1, r2 self.theta1, self.theta2 = theta1, theta2 # Inner and outer rings are connected unless the annulus is complete delta = abs(theta2 - theta1) if fmod(delta, 360) <= 1e-12 * delta: theta1, theta2 = 0, 360 connector = Path.MOVETO else: connector = Path.LINETO # Form the outer ring arc = Path.arc(theta1, theta2) if r1 > 0: # Partial annulus needs to draw the outter ring # followed by a reversed and scaled inner ring v1 = arc.vertices v2 = arc.vertices[::-1] * float(r1) / r2 v = numpy.vstack([v1, v2, v1[0, :], (0, 0)]) c = numpy.hstack([arc.codes, arc.codes, connector, Path.CLOSEPOLY]) c[len(arc.codes)] = connector else: # Wedge doesn't need an inner ring v = numpy.vstack( [arc.vertices, [(0, 0), arc.vertices[0, :], (0, 0)]]) c = numpy.hstack( [arc.codes, [connector, connector, Path.CLOSEPOLY]]) v *= r2 v += numpy.array(center) self._path = Path(v, c) self._patch_transform = transforms.IdentityTransform()
def __init__(self, center=(0,0), r1=0, r2=None, theta1=0, theta2=360, **kwargs ): """ Draw a ring centered at *x*, *y* center with inner radius *r1* and outer radius *r2* that sweeps *theta1* to *theta2* (in degrees). Valid kwargs are: %(Patch)s """ patches.Patch.__init__(self, **kwargs) self.center = center self.r1, self.r2 = r1,r2 self.theta1, self.theta2 = theta1,theta2 # Inner and outer rings are connected unless the annulus is complete delta=abs(theta2-theta1) if fmod(delta,360)<=1e-12*delta: theta1,theta2 = 0,360 connector = Path.MOVETO else: connector = Path.LINETO # Form the outer ring arc = Path.arc(theta1,theta2) if r1 > 0: # Partial annulus needs to draw the outter ring # followed by a reversed and scaled inner ring v1 = arc.vertices v2 = arc.vertices[::-1]*float(r1)/r2 v = numpy.vstack([v1,v2,v1[0,:],(0,0)]) c = numpy.hstack([arc.codes,arc.codes,connector,Path.CLOSEPOLY]) c[len(arc.codes)]=connector else: # Wedge doesn't need an inner ring v = numpy.vstack([arc.vertices,[(0,0),arc.vertices[0,:],(0,0)]]) c = numpy.hstack([arc.codes,[connector,connector,Path.CLOSEPOLY]]) v *= r2 v += numpy.array(center) self._path = Path(v,c) self._patch_transform = transforms.IdentityTransform()
def path_arc_cw(theta1, theta2): """used if theta1 >= theta2""" # construct the normal arc ccw arc1 = Path.arc(theta2, theta1) # flip the vertices and control points verts = list(arc1.vertices[0::2]) verts.reverse() controls = list(arc1.vertices[1::2]) controls.reverse() new_verts = [] for i in range(len(controls)): new_verts.append(verts[i]) new_verts.append(controls[i]) new_verts.append(verts[-1]) return Path(np.array(new_verts), arc1.codes)
def _recompute_path(self): # Form the outer ring arc = Path.arc(theta1=0.0, theta2=360.0) print(f'{arc=}') # Draw the outer unit circle followed by a reversed and scaled inner circle v1 = arc.vertices v2 = np.zeros_like(v1) v2[:, 0] = v1[::-1, 0] * float(1.0 - self.thick[0]) v2[:, 1] = v1[::-1, 1] * float(1.0 - self.thick[1]) print(f'{v1=} {v2=}') v = np.vstack([v1, v2, v1[0, :], (0, 0)]) print(f'{v=}') c = np.hstack([arc.codes, arc.codes, Path.MOVETO, Path.CLOSEPOLY]) print(f'{c=}') c[len(arc.codes)] = Path.MOVETO # Final shape acheieved through axis transformation. See _recompute_transform self._path = Path(v, c)
def draw(self, renderer): """ Ellipses are normally drawn using an approximation that uses eight cubic bezier splines. The error of this approximation is 1.89818e-6, according to this unverified source: Lancaster, Don. Approximating a Circle or an Ellipse Using Four Bezier Cubic Splines. http://www.tinaja.com/glib/ellipse4.pdf There is a use case where very large ellipses must be drawn with very high accuracy, and it is too expensive to render the entire ellipse with enough segments (either splines or line segments). Therefore, in the case where either radius of the ellipse is large enough that the error of the spline approximation will be visible (greater than one pixel offset from the ideal), a different technique is used. In that case, only the visible parts of the ellipse are drawn, with each visible arc using a fixed number of spline segments (8). The algorithm proceeds as follows: 1. The points where the ellipse intersects the axes bounding box are located. (This is done be performing an inverse transformation on the axes bbox such that it is relative to the unit circle -- this makes the intersection calculation much easier than doing rotated ellipse intersection directly). This uses the "line intersecting a circle" algorithm from: Vince, John. Geometry for Computer Graphics: Formulae, Examples & Proofs. London: Springer-Verlag, 2005. 2. The angles of each of the intersection points are calculated. 3. Proceeding counterclockwise starting in the positive x-direction, each of the visible arc-segments between the pairs of vertices are drawn using the bezier arc approximation technique implemented in Path.arc(). """ if not hasattr(self, 'axes'): raise RuntimeError('Arcs can only be used in Axes instances') self._recompute_transform() # Get the width and height in pixels width = self.convert_xunits(self.width) height = self.convert_yunits(self.height) width, height = self.get_transform().transform_point( (width, height)) inv_error = (1.0 / 1.89818e-6) * 0.5 if width < inv_error and height < inv_error: self._path = Path.arc(self.theta1, self.theta2) return Patch.draw(self, renderer) def iter_circle_intersect_on_line(x0, y0, x1, y1): dx = x1 - x0 dy = y1 - y0 dr2 = dx*dx + dy*dy D = x0*y1 - x1*y0 D2 = D*D discrim = dr2 - D2 # Single (tangential) intersection if discrim == 0.0: x = (D*dy) / dr2 y = (-D*dx) / dr2 yield x, y elif discrim > 0.0: # The definition of "sign" here is different from # npy.sign: we never want to get 0.0 if dy < 0.0: sign_dy = -1.0 else: sign_dy = 1.0 sqrt_discrim = npy.sqrt(discrim) for sign in (1., -1.): x = (D*dy + sign * sign_dy * dx * sqrt_discrim) / dr2 y = (-D*dx + sign * npy.abs(dy) * sqrt_discrim) / dr2 yield x, y def iter_circle_intersect_on_line_seg(x0, y0, x1, y1): epsilon = 1e-9 if x1 < x0: x0e, x1e = x1, x0 else: x0e, x1e = x0, x1 if y1 < y0: y0e, y1e = y1, y0 else: y0e, y1e = y0, y1 x0e -= epsilon y0e -= epsilon x1e += epsilon y1e += epsilon for x, y in iter_circle_intersect_on_line(x0, y0, x1, y1): if x >= x0e and x <= x1e and y >= y0e and y <= y1e: yield x, y # Transforms the axes box_path so that it is relative to the unit # circle in the same way that it is relative to the desired # ellipse. box_path = Path.unit_rectangle() box_path_transform = transforms.BboxTransformTo(self.axes.bbox) + \ self.get_transform().inverted() box_path = box_path.transformed(box_path_transform) PI = npy.pi TWOPI = PI * 2.0 RAD2DEG = 180.0 / PI DEG2RAD = PI / 180.0 theta1 = self.theta1 theta2 = self.theta2 thetas = {} # For each of the point pairs, there is a line segment for p0, p1 in zip(box_path.vertices[:-1], box_path.vertices[1:]): x0, y0 = p0 x1, y1 = p1 for x, y in iter_circle_intersect_on_line_seg(x0, y0, x1, y1): theta = npy.arccos(x) if y < 0: theta = TWOPI - theta # Convert radians to angles theta *= RAD2DEG if theta > theta1 and theta < theta2: thetas[theta] = None thetas = thetas.keys() thetas.sort() thetas.append(theta2) last_theta = theta1 theta1_rad = theta1 * DEG2RAD inside = box_path.contains_point((npy.cos(theta1_rad), npy.sin(theta1_rad))) for theta in thetas: if inside: self._path = Path.arc(last_theta, theta, 8) Patch.draw(self, renderer) inside = False else: inside = True last_theta = theta
def _render_patches(self, axes, aa_pixel_size=0, rotation=(1, 0, 0, 0), ambient_light=0, directional_light=(-.1, -.25, -1), **kwargs): rotation = np.asarray(rotation) start_points, end_points, widths, colors = mesh.unfoldProperties( [self.start_points, self.end_points, self.widths, self.colors]) # rotate into scene orientation start_points = math.quatrot(rotation[np.newaxis], start_points) end_points = math.quatrot(rotation[np.newaxis], end_points) midpoints = 0.5 * (start_points + end_points) zs = midpoints[:, 2] # calculate the vector perpendicular to each line segment deltas = end_points[:, :2] - start_points[:, :2] perps = np.array([-deltas[:, 1], deltas[:, 0]]).T perps /= np.linalg.norm(perps, axis=-1, keepdims=True) perps *= 0.5 * widths perps[np.any(np.logical_not(np.isfinite(perps)), axis=-1)] = (1, 0) # angle of the vector perpendicular to each line segment angles = np.arctan2(perps[:, 1], perps[:, 0]) + np.pi angles[np.logical_not(np.isfinite(angles))] = 0 angles_degrees = angles * 180 / np.pi # construct rectangles with offset vertices rectangles = [ start_points[:, :2] - perps, end_points[:, :2] - perps, end_points[:, :2] + perps, start_points[:, :2] + perps ] if aa_pixel_size: for elt in rectangles: elt -= midpoints[:, :2] elt += np.sign(elt) * aa_pixel_size elt += midpoints[:, :2] patches = [] for (z, angle, width, start, end, a, b, c, d) in zip(zs, angles_degrees, widths[:, 0], start_points[:, :2], end_points[:, :2], *rectangles): arc = Path.arc(angle, angle + 180) commands = [Path.MOVETO, Path.LINETO] vertices = [a, b] for (pos, cmd) in arc.iter_segments(): cmd = cmd if cmd != Path.MOVETO else Path.LINETO if cmd == Path.STOP: continue pos = pos.reshape((-1, 2)) * width * 0.5 commands.extend(pos.shape[0] * [cmd]) vertices.extend(end[np.newaxis] + pos) commands.append(Path.LINETO) vertices.append(c) commands.append(Path.LINETO) vertices.append(d) for (pos, cmd) in arc.iter_segments(): cmd = cmd if cmd != Path.MOVETO else Path.LINETO if cmd == Path.STOP: continue pos = pos.reshape((-1, 2)) * width * 0.5 commands.extend(pos.shape[0] * [cmd]) vertices.extend(start[np.newaxis] - pos) commands.append(Path.CLOSEPOLY) vertices.append(a) path = Path(vertices, commands) patches.append(PathPatch(path, zorder=z)) return [(patches, colors)]
fig, ax = plt.subplots(subplot_kw={'aspect': 'equal'}) e3 = EllipticalShell(center=(0, 0), width=1, height=1, thick=(0.5, 0.0), angle=0) ax.add_patch(e3) e3.set_facecolor((1, 0, 0)) e1 = Ellipse(xy=(0, 0), width=0.1, height=1, angle=0) ax.add_patch(e1) e1.set_facecolor((0, 0, 1)) e2 = Ellipse(xy=(0, 0), width=0.1, height=1, angle=90) ax.add_patch(e2) e2.set_facecolor((0, 1, 0)) ax.set_xlim(-1, 1) ax.set_ylim(-1, 1) arc = Path.arc(theta1=0.0, theta2=360.0) v1 = arc.vertices v2 = arc.vertices[::-1] * float( 1.0 - 0.5) # self.thick is fractional thickness ax.scatter(v1[:, 0], v1[:, 1]) ax.scatter(v2[:, 0], v2[:, 1]) plt.show()
def _parse_path(pathdef, current_pos): # In the SVG specs, initial movetos are absolute, even if # specified as 'm'. This is the default behavior here as well. # But if you pass in a current_pos variable, the initial moveto # will be relative to that current_pos. This is useful. elements = list(_tokenize_path(pathdef)) # Reverse for easy use of .pop() elements.reverse() start_pos = None command = None while elements: # 1. Determine the current command if elements[-1] in COMMANDS: # New command. last_command = command # Used by S and T command = elements.pop() absolute = command in UPPERCASE command = command.upper() else: # Implicit command. # If this element starts with numbers, it is an implicit command # and we don't change the command. Check that it's allowed: if command is None: raise ValueError( "Unallowed implicit command in {}, position {}".format( pathdef, len(pathdef.split()) - len(elements))) last_command = command # Used by S and T # 2. Parse the current command # MOVETO if command == 'M': pos = _next_pos(elements) if absolute: current_pos = pos else: current_pos += pos # when M is called, reset start_pos # This behavior of Z is defined in svg spec: # http://www.w3.org/TR/SVG/paths.html#PathDataClosePathCommand start_pos = current_pos yield COMMAND_CODES['M'], [(current_pos.real, current_pos.imag)] # Implicit moveto commands are treated as lineto commands. # So we set command to lineto here, in case there are # further implicit commands after this moveto. command = 'L' # CLOSEPATH elif command == 'Z': # path closure if current_pos != start_pos: verts = [(start_pos.real, start_pos.imag)] yield COMMAND_CODES['L'], verts # mpl.Path: a point is required but ignored verts = [(start_pos.real, start_pos.imag)] yield COMMAND_CODES['Z'], verts current_pos = start_pos start_pos = None command = None # You can't have implicit commands after closing. # LINETO elif command == 'L': pos = _next_pos(elements) if not absolute: pos += current_pos verts = [(pos.real, pos.imag)] yield COMMAND_CODES['L'], verts current_pos = pos # HORIZONTAL_PATHTO elif command == 'H': x = elements.pop() pos = float(x) + current_pos.imag * 1j if not absolute: pos += current_pos.real verts = [(pos.real, pos.imag)] yield COMMAND_CODES['H'], verts current_pos = pos # VERTICAL_PATHTO elif command == 'V': y = elements.pop() pos = current_pos.real + float(y) * 1j if not absolute: pos += current_pos.imag * 1j verts = [(pos.real, pos.imag)] yield COMMAND_CODES['V'], verts current_pos = pos # CUBIC_BEZIER elif command == 'C': control1 = _next_pos(elements) control2 = _next_pos(elements) end = _next_pos(elements) if not absolute: control1 += current_pos control2 += current_pos end += current_pos verts = [ (control1.real, control1.imag), (control2.real, control2.imag), (end.real, end.imag) ] yield COMMAND_CODES['C'], verts current_pos = end # SMOOTH_CUBIC_BEZIER elif command == 'S': # Smooth curve. First control point is the "reflection" of # the second control point in the previous path. if last_command not in 'CS': # If there is no previous command or if the previous command # was not an C, c, S or s, assume the first control point is # coincident with the current point. control1 = current_pos else: # The first control point is assumed to be the reflection of # the second control point on the previous command relative # to the current point. last_control = control2 control1 = current_pos + current_pos - last_control control2 = _next_pos(elements) end = _next_pos(elements) if not absolute: control2 += current_pos end += current_pos verts = [ (control1.real, control1.imag), (control2.real, control2.imag), (end.real, end.imag) ] yield COMMAND_CODES['S'], verts current_pos = end # QUADRATIC_BEZIER elif command == 'Q': control = _next_pos(elements) end = _next_pos(elements) if not absolute: control += current_pos end += current_pos verts = [ (control.real, control.imag), (end.real, end.imag) ] yield COMMAND_CODES['Q'], verts current_pos = end # SMOOTH_QUADRATIC_BEZIER elif command == 'T': # Smooth curve. Control point is the "reflection" of # the second control point in the previous path. if last_command not in 'QT': # If there is no previous command or if the previous command # was not an Q, q, T or t, assume the first control point is # coincident with the current point. control = current_pos else: # The control point is assumed to be the reflection of # the control point on the previous command relative # to the current point. last_control = control control = current_pos + current_pos - last_control end = _next_pos(elements) if not absolute: end += current_pos verts = [ (control.real, control.imag), (end.real, end.imag) ] yield COMMAND_CODES['T'], verts current_pos = end # ELLIPTICAL_ARC elif command == 'A': radius = _next_pos(elements) rotation = float(elements.pop()) large = float(elements.pop()) sweep = float(elements.pop()) end = _next_pos(elements) if not absolute: end += current_pos center, theta1, theta2 = endpoint_to_center( current_pos, radius, rotation, large, sweep, end ) # Create an arc on the unit circle if theta2 > theta1: arc = Path.arc(theta1=theta1, theta2=theta2) else: arc = Path.arc(theta1=theta2, theta2=theta1) # Transform it into an elliptical arc: # * scale the minor and major axes # * translate it to the center # * rotate the x-axis of the ellipse from the x-axis of the current # coordinate system trans = ( transforms.Affine2D() .scale(radius.real, radius.imag) .translate(center.real, center.imag) .rotate_deg_around(center.real, center.imag, rotation) ) arc = trans.transform_path(arc) verts = np.array(arc.vertices) codes = np.array(arc.codes) if sweep: # mysterious hack needed to render properly when sweeping the # arc angle in the "positive" angular direction yield codes[1:], verts[1:, :] else: yield codes, verts current_pos = end
def plot_working_area(l_1, l_2, theta_1_min, theta_1_max, theta_2_min, theta_2_max, plot=True): ################################################################ ################ Transform angles to radians ################ ################################################################ theta_1_min = math.radians(theta_1_min) theta_1_max = math.radians(theta_1_max) theta_2_min = math.radians(theta_2_min) theta_2_max = math.radians(theta_2_max) ################################################################ ################ Calculate Working Area ################ ################################################################ wa = l_1 * l_2 * (math.cos(theta_2_min) - math.cos(theta_2_max)) * (theta_1_max - theta_1_min) ################################################################ ################ Setup Graph ################ ################################################################ fig = plt.figure() ax = fig.add_subplot(1, 1, 1) ax.spines['left'].set_position('center') ax.spines['bottom'].set_position('center') ax.spines['right'].set_color('none') ax.spines['top'].set_color('none') ax.xaxis.set_ticks_position('bottom') ax.yaxis.set_ticks_position('left') ax.autoscale(True, 'both') ################################################################ ################ Common Variables ################ ################################################################ o = [0, 0] ################################################################ ################ Link 2 at minimum ################ ################################################################ start_point = forward_kinematics(l_1, l_2, theta_1_min, theta_2_min, True) end_point = forward_kinematics(l_1, l_2, theta_1_max, theta_2_min, True) radius = math.dist(end_point, o) start_angle = angle(start_point, [1, 0]) end_angle = angle(end_point, [1, 0]) ################################################################ current_path = Path.arc(start_angle, end_angle) new_verts = current_path.deepcopy().vertices for new_vert in new_verts: new_vert[0] = new_vert[0] * radius new_vert[1] = new_vert[1] * radius new_verts = new_verts[:] current_path = Path(new_verts, current_path.deepcopy().codes[:]) paths.append(current_path) patches.append(PathPatch(current_path)) ax.add_patch(patches[0]) ################################################################ ################ Link 2 at maximum ################ ################################################################ end_point = forward_kinematics(l_1, l_2, theta_1_min, theta_2_max, True) start_point = forward_kinematics(l_1, l_2, theta_1_max, theta_2_max, True) radius = math.dist(end_point, o) start_angle = angle_clockwise(start_point, [1, 0]) end_angle = angle_clockwise(end_point, [1, 0]) ################################################################ current_path = Path.arc(start_angle, end_angle) new_verts = current_path.deepcopy().vertices for new_vert in new_verts: new_vert[0] = new_vert[0] * radius new_vert[1] = new_vert[1] * radius * -1 new_verts = new_verts[:] current_path = Path(new_verts, current_path.deepcopy().codes[:]) paths.append(current_path) patches.append(PathPatch(current_path)) ax.add_patch(patches[1]) ################################################################ ################ Link 1 at minimum ################ ################################################################ center = (l_1 * math.cos(theta_1_min), l_1 * math.sin(theta_1_min)) end_point = forward_kinematics(l_1, l_2, theta_1_min, theta_2_min, True) end_point = [end_point[0] - center[0], end_point[1] - center[1]] start_point = forward_kinematics(l_1, l_2, theta_1_min, theta_2_max, True) start_point = [start_point[0] - center[0], start_point[1] - center[1]] radius = l_2 start_angle = angle_clockwise(start_point, [1, 0]) end_angle = angle_clockwise(end_point, [1, 0]) ################################################################ current_path = Path.arc(start_angle, end_angle) new_verts = current_path.deepcopy().vertices for new_vert in new_verts: new_vert[0] = (new_vert[0] * radius + center[0]) new_vert[1] = (new_vert[1] * radius + center[1]) * -1 new_verts[np.shape(new_verts)[0] - 1] = paths[0].vertices[0] new_verts = new_verts[:] current_path = Path(new_verts, current_path.deepcopy().codes[:]) paths.append(current_path) patches.append(PathPatch(current_path)) ax.add_patch(patches[2]) ################################################################ ################ Link 1 at maximum ################ ################################################################ center = (l_1 * math.cos(theta_1_max), l_1 * math.sin(theta_1_max)) start_point = forward_kinematics(l_1, l_2, theta_1_max, theta_2_min, True) start_point = [start_point[0] - center[0], start_point[1] - center[1]] end_point = forward_kinematics(l_1, l_2, theta_1_max, theta_2_max, True) end_point = [end_point[0] - center[0], end_point[1] - center[1]] radius = l_2 start_angle = angle(start_point, [1, 0]) end_angle = angle(end_point, [1, 0]) ################################################################ current_path = Path.arc(start_angle, end_angle) new_verts = current_path.deepcopy().vertices for new_vert in new_verts: new_vert[0] = new_vert[0] * radius + center[0] new_vert[1] = new_vert[1] * radius + center[1] new_verts = new_verts[:] current_path = Path(new_verts, current_path.deepcopy().codes[:]) paths.append(current_path) patches.append(PathPatch(current_path)) ax.add_patch(patches[3]) ################################################################ ################ Plot curves ################ ################################################################ ax.set_title("Working Area = {wa:.3f}".format(wa=wa)) new_path = Path.make_compound_path( paths[0], paths[3], paths[1], paths[2], ) old_vertices = new_path.deepcopy().vertices old_codes = new_path.deepcopy().codes new_path_vertices = [] new_path_codes = [] for i, old_vertex in enumerate(old_vertices, start=0): if (i == 0): new_path_vertices.append(old_vertex) new_path_codes.append(old_codes[i]) else: if (old_codes[i] != 1): new_path_vertices.append(old_vertex) new_path_codes.append(old_codes[i]) new_path = Path(new_path_vertices, new_path_codes) patches[0].remove() patches[1].remove() patches[2].remove() patches[3].remove() path_patch = PathPatch(new_path, fill=False, hatch='/', clip_on=True) patches.append(path_patch) ax.add_patch(path_patch) if (plot == True): plt.show() return [ax, plt, fig]
def set_theta2(self, angle): self.theta2 = angle self._path = Path.arc(self.theta1, self.theta2)
def path(self): path = Path.arc(self.start_angle, self.end_angle) transform = Affine2D().scale(self.radius).translate(*self.center.xy()) return path.transformed(transform)
def draw(self, renderer): """ Ellipses are normally drawn using an approximation that uses eight cubic bezier splines. The error of this approximation is 1.89818e-6, according to this unverified source: Lancaster, Don. Approximating a Circle or an Ellipse Using Four Bezier Cubic Splines. http://www.tinaja.com/glib/ellipse4.pdf There is a use case where very large ellipses must be drawn with very high accuracy, and it is too expensive to render the entire ellipse with enough segments (either splines or line segments). Therefore, in the case where either radius of the ellipse is large enough that the error of the spline approximation will be visible (greater than one pixel offset from the ideal), a different technique is used. In that case, only the visible parts of the ellipse are drawn, with each visible arc using a fixed number of spline segments (8). The algorithm proceeds as follows: 1. The points where the ellipse intersects the axes bounding box are located. (This is done be performing an inverse transformation on the axes bbox such that it is relative to the unit circle -- this makes the intersection calculation much easier than doing rotated ellipse intersection directly). This uses the "line intersecting a circle" algorithm from: Vince, John. Geometry for Computer Graphics: Formulae, Examples & Proofs. London: Springer-Verlag, 2005. 2. The angles of each of the intersection points are calculated. 3. Proceeding counterclockwise starting in the positive x-direction, each of the visible arc-segments between the pairs of vertices are drawn using the bezier arc approximation technique implemented in Path.arc(). """ if not hasattr(self, 'axes'): raise RuntimeError('Arcs can only be used in Axes instances') self._recompute_transform() # Get the width and height in pixels width = self.convert_xunits(self.width) height = self.convert_yunits(self.height) width, height = self.get_transform().transform_point( (width, height)) inv_error = (1.0 / 1.89818e-6) * 0.5 if width < inv_error and height < inv_error: self._path = Path.arc(self.theta1, self.theta2) return Patch.draw(self, renderer) def iter_circle_intersect_on_line(x0, y0, x1, y1): dx = x1 - x0 dy = y1 - y0 dr2 = dx*dx + dy*dy D = x0*y1 - x1*y0 D2 = D*D discrim = dr2 - D2 # Single (tangential) intersection if discrim == 0.0: x = (D*dy) / dr2 y = (-D*dx) / dr2 yield x, y elif discrim > 0.0: # The definition of "sign" here is different from # np.sign: we never want to get 0.0 if dy < 0.0: sign_dy = -1.0 else: sign_dy = 1.0 sqrt_discrim = np.sqrt(discrim) for sign in (1., -1.): x = (D*dy + sign * sign_dy * dx * sqrt_discrim) / dr2 y = (-D*dx + sign * np.abs(dy) * sqrt_discrim) / dr2 yield x, y def iter_circle_intersect_on_line_seg(x0, y0, x1, y1): epsilon = 1e-9 if x1 < x0: x0e, x1e = x1, x0 else: x0e, x1e = x0, x1 if y1 < y0: y0e, y1e = y1, y0 else: y0e, y1e = y0, y1 x0e -= epsilon y0e -= epsilon x1e += epsilon y1e += epsilon for x, y in iter_circle_intersect_on_line(x0, y0, x1, y1): if x >= x0e and x <= x1e and y >= y0e and y <= y1e: yield x, y # Transforms the axes box_path so that it is relative to the unit # circle in the same way that it is relative to the desired # ellipse. box_path = Path.unit_rectangle() box_path_transform = transforms.BboxTransformTo(self.axes.bbox) + \ self.get_transform().inverted() box_path = box_path.transformed(box_path_transform) PI = np.pi TWOPI = PI * 2.0 RAD2DEG = 180.0 / PI DEG2RAD = PI / 180.0 theta1 = self.theta1 theta2 = self.theta2 thetas = {} # For each of the point pairs, there is a line segment for p0, p1 in zip(box_path.vertices[:-1], box_path.vertices[1:]): x0, y0 = p0 x1, y1 = p1 for x, y in iter_circle_intersect_on_line_seg(x0, y0, x1, y1): theta = np.arccos(x) if y < 0: theta = TWOPI - theta # Convert radians to angles theta *= RAD2DEG if theta > theta1 and theta < theta2: thetas[theta] = None thetas = thetas.keys() thetas.sort() thetas.append(theta2) last_theta = theta1 theta1_rad = theta1 * DEG2RAD inside = box_path.contains_point((np.cos(theta1_rad), np.sin(theta1_rad))) for theta in thetas: if inside: self._path = Path.arc(last_theta, theta, 8) Patch.draw(self, renderer) inside = False else: inside = True last_theta = theta
def compress_points(points): bi, bj, bx, by = get_boundary_intersections(points) f = bi == bj alone_points = points[bi[f]] alone_paths = [Path.circle(xy, 0.5) for xy in alone_points] edge_lists = [[] for i in range(len(points))] n = 0 for i, j, x, y in zip(bi, bj, bx, by): if i != j: edge_lists[j].append((i, x, y)) n += 1 print("%s points in total: %s edges, %s alone points" % (len(points), n, len(alone_points))) def patan2(dy, dx): """ Return pseudo-arctangent of dy/dx such that patan2(y1, x1) < patan2(y2, x2) if and only if atan2(y1, x1) < atan2(y2, x2) """ if dy > 0 and dx > 0: return (0, dy - dx) elif dy > 0 and dx <= 0: return (1, -dy - dx) elif dy <= 0 and dx > 0: return (2, dx - dy) else: return (3, dx + dy) def shift(u, v): if v < u: return (v[0] + 4, v[1]) else: return v def pop_next(i, ox, oy): def local_patan2(y, x): return patan2(y - points[i, 1], x - points[i, 0]) u = local_patan2(oy, ox) j = min(range(len(edge_lists[i])), key=lambda j: shift(u, local_patan2(edge_lists[i][j][2], edge_lists[i][j][1]))) return edge_lists[i].pop(j) paths = [] # print("<path fill=\"black\" fillrule=\"wind\">") while n > 0: assert sum(len(e) for e in edge_lists) == n i = 0 while not edge_lists[i]: i += 1 start = i j, ox, oy = edge_lists[i].pop(0) startx, starty = ox, oy ux, uy = ox, oy # path = ['%s %s m' % (startx, starty)] path_vert_lists = [[[startx, starty]]] path_code_lists = [[Path.MOVETO]] n -= 1 while j != start: i = j j, vx, vy = pop_next(i, ux, uy) n -= 1 # path.append( # '%s 0 0 %s %s %s %s %s a' % # (R, R, points[i, 0], points[i, 1], ox, oy)) ox, oy = points[i] theta1 = np.arctan2(uy - oy, ux - ox) theta2 = np.arctan2(vy - oy, vx - ox) a = Path.arc(theta1 * 180 / np.pi, theta2 * 180 / np.pi) a = a.transformed(Affine2D().scale(0.5).translate(ox, oy)) path_vert_lists.append(a._vertices[1:]) path_code_lists.append(a._codes[1:]) ux, uy = vx, vy # path.append( # '%s 0 0 %s %s %s %s %s a' % # (R, R, points[j, 0], points[j, 1], startx, starty)) ox, oy = points[j] theta1 = np.arctan2(uy - oy, ux - ox) theta2 = np.arctan2(starty - oy, startx - ox) a = Path.arc(theta1 * 180 / np.pi, theta2 * 180 / np.pi) a = a.transformed(Affine2D().scale(0.5).translate(ox, oy)) path_vert_lists.append(a._vertices[1:]) path_code_lists.append(a._codes[1:]) # print('\n'.join(path)) paths.append( Path(np.concatenate(path_vert_lists), np.concatenate(path_code_lists).astype(Path.code_type))) # print("</path>") return Path.make_compound_path(*(alone_paths + paths))
def _render_patches(self, axes, aa_pixel_size=0, **kwargs): result = [] vertices = self.vertices # distance vector from each vertex to the next vertex in a given shape delta_verts = np.roll(vertices, -1, axis=0) - vertices delta_verts /= np.linalg.norm(delta_verts, axis=-1, keepdims=True) # the normal vector of the edge originating at each vertex vert_normals = np.transpose([delta_verts[:, 1], -delta_verts[:, 0]]) # start and end angles for the arc around each vertex, in degrees degree_ends = 180/np.pi*np.arctan2(vert_normals[..., 1], vert_normals[..., 0]) degree_starts = np.roll(degree_ends, 1, axis=0) radius = self.radius outline = self.outline delta_outline = radius - outline if outline > 0: # sketch out the outline, using the full radius commands = [Path.MOVETO] positions = [vertices[-1] + vert_normals[-1]*radius] for (vert, norm, degrees_start, degrees_end) in zip( vertices, np.roll(vert_normals, 1, axis=0), degree_starts, degree_ends): commands.append(Path.LINETO) positions.append(vert + norm*radius) arc = Path.arc(degrees_start, degrees_end) for (pos, cmd) in arc.iter_segments(): if cmd in {Path.STOP, Path.MOVETO}: continue pos = pos.reshape((-1, 2)) commands.extend(pos.shape[0]*[cmd]) positions.extend(vert[np.newaxis] + pos*radius) # repeat the outline path creation but in reverse to make # the hole for the colored portion of the shape commands.append(Path.MOVETO) positions.append(vertices[0] + vert_normals[-1]*delta_outline) for (vert, norm, degrees_end, degrees_start) in zip( vertices[::-1], vert_normals[::-1], degree_ends[::-1], degree_starts[::-1]): commands.append(Path.LINETO) positions.append(vert + norm*delta_outline) arc = Path.arc(degrees_start, degrees_end) for (pos, cmd) in reversed(list(arc.iter_segments())): if cmd in {Path.STOP, Path.MOVETO}: continue pos = pos.reshape((-1, 2))[::-1] commands.extend(pos.shape[0]*[cmd]) positions.extend(vert[np.newaxis] + pos*delta_outline) path = Path(positions, commands) patches = [] for (position, angle) in zip(self.positions, self.angles): tf = Affine2D().rotate(angle).translate(*position) patches.append(PathPatch(path.transformed(tf))) outline_colors = np.zeros_like(self.colors) outline_colors[:, 3] = self.colors[:, 3] result.append((patches, outline_colors)) vertices += np.sign(vertices)*aa_pixel_size # create the path for the filled/colored portion of the shape commands = [Path.MOVETO] positions = [vertices[-1] + vert_normals[-1]*delta_outline] for (vert, norm, degrees_start, degrees_end) in zip( vertices, np.roll(vert_normals, 1, axis=0), degree_starts, degree_ends): commands.append(Path.LINETO) positions.append(vert + norm*delta_outline) arc = Path.arc(degrees_start, degrees_end) for (pos, cmd) in arc.iter_segments(): if cmd in {Path.STOP, Path.MOVETO}: continue pos = pos.reshape((-1, 2))*delta_outline commands.extend(pos.shape[0]*[cmd]) positions.extend(vert[np.newaxis] + pos) path = Path(positions, commands) patches = [] for (position, angle) in zip(self.positions, self.angles): tf = Affine2D().rotate(angle).translate(*position) patches.append(PathPatch(path.transformed(tf))) result.append((patches, self.colors)) return result
def visualize_min_sum_sol_2d(solution: 'AngularGraphSolution'): graph = solution.graph fig = plt.figure() axis = plt.subplot() axis.axis('off') _visualize_edges_2d(solution.graph) _visualize_vertices_2d(solution.graph) _visualize_celest_body_2d(axis, solution.graph) # Make an edge order for vertices vertex_order = Multidict() ordered_times = solution.get_ordered_times() for time_key in ordered_times.get_ordered_keys(): for edges in ordered_times[time_key]: if edges[0] < edges[1]: vertex_order[edges[0]] = edges vertex_order[edges[1]] = edges # Get minimum edge length min_length = max( np.array([ np.linalg.norm(solution.graph.vertices[i] - solution.graph.vertices[j]) for i, j in solution.graph.edges ]).min(), 0.4) # Draws the angle paths in a circular fashion path_list = [] last_points = [] for vertex_key in vertex_order: last_edge = None last_direction = None current_min_length = min_length * 0.3 last_point = None for edge in vertex_order[vertex_key]: if last_edge: other_vertices = np.hstack([ np.setdiff1d(np.array(last_edge), np.array([vertex_key])), np.setdiff1d(np.array(edge), np.array([vertex_key])) ]) angles = [ get_angle(graph.vertices[vertex_key], graph.vertices[vertex_key] + [1, 0], graph.vertices[other_vertex]) for other_vertex in other_vertices ] # If y-coord is below the current vertex we need to calculate the angle different for i in range(len(angles)): if graph.vertices[other_vertices[i]][1] < graph.vertices[ vertex_key][1]: angles[i] = 360 - angles[i] # Calculate if we need to go from angle[0] to angle[1] or other way around # to not create an arc over 180 degrees diff = abs(angles[0] - angles[1]) if diff > 180: diff = 360 - diff normal_angle_direction = math.isclose((angles[0] + diff) % 360, angles[1], rel_tol=1e-5) if not normal_angle_direction: angles = reversed(angles) # 1 shall be clockwise and -1 counter-clockwise direction current_direction = 1 if normal_angle_direction else -1 if last_direction: if current_direction != last_direction: # direction change happened current_min_length *= 1.25 # Transform the arc to the right position transform = mtransforms.Affine2D().scale( current_min_length, current_min_length) transform = transform.translate(*graph.vertices[vertex_key]) arc = Path.arc(*angles) arc_t = arc.transformed(transform) if last_direction: if current_direction != last_direction: # direction change happened last_vertex = path_list[-1].vertices[ -1] if last_direction == 1 else path_list[ -1].vertices[0] new_vertex = arc_t.vertices[ 0] if current_direction == 1 else arc_t.vertices[-1] bridge_path = Path([last_vertex, new_vertex]) path_list.append(bridge_path) last_direction = current_direction path_list.append(arc_t) last_point = path_list[-1].vertices[ -1] if last_direction == 1 else path_list[-1].vertices[0] last_points.append(last_point) last_edge = edge # Add these points to detect direction last_points.append(last_point) path_collection = PathCollection(path_list, edgecolor='r', facecolor='#00000000') axis.add_collection(path_collection) a_last_points = np.array([l for l in last_points if l is not None]) plt.plot(a_last_points[:, 0], a_last_points[:, 1], 'r.') axis.autoscale() plt.show()
def arc_path(start, radius, rotation, large, sweep, end): """ Generate an elliptical arc path given an endpoint parameterization. Uses matplotlib to draw the arc using quadratic Bezier curves. Parameters ---------- start : complex Starting point (x1, y1). radius : complex Two elliptical radii (rx, ry). rotation : float Angle from the x-axis of the current coordinate system to the x-axis of the ellipse. large : bool False if an arc spanning < 180 degrees is to be drawn, True if an arc spanning >= 180 degrees is to be drawn. sweep : bool If sweep-flag is True, then the arc will be drawn in a "positive-angle" direction from start to end. end : complex End point (x2, y2). Returns ------- codes : array (n,) Command codes verts : array (n,2) Vertices Notes ----- We first perform a conversion to center parameterization and generate a circular arc at the origin. Then we apply scaling, translation and rotation. One can think of an ellipse as a circle that has been stretched and then rotated. Start by making an arc along the unit circle from `theta1` to `theta2`, centered at `center`. Then scale the circle along the x and y axes according to the given radii. Finally, rotate the arc around the center through the given angle `rotation`. """ radius, center, theta1, theta2 = endpoint_to_center( start, radius, rotation, large, sweep, end) # Create an arc on the unit circle # Matplotlib does this using CURVE4 operations # https://matplotlib.org/stable/_modules/matplotlib/path.html#Path.arc if theta2 > theta1: arc = Path.arc(theta1=theta1, theta2=theta2) reverse_path = False else: arc = Path.arc(theta1=theta2, theta2=theta1) reverse_path = True # Transform it into an elliptical arc: # * scale the minor and major axes # * translate it to the center # * rotate the x-axis of the ellipse from the x-axis of the current # coordinate system trans = (transforms.Affine2D().scale(radius.real, radius.imag).translate( center.real, center.imag).rotate_deg_around(center.real, center.imag, rotation)) arc = trans.transform_path(arc) codes = np.array(arc.codes) verts = np.array(arc.vertices) # Make sure we are drawing from start to end if reverse_path: verts = verts[::-1, :] # Change the initial MOVETO operation into a LINETO to connect to the # previous path codes[0] = Path.LINETO return codes, verts
def path_arc_smart(theta1, theta2): if theta1 >= theta2: return path_arc_cw(theta1, theta2) else: return Path.arc(theta1, theta2)