Source code for tcnc

#!/usr/bin/env python
#-----------------------------------------------------------------------------
# Copyright 2012-2016 Claude Zervas
# email: claude@utlco.com
#-----------------------------------------------------------------------------
"""
An Inkscape extension that will output G-code from
selected paths. The G-code is suitable for a CNC machine
that has a tangential tool (ie a knife or a brush).
"""
# Python 3 compatibility boilerplate
from __future__ import (absolute_import, division, unicode_literals)
# Uncomment this if any of these builtins are used.
# from future_builtins import (ascii, filter, hex, map, oct, zip)

import os
import io
import gettext
import fnmatch
import math

# For performance measuring and debugging
import timeit
from datetime import timedelta
import logging

import geom.debug

from geom import transform2d

from cam import gcode
from cam import paintcam
from cam import gcodesvg
from cam.output import create_pathname

from svg import geomsvg

from inkscape import inkext
from inkscape import inksvg

__version__ = '0.2.1'

_ = gettext.gettext
logger = logging.getLogger(__name__)

[docs]class Tcnc(inkext.InkscapeExtension): """Inkscape plugin that converts selected SVG elements into gcode suitable for a four axis (XYZA) CNC machine with a tangential tool, such as a knife or a brush, that rotates about the Z axis. """ OPTIONSPEC = ( inkext.ExtOption('--origin-ref', default='doc', help=_('Lower left origin reference.')), inkext.ExtOption('--path-sort-method', default='none', help=_('Path sorting method.')), inkext.ExtOption('--biarc-tolerance', type='docunits', default=0.01, help=_('Biarc approximation fitting tolerance.')), inkext.ExtOption('--biarc-max-depth', type='int', default=4, help=_('Biarc approximation maximum curve ' 'splitting recursion depth.')), inkext.ExtOption('--line-flatness', type='docunits', default=0.001, help=_('Curve to line flatness.')), inkext.ExtOption('--min-arc-radius', type='degrees', default=0.01, help=_('All arcs having radius less than minimum ' 'will be considered as straight line.')), inkext.ExtOption('--tolerance', type='float', default=0.00001, help=_('Tolerance')), inkext.ExtOption('--gcode-units', default='in', help=_('G code output units (inch or mm).')), inkext.ExtOption('--xy-feed', type='float', default=10.0, help=_('XY axis feed rate in unit/m')), inkext.ExtOption('--z-feed', type='float', default=10.0, help=_('Z axis feed rate in unit/m')), inkext.ExtOption('--a-feed', type='float', default=60.0, help=_('A axis feed rate in deg/m')), inkext.ExtOption('--z-safe', type='float', default=1.0, help=_('Z axis safe height for rapid moves')), inkext.ExtOption('--z-wait', type='float', default=500, help=_('Z axis wait (milliseconds)')), inkext.ExtOption('--blend-mode', default='', help=_('Trajectory blending mode.')), inkext.ExtOption('--blend-tolerance', type='float', default='0', help=_('Trajectory blending tolerance.')), inkext.ExtOption('--disable-tangent', type='inkbool', default=False, help=_('Disable tangent rotation')), inkext.ExtOption('--z-depth', type='float', default=-0.125, help=_('Z full depth of cut')), inkext.ExtOption('--z-step', type='float', default=-0.125, help=_('Z cutting step depth')), inkext.ExtOption('--tool-width', type='docunits', default=1.0, help=_('Tool width')), inkext.ExtOption('--a-feed-match', type='inkbool', default=False, help=_('A axis feed rate match XY feed')), inkext.ExtOption('--tool-trail-offset', type='docunits', default=0.25, help=_('Tool trail offset')), inkext.ExtOption('--a-offset', type='degrees', default=0, help=_('Tool offset angle')), inkext.ExtOption('--allow-tool-reversal', type='inkbool', default=False, help=_('Allow tool reversal')), inkext.ExtOption('--tool-wait', type='float', default=0, help=_('Tool up/down wait time in seconds')), inkext.ExtOption('--spindle-mode', default='', help=_('Spindle startup mode.')), inkext.ExtOption('--spindle-speed', type='int', default=0, help=_('Spindle RPM')), inkext.ExtOption('--spindle-wait-on', type='float', default=0, help=_('Spindle warmup delay')), inkext.ExtOption('--spindle-clockwise', type='inkbool', default=True, help=_('Clockwise spindle rotation')), inkext.ExtOption('--skip-path-count', type='int', default=0, help=_('Number of paths to skip.')), inkext.ExtOption('--ignore-segment-angle', type='inkbool', default=False, help=_('Ignore segment start angle.')), inkext.ExtOption('--path-tool-fillet', type='inkbool', default=False, help=_('Fillet paths for tool width')), inkext.ExtOption('--path-tool-offset', type='inkbool', default=False, help=_('Offset paths for tool trail offset')), inkext.ExtOption('--path-preserve-g1', type='inkbool', default=False, help=_('Preserve G1 continuity for offset arcs')), inkext.ExtOption('--path-smooth-fillet', type='inkbool', default=False, help=_('Fillets at sharp corners')), inkext.ExtOption('--path-smooth-radius', type='docunits', default=0.0, help=_('Smoothing radius')), inkext.ExtOption('--path-close-polygons', type='inkbool', default=False, help=_('Close polygons with fillet')), inkext.ExtOption('--path-split-cusps', type='inkbool', default=False, help=_('Split paths at non-tangent control points')), # inkext.ExtOption('--brush-flip-stroke', type='inkbool', default=False, # help=_('Flip brush before every stroke.')), # inkext.ExtOption('--brush-flip-path', type='inkbool', default=False, # help=_('Flip after each path.')), # inkext.ExtOption('--brush-flip-reload', type='inkbool', default=False, # help=_('Flip before reload.')), inkext.ExtOption('--brush-reload-enable', type='inkbool', default=False, help=_('Enable brush reload.')), inkext.ExtOption('--brush-reload-rotate', type='inkbool', default=False, help=_('Rotate brush before reload.')), inkext.ExtOption('--brush-pause-mode', default='', help=_('Brush reload pause mode.')), inkext.ExtOption('--brush-reload-max-paths', type='int', default=1, help=_('Number of paths between reload.')), inkext.ExtOption('--brush-reload-dwell', type='float', default=0.0, help=_('Brush reload time (seconds).')), inkext.ExtOption('--brush-reload-angle', type='degrees', default=90.0, help=_('Brush reload angle (degrees).')), inkext.ExtOption('--brush-overshoot-mode', default='', help=_('Brush overshoot mode.')), inkext.ExtOption('--brush-overshoot-distance', type='docunits', default=0.0, help=_('Brush overshoot distance.')), inkext.ExtOption('--brush-soft-landing', type='inkbool', default=False, help=_('Enable soft landing.')), inkext.ExtOption('--brush-landing-strip', type='docunits', default=0.0, help=_('Landing strip distance.')), inkext.ExtOption('--brushstroke-max', type='docunits', default=0.0, help=_('Max brushstroke distance before reload.')), inkext.ExtOption('--output-path', default='~/output.ngc', help=_('Output path name')), inkext.ExtOption('--append-suffix', type='inkbool', default=False, help=_('Append auto-incremented numeric' ' suffix to filename')), inkext.ExtOption('--separate-layers', type='inkbool', default=False, help=_('Separate gcode file per layer')), inkext.ExtOption('--preview-toolmarks', type='inkbool', default=False, help=_('Show tangent tool preview.')), inkext.ExtOption('--preview-toolmarks-outline', type='inkbool', default=False, help=_('Show tangent tool preview outline.')), inkext.ExtOption('--preview-scale', default='medium', help=_('Preview scale.')), inkext.ExtOption('--write-settings', type='inkbool', default=False, help=_('Write Tcnc command line options in header.')), inkext.ExtOption('--x-subpath-render', type='inkbool', default=False, help=_('Render subpaths')), inkext.ExtOption('--x-subpath-offset', type='docunits', default=0.0, help=_('Subpath spacing')), inkext.ExtOption('--x-subpath-smoothness', type='float', default=0.0, help=_('Subpath smoothness')), inkext.ExtOption('--x-subpath-layer', default='subpaths (tcnc)', help=_('Subpath layer name')), ) # Document units that can be expressed as imperial (inches) _IMPERIAL_UNITS = ('in', 'ft', 'yd', 'pc', 'pt', 'px') # Document units that can be expressed as metric (mm) _METRIC_UNITS = ('mm', 'cm', 'm', 'km') _DEFAULT_DIR = '~' _DEFAULT_FILEROOT = 'output' _DEFAULT_FILEEXT = '.ngc'
[docs] def run(self): """Main entry point for Inkscape plugins. """ # Initialize the geometry module with tolerances and debug output geom.set_epsilon(self.options.tolerance) geom.debug.set_svg_context(self.debug_svg) # Create a transform to flip the Y axis. page_height = self.svg.get_document_size()[1] flip_transform = transform2d.matrix_scale_translate(1.0, -1.0, 0.0, page_height) timer_start = timeit.default_timer() # skip_layers = (gcodesvg.SVGPreviewPlotter.PATH_LAYER_NAME, # gcodesvg.SVGPreviewPlotter.TOOL_LAYER_NAME) skip_layers = ['tcnc .*'] # Get a list of selected SVG shape elements and their transforms svg_elements = self.svg.get_shape_elements(self.get_elements(), skip_layers=skip_layers) if not svg_elements: # Nothing selected or document is empty return # Convert SVG elements to path geometry path_list = geomsvg.svg_to_geometry(svg_elements, flip_transform) # Create the output file path name filepath = create_pathname( self.options.output_path, append_suffix=self.options.append_suffix) try: with io.open(filepath, 'w') as output: gcgen = self._init_gcode(output) cam = self._init_cam(gcgen) cam.generate_gcode(path_list) except IOError as error: self.errormsg(str(error)) timer_end = timeit.default_timer() total_time = timer_end - timer_start logger.info('Tcnc time: %s', str(timedelta(seconds=total_time)))
def _init_gcode(self, output): """Create and initialize the G code generator with machine details. """ if self.options.a_feed_match: # This option sets the angular feed rate of the A axis so # that the outside edge of the brush matches the linear feed # rate of the XY axes when doing a simple rotation. # TODO: verify correctness here angular_rate = self.options.xy_feed / self.options.tool_width / 2 self.options.a_feed = math.degrees(angular_rate) # Create G-code preview plotter. preview_svg_context = inksvg.InkscapeSVGContext(self.svg.document) preview_plotter = gcodesvg.SVGPreviewPlotter( preview_svg_context, tool_width=self.options.tool_width, tool_offset=self.options.tool_trail_offset, style_scale=self.options.preview_scale, show_toolmarks=self.options.preview_toolmarks, show_tm_outline=self.options.preview_toolmarks_outline) # Experimental options preview_plotter.x_subpath_render = self.options.x_subpath_render preview_plotter.x_subpath_layer_name = self.options.x_subpath_layer preview_plotter.x_subpath_offset = self.options.x_subpath_offset preview_plotter.x_subpath_smoothness = self.options.x_subpath_smoothness # Create G-code generator. gcgen = gcode.GCodeGenerator(xyfeed=self.options.xy_feed, zsafe=self.options.z_safe, zfeed=self.options.z_feed, afeed=self.options.a_feed, plotter=preview_plotter, output=output) gcgen.add_header_comment(('Generated by TCNC Version %s' % __version__, '',)) # Show option settings in header if self.options.write_settings: gcgen.add_header_comment('Settings:') option_dict = vars(self.options) for option in self.OPTIONSPEC: val = option_dict.get(option.dest) if val is not None: if val == None or val == option.default: # Skip default settings... continue # valstr = '%s (default)' % str(val) else: valstr = str(val) optname = option.dest.replace('_', '-') gcgen.add_header_comment('--%s = %s' % (optname, valstr)) # This will be 'doc', 'in', or 'mm' units = self.options.gcode_units doc_units = self.svg.get_document_units() if units == 'doc': if doc_units != 'in' and doc_units != 'mm': # Determine if the units are metric or imperial. # Pica and pixel units are considered imperial for now... if doc_units in self._IMPERIAL_UNITS: units = 'in' elif doc_units in self._METRIC_UNITS: units = 'mm' else: self.errormsg(_('Document units must be imperial or metric.')) raise Exception() else: units = doc_units unit_scale = self.svg.uu2unit('1.0', to_unit=units) gcgen.set_units(units, unit_scale) # logger = logging.getLogger(__name__) # logger.debug('doc units: %s' % doc_units) # logger.debug('view_scale: %f' % self.svg.view_scale) # logger.debug('unit_scale: %f' % unit_scale) # gcgen.set_tolerance(geom.const.EPSILON) # gcgen.set_output_precision(geom.const.EPSILON_PRECISION) gcgen.set_tolerance(self.options.tolerance) precision = max(0, int(round(abs(math.log(self.options.tolerance, 10))))) gcgen.set_output_precision(precision) if self.options.blend_mode: gcgen.set_path_blending(self.options.blend_mode, self.options.blend_tolerance) gcgen.spindle_speed = self.options.spindle_speed gcgen.spindle_wait_on = self.options.spindle_wait_on * 1000 gcgen.spindle_clockwise = self.options.spindle_clockwise gcgen.spindle_auto = (self.options.spindle_mode == 'path') gcgen.tool_wait_down = self.options.tool_wait gcgen.tool_wait_up = self.options.tool_wait return gcgen def _init_cam(self, gc): """Create and initialize the tool path generator.""" enable_tangent = not self.options.disable_tangent cam = paintcam.PaintCAM(gc) cam.debug_svg = self.debug_svg cam.z_depth = self.options.z_depth cam.z_step = max(-(abs(self.options.z_step)), cam.z_depth) if self.options.path_sort_method != 'none': cam.path_sort_method = self.options.path_sort_method cam.tool_width = self.options.tool_width cam.tool_trail_offset = self.options.tool_trail_offset cam.biarc_tolerance = self.options.biarc_tolerance cam.biarc_max_depth = self.options.biarc_max_depth cam.line_flatness = self.options.line_flatness cam.skip_path_count = self.options.skip_path_count cam.enable_tangent = enable_tangent cam.path_tool_fillet = self.options.path_tool_fillet and enable_tangent cam.path_tool_offset = self.options.path_tool_offset and enable_tangent cam.path_preserve_g1 = self.options.path_preserve_g1 and enable_tangent cam.path_close_polygons = self.options.path_close_polygons and enable_tangent cam.path_smooth_fillet = self.options.path_smooth_fillet cam.path_smooth_radius = self.options.path_smooth_radius cam.path_split_cusps = self.options.path_split_cusps cam.allow_tool_reversal = self.options.allow_tool_reversal # cam.brush_landing_angle = self.options.brush_landing_angle # cam.brush_landing_end_height = self.options.brush_landing_end_height # cam.brush_landing_start_height = self.options.brush_landing_start_height # cam.brush_liftoff_angle = self.options.brush_liftoff_angle # cam.brush_liftoff_height = self.options.brush_liftoff_height # cam.brush_overshoot = self.options.brush_overshoot cam.brush_reload_enable = self.options.brush_reload_enable cam.brush_reload_rotate = self.options.brush_reload_rotate if self.options.brush_pause_mode in ('restart', 'time'): cam.brush_reload_pause = True if self.options.brush_pause_mode == 'time': cam.brush_reload_dwell = self.options.brush_reload_dwell else: cam.brush_reload_dwell = 0 cam.brush_reload_max_paths = self.options.brush_reload_max_paths cam.brush_reload_angle = self.options.brush_reload_angle # cam.brush_reload_after_interval = self.options.brushstroke_max > 0.0 cam.brush_depth = self.options.z_depth cam.brush_soft_landing = self.options.brush_soft_landing cam.brush_landing_strip = self.options.brush_landing_strip if self.options.brush_overshoot_mode == 'auto': cam.brush_overshoot_enable = True cam.brush_overshoot_auto = True cam.brush_overshoot_distance = cam.tool_width / 2 elif self.options.brush_overshoot_mode == 'manual': cam.brush_overshoot_enable = True cam.brush_overshoot_distance = self.options.brush_overshoot_distance # if self.options.brushstroke_max > 0.0: # cam.feed_interval = self.options.brushstroke_max return cam
# _unused_options = [ # # Unused options # # TODO: use or delete # inkext.ExtOption('--brush-landing-angle', type='degrees', default=45, help='Brushstroke landing angle.'), # inkext.ExtOption('--brush-overshoot', type='docunits', default=0.5, help='Brushstroke overshoot distance.'), # inkext.ExtOption('--brush-liftoff-height', type='float', default=0.1, help='Brushstroke liftoff height.'), # inkext.ExtOption('--brush-liftoff-angle', type='degrees', default=45, help='Brushstroke liftoff angle.'), # inkext.ExtOption('--brush-landing-start-height', type='float', default=0.1, help='Brushstroke landing start height.'), # inkext.ExtOption('--brush-landing-end-height', type='float', default=-0.2, help='Brushstroke landing end height.'), # inkext.ExtOption('--brushstroke-overlap', type='docunits', default=0.0, help='Brushstroke overlap.'), # inkext.ExtOption('--angle-tolerance', type='degrees', default=0.00001, # help='Angle tolerance'), # # inkext.ExtOption('--preview-show', type='inkbool', default=True, help='Show generated cut paths on preview layer.'), # inkext.ExtOption('--debug-biarcs', type='inkbool', default=True), # # inkext.ExtOption('--z-offset', type='float', default=0.0, help='Offset along Z'), # inkext.ExtOption('--x-offset', type='float', default=0.0, help='Offset along X'), # inkext.ExtOption('--y-offset', type='float', default=0.0, help='Offset along Y'), # inkext.ExtOption('--a-offset', type='degrees', default=0.0, help='Angular offset along rotational axis'), # inkext.ExtOption('--z-scale', type='float', default=1.0, help='Scale factor Z'), # inkext.ExtOption('--x-scale', type='float', default=1.0, help='Scale factor X'), # inkext.ExtOption('--y-scale', type='float', default=1.0, help='Scale factor Y'), # inkext.ExtOption('--a-scale', type='float', default=1.0, help='Angular scale along rotational axis'), # # inkext.ExtOption('--z-depth', type='float', default=-0.125, help='Z full depth of cut'), # inkext.ExtOption('--z-step', type='float', default=-0.125, help='Z cutting step depth'), # inkext.ExtOption('--gc-precision', type='float', default=0.0001, # help='G code precision'), # ] if __name__ == '__main__': Tcnc().main(optionspec=Tcnc.OPTIONSPEC, flip_debug_layer=True, debug_layer_name='tcnc debug')