예제 #1
0
    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

        point_transformation = Transformation()

        if self.transform_origin:
            point_transformation.add_translation(0, canvas_height)
            point_transformation.add_scale(1, -1)

        if transformation is not None:
            point_transformation.extend(transformation)

        self.point_transformation = point_transformation

        try:
            self._parse_commands(d)
        except Exception as generic_exception:
            raise generic_exception
            warnings.warn(
                f"Terminating path. The following unforeseen exception occurred: {generic_exception}"
            )
예제 #2
0
def generate_debug(approximations, svg_file_name, debug_file_name):
    tree = ElementTree()
    tree.parse(svg_file_name)

    root = tree.getroot()

    height_str = root.get("height")
    canvas_height = float(height_str) if height_str.isnumeric() else float(
        height_str[:-2])

    for path in root.iter("{%s}path" % name_space):
        path.set("fill", "none")
        path.set("stroke", "black")
        path.set("stroke-width", f"{TOLERANCES['approximation']}mm")

        style = path.get("style")
        if style and "display:none" in style:
            path.set("style", "display:none")
        elif style and ("visibility:hidden" in style
                        or "visibility:collapse" in style):
            path.set("style", "visibility:hidden")
        else:
            path.set("style", "")

    group = Element("{%s}g" % name_space)

    change_origin = Transformation()
    change_origin.add_scale(1, -1)
    change_origin.add_translation(0, -canvas_height)

    defs = debug_methods.arrow_defs()
    group.append(defs)
    for approximation in approximations:
        path = debug_methods.to_svg_path(
            approximation,
            color="red",
            stroke_width=f"{TOLERANCES['approximation']/2}mm",
            transformation=change_origin,
            draw_arrows=True)
        """
        path = Element("{%s}path" % name_space)
        path.set("d", )
        add_def = False
        path.set("fill", "none")
        """

        group.append(path)

    root.append(group)

    tree.write(debug_file_name)
예제 #3
0
    def draw_debug_traces(self, curves):
        """Traces arrows over all parsed paths"""

        root = self.document.getroot()
        origin = self.options.machine_origin
        bed_width = self.options.bed_width
        bed_height = self.options.bed_height

        height_str = root.get("height")
        canvas_height = float(height_str) if height_str.isnumeric() else float(
            height_str[:-2])

        group = etree.Element("{%s}g" % svg_name_space)
        group.set("id", "debug_traces")
        group.set("{%s}groupmode" % inkscape_name_space, "layer")
        group.set("{%s}label" % inkscape_name_space, "debug traces")

        group.append(
            etree.fromstring(
                xml_tree.tostring(
                    debug_methods.arrow_defs(
                        arrow_scale=self.options.debug_arrow_scale))))

        for curve in curves:
            approximation = LineSegmentChain.line_segment_approximation(curve)

            change_origin = Transformation()

            if origin != "top-left":
                change_origin.add_scale(1, -1)
                change_origin.add_translation(0, -canvas_height)

            if origin == "center":
                change_origin.add_translation(bed_width / 2, bed_height / 2)

            path_string = xml_tree.tostring(
                debug_methods.to_svg_path(
                    approximation,
                    color="red",
                    stroke_width=f"{self.options.debug_line_width}px",
                    transformation=change_origin,
                    draw_arrows=True))

            group.append(etree.fromstring(path_string))

        root.append(group)
예제 #4
0
class Path:
    """The Path class represents a generic svg path."""

    command_lengths = {
        'M': 2,
        'm': 2,
        'L': 2,
        'l': 2,
        'H': 1,
        'h': 1,
        'V': 1,
        'v': 1,
        'Z': 0,
        'z': 0,
        'C': 6,
        'c': 6,
        'Q': 4,
        'q': 4,
        'S': 4,
        's': 4,
        'T': 2,
        't': 2,
        'A': 7,
        'a': 7
    }

    __slots__ = "curves", "initial_point", "current_point", "last_control", "canvas_height", "draw_move", \
                "transform_origin", "transformation"

    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 __repr__(self):
        return f"Path({self.curves})"

    def _parse_commands(self, d: str):
        """Parse svg commands (stored in value of the d key) into geometric curves."""

        command_key = ''  # A character representing a specific command based on the svg standard
        command_arguments = [
        ]  # A list containing the arguments for the current command_key

        number_str = ''  # A buffer used to store numeric characters before conferring them to a number

        # Parse each character in d
        i = 0
        while i < len(d):
            character = d[i]

            is_numeric = character.isnumeric() or character in [
                '-', '.', 'e'
            ]  # Yes, "-6.2e-4" is a valid float.
            is_delimiter = character.isspace() or character in [',']
            is_command_key = character in self.command_lengths.keys()
            is_final = i == len(d) - 1

            # If the current command is complete, however the next command does not specify a new key, assume the next
            # command has the same key. This is implemented by inserting the current key before the next command and
            # restarting the loop without incrementing i
            try:
                if command_key and len(
                        command_arguments
                ) == self.command_lengths[command_key] and is_numeric:
                    duplicate = command_key
                    # If a moveto is followed by multiple pairs of coordinates, the subsequent pairs are treated as
                    # implicit lineto commands. https://www.w3.org/TR/SVG2/paths.html#PathDataMovetoCommands
                    if command_key == 'm':
                        duplicate = 'l'

                    if command_key == 'M':
                        duplicate = 'L'

                    d = d[:i] + duplicate + d[i:]
                    continue
            except KeyError as key_error:
                warnings.warn(
                    f"Unknown command key {command_key}. Skipping curve.")

            # If the character is part of a number, keep on composing it
            if is_numeric:
                number_str += character

                # if a negative number follows another number, no delimiter is required.
                # implicitly stated decimals like .6 don't require a delimiter. In either case we add a delimiter.
                negatives = not is_final and character != 'e' and d[i +
                                                                    1] == '-'
                implicit_decimals = not is_final and d[
                    i + 1] == '.' and '.' in number_str
                if negatives or implicit_decimals:
                    d = d[:i + 1] + ',' + d[i + 1:]

            # If the character is a delimiter or a command key or the last character, complete the number and save it
            # as an argument
            if is_delimiter or is_command_key or is_final:
                if number_str:
                    # In svg form '-.5' can be written as '-.5'. Python doesn't like that notation.
                    if number_str[0] == '.':
                        number_str = '0' + number_str
                    if number_str[0] == '-' and number_str[1] == '.':
                        number_str = '-0' + number_str[1:]

                    command_arguments.append(float(number_str))
                    number_str = ''

            # If it's a command key or the last character, parse the previous (now complete) command and save the letter
            # as the new command key
            if is_command_key or is_final:
                if command_key:
                    self._add_svg_curve(command_key, command_arguments)

                command_key = character
                command_arguments.clear()

            # If the last character is a command key (only useful for Z), save
            if is_command_key and is_final:
                self._add_svg_curve(command_key, command_arguments)

            i += 1

    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
        """

        # Establish a new initial point and a new current point. (multiple coordinates are parsed as lineto commands)
        def absolute_move(x, y):
            self.initial_point = Vector(x, y)
            self.current_point = Vector(x, y)
            return None

        def relative_move(dx, dy):
            return absolute_move(*(self.current_point + Vector(dx, dy)))

        # Draw straight line
        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 relative_line(dx, dy):
            return absolute_line(*(self.current_point + Vector(dx, dy)))

        def absolute_horizontal_line(x):
            return absolute_line(x, self.current_point.y)

        def relative_horizontal_line(dx):
            return absolute_horizontal_line(self.current_point.x + dx)

        def absolute_vertical_line(y):
            return absolute_line(self.current_point.x, y)

        def relative_vertical_line(dy):
            return absolute_vertical_line(self.current_point.y + dy)

        def close_path():
            return absolute_line(*self.initial_point)

        # Draw curvy curves
        def absolute_cubic_bazier(control1_x, control1_y, control2_x,
                                  control2_y, x, y):

            trans_start = self.transformation.apply_affine_transformation(
                self.current_point)
            trans_end = self.transformation.apply_affine_transformation(
                Vector(x, y))
            trans_control1 = self.transformation.apply_affine_transformation(
                Vector(control1_x, control1_y))
            trans_control2 = self.transformation.apply_affine_transformation(
                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 relative_cubic_bazier(dx1, dy1, dx2, dy2, dx, dy):
            return absolute_cubic_bazier(self.current_point.x + dx1,
                                         self.current_point.y + dy1,
                                         self.current_point.x + dx2,
                                         self.current_point.y + dy2,
                                         self.current_point.x + dx,
                                         self.current_point.y + dy)

        def absolute_cubic_bezier_extension(x2, y2, x, y):
            start = self.current_point
            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.current_point = start

            return bazier

        def relative_cubic_bazier_extension(dx2, dy2, dx, dy):
            return absolute_cubic_bezier_extension(self.current_point.x + dx2,
                                                   self.current_point.y + dy2,
                                                   self.current_point.x + dx,
                                                   self.current_point.y + dy)

        def absolute_quadratic_bazier(control1_x, control1_y, x, y):

            trans_end = self.transformation.apply_affine_transformation(
                self.current_point)
            trans_new_end = self.transformation.apply_affine_transformation(
                Vector(x, y))
            trans_control1 = self.transformation.apply_affine_transformation(
                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 relative_quadratic_bazier(dx1, dy1, dx, dy):
            return absolute_quadratic_bazier(self.current_point.x + dx1,
                                             self.current_point.y + dy1,
                                             self.current_point.x + dx,
                                             self.current_point.y + dy)

        def absolute_quadratic_bazier_extension(x, y):
            start = self.current_point
            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.current_point = end
            return bazier

        def relative_quadratic_bazier_extension(dx, dy):
            return absolute_quadratic_bazier_extension(
                self.current_point.x + dx, self.current_point.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
        # Todo transformations aren't applied correctly to elliptical arcs
        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 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.current_point.x + dx,
                                self.current_point.y + 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 verbose:
                print(f"{command_key}{tuple(command_arguments)} -> {curve}")
예제 #5
0
def generate_debug(approximations, svg_file_name, debug_file_name):
    tree = ElementTree()
    tree.parse(svg_file_name)

    root = tree.getroot()

    height_str = root.get("height")
    (canvas_height_raw, scale_factor) = _parse_length(height_str)
    canvas_height = canvas_height_raw * scale_factor

    for path in root.iter("{%s}path" % name_space):
        path.set("fill", "none")
        path.set("stroke", "black")
        path.set("stroke-width", f"{TOLERANCES['approximation']}mm")

        style = path.get("style")
        if style and "display:none" in style:
            path.set("style", "display:none")
        elif style and ("visibility:hidden" in style
                        or "visibility:collapse" in style):
            path.set("style", "visibility:hidden")
        else:
            path.set("style", "")

    group = Element("{%s}g" % name_space)

    change_origin = Transformation()
    viewbox_str = root.get("viewBox")
    if viewbox_str is None:
        # Inverse scale
        scale = 25.4 / 96.0
        change_origin.add_scale(1.0 / scale, 1.0 / scale)
    else:
        # TODO Build a more resilient parser here
        parts = re.search(
            r'([\d\.\-e]+)[,\s]+([\d\.\-e]+)[,\s]+([\d\.\-e]+)[,\s]+([\d\.\-e]+)',
            viewbox_str)
        if parts is not None:
            # TODO Can these values be anything other than numbers?  "123mm" maybe?
            #      The spec says they're "number"s, so no units, but possibly + or - or e-notation
            vb_x = float(parts[1])
            vb_y = float(parts[2])
            vb_width = float(parts[3])
            vb_height = float(parts[4])
            # TODO handle the preserveAspectRatio attribute
            # Defaults if not otherwise specified
            align = "xMidYMid"
            meet_or_slice = "meet"

            e_x = 0.0
            e_y = 0.0
            width_str = root.get("width")
            (canvas_width_raw, _) = _parse_length(width_str)
            e_width = canvas_width_raw  # use raw number
            e_height = canvas_height_raw  # use raw number

            scale_x = e_width / vb_width
            scale_y = e_height / vb_height
            if align != "none" and meet_or_slice == "meet":
                if scale_x > scale_y:
                    scale_x = scale_y
                else:
                    scale_y = scale_x
            if align != "none" and meet_or_slice == "slice":
                if scale_x < scale_y:
                    scale_x = scale_y
                else:
                    scale_y = scale_x
            # Inverse scale
            if scale_x != 1.0 or scale_y != 1.0:
                change_origin.add_scale(1.0 / scale_factor / scale_x,
                                        1.0 / scale_factor / scale_y)
            else:
                change_origin.add_scale(1.0 / scale_factor, 1.0 / scale_factor)

            # Inverse translation
            translate_x = e_x + (vb_x * scale_x)
            translate_y = e_y + (vb_y * scale_y)
            if translate_x != 0 or translate_y != 0:
                change_origin.add_translation(translate_x, translate_y)
    change_origin.add_scale(1, -1)
    change_origin.add_translation(0, -canvas_height)

    group.append(debug_methods.arrow_defs())
    for approximation in approximations:
        path = debug_methods.to_svg_path(
            approximation,
            color="red",
            stroke_width=f"{TOLERANCES['approximation']/2}mm",
            transformation=change_origin,
            draw_arrows=True)
        group.append(path)

    root.append(group)

    tree.write(debug_file_name)
예제 #6
0
    def effect(self):
        """Takes the SVG from Inkscape, generates gcode, returns the SVG after adding debug lines."""

        root = self.document.getroot()

        # Change svg_to_gcode's approximation tolerance
        TOLERANCES["approximation"] = float(
            self.options.approximation_tolerance.replace(',', '.'))

        # Construct output path
        output_path = os.path.join(self.options.directory,
                                   self.options.filename)
        if self.options.filename_suffix:
            try:
                filename, extension = output_path.split('.')
            except:
                self.msg("Error in output directory!")
                exit(1)

            n = 1
            while os.path.isfile(output_path):
                output_path = filename + str(n) + '.' + extension
                n += 1

        # Load header and footer files
        header = []
        if os.path.isfile(self.options.header_path):
            with open(self.options.header_path, 'r') as header_file:
                header = header_file.read().splitlines()
        elif self.options.header_path != os.getcwd(
        ):  # The Inkscape file selector defaults to the working directory
            self.debug(
                f"Header file does not exist at {self.options.header_path}")

        footer = []
        if os.path.isfile(self.options.footer_path):
            with open(self.options.footer_path, 'r') as footer_file:
                footer = footer_file.read().splitlines()
        elif self.options.footer_path != os.getcwd():
            self.debug(
                f"Footer file does not exist at {self.options.footer_path}")

        # Customize header/footer
        custom_interface = generate_custom_interface(
            self.options.tool_off_command, self.options.tool_power_command)
        interface_instance = custom_interface()

        if self.options.do_laser_off_start:
            header.append(interface_instance.laser_off())
        if self.options.do_laser_off_end:
            footer.append(interface_instance.laser_off())

        header.append(
            interface_instance.set_movement_speed(self.options.travel_speed))
        if self.options.do_z_axis_start:
            header.append(
                interface_instance.linear_move(z=self.options.z_axis_start))
        if self.options.move_to_origin_end:
            footer.append(interface_instance.linear_move(x=0, y=0))

        # Generate gcode
        gcode_compiler = Compiler(custom_interface,
                                  self.options.travel_speed,
                                  self.options.cutting_speed,
                                  self.options.pass_depth,
                                  dwell_time=self.options.dwell_time,
                                  custom_header=header,
                                  custom_footer=footer,
                                  unit=self.options.unit)

        transformation = Transformation()

        transformation.add_translation(self.options.horizontal_offset,
                                       self.options.vertical_offset)
        transformation.add_scale(self.options.scaling_factor)

        if self.options.machine_origin == "center":
            transformation.add_translation(-self.options.bed_width / 2,
                                           self.options.bed_height / 2)
        elif self.options.machine_origin == "top-left":
            transformation.add_translation(0, self.options.bed_height)

        curves = parse_root(root,
                            transform_origin=not self.options.invert_y_axis,
                            root_transformation=transformation,
                            canvas_height=self.options.bed_height)

        gcode_compiler.append_curves(curves)
        gcode_compiler.compile_to_file(output_path, passes=self.options.passes)

        # Draw debug lines
        self.clear_debug()
        if self.options.draw_debug:
            self.draw_debug_traces(curves)
            self.draw_unit_reference()
            self.select_non_debug_layer()

        return self.document
예제 #7
0
    def effect(self):
        """Takes the SVG from Inkscape, generates gcode, returns the SVG after adding debug lines."""

        root = self.document.getroot()

        approximation_tolerance = float(
            self.options.approximation_tolerance.replace(',', '.'))

        output_path = os.path.join(self.options.directory,
                                   self.options.filename)
        if self.options.filename_suffix:
            filename, extension = output_path.split('.')

            n = 1
            while os.path.isfile(output_path):
                output_path = filename + str(n) + '.' + extension
                n += 1

        header = None
        if self.options.header_path:
            with open(self.options.header_path, 'r') as header_file:
                header = header_file.readlines()

        footer = None
        if self.options.footer_path:
            with open(self.options.footer_path, 'r') as footer_file:
                footer = footer_file.readlines()

        # Generate gcode
        self.clear_debug()

        TOLERANCES["approximation"] = approximation_tolerance
        custom_interface = generate_custom_interface(
            self.options.laser_off_command, self.options.laser_power_command,
            self.options.laser_power_range)

        gcode_compiler = Compiler(custom_interface,
                                  self.options.travel_speed,
                                  self.options.cutting_speed,
                                  self.options.pass_depth,
                                  dwell_time=self.options.dwell_time,
                                  custom_header=header,
                                  custom_footer=footer,
                                  unit=self.options.unit)

        transformation = Transformation()

        transformation.add_translation(self.options.horizontal_offset,
                                       self.options.vertical_offset)
        transformation.add_scale(self.options.scaling_factor)

        if self.options.machine_origin == "center":
            transformation.add_translation(-self.options.bed_width / 2,
                                           self.options.bed_height / 2)

        transform_origin = True
        if self.options.machine_origin == "top-left":
            transform_origin = False

        curves = parse_root(root,
                            transform_origin=transform_origin,
                            root_transformation=transformation)

        gcode_compiler.append_curves(curves)
        gcode_compiler.compile_to_file(output_path, passes=self.options.passes)

        # Generate debug lines
        if self.options.draw_debug:
            self.draw_debug_traces(curves)
            self.draw_unit_reference()
            self.select_non_debug_layer()

        return self.document
예제 #8
0
    def effect(self):
        """Takes the SVG from Inkscape, generates gcode, returns the SVG after adding debug lines."""

        root = self.document.getroot()

        approximation_tolerance = float(
            self.options.approximation_tolerance.replace(',', '.'))

        output_path = os.path.join(self.options.directory,
                                   self.options.filename)
        if self.options.filename_suffix:
            filename, extension = output_path.split('.')

            n = 1
            while os.path.isfile(output_path):
                output_path = filename + str(n) + '.' + extension
                n += 1

        header = None
        if os.path.isfile(self.options.header_path):
            logger.debug(F"going to read{self.options.header_path}")
            with open(self.options.header_path, 'r') as header_file:
                header = header_file.read().splitlines()
                logger.debug(F"This is my header: >>>{header}<<<")
        elif self.options.header_path != os.getcwd():
            self.debug(
                f"Header file does not exist at {self.options.header_path}")

        if self.options.set_z_axis_start_pos:
            unit = "G21"
            if self.options.unit == "in":
                unit = "G20"
            temp = F"{unit};\nG1 Z{self.options.z_axis_start};"
            if header is None:
                header = [temp]
            else:
                header.append(temp)

        footer = None
        if os.path.isfile(self.options.footer_path):
            with open(self.options.footer_path, 'r') as footer_file:
                footer = footer_file.read().splitlines()
        elif self.options.footer_path != os.getcwd():
            self.debug(
                f"Footer file does not exist at {self.options.footer_path}")

        if self.options.move_to_zero_at_end:
            temp = F"M5;\nG1 F{self.options.travel_speed} X0.0 Y0.0 Z0.0;"
            if footer is None:
                footer = [temp]
            else:
                footer.append(temp)

        # Generate gcode
        self.clear_debug()

        TOLERANCES["approximation"] = approximation_tolerance
        custom_interface = generate_custom_interface(
            self.options.laser_off_command, self.options.laser_power_command,
            self.options.laser_power_range)

        gcode_compiler = Compiler(custom_interface,
                                  self.options.travel_speed,
                                  self.options.cutting_speed,
                                  self.options.pass_depth,
                                  dwell_time=self.options.dwell_time,
                                  custom_header=header,
                                  custom_footer=footer,
                                  unit=self.options.unit)

        transformation = Transformation()

        transformation.add_translation(self.options.horizontal_offset,
                                       self.options.vertical_offset)
        transformation.add_scale(self.options.scaling_factor)

        if self.options.machine_origin == "center":
            transformation.add_translation(-self.options.bed_width / 2,
                                           self.options.bed_height / 2)

        curves = parse_root(root,
                            transform_origin=not self.options.invert_y_axis,
                            root_transformation=transformation,
                            canvas_height=self.options.bed_height)

        gcode_compiler.append_curves(curves)
        gcode_compiler.compile_to_file(output_path, passes=self.options.passes)

        # Generate debug lines
        if self.options.draw_debug:
            self.draw_debug_traces(curves)
            self.draw_unit_reference()
            self.select_non_debug_layer()

        return self.document