def __init__(self, d: str, canvas_height: float, transform_origin=True, transformation=None): self.canvas_height = canvas_height self.transform_origin = transform_origin self.curves = [] self.initial_point = Vector(0, 0) # type: Vector self.current_point = Vector(0, 0) self.last_control = None # type: Vector self.transformation = Transformation() if self.transform_origin: self.transformation.add_translation(0, canvas_height) self.transformation.add_scale(1, -1) if transformation is not None: self.transformation.extend(transformation) try: self._parse_commands(d) except Exception as generic_exception: warnings.warn( f"Terminating path. The following unforeseen exception occurred: {generic_exception}" )
def to_svg_path(self, wrapped=True, transform=False, height=None): """A handy debugging function which the current line-chain in svg form""" if transform: assert height start_ = Vector(self._curves[0].start.x, height - self._curves[0].start.y) else: start_ = Vector(self._curves[0].start.x, self._curves[0].start.y) d = f"M{start_.x} {start_.y}" for line in self._curves: end_ = Vector(line.end.x, height - line.end.y) if transform else Vector( line.end.x, line.end.y) d += f" L {end_.x} {end_.y}" if not wrapped: return d style = "fill:none;stroke:black;stroke-width:0.864583px;stroke-linecap:butt;stroke-linejoin:miter;stroke" \ "-opacity:1 " return f"""<path\nd="{d}"\nstyle="{style}"\n/>"""
def to_svg_path(line_segment_chain: LineSegmentChain, transformation=None, color="black", stroke_width="0.864583px", draw_arrows=False) -> ElementTree.Element: """ A handy debugging function which converts the current line-chain to svg form :param line_segment_chain: The LineSegmentChain to the converted. :param transformation: A transformation to apply to every line before converting it. :param color: The path's color. :param stroke_width: The path's stroke width. :param stroke_width: Whether or not to draw arrows at the end of each segment. Requires placing the output of arrow_defs() in the document. """ start = Vector(line_segment_chain.get(0).start.x, line_segment_chain.get(0).start.y) if transformation: start = transformation.apply_transformation(start) d = f"M{start.x} {start.y}" for line in line_segment_chain: end = Vector(line.end.x, line.end.y) if transformation: end = transformation.apply_transformation(end) d += f" L {end.x} {end.y}" style = f"fill:none;stroke:{color};stroke-width:{stroke_width};stroke-linecap:butt;stroke-linejoin:miter;stroke" \ "-opacity:1 " path = ElementTree.Element("{%s}path" % svg_namespace) path.set("d", d) path.set("style", style) if draw_arrows: path.set("marker-mid", "url(#arrow-346)") return path
def rotate(p, r, inverted=False): """Rotate a point p by r radians. Remember that the y-axis is inverted in the svg standard.""" x, y = p if inverted: return Vector(x * math.cos(r) + y * math.sin(r), -x * math.sin(r) + y * math.cos(r)) return Vector(x * math.cos(r) - y * math.sin(r), +x * math.sin(r) + y * math.cos(r))
def _transform_coordinate_system(self, point: Vector): """ If both do_vertical_mirror and do_vertical_translate are true, it will transform a point form a coordinate system with the origin at the top-left, to one with origin at the bottom-right. """ if self.do_vertical_mirror: point = Vector(point.x, -point.y) if self.do_vertical_translate: point += Vector(0, self.canvas_height) return point
def absolute_cubic_bezier_extension(x2, y2, x, y): start = self.end control2 = Vector(x2, y2) end = Vector(x, y) if self.last_control: control1 = 2 * start - self.last_control bazier = absolute_cubic_bazier(*control1, *control2, *end) else: bazier = absolute_quadratic_bazier(*control2, *end) self.end = start return bazier
def absolute_quadratic_bazier(control1_x, control1_y, x, y): trans_end = self._apply_transformations(self.current_point) trans_new_end = self._apply_transformations(Vector(x, y)) trans_control1 = self._apply_transformations( Vector(control1_x, control1_y)) quadratic_bezier = QuadraticBezier(trans_end, trans_new_end, trans_control1) self.last_control = Vector(control1_x, control1_y) self.current_point = Vector(x, y) return quadratic_bezier
def absolute_quadratic_bazier(control1_x, control1_y, x, y): trans_end = self._transform_coordinate_system(self.end) trans_new_end = self._transform_coordinate_system(Vector(x, y)) trans_control1 = self._transform_coordinate_system( Vector(control1_x, control1_y)) quadratic_bezier = QuadraticBezier(trans_end, trans_new_end, trans_control1) self.last_control = Vector(control1_x, control1_y) self.end = Vector(x, y) return quadratic_bezier
def center_to_endpoint_parameterization(center, radii, rotation, start_angle, sweep_angle): rotation_matrix = RotationMatrix(rotation) start = rotation_matrix * Vector(radii.x * math.cos(start_angle), radii.y * math.sin(start_angle)) + center end_angle = start_angle + sweep_angle end = rotation_matrix * Vector(radii.x * math.cos(end_angle), radii.y * math.sin(end_angle)) + center large_arc_flag = 1 if abs(sweep_angle) > math.pi else 0 sweep_flag = 1 if sweep_angle > 0 else 0 return start, end, large_arc_flag, sweep_flag
def linear_move(self, x=None, y=None, z=None): if self._next_speed is None: raise ValueError("Undefined movement speed. Call set_movement_speed before executing movement commands.") # Don't do anything if linear move was called without passing a value. if x is None and y is None and z is None: warnings.warn("linear_move command invoked without arguments.") return '' # Todo, investigate G0 command and replace movement speeds with G1 (normal speed) and G0 (fast move) command = "G1" if self._current_speed != self._next_speed: self._current_speed = self._next_speed command += f" F{self._current_speed}" # Move if not 0 and not None command += f" X{x:.{self.precision}f}" if x is not None else '' command += f" Y{y:.{self.precision}f}" if y is not None else '' command += f" Z{z:.{self.precision}f}" if z is not None else '' if self.position is not None or (x is not None and y is not None): if x is None: x = self.position.x if y is None: y = self.position.y self.position = Vector(x, y) if verbose: print(f"Move to {x}, {y}, {z}") return command + ';'
def angle_between_vectors(v1, v2): """Compute angle between two vectors v1, v2""" angle = math.acos(Vector.dot_product(v1, v2) / (abs(v1) * abs(v2))) angle *= -1 if v1.x * v2.y - v1.y * v2.x > 0 else 1 return angle
def absolute_cubic_bazier(control1_x, control1_y, control2_x, control2_y, x, y): trans_start = self._apply_transformations(self.current_point) trans_end = self._apply_transformations(Vector(x, y)) trans_control1 = self._apply_transformations( Vector(control1_x, control1_y)) trans_control2 = self._apply_transformations( Vector(control2_x, control2_y)) cubic_bezier = CubicBazier(trans_start, trans_end, trans_control1, trans_control2) self.last_control = Vector(control2_x, control2_y) self.current_point = Vector(x, y) return cubic_bezier
def angle_to_point(self, angle): transformed_radii = Vector(self.radii.x * math.cos(angle), self.radii.y * math.sin(angle)) point = RotationMatrix(self.rotation) * transformed_radii + self.center if self.transformation: point = self.transformation.apply_affine_transformation(point) return point
def apply_affine_transformation(self, vector: Vector) -> Vector: """ Apply the full affine transformation (linear + translation) to a vector. Generally used to transform points. Eg the center of an ellipse. """ vector_4d = Matrix([[vector.x], [vector.y], [1], [1]]) vector_4d = self.translation_matrix * vector_4d return Vector(vector_4d.matrix_list[0][0], vector_4d.matrix_list[1][0])
def absolute_line(x, y): start = self.current_point end = Vector(x, y) line = Line(self.transformation.apply_affine_transformation(start), self.transformation.apply_affine_transformation(end)) self.current_point = end return line
def absolute_line(x, y): start = self.end end = Vector(x, y) line = Line(self._transform_coordinate_system(start), self._transform_coordinate_system(end)) self.end = end return line
def linear_move(self, x=None, y=None, z=None) -> str: if self.position is not None or (x is not None and y is not None): if x is None: x = self.position.x if y is None: y = self.position.y self.position = Vector(x, y) return f"g{x:.1f},{y:.1f}"
def angle_between_vectors(v1, v2): """Compute angle between two vectors v1, v2""" cos_angle = Vector.dot_product(v1, v2) / (abs(v1) * abs(v2)) cos_angle = tolerance_constrain(cos_angle, 1, -1) angle = math.acos(cos_angle) angle *= 1 if v1.x * v2.y - v1.y * v2.x > 0 else -1 return angle
def absolute_quadratic_bazier_extension(x, y): start = self.end end = Vector(x, y) if self.last_control: control = 2 * start - self.last_control bazier = absolute_quadratic_bazier(*control, *end) else: bazier = absolute_quadratic_bazier(*start, *end) self.end = end return bazier
def multiply_vector(self, other_vector: Vector): if self.number_of_columns != 2: raise ValueError( f"can't multiply matrix with 2D vector. The matrix must have 2 columns, not " f"{self.number_of_columns}") x = sum([ self[0][k] * other_vector[k] for k in range(self.number_of_columns) ]) y = sum([ self[1][k] * other_vector[k] for k in range(self.number_of_columns) ]) return Vector(x, y)
def absolute_arc(rx, ry, deg_from_horizontal, large_arc_flag, sweep_flag, x, y): end = Vector(x, y) start = self.current_point radii = Vector(rx, ry) rotation_rad = math.radians(deg_from_horizontal) if abs(start - end) == 0: raise ValueError("start and end points can't be equal") radii, center, start_angle, sweep_angle = formulas.endpoint_to_center_parameterization( start, end, radii, rotation_rad, large_arc_flag, sweep_flag) arc = EllipticalArc(center, radii, rotation_rad, start_angle, sweep_angle, transformation=self.transformation) self.current_point = end return arc
def endpoint_to_center_parameterization(start, end, radii, rotation_rad, large_arc_flag, sweep_flag): # Find and select one of the two possible eclipse centers by undoing the rotation (to simplify the math) and # then re-applying it. rotated_primed_values = ( start - end) / 2 # Find the primed_values of the start and the end points. primed_values = RotationMatrix(rotation_rad, True) * rotated_primed_values px, py = primed_values.x, primed_values.y # Correct out-of-range radii rx = abs(radii.x) ry = abs(radii.y) delta = px**2 / rx**2 + py**2 / ry**2 if delta > 1: rx *= math.sqrt(delta) ry *= math.sqrt(delta) if math.sqrt(delta) > 1: center = Vector(0, 0) else: radicant = ((rx * ry)**2 - (rx * py)**2 - (ry * px)**2) / ((rx * py)**2 + (ry * px)**2) radicant = max(0, radicant) # Find center using w3.org's formula center = math.sqrt(radicant) * Vector((rx * py) / ry, -(ry * px) / rx) center *= -1 if large_arc_flag == sweep_flag else 1 # Select one of the two solutions based on flags rotated_center = RotationMatrix(rotation_rad) * center + ( start + end) / 2 # re-apply the rotation cx, cy = center.x, center.y u = Vector((px - cx) / rx, (py - cy) / ry) v = Vector((-px - cx) / rx, (-py - cy) / ry) max_angle = 2 * math.pi start_angle = angle_between_vectors(Vector(1, 0), u) sweep_angle_unbounded = angle_between_vectors(u, v) sweep_angle = sweep_angle_unbounded % max_angle if not sweep_flag and sweep_angle > 0: sweep_angle -= max_angle if sweep_flag and sweep_angle < 0: sweep_angle += max_angle return Vector(rx, ry), rotated_center, start_angle, sweep_angle
def __init__(self, d: str, canvas_height: float, do_vertical_mirror=True, do_vertical_translate=True): self.canvas_height = canvas_height self.do_vertical_mirror = do_vertical_mirror self.do_vertical_translate = do_vertical_translate self.curves = [] self.start = None # type: Vector self.end = Vector(0, 0) self.last_control = None # type: Vector try: self._parse_commands(d) except Exception as generic_exception: warnings.warn( f"Terminating path. The following unforeseen exception occurred: {generic_exception}" )
def absolute_cubic_bazier(control1_x, control1_y, control2_x, control2_y, x, y): self.start = Vector(x, y) trans_start = self._transform_coordinate_system(self.end) trans_end = self._transform_coordinate_system(Vector(x, y)) trans_control1 = self._transform_coordinate_system( Vector(control1_x, control1_y)) trans_control2 = self._transform_coordinate_system( Vector(control2_x, control2_y)) cubic_bezier = CubicBazier(trans_start, trans_end, trans_control1, trans_control2) self.last_control = Vector(control2_x, control2_y) self.end = Vector(x, y) return cubic_bezier
def absolute_arc(rx, ry, deg_from_horizontal, large_arc_flag, sweep_flag, x, y): start = self.end end = Vector(x, y) rotation_rad = math.radians(deg_from_horizontal) max_angle = 2 * math.pi rotation_rad = formulas.mod_constrain(rotation_rad, -max_angle, max_angle) # Find and select one of the two possible eclipse centers by undoing the rotation (to simplify the math) and # then re-applying it. rotated_primed_values = ( start - end ) / 2 # Find the primed_values of the start and the end points. primed_values = formulas.rotate( rotated_primed_values, -rotation_rad, True) # Undo the ellipse's rotation. px, py = primed_values.x, primed_values.y # Correct out-of-range radii # ToDo investigate buggy behaviour when sweep angle > 180 deg rx = abs(rx) ry = abs(ry) if rx <= TOLERANCES['operation'] or ry <= TOLERANCES['operation']: return absolute_line(x, y) delta = px**2 / rx**2 + py**2 / ry**2 if delta > 1: rx *= math.sqrt(delta) ry *= math.sqrt(delta) if math.sqrt(delta) > 1: center = Vector(0, 0) else: radicant = ((rx * ry)**2 - (rx * py)**2 - (ry * px)**2) / ((rx * py)**2 + (ry * px)**2) # Find center using w3.org's formula center = math.sqrt(radicant) * Vector( (rx * py) / ry, -(ry * px) / rx) center *= -1 if large_arc_flag == sweep_flag else 1 # Select one of the two solutions based on flags rotated_center = formulas.rotate( center, rotation_rad, False) + (start + end) / 2 # re-apply the rotation cx, cy = center.x, center.y u = Vector((px - cx) / rx, (py - cy) / ry) v = Vector((-px - cx) / rx, (-py - cy) / ry) start_angle = formulas.angle_between_vectors(Vector(1, 0), u) sweep_angle_unbounded = formulas.angle_between_vectors(u, v) sweep_angle = sweep_angle_unbounded % max_angle if not sweep_flag and sweep_angle_unbounded > 0: sweep_angle -= max_angle if sweep_flag and sweep_angle_unbounded < 0: sweep_angle += max_angle transformed_center = self._transform_coordinate_system( rotated_center) sweep_angle *= -1 if self.do_vertical_mirror else 1 start_angle *= 1 if self.do_vertical_mirror else 1 arc = EllipticalArc(transformed_center, Vector(rx, ry), rotation_rad, start_angle, sweep_angle) self.end = Vector(x, y) return arc
def angle_to_point(self, rad): at_origin = self.radius * Vector(math.cos(rad), math.sin(rad)) translated = at_origin + self.center return translated
def relative_line(dx, dy): return absolute_line(*(self.end + Vector(dx, dy)))
from svg_to_gcode.geometry import Vector from svg_to_gcode.formulas import center_to_endpoint_parameterization from svg_to_gcode.formulas import endpoint_to_center_parameterization as endpoint_to_center_parameterization from svg_to_gcode import TOLERANCES def to_svg(start, end, radii, rotation, large_arc_flag, sweep_flag): return f"M {start.x} {start.y} A {radii.x} {radii.y} {rotation} {large_arc_flag} {sweep_flag} {end.x} {end.y}" # center parametrization arc = "why_not" if arc == "simple": center = Vector(100, 100) radii = Vector(20, 60) rotation = 0 start_angle = 0 sweep_angle = math.pi else: center = Vector(100, 100.0) radii = Vector(50, 50) rotation = math.radians(90) start_angle = math.radians(0) sweep_angle = math.radians(270) # end-pint parametrization start, end, large_arc_flag, sweep_flag = center_to_endpoint_parameterization(center, radii, rotation, start_angle, sweep_angle)
def absolute_move(x, y): self.end = Vector(x, y) return None
def _add_svg_curve(self, command_key: str, command_arguments: List[float]): """ Offer a representation of a curve using the geometry sub-module. Based on Mozilla Docs: https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths Each sub-method must be implemented with the following structure: def descriptive_name(*command_arguments): execute calculations and transformations, **do not modify or create any instance variables** generate curve modify instance variables return curve Alternatively a sub-method may simply call a base command. :param command_key: a character representing a specific command based on the svg standard :param command_arguments: A list containing the arguments for the current command_key """ # Only move end point def absolute_move(x, y): self.end = Vector(x, y) return None def relative_move(dx, dy): return absolute_move(*(self.end + Vector(dx, dy))) # Draw straight line def absolute_line(x, y): start = self.end end = Vector(x, y) line = Line(self._transform_coordinate_system(start), self._transform_coordinate_system(end)) self.end = end return line def relative_line(dx, dy): return absolute_line(*(self.end + Vector(dx, dy))) def absolute_horizontal_line(x): return absolute_line(x, self.end.y) def relative_horizontal_line(dx): return absolute_horizontal_line(self.end.x + dx) def absolute_vertical_line(y): return absolute_line(self.end.x, y) def relative_vertical_line(dy): return absolute_vertical_line(self.end.y + dy) def close_path(): return absolute_line(*self.start) # Draw Curves def absolute_cubic_bazier(control1_x, control1_y, control2_x, control2_y, x, y): self.start = Vector(x, y) trans_start = self._transform_coordinate_system(self.end) trans_end = self._transform_coordinate_system(Vector(x, y)) trans_control1 = self._transform_coordinate_system( Vector(control1_x, control1_y)) trans_control2 = self._transform_coordinate_system( Vector(control2_x, control2_y)) cubic_bezier = CubicBazier(trans_start, trans_end, trans_control1, trans_control2) self.last_control = Vector(control2_x, control2_y) self.end = Vector(x, y) return cubic_bezier def relative_cubic_bazier(dx1, dy1, dx2, dy2, dx, dy): return absolute_cubic_bazier(self.end.x + dx1, self.end.y + dy1, self.end.x + dx2, self.end.y + dy2, self.end.x + dx, self.end.y + dy) def absolute_cubic_bezier_extension(x2, y2, x, y): start = self.end control2 = Vector(x2, y2) end = Vector(x, y) if self.last_control: control1 = 2 * start - self.last_control bazier = absolute_cubic_bazier(*control1, *control2, *end) else: bazier = absolute_quadratic_bazier(*control2, *end) self.end = start return bazier def relative_cubic_bazier_extension(dx2, dy2, dx, dy): return absolute_cubic_bezier_extension(self.end.x + dx2, self.end.y + dy2, self.end.x + dx, self.end.y + dy) def absolute_quadratic_bazier(control1_x, control1_y, x, y): trans_end = self._transform_coordinate_system(self.end) trans_new_end = self._transform_coordinate_system(Vector(x, y)) trans_control1 = self._transform_coordinate_system( Vector(control1_x, control1_y)) quadratic_bezier = QuadraticBezier(trans_end, trans_new_end, trans_control1) self.last_control = Vector(control1_x, control1_y) self.end = Vector(x, y) return quadratic_bezier def relative_quadratic_bazier(dx1, dy1, dx, dy): return absolute_quadratic_bazier(self.end.x + dx1, self.end.y + dy1, self.end.x + dx, self.end.y + dy) def absolute_quadratic_bazier_extension(x, y): start = self.end end = Vector(x, y) if self.last_control: control = 2 * start - self.last_control bazier = absolute_quadratic_bazier(*control, *end) else: bazier = absolute_quadratic_bazier(*start, *end) self.end = end return bazier def relative_quadratic_bazier_extension(dx, dy): return absolute_quadratic_bazier_extension(self.end.x + dx, self.end.y + dy) # Generate EllipticalArc with center notation from svg endpoint notation. # Based on w3.org implementation notes. https://www.w3.org/TR/SVG2/implnote.html def absolute_arc(rx, ry, deg_from_horizontal, large_arc_flag, sweep_flag, x, y): start = self.end end = Vector(x, y) rotation_rad = math.radians(deg_from_horizontal) max_angle = 2 * math.pi rotation_rad = formulas.mod_constrain(rotation_rad, -max_angle, max_angle) # Find and select one of the two possible eclipse centers by undoing the rotation (to simplify the math) and # then re-applying it. rotated_primed_values = ( start - end ) / 2 # Find the primed_values of the start and the end points. primed_values = formulas.rotate( rotated_primed_values, -rotation_rad, True) # Undo the ellipse's rotation. px, py = primed_values.x, primed_values.y # Correct out-of-range radii # ToDo investigate buggy behaviour when sweep angle > 180 deg rx = abs(rx) ry = abs(ry) if rx <= TOLERANCES['operation'] or ry <= TOLERANCES['operation']: return absolute_line(x, y) delta = px**2 / rx**2 + py**2 / ry**2 if delta > 1: rx *= math.sqrt(delta) ry *= math.sqrt(delta) if math.sqrt(delta) > 1: center = Vector(0, 0) else: radicant = ((rx * ry)**2 - (rx * py)**2 - (ry * px)**2) / ((rx * py)**2 + (ry * px)**2) # Find center using w3.org's formula center = math.sqrt(radicant) * Vector( (rx * py) / ry, -(ry * px) / rx) center *= -1 if large_arc_flag == sweep_flag else 1 # Select one of the two solutions based on flags rotated_center = formulas.rotate( center, rotation_rad, False) + (start + end) / 2 # re-apply the rotation cx, cy = center.x, center.y u = Vector((px - cx) / rx, (py - cy) / ry) v = Vector((-px - cx) / rx, (-py - cy) / ry) start_angle = formulas.angle_between_vectors(Vector(1, 0), u) sweep_angle_unbounded = formulas.angle_between_vectors(u, v) sweep_angle = sweep_angle_unbounded % max_angle if not sweep_flag and sweep_angle_unbounded > 0: sweep_angle -= max_angle if sweep_flag and sweep_angle_unbounded < 0: sweep_angle += max_angle transformed_center = self._transform_coordinate_system( rotated_center) sweep_angle *= -1 if self.do_vertical_mirror else 1 start_angle *= 1 if self.do_vertical_mirror else 1 arc = EllipticalArc(transformed_center, Vector(rx, ry), rotation_rad, start_angle, sweep_angle) self.end = Vector(x, y) return arc def relative_arc(rx, ry, deg_from_horizontal, large_arc_flag, sweep_flag, dx, dy): return absolute_arc(rx, ry, deg_from_horizontal, large_arc_flag, sweep_flag, self.end.x + dx, self.end.x + dy) command_methods = { # Only move end point 'M': absolute_move, 'm': relative_move, # Draw straight line 'L': absolute_line, 'l': relative_line, 'H': absolute_horizontal_line, 'h': relative_horizontal_line, 'V': absolute_vertical_line, 'v': relative_vertical_line, 'Z': close_path, 'z': close_path, # Draw bazier curves 'C': absolute_cubic_bazier, 'c': relative_cubic_bazier, 'S': absolute_cubic_bezier_extension, 's': relative_cubic_bazier_extension, 'Q': absolute_quadratic_bazier, 'q': relative_quadratic_bazier, 'T': absolute_quadratic_bazier_extension, 't': relative_quadratic_bazier_extension, # Draw elliptical arcs 'A': absolute_arc, 'a': relative_arc } try: curve = command_methods[command_key](*command_arguments) except TypeError as type_error: warnings.warn( f"Mis-formed input. Skipping command {command_key, command_arguments} because it caused the " f"following error: \n{type_error}") except ValueError as value_error: warnings.warn( f"Impossible geometry. Skipping curve {command_key, command_arguments} because it caused the " f"following value error:\n{value_error}") else: if curve is not None: self.curves.append(curve) if self.start is None: self.start = Vector(*self.end) if verbose: print(f"{command_key}{tuple(command_arguments)} -> {curve}")