def test_straight_joint_headings(): # The math in calculating joint geometry can get numerically unstable # very close to straight joints at various headings. for heading_angle in range(0, 45): p = Pen() p.stroke_mode(1.0) p.move_to((0, 0)) p.turn_to(heading_angle) p.line_forward(10) p.line_forward(10) path = p.paper.paths[0] path.render_path(2) # Doesn't crash. # Check that the joint angle is 90 degrees from the heading. assert_equal(len(p.paper.paths), 1) segments = p.paper.paths[0].segments assert_equal(len(segments), 2) s0, s1 = segments target_angle = (heading_angle + 90) % 180 joint_angle = math.degrees(vec.heading(vec.vfrom(s0.b_right, s0.b_left))) assert_almost_equal(joint_angle % 180, target_angle) joint_angle = math.degrees(vec.heading(vec.vfrom(s1.a_right, s1.a_left))) assert_almost_equal(joint_angle % 180, target_angle)
def arc_to(self, endpoint, center=None, start_slant=None, end_slant=None): """ Draw an arc ending at the specified point, starting tangent to the current position and heading. """ if points_equal(self._position, endpoint): return # Handle unspecified center. # We need to find the center of the arc, so we can find its radius. The # center of this arc is uniquely defined by the intersection of two # lines: # 1. The first line is perpendicular to the pen heading, passing # through the pen position. # 2. The second line is the perpendicular bisector of the pen position # and the target arc end point. v_pen = self._vector() v_perp = vec.perp(self._vector()) v_chord = vec.vfrom(self._position, endpoint) if center is None: midpoint = vec.div(vec.add(self._position, endpoint), 2) v_bisector = vec.perp(v_chord) center = intersect_lines( self._position, vec.add(self._position, v_perp), midpoint, vec.add(midpoint, v_bisector), ) # Determine true start heading. This may not be the same as the # original pen heading in some circumstances. assert not points_equal(center, self._position) v_radius_start = vec.vfrom(center, self._position) v_radius_perp = vec.perp(v_radius_start) if vec.dot(v_radius_perp, v_pen) < 0: v_radius_perp = vec.neg(v_radius_perp) start_heading = math.degrees(vec.heading(v_radius_perp)) self.turn_to(start_heading) # Refresh v_pen and v_perp based on the new start heading. v_pen = self._vector() v_perp = vec.perp(self._vector()) # Calculate the arc angle. # The arc angle is double the angle between the pen vector and the # chord vector. Arcing to the left is a positive angle, and arcing to # the right is a negative angle. arc_angle = 2 * math.degrees(vec.angle(v_pen, v_chord)) radius = vec.mag(v_radius_start) # Check which side of v_pen the goes toward. if vec.dot(v_chord, v_perp) < 0: arc_angle = -arc_angle radius = -radius self._arc( center, radius, endpoint, arc_angle, start_slant, end_slant, )
def turn_toward(self, point): v = vec.vfrom(self._position, point) heading = math.degrees(vec.heading(v)) self.turn_to(heading)
def join_with_line(self, other): v_self = self._vector() v_other = other._vector() # Check turn angle. self_heading = Heading.from_rad(vec.heading(v_self)) other_heading = Heading.from_rad(vec.heading(v_other)) turn_angle = self_heading.angle_to(other_heading) # Special case equal widths. if( abs(turn_angle) <= MAX_TURN_ANGLE and float_equal(self.width, other.width) ): # When joints between segments of equal width are straight or # almost straight, the line-intersection method becomes very # numerically unstable, so use another method instead. # For each segment, get a vector perpendicular to the # segment, then add them. This is an angle bisector for # the angle of the joint. w_self = self._width_vector() w_other = other._width_vector() v_bisect = vec.add(w_self, w_other) # Make the bisector have the correct length. half_angle = vec.angle(v_other, v_bisect) v_bisect = vec.norm( v_bisect, (self.width / 2) / math.sin(half_angle) ) # Determine the left and right joint spots. p_left = vec.add(self.b, v_bisect) p_right = vec.sub(self.b, v_bisect) else: a, b = self.offset_line_left() c, d = other.offset_line_left() p_left = intersect_lines(a, b, c, d) a, b = self.offset_line_right() c, d = other.offset_line_right() p_right = intersect_lines(a, b, c, d) # Make sure the joint points are "forward" from the perspective # of each segment. if p_left is not None: if vec.dot(vec.vfrom(self.a_left, p_left), v_self) < 0: p_left = None if p_right is not None: if vec.dot(vec.vfrom(self.a_right, p_right), v_self) < 0: p_right = None # Don't join the outer sides if the turn angle is too steep. if abs(turn_angle) > MAX_TURN_ANGLE: if turn_angle > 0: p_right = None else: p_left = None if p_left is not None: self.b_left = other.a_left = Point(*p_left) if p_right is not None: self.b_right = other.a_right = Point(*p_right) if p_left is None or p_right is None: self.end_joint_illegal = True other.start_joint_illegal = True
def heading(self): return Heading.from_rad(vec.heading(vec.vfrom(self.a, self.b)))