From 1bde0699d184b05c9bec276a68312ca704099dbe Mon Sep 17 00:00:00 2001 From: Iridium IO Date: Sat, 9 Dec 2023 18:36:16 +1000 Subject: [PATCH 01/15] Add Moonraker API Config Added Moonraker config, and separated connection settings (Moonraker, Serial) to its own tab --- gcodeplot.inx | 55 ++++++++++++++++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 20 deletions(-) diff --git a/gcodeplot.inx b/gcodeplot.inx index 6c4cea9..7028dde 100644 --- a/gcodeplot.inx +++ b/gcodeplot.inx @@ -1,6 +1,6 @@ - <_name>GcodePlot + GcodePlot mobi.omegacentauri.gcodeplot org.inkscape.output.svg.inkscape gcodeplot.py @@ -22,23 +22,7 @@ 40 35 5 - - - 115200 - 300 - 600 - 1200 - 2400 - 4800 - 9600 - 14400 - 19200 - 28800 - 38400 - 56000 - 57600 - 115200 - + @@ -86,12 +70,43 @@ + + + + + + Inkscape.gcode + false + + + + + + + + 115200 + 300 + 600 + 1200 + 2400 + 4800 + 9600 + 14400 + 19200 + 28800 + 38400 + 56000 + 57600 + 115200 + + + .gcode text/plain - <_filetypename>3-axis gcode plotter (*.gcode) - <_filetypetooltip>Export 3-axis gcode plotter file + 3-axis gcode plotter (*.gcode) + Export 3-axis gcode plotter file true - + \ No newline at end of file From 31816fb2f06bbde9e545a2b0e2bc554aa4667b68 Mon Sep 17 00:00:00 2001 From: IridiumIO Date: Fri, 29 Dec 2023 20:48:26 +1000 Subject: [PATCH 05/15] - Removed uneccesary argument creation - Separated svgTree parsing --- gcodeplot.py | 27 +++++---------------------- 1 file changed, 5 insertions(+), 22 deletions(-) diff --git a/gcodeplot.py b/gcodeplot.py index b47cc95..bcbebf4 100755 --- a/gcodeplot.py +++ b/gcodeplot.py @@ -839,19 +839,13 @@ def parse_arguments(argparser:argparse.ArgumentParser): argparser.add_argument('--boolean-shading-crosshatch', metavar='TRUE/FALSE', dest='shading_crosshatch', help=argparse.SUPPRESS) argparser.add_argument('--boolean-sort', metavar='TRUE/FALSE', dest='sort', help=argparse.SUPPRESS) - - - # First pass parsing to set values that will be used for the rest of the calculations - args,_ = argparser.parse_known_args() - - argparser.add_argument('--align', help=argparse.SUPPRESS, default=[parse_alignment(args.align_x, enumMode=True), parse_alignment(args.align_y, enumMode=True)]) - return argparser.parse_known_args() def parse_svg_file(data): try: - return (elem := ET.fromstring(data)) if 'svg' in elem.tag else None + svgTree = ET.fromstring(data) + return svgTree if 'svg' in svgTree.tag else None except: return None @@ -914,18 +908,7 @@ def parse_svg_file(data): with open(rem[0], 'r') as f: #TODO: Change this back to 'r' instead of binary if Inkscape works data = f.read() - svgTree = None - - try: - svgTree = ET.fromstring(data) - if not 'svg' in svgTree.tag: - svgTree = None - except: - svgTree = None - - if svgTree is None and 'PD' not in data and 'PU' not in data: - sys.stderr.write("Unrecognized file.\n") - exit(1) + svgTree = parse_svg_file(data) shader.setDrawingDirectionAngle(args.direction) @@ -969,8 +952,8 @@ def parse_svg_file(data): if args.hpgl_out and not args.simulation: g = emitHPGL(penData, pens=args.pens) else: - - g = emitGcode(penData, align=args.align, scalingMode=scalingMode, tolerance=args.tolerance, + align = [parse_alignment(args.align_x, enumMode=True), parse_alignment(args.align_y, enumMode=True)] + g = emitGcode(penData, align=align, scalingMode=scalingMode, tolerance=args.tolerance, plotter=plotter, gcodePause=args.gcode_pause, pens=args.pens, pauseAtStart=args.pause_at_start, simulation=args.simulation, quiet=args.quiet) if not g: From d9f71da2b1e510384a31c157470b55a11491021a Mon Sep 17 00:00:00 2001 From: IridiumIO Date: Fri, 29 Dec 2023 20:49:52 +1000 Subject: [PATCH 06/15] Update rgbFromColor() to accept the new handling of colors from Inkscape as uint32 numbers. --- svgpath/parser.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/svgpath/parser.py b/svgpath/parser.py index 2f6f867..79c6e3c 100644 --- a/svgpath/parser.py +++ b/svgpath/parser.py @@ -412,7 +412,7 @@ def sizeFromString(text): def rgbFromColor(colorName): colorName = colorName.strip().lower() - if colorName == 'none': + if colorName == 'none' or colorName == 'all' or len(colorName) == 0 or colorName == False: return None cmd = re.split(r'[\s(),]+', colorName) if cmd[0] == 'rgb': @@ -429,6 +429,9 @@ def rgbFromColor(colorName): return (int(colorName[1],16)/15., int(colorName[2],16)/15., int(colorName[3],16)/15.) else: return (int(colorName[1:3],16)/255., int(colorName[3:5],16)/255., int(colorName[5:7],16)/255.) + elif colorName.isdigit(): + hex_color = '#' + hex(int(colorName))[2:].zfill(8) + return rgbFromColor(hex_color) else: return SVG_COLORS[colorName] From 44ff30c1f519f1c00a0cd96631e529860dd0f4ee Mon Sep 17 00:00:00 2001 From: IridiumIO Date: Fri, 29 Dec 2023 20:57:51 +1000 Subject: [PATCH 07/15] Undid the previous change to use "-1" instead of "none" for null direction angle values. Now accepts "none" at the commandline again - possible that this was causing the variation in the order that shapes were being cut when using 'cut' mode. --- gcodeplot.inx | 2 +- gcodeplot.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/gcodeplot.inx b/gcodeplot.inx index e1a0bb6..b524409 100644 --- a/gcodeplot.inx +++ b/gcodeplot.inx @@ -70,7 +70,7 @@ false 60 - none + none 0 (positive x) 45 90 (positive y) diff --git a/gcodeplot.py b/gcodeplot.py index bcbebf4..8532916 100755 --- a/gcodeplot.py +++ b/gcodeplot.py @@ -807,7 +807,7 @@ def parse_arguments(argparser:argparse.ArgumentParser): argparser.add_argument('-L', '--stroke-all', action=argparse.BooleanOptionalAction, default=False, help='stroke even regions specified by SVG to have no stroke') argparser.add_argument('-O', '--shading-avoid-outline', action=argparse.BooleanOptionalAction, default=False, help='avoid going over outline twice when shading') #?Unused - argparser.add_argument('-e', '--direction', metavar='ANGLE', default=None, type=float, help='for slanted pens: prefer to draw in given direction (degrees; 0=positive x, 90=positive y, -1=no preferred direction) [default none]') + argparser.add_argument('-e', '--direction', metavar='ANGLE', default=None, type=lambda value: None if value.lower() == 'none' else float(value), help='for slanted pens: prefer to draw in given direction (degrees; 0=positive x, 90=positive y, none=no preferred direction) [default none]') argparser.add_argument('-o', '--optimization-time', metavar='T', default=60, type=int, help='max time to spend optimizing (seconds; set to 0 to turn off optimization) [default 60]') argparser.add_argument('-d', '--sort', action=argparse.BooleanOptionalAction, default=False, help='sort paths from inside to outside for cutting [default off]') @@ -859,8 +859,7 @@ def parse_svg_file(data): sendAndSave = args.send_and_save is not None scalingMode = parse_alignment(args.scale, enumMode=True) optimizationTime = 0 if args.sort else args.optimization_time - sortPaths = False if optimizationTime > 0 else args.sort - # directionAngle = args.direction #TODO: go back to the argument and replace "-1" with "none" using type=lamba allocation + sortPaths = False if optimizationTime > 0 else args.sort plotter = Plotter(xyMin=tuple((args.min_x if args.min_x is not None else args.area[0], args.min_y if args.min_y is not None else args.area[1])), xyMax=tuple((args.max_x if args.max_x is not None else args.area[2], args.max_y if args.max_y is not None else args.area[3])), From d7768599e3dfbe04f818cd11b759a8a73ab586d3 Mon Sep 17 00:00:00 2001 From: IridiumIO Date: Fri, 29 Dec 2023 21:24:03 +1000 Subject: [PATCH 08/15] Split penData creation into separate function --- gcodeplot.py | 115 +++++++++++++++++++++++++++------------------------ 1 file changed, 60 insertions(+), 55 deletions(-) diff --git a/gcodeplot.py b/gcodeplot.py index 8532916..3641d7c 100755 --- a/gcodeplot.py +++ b/gcodeplot.py @@ -850,10 +850,49 @@ def parse_svg_file(data): return None + +def generate_pen_data(svgTree, data, args, sortPaths, optimizationTime, shader:Shader): + penData = None + + if svgTree is not None: + penData = parseSVG(svgTree, tolerance=args.tolerance, shader=shader, strokeAll=args.stroke_all, pens=args.pens, extractColor=args.extract_color if args.extract_color is not False else None) + else: + penData = parseHPGL(data, dpi=args.input_dpi) + + penData = removePenBob(penData) + + if args.deduplicate: + penData = dedup(penData) + + if sortPaths and penData: + penData = {pen: safeSorted(paths, comparison=comparePaths) for pen, paths in penData.items()} + penData = removePenBob(penData) + + if optimizationTime > 0. and args.direction is None and penData: + penData = {pen: anneal.optimize(paths, timeout=optimizationTime/2., quiet=args.quiet) for pen, paths in penData.items()} + penData = removePenBob(penData) + + if (args.tool_offset > 0. or args.overcut > 0.) and penData: + if args.scalingMode != SCALE_NONE: + sys.stderr.write("Scaling with tool-offset > 0 will produce unpredictable results.\n") + op = OffsetProcessor(toolOffset=args.tool_offset, overcut=args.overcut, tolerance=args.tolerance) + penData = {pen: op.processPath(paths) for pen, paths in penData.items()} + + if args.direction is not None and penData: + penData = {pen: directionalize(paths, args.direction) for pen, paths in penData.items()} + penData = removePenBob(penData) + + if len(penData) > 1 and penData: + sys.stderr.write("Uses the following pens:\n") + for pen in sorted(penData): + sys.stderr.write(describePen(args.pens, pen)+"\n") + + + if __name__ == '__main__': argparser = argparse.ArgumentParser(prog='Gcode Plot', description='test', fromfile_prefix_chars='$', epilog="You can load options from a text file by passing the filename prefixed with a '$' e.g. [python gcodeplot.py $'args.txt']", formatter_class=argparse.ArgumentDefaultsHelpFormatter) - args, rem = parse_arguments(argparser) + args, positional = parse_arguments(argparser) sendPort = args.send if args.send is not None else args.send_and_save sendAndSave = args.send_and_save is not None @@ -862,25 +901,25 @@ def parse_svg_file(data): sortPaths = False if optimizationTime > 0 else args.sort plotter = Plotter(xyMin=tuple((args.min_x if args.min_x is not None else args.area[0], args.min_y if args.min_y is not None else args.area[1])), - xyMax=tuple((args.max_x if args.max_x is not None else args.area[2], args.max_y if args.max_y is not None else args.area[3])), - drawSpeed=args.pen_down_speed, - moveSpeed=args.pen_up_speed, - zSpeed=args.z_speed, - workZ=args.work_z, - liftDeltaZ=args.lift_delta_z, - safeDeltaZ=args.safe_delta_z, - liftCommand=args.lift_command, - safeLiftCommand=None, - downCommand=args.down_command, - initCode=args.init_code, - endCode=args.end_code, - comment=args.comment_delimiters) + xyMax=tuple((args.max_x if args.max_x is not None else args.area[2], args.max_y if args.max_y is not None else args.area[3])), + drawSpeed=args.pen_down_speed, + moveSpeed=args.pen_up_speed, + zSpeed=args.z_speed, + workZ=args.work_z, + liftDeltaZ=args.lift_delta_z, + safeDeltaZ=args.safe_delta_z, + liftCommand=args.lift_command, + safeLiftCommand=None, + downCommand=args.down_command, + initCode=args.init_code, + endCode=args.end_code, + comment=args.comment_delimiters) shader = Shader(unshadedThreshold=args.shading_threshold, - lightestSpacing=args.shading_lightest, - darkestSpacing=args.shading_darkest, - angle=args.shading_angle, - crossHatch=args.shading_crosshatch) + lightestSpacing=args.shading_lightest, + darkestSpacing=args.shading_darkest, + angle=args.shading_angle, + crossHatch=args.shading_crosshatch) if args.tool_mode == 'cut': shader.unshadedThreshold = 0 @@ -893,7 +932,7 @@ def parse_svg_file(data): plotter.updateVariables() - if len(rem) == 0: + if len(positional) == 0: if not args.pause_at_start: argparser.print_help() if sendPort is None: @@ -904,49 +943,15 @@ def parse_svg_file(data): sendgcode.sendGcode(port=sendPort, speed=args.send_speed, commands=gcodeHeader(plotter) + [args.gcode_pause], gcodePause=args.gcode_pause, variables=plotter.variables, formulas=plotter.formulas) sys.exit(0) - with open(rem[0], 'r') as f: #TODO: Change this back to 'r' instead of binary if Inkscape works + with open(positional[0], 'r') as f: data = f.read() svgTree = parse_svg_file(data) shader.setDrawingDirectionAngle(args.direction) - if svgTree is not None: - penData = parseSVG(svgTree, tolerance=args.tolerance, shader=shader, strokeAll=args.stroke_all, pens=args.pens, extractColor=args.extract_color if args.extract_color != False else None) - else: - penData = parseHPGL(data, dpi=args.input_dpi) - penData = removePenBob(penData) - - if args.deduplicate: - penData = dedup(penData) - - if sortPaths: - for pen in penData: - penData[pen] = safeSorted(penData[pen], comparison=comparePaths) - penData = removePenBob(penData) - - if optimizationTime > 0. and args.direction is None: - for pen in penData: - penData[pen] = anneal.optimize(penData[pen], timeout=optimizationTime/2., quiet=args.quiet) - penData = removePenBob(penData) - - if args.tool_offset > 0. or args.overcut > 0.: - if scalingMode != SCALE_NONE: - sys.stderr.write("Scaling with tool-offset > 0 will produce unpredictable results.\n") - op = OffsetProcessor(toolOffset=args.tool_offset, overcut=args.overcut, tolerance=args.tolerance) - for pen in penData: - penData[pen] = op.processPath(penData[pen]) - + penData = generate_pen_data(svgTree, data, args, sortPaths, optimizationTime, shader) - if args.direction is not None: - for pen in penData: - penData[pen] = directionalize(penData[pen], args.direction) - penData = removePenBob(penData) - - if len(penData) > 1: - sys.stderr.write("Uses the following pens:\n") - for pen in sorted(penData): - sys.stderr.write(describePen(args.pens, pen)+"\n") if args.hpgl_out and not args.simulation: g = emitHPGL(penData, pens=args.pens) From dd33b3ee6d0272b7ee321250487c288973ade063 Mon Sep 17 00:00:00 2001 From: IridiumIO Date: Fri, 29 Dec 2023 23:13:07 +1000 Subject: [PATCH 09/15] Add missing return in generate_pen_data Clean up imports --- gcodeplot.py | 48 ++++++++++++++++++++++-------------------------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/gcodeplot.py b/gcodeplot.py index 3641d7c..2ef8e4a 100755 --- a/gcodeplot.py +++ b/gcodeplot.py @@ -1,20 +1,21 @@ #!/usr/bin/python from __future__ import print_function +from gcodeplotutils.evaluate import evaluate +from gcodeplotutils.processoffset import OffsetProcessor from pathlib import Path -import re -import sys -import math -import xml.etree.ElementTree as ET -import gcodeplotutils.anneal as anneal -import svgpath.parser as parser -import cmath -import requests -import io from random import sample from svgpath.shader import Shader -from gcodeplotutils.processoffset import OffsetProcessor -from gcodeplotutils.evaluate import evaluate import argparse +import cmath +import gcodeplotutils.anneal as anneal +import gcodeplotutils.sendgcode as sendgcode +import io +import math +import re +import requests +import svgpath.parser as parser +import sys +import xml.etree.ElementTree as ET SCALE_NONE = 0 SCALE_DOWN_ONLY = 1 @@ -851,8 +852,8 @@ def parse_svg_file(data): -def generate_pen_data(svgTree, data, args, sortPaths, optimizationTime, shader:Shader): - penData = None +def generate_pen_data(svgTree, data, args, sortPaths, optimizationTime, scalingMode, shader:Shader): + penData = {} if svgTree is not None: penData = parseSVG(svgTree, tolerance=args.tolerance, shader=shader, strokeAll=args.stroke_all, pens=args.pens, extractColor=args.extract_color if args.extract_color is not False else None) @@ -873,7 +874,7 @@ def generate_pen_data(svgTree, data, args, sortPaths, optimizationTime, shader:S penData = removePenBob(penData) if (args.tool_offset > 0. or args.overcut > 0.) and penData: - if args.scalingMode != SCALE_NONE: + if scalingMode != SCALE_NONE: sys.stderr.write("Scaling with tool-offset > 0 will produce unpredictable results.\n") op = OffsetProcessor(toolOffset=args.tool_offset, overcut=args.overcut, tolerance=args.tolerance) penData = {pen: op.processPath(paths) for pen, paths in penData.items()} @@ -887,7 +888,7 @@ def generate_pen_data(svgTree, data, args, sortPaths, optimizationTime, shader:S for pen in sorted(penData): sys.stderr.write(describePen(args.pens, pen)+"\n") - + return penData if __name__ == '__main__': @@ -932,25 +933,25 @@ def generate_pen_data(svgTree, data, args, sortPaths, optimizationTime, shader:S plotter.updateVariables() + # If no file is provided on the input, assume the intent is to run the init g-code over serial. if len(positional) == 0: if not args.pause_at_start: argparser.print_help() if sendPort is None: sys.stderr.write("Need to specify --send=port to be able to pause without any file.") sys.exit(1) - import gcodeplotutils.sendgcode as sendgcode - + sendgcode.sendGcode(port=sendPort, speed=args.send_speed, commands=gcodeHeader(plotter) + [args.gcode_pause], gcodePause=args.gcode_pause, variables=plotter.variables, formulas=plotter.formulas) sys.exit(0) - + + # Otherwise, open the input file with open(positional[0], 'r') as f: data = f.read() svgTree = parse_svg_file(data) - shader.setDrawingDirectionAngle(args.direction) - penData = generate_pen_data(svgTree, data, args, sortPaths, optimizationTime, shader) + penData = generate_pen_data(svgTree, data, args, sortPaths, optimizationTime, scalingMode, shader) if args.hpgl_out and not args.simulation: @@ -964,12 +965,9 @@ def generate_pen_data(svgTree, data, args, sortPaths, optimizationTime, shader:S sys.stderr.write("No points.") sys.exit(1) - dump = True if sendPort is not None and not args.simulation: - import gcodeplotutils.sendgcode as sendgcode - dump = sendAndSave if args.hpgl_out: @@ -979,7 +977,6 @@ def generate_pen_data(svgTree, data, args, sortPaths, optimizationTime, shader:S if not dump: sys.exit(0) - if args.hpgl_out: sys.stdout.write(g) @@ -987,8 +984,7 @@ def generate_pen_data(svgTree, data, args, sortPaths, optimizationTime, shader:S filtered = '\n'.join(fixComments(plotter, g, comment=plotter.comment)) + '\n' if args.moonraker != "" and args.moonraker is not None: - moonraker = args.moonraker.strip("/") + "/server/files/upload" - + moonraker = args.moonraker.strip("/") + "/server/files/upload" virtual_file = io.BytesIO(filtered.encode('utf-8')) files = {'file': (args.moonraker_filename, virtual_file), 'print': args.moonraker_autoprint} response = requests.post(moonraker, files=files) From 2cb4dc9ab19ad53ef5ddedfce10814ee9dfb8f0b Mon Sep 17 00:00:00 2001 From: IridiumIO Date: Sat, 30 Dec 2023 00:00:10 +1000 Subject: [PATCH 10/15] Remove unecessary(?) variable assignment --- gcodeplot.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/gcodeplot.py b/gcodeplot.py index 2ef8e4a..a2e57eb 100755 --- a/gcodeplot.py +++ b/gcodeplot.py @@ -852,7 +852,7 @@ def parse_svg_file(data): -def generate_pen_data(svgTree, data, args, sortPaths, optimizationTime, scalingMode, shader:Shader): +def generate_pen_data(svgTree, data, args, scalingMode, shader:Shader): penData = {} if svgTree is not None: @@ -865,12 +865,12 @@ def generate_pen_data(svgTree, data, args, sortPaths, optimizationTime, scalingM if args.deduplicate: penData = dedup(penData) - if sortPaths and penData: + if args.sort and penData: penData = {pen: safeSorted(paths, comparison=comparePaths) for pen, paths in penData.items()} penData = removePenBob(penData) - if optimizationTime > 0. and args.direction is None and penData: - penData = {pen: anneal.optimize(paths, timeout=optimizationTime/2., quiet=args.quiet) for pen, paths in penData.items()} + if args.optimization_time > 0. and args.direction is None and penData: + penData = {pen: anneal.optimize(paths, timeout=args.optimization_time/2., quiet=args.quiet) for pen, paths in penData.items()} penData = removePenBob(penData) if (args.tool_offset > 0. or args.overcut > 0.) and penData: @@ -898,8 +898,8 @@ def generate_pen_data(svgTree, data, args, sortPaths, optimizationTime, scalingM sendPort = args.send if args.send is not None else args.send_and_save sendAndSave = args.send_and_save is not None scalingMode = parse_alignment(args.scale, enumMode=True) - optimizationTime = 0 if args.sort else args.optimization_time - sortPaths = False if optimizationTime > 0 else args.sort + args.optimization_time = 0 if args.sort else args.optimization_time + args.sort = False if args.optimization_time > 0 else args.sort plotter = Plotter(xyMin=tuple((args.min_x if args.min_x is not None else args.area[0], args.min_y if args.min_y is not None else args.area[1])), xyMax=tuple((args.max_x if args.max_x is not None else args.area[2], args.max_y if args.max_y is not None else args.area[3])), @@ -924,12 +924,12 @@ def generate_pen_data(svgTree, data, args, sortPaths, optimizationTime, scalingM if args.tool_mode == 'cut': shader.unshadedThreshold = 0 - optimizationTime = 0 - sortPaths = True + args.optimization_time = 0 + args.sort = True args.direction = None elif args.tool_mode == 'draw': args.tool_offset = 0. - sortPaths = False + args.sort = False plotter.updateVariables() @@ -951,7 +951,7 @@ def generate_pen_data(svgTree, data, args, sortPaths, optimizationTime, scalingM svgTree = parse_svg_file(data) shader.setDrawingDirectionAngle(args.direction) - penData = generate_pen_data(svgTree, data, args, sortPaths, optimizationTime, scalingMode, shader) + penData = generate_pen_data(svgTree, data, args, scalingMode, shader) if args.hpgl_out and not args.simulation: From b8016e8dc2ae9c5214081a31c29b4f96e9cdbae0 Mon Sep 17 00:00:00 2001 From: IridiumIO Date: Sat, 30 Dec 2023 02:26:35 +1000 Subject: [PATCH 11/15] Fixed shading and color extraction not working in 'draw' mode. --- .gitignore | 1 + gcodeplot.py | 4 ++-- svgpath/shader.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 5cf4994..4ed0ec4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /svgpath/__pycache__/ *.pyc +/test \ No newline at end of file diff --git a/gcodeplot.py b/gcodeplot.py index a2e57eb..211410b 100755 --- a/gcodeplot.py +++ b/gcodeplot.py @@ -836,7 +836,7 @@ def parse_arguments(argparser:argparse.ArgumentParser): argparser.add_argument('--tool-mode', metavar='MODE', choices=['custom','cut','draw'], default='custom', help=argparse.SUPPRESS) #Inkscape specific boolean parameters - argparser.add_argument('--boolean-extract-color', metavar='TRUE/FALSE', dest='extract_color', help=argparse.SUPPRESS) + argparser.add_argument('--boolean-extract-color', metavar='TRUE/FALSE', type=lambda val: True if val.lower() == 'true' else False, help=argparse.SUPPRESS) argparser.add_argument('--boolean-shading-crosshatch', metavar='TRUE/FALSE', dest='shading_crosshatch', help=argparse.SUPPRESS) argparser.add_argument('--boolean-sort', metavar='TRUE/FALSE', dest='sort', help=argparse.SUPPRESS) @@ -856,7 +856,7 @@ def generate_pen_data(svgTree, data, args, scalingMode, shader:Shader): penData = {} if svgTree is not None: - penData = parseSVG(svgTree, tolerance=args.tolerance, shader=shader, strokeAll=args.stroke_all, pens=args.pens, extractColor=args.extract_color if args.extract_color is not False else None) + penData = parseSVG(svgTree, tolerance=args.tolerance, shader=shader, strokeAll=args.stroke_all, pens=args.pens, extractColor=args.extract_color if args.boolean_extract_color else None) else: penData = parseHPGL(data, dpi=args.input_dpi) diff --git a/svgpath/shader.py b/svgpath/shader.py index a49a104..8d43717 100755 --- a/svgpath/shader.py +++ b/svgpath/shader.py @@ -11,7 +11,7 @@ def __init__(self, unshadedThreshold=1., lightestSpacing=3., darkestSpacing=0.5, self.darkestSpacing = darkestSpacing self.angle = angle self.secondaryAngle = angle + 90 - self.crossHatch = False + self.crossHatch = crossHatch def isActive(self): return self.unshadedThreshold > 0.000001 From 5f19c3f61cc8941bf9d0d0eef8b1d70e310425cf Mon Sep 17 00:00:00 2001 From: IridiumIO Date: Sat, 30 Dec 2023 21:03:18 +1000 Subject: [PATCH 12/15] - Breakout argparse functions and classes into separate file - Cleanup unecessary arguments and enable input parity with old "--no" prefixes - Create enums.py so that argparser_c.py can use it too. - Tidy up __main__ section --- .gitignore | 3 +- gcodeplot.py | 292 ++++++++++++++-------------------- gcodeplotutils/argparser_c.py | 116 ++++++++++++++ gcodeplotutils/enums.py | 9 ++ 4 files changed, 247 insertions(+), 173 deletions(-) create mode 100644 gcodeplotutils/argparser_c.py create mode 100644 gcodeplotutils/enums.py diff --git a/.gitignore b/.gitignore index 4ed0ec4..f596c91 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /svgpath/__pycache__/ *.pyc -/test \ No newline at end of file +/test +gcodeplot_orig.py \ No newline at end of file diff --git a/gcodeplot.py b/gcodeplot.py index 211410b..763a76e 100755 --- a/gcodeplot.py +++ b/gcodeplot.py @@ -16,16 +16,8 @@ import svgpath.parser as parser import sys import xml.etree.ElementTree as ET - -SCALE_NONE = 0 -SCALE_DOWN_ONLY = 1 -SCALE_FIT = 2 -ALIGN_SCALE_NONE = 0 -ALIGN_BOTTOM = 1 -ALIGN_TOP = 2 -ALIGN_LEFT = ALIGN_BOTTOM -ALIGN_RIGHT = ALIGN_TOP -ALIGN_CENTER = 3 +from gcodeplotutils.enums import * +from gcodeplotutils.argparser_c import cArgumentParser, PrintDefaultsAction, CustomBooleanAction, PenAction, parse_alignment, none_or_str class Plotter(object): def __init__(self, xyMin:tuple=(7,8), xyMax:tuple=(204,178), @@ -703,84 +695,91 @@ def fixComments(plotter, data, comment = ";"): - -class PrintDefaultsAction(argparse.Action): - def __call__(self, parser, namespace, values, option_string=None): - printed = set() - formatted_strings = [ - self.format_argument(action, namespace) - for action in parser._actions - if not isinstance(action, argparse._HelpAction) - and action.help != argparse.SUPPRESS - and (formatted := f'{action.dest}: {action.default}') not in printed and not printed.add(formatted) - ] - print('\n'.join(formatted_strings)) - # parser.exit() - - def format_argument(self, action, namespace): - - if action.dest in ('scale', 'align_x', 'align_y'): - value = parse_alignment(getattr(namespace, action.dest, action.default), reverse=True) - elif action.dest == 'extract_color' and (value := getattr(namespace, action.dest, action.default) ) == None: - value = 'all' - else: - value = getattr(namespace, action.dest, action.default) - return f'{action.dest + ":":<25}{value}' +def parse_svg_file(data): + try: + svgTree = ET.fromstring(data) + return svgTree if 'svg' in svgTree.tag else None + except: + return None -class PenAction(argparse.Action): - def __call__(self, parser, namespace, values, option_string=None): - pens = {} - pen_file = Path(values) - if pen_file.is_file(): - pens = {p.pen: p for line in open(pen_file) if (line_stripped := line.strip()) and (p := Pen(line_stripped))} - else: - parser.error(f'Invalid filename provided in {self.dest} \n') - setattr(namespace, self.dest, pens) +def generate_pen_data(svgTree, data, args, shader:Shader): + penData = {} + + if svgTree is not None: + penData = parseSVG(svgTree, tolerance=args.tolerance, shader=shader, strokeAll=args.stroke_all, pens=args.pens, extractColor=args.extract_color if args.boolean_extract_color else None) + else: + penData = parseHPGL(data, dpi=args.input_dpi) + + penData = removePenBob(penData) + + if not args.allow_repeats: + penData = dedup(penData) + + if args.sort and penData: + penData = {pen: safeSorted(paths, comparison=comparePaths) for pen, paths in penData.items()} + penData = removePenBob(penData) + + if args.optimization_time > 0. and args.direction is None and penData: + penData = {pen: anneal.optimize(paths, timeout=args.optimization_time/2., quiet=args.quiet) for pen, paths in penData.items()} + penData = removePenBob(penData) + + if (args.tool_offset > 0. or args.overcut > 0.) and penData: + if parse_alignment(args.scale, enumMode=True) != SCALE_NONE: + sys.stderr.write("Scaling with tool-offset > 0 will produce unpredictable results.\n") + op = OffsetProcessor(toolOffset=args.tool_offset, overcut=args.overcut, tolerance=args.tolerance) + penData = {pen: op.processPath(paths) for pen, paths in penData.items()} + if args.direction is not None and penData: + penData = {pen: directionalize(paths, args.direction) for pen, paths in penData.items()} + penData = removePenBob(penData) + + if len(penData) > 1 and penData: + sys.stderr.write("Uses the following pens:\n") + for pen in sorted(penData): + sys.stderr.write(describePen(args.pens, pen)+"\n") + + return penData -def parse_alignment(arg, enumMode=False, reverse=False): - verbose_mapping = {'none': 'n', 'left': 'l', 'right': 'r', 'center': 'c', 'bottom': 'b', 'top': 't', 'down': 'd', 'fit': 'f'} - enum_mapping = {'n': ALIGN_SCALE_NONE, 'l': ALIGN_LEFT, 'r': ALIGN_RIGHT, 'c': ALIGN_CENTER, 'b': ALIGN_BOTTOM, 't': ALIGN_TOP, 'd': SCALE_DOWN_ONLY, 'f': SCALE_FIT} - if enumMode: return enum_mapping.get(arg, ALIGN_SCALE_NONE) - if reverse: return next((key for key, value in verbose_mapping.items() if value == arg), None) - return verbose_mapping.get(arg.lower(), 'n') if len(arg) > 1 else arg -def none_or_str(value): - return None if value=='none' else value +def generate_HPGL_or_GCODE(penData, args, plotter): + + if args.hpgl_out and not args.simulation: + res = emitHPGL(penData, pens=args.pens) + else: + align = [parse_alignment(args.align_x, enumMode=True), parse_alignment(args.align_y, enumMode=True)] + res = emitGcode(penData, align=align, scalingMode=parse_alignment(args.scale, enumMode=True), tolerance=args.tolerance, + plotter=plotter, gcodePause=args.gcode_pause, pens=args.pens, pauseAtStart=args.pause_at_start, simulation=args.simulation, quiet=args.quiet) + + if not res: + sys.stderr.write("No points.") + sys.exit(1) + + return res -def parse_arguments(argparser:argparse.ArgumentParser): +def parse_arguments(argparser:cArgumentParser): argparser.add_argument('--dump-options', help='show current settings instead of doing anything', action=PrintDefaultsAction, nargs=0) - argparser.add_argument('-r', '--allow-repeats', dest='deduplicate', help='do not deduplicate paths',action='store_false') + argparser.add_argument('-r', '--allow-repeats', help='do not deduplicate paths', action=CustomBooleanAction, default=False) argparser.add_argument('-f', '--scale', metavar='MODE', choices=['n', 'f', 'd'], default='n', type=parse_alignment, help='scaling option: none(n), fit(f), down-only(d) [default none; other options do not work with tool-offset]') argparser.add_argument('-D', '--input-dpi', metavar='x[,y]', default=(1016., 1016.), help='hpgl dpi', type=lambda s: tuple(map(float, s.split(','))) if ',' in s else (float(s), float(s))) # returns (x,x) if only one number provided, otherwise returns (x,y) argparser.add_argument('-t', '--tolerance', metavar='x', default=0.05, type=float, help='ignore (some) deviations of x millimeters or less [default: %(default)s]') - - group_send = argparser.add_mutually_exclusive_group() - group_send.add_argument('-s', '--send', type=int, metavar='PORT', default=None, help='Send gcode to serial port instead of stdout') - group_send.add_argument('--no-send', dest='send', action='store_const', const=None, help='Set sendport to None') + argparser.add_argument('-s', '--send', metavar='PORT', default=None, action=CustomBooleanAction, help='Send gcode to serial port instead of stdout') argparser.add_argument('-S', '--send-speed', metavar='BAUD', default=115200, help='set baud rate for sending') - argparser.add_argument('--send-and-save', metavar='PORT', default=None, help=argparse.SUPPRESS) - argparser.add_argument('-x', '--align-x', metavar='MODE', choices=['n', 'l', 'r', 'c'], default='l', type=parse_alignment, help='horizontal alignment: none(n), left(l), right(r) or center(c)') argparser.add_argument('-y', '--align-y', metavar='MODE', choices=['n', 'b', 't', 'c'], default='t', type=parse_alignment, help='horizontal alignment: none(n), bottom(b), top(t) or center(c)') - - # PLOTTER INIT - argparser.add_argument('-a', '--area', metavar='x1,y1,x2,y2', default=[7, 8, 204, 178], type=lambda s: list(map(float, s.split(','))), help='gcode print area in millimeters') argparser.add_argument('--min-x', type=float, default=None, help=argparse.SUPPRESS) argparser.add_argument('--min-y', type=float, default=None, help=argparse.SUPPRESS) argparser.add_argument('--max-x', type=float, default=None, help=argparse.SUPPRESS) argparser.add_argument('--max-y', type=float, default=None, help=argparse.SUPPRESS) - argparser.add_argument('-Z', '--lift-delta-z', metavar='Z', default=2.5, type=float, help='amount to lift for pen-up (millimeters)') argparser.add_argument('-z', '--work-z', metavar='Z', default=14.5, type=float, help='z-position for drawing (millimeters)') argparser.add_argument('-V', '--pen-up-speed', metavar='S', default=40, type=float, help='speed for moving with pen up (millimeters/second)') @@ -793,114 +792,72 @@ def parse_arguments(argparser:argparse.ArgumentParser): argparser.add_argument('--init-code', metavar='GCODE', type=none_or_str, default="G00 S1; endstops|G00 E0; no extrusion|G01 S1; endstops|G01 E0; no extrusion|G21; millimeters|G91 G0 F%.1f{{zspeed*60}} Z%.3f{{safe}}; pen park !!Zsafe|G90; absolute|G28 X; home|G28 Y; home|G28 Z; home", help='gcode init commands (separate lines with |)') argparser.add_argument('--end-code', metavar='GCODE', type=none_or_str, default=None, help='Gcode to run at end of task') - argparser.add_argument('-H', '--hpgl-out', action=argparse.BooleanOptionalAction, default=False, help='output is HPGL, not gcode; most options are ignored.') + argparser.add_argument('-P', '--pens', metavar='PENFILE', default={1:Pen('1 (0.,0.) black default')}, action=PenAction, PenClass=Pen, help='read output pens from penfile') argparser.add_argument('-T', '--shading-threshold', metavar='N', default=1.0, type=float, help='darkest grayscale to leave unshaded (decimal, 0. to 1.; set to 0 to turn off SVG shading) [default 1.0]') - - argparser.add_argument('-m', '--shading-lightest', metavar='X', default=3.0, type=float, help='shading spacing for lightest colors (millimeters) [default 3.0]') argparser.add_argument('-M', '--shading-darkest', metavar='X', default=0.5, type=float, help='shading spacing for darkest color (millimeters) [default 0.5]') argparser.add_argument('-A', '--shading-angle', metavar='X', default=45, type=float, help='shading angle (degrees) [default 45]') - argparser.add_argument('-X', '--shading-crosshatch', action=argparse.BooleanOptionalAction, default=False, help='cross hatch shading') - - argparser.add_argument('-L', '--stroke-all', action=argparse.BooleanOptionalAction, default=False, help='stroke even regions specified by SVG to have no stroke') argparser.add_argument('-O', '--shading-avoid-outline', action=argparse.BooleanOptionalAction, default=False, help='avoid going over outline twice when shading') #?Unused + argparser.add_argument('-R', '--extract-color', metavar='C', default=None, type=parser.rgbFromColor, help='extract color (specified in SVG format , e.g., rgb(1,0,0) or #ff0000 or red)') + argparser.add_argument('-L', '--stroke-all', action=argparse.BooleanOptionalAction, default=False, help='stroke even regions specified by SVG to have no stroke') argparser.add_argument('-e', '--direction', metavar='ANGLE', default=None, type=lambda value: None if value.lower() == 'none' else float(value), help='for slanted pens: prefer to draw in given direction (degrees; 0=positive x, 90=positive y, none=no preferred direction) [default none]') argparser.add_argument('-o', '--optimization-time', metavar='T', default=60, type=int, help='max time to spend optimizing (seconds; set to 0 to turn off optimization) [default 60]') argparser.add_argument('-d', '--sort', action=argparse.BooleanOptionalAction, default=False, help='sort paths from inside to outside for cutting [default off]') - - - - # parser.add_argument('-c', '--config-file', metavar='$FILENAME', help='read arguments, one per line, from filename. Prepend the filename with "$" e.g. $"args.txt"') + argparser.add_argument('-w', '--gcode-pause', metavar='CMD', default='@pause', help='gcode pause command [default: @pause]') - argparser.add_argument('-P', '--pens', metavar='PENFILE', default={1:Pen('1 (0.,0.) black default')}, action=PenAction, help='read output pens from penfile') - argparser.add_argument('-U', '--pause-at-start', action=argparse.BooleanOptionalAction, default=False, help='pause at start (can be included without any input file to manually move stuff)') - argparser.add_argument('-R', '--extract-color', metavar='C', default=None, type=parser.rgbFromColor, help='extract color (specified in SVG format , e.g., rgb(1,0,0) or #ff0000 or red)') - + argparser.add_argument('--tool-mode', metavar='MODE', choices=['custom','cut','draw'], default='custom', help=argparse.SUPPRESS) argparser.add_argument('--tool-offset', metavar='X', default=0.0, type=float, help='cutting tool offset (millimeters) [default 0.0]') argparser.add_argument('--overcut', metavar='X', default=0.0, type=float, help='overcut (millimeters) [default 0.0]') - argparser.add_argument('--moonraker', metavar='URL', default=None, help='moonraker url') argparser.add_argument('--moonraker-filename', metavar='FILENAME', default='toolpath.gcode', help='name of uploaded file') argparser.add_argument('--moonraker-autoprint', metavar='TRUE/FALSE', default=False, help='whether to automatically begin the print job after upload') argparser.add_argument('--simulation', metavar='TRUE/FALSE', action=argparse.BooleanOptionalAction, default=False, help=argparse.SUPPRESS) - argparser.add_argument('--tab', dest='quiet', default=False, type=bool, help=argparse.SUPPRESS) - argparser.add_argument('--tool-mode', metavar='MODE', choices=['custom','cut','draw'], default='custom', help=argparse.SUPPRESS) #Inkscape specific boolean parameters argparser.add_argument('--boolean-extract-color', metavar='TRUE/FALSE', type=lambda val: True if val.lower() == 'true' else False, help=argparse.SUPPRESS) argparser.add_argument('--boolean-shading-crosshatch', metavar='TRUE/FALSE', dest='shading_crosshatch', help=argparse.SUPPRESS) argparser.add_argument('--boolean-sort', metavar='TRUE/FALSE', dest='sort', help=argparse.SUPPRESS) + argparser.add_argument('--send-and-save', metavar='PORT', default=False, help=argparse.SUPPRESS) #Could probably roll this into "send" and check if we're in Inkscape at the end of __main__ by using tab/quiet instead + argparser.add_argument('--tab', dest='quiet', default=False, type=bool, help=argparse.SUPPRESS) - return argparser.parse_known_args() - - -def parse_svg_file(data): - try: - svgTree = ET.fromstring(data) - return svgTree if 'svg' in svgTree.tag else None - except: - return None - - - -def generate_pen_data(svgTree, data, args, scalingMode, shader:Shader): - penData = {} + args, positional = argparser.parse_known_args() - if svgTree is not None: - penData = parseSVG(svgTree, tolerance=args.tolerance, shader=shader, strokeAll=args.stroke_all, pens=args.pens, extractColor=args.extract_color if args.boolean_extract_color else None) - else: - penData = parseHPGL(data, dpi=args.input_dpi) - - penData = removePenBob(penData) + # I probably shouldn't have done this. If a port is provided on SEND, use it, + # otherwise check if it was provided on send_and_save, otherwise set SEND to None + # If a port is provided on send_and_save, then it sets SEND to the port, then sets itself to True. + args.send = args.send if str(args.send).isdigit() else args.send_and_save if str(args.send_and_save).isdigit() else None + args.send_and_save = True if str(args.send_and_save).isdigit() else False - if args.deduplicate: - penData = dedup(penData) - - if args.sort and penData: - penData = {pen: safeSorted(paths, comparison=comparePaths) for pen, paths in penData.items()} - penData = removePenBob(penData) - - if args.optimization_time > 0. and args.direction is None and penData: - penData = {pen: anneal.optimize(paths, timeout=args.optimization_time/2., quiet=args.quiet) for pen, paths in penData.items()} - penData = removePenBob(penData) + args.optimization_time = 0 if args.sort else args.optimization_time + args.sort = False if args.optimization_time > 0 else args.sort - if (args.tool_offset > 0. or args.overcut > 0.) and penData: - if scalingMode != SCALE_NONE: - sys.stderr.write("Scaling with tool-offset > 0 will produce unpredictable results.\n") - op = OffsetProcessor(toolOffset=args.tool_offset, overcut=args.overcut, tolerance=args.tolerance) - penData = {pen: op.processPath(paths) for pen, paths in penData.items()} + if args.tool_mode == 'cut': + args.optimization_time = 0 + args.sort = True + args.direction = None + elif args.tool_mode == 'draw': + args.tool_offset = 0. + args.sort = False + + + return args, positional + - if args.direction is not None and penData: - penData = {pen: directionalize(paths, args.direction) for pen, paths in penData.items()} - penData = removePenBob(penData) - - if len(penData) > 1 and penData: - sys.stderr.write("Uses the following pens:\n") - for pen in sorted(penData): - sys.stderr.write(describePen(args.pens, pen)+"\n") - - return penData if __name__ == '__main__': - argparser = argparse.ArgumentParser(prog='Gcode Plot', description='test', fromfile_prefix_chars='$', epilog="You can load options from a text file by passing the filename prefixed with a '$' e.g. [python gcodeplot.py $'args.txt']", formatter_class=argparse.ArgumentDefaultsHelpFormatter) + argparser = cArgumentParser(prog='Gcode Plot', description='test', fromfile_prefix_chars='$', epilog="You can load options from a text file by passing the filename prefixed with a '$' e.g. [python gcodeplot.py $'args.txt']", formatter_class=argparse.ArgumentDefaultsHelpFormatter) args, positional = parse_arguments(argparser) - sendPort = args.send if args.send is not None else args.send_and_save - sendAndSave = args.send_and_save is not None - scalingMode = parse_alignment(args.scale, enumMode=True) - args.optimization_time = 0 if args.sort else args.optimization_time - args.sort = False if args.optimization_time > 0 else args.sort - plotter = Plotter(xyMin=tuple((args.min_x if args.min_x is not None else args.area[0], args.min_y if args.min_y is not None else args.area[1])), xyMax=tuple((args.max_x if args.max_x is not None else args.area[2], args.max_y if args.max_y is not None else args.area[3])), drawSpeed=args.pen_down_speed, @@ -916,73 +873,52 @@ def generate_pen_data(svgTree, data, args, scalingMode, shader:Shader): endCode=args.end_code, comment=args.comment_delimiters) - shader = Shader(unshadedThreshold=args.shading_threshold, + shader = Shader(unshadedThreshold= 0 if args.tool_mode == 'cut' else args.shading_threshold, lightestSpacing=args.shading_lightest, darkestSpacing=args.shading_darkest, angle=args.shading_angle, crossHatch=args.shading_crosshatch) - if args.tool_mode == 'cut': - shader.unshadedThreshold = 0 - args.optimization_time = 0 - args.sort = True - args.direction = None - elif args.tool_mode == 'draw': - args.tool_offset = 0. - args.sort = False - + plotter.updateVariables() - # If no file is provided on the input, assume the intent is to run the init g-code over serial. + # If no input SVG is provided on stdin, assume the intent is to just run the init g-code over serial. if len(positional) == 0: if not args.pause_at_start: argparser.print_help() - if sendPort is None: + if args.send is None: sys.stderr.write("Need to specify --send=port to be able to pause without any file.") sys.exit(1) - sendgcode.sendGcode(port=sendPort, speed=args.send_speed, commands=gcodeHeader(plotter) + [args.gcode_pause], gcodePause=args.gcode_pause, variables=plotter.variables, formulas=plotter.formulas) + sendgcode.sendGcode(port=args.send, speed=args.send_speed, commands=gcodeHeader(plotter) + [args.gcode_pause], gcodePause=args.gcode_pause, variables=plotter.variables, formulas=plotter.formulas) sys.exit(0) - # Otherwise, open the input file + # Otherwise, open the input file... with open(positional[0], 'r') as f: data = f.read() + # Gather the SVG data and generate pen data, then generate the output HPGL/GCode... + # Note the program will exit if HPGL/GCode cannot be created svgTree = parse_svg_file(data) shader.setDrawingDirectionAngle(args.direction) - - penData = generate_pen_data(svgTree, data, args, scalingMode, shader) - - - if args.hpgl_out and not args.simulation: - g = emitHPGL(penData, pens=args.pens) - else: - align = [parse_alignment(args.align_x, enumMode=True), parse_alignment(args.align_y, enumMode=True)] - g = emitGcode(penData, align=align, scalingMode=scalingMode, tolerance=args.tolerance, - plotter=plotter, gcodePause=args.gcode_pause, pens=args.pens, pauseAtStart=args.pause_at_start, simulation=args.simulation, quiet=args.quiet) - - if not g: - sys.stderr.write("No points.") - sys.exit(1) + penData = generate_pen_data(svgTree, data, args, shader) + g = generate_HPGL_or_GCODE(penData, args, plotter) + filtered = '\n'.join(fixComments(plotter, g, comment=plotter.comment)) + '\n' + # "Dump" here refers to whether the output code will be sent to stdout or not. dump = True - - if sendPort is not None and not args.simulation: - dump = sendAndSave + + # If we have a port to send to, and we're not in simulation mode, send either the GCode or HPGL over serial. + # If send_and_save is false, then it means we don't want to save the data (from Inkscape; saving is done by returning the data via stdout) + if args.send is not None and not args.simulation: + dump = args.send_and_save if args.hpgl_out: - sendgcode.sendHPGL(port=sendPort, speed=args.send_speed, commands=g) + sendgcode.sendHPGL(port=args.send, speed=args.send_speed, commands=g) else: - sendgcode.sendGcode(port=sendPort, speed=args.send_speed, commands=g, gcodePause=args.gcode_pause, plotter=plotter, variables=plotter.variables, formulas=plotter.formulas) - - if not dump: - sys.exit(0) - - if args.hpgl_out: - sys.stdout.write(g) - sys.exit(0) + sendgcode.sendGcode(port=args.send, speed=args.send_speed, commands=g, gcodePause=args.gcode_pause, plotter=plotter, variables=plotter.variables, formulas=plotter.formulas) - filtered = '\n'.join(fixComments(plotter, g, comment=plotter.comment)) + '\n' + # If we want to upload to Klipper via Moonraker if args.moonraker != "" and args.moonraker is not None: moonraker = args.moonraker.strip("/") + "/server/files/upload" virtual_file = io.BytesIO(filtered.encode('utf-8')) @@ -991,5 +927,17 @@ def generate_pen_data(svgTree, data, args, scalingMode, shader:Shader): if response.status_code != 201: sys.stderr.write(f"Error uploading file. Status code: {response.status_code}") + # If we don't want to return the file over stdout, we exit here... + if not dump: + sys.exit(0) + + # Otherwise, save the file to stdout if it's HPGL... + if args.hpgl_out: + sys.stdout.write(g) + sys.exit(0) + + # Or, if it's GCode, check if we want to send it to Moonraker in addition to saving it to stdout. + + print('\n'.join(fixComments(plotter, g, comment=plotter.comment))) \ No newline at end of file diff --git a/gcodeplotutils/argparser_c.py b/gcodeplotutils/argparser_c.py new file mode 100644 index 0000000..8061240 --- /dev/null +++ b/gcodeplotutils/argparser_c.py @@ -0,0 +1,116 @@ +#Custom argparse classes for additional function. Allows stripping '#' comments +#from files passed in, and also allows 'arg=value' format instead of 'arg value' +# +#Also allows negatable arguments; --arg=124 --arg=true --arg=false --no-arg + + +import argparse +from pathlib import Path +from .enums import * + +class cArgumentParser(argparse.ArgumentParser): + def convert_arg_line_to_args(self, arg_line): + + if arg_line.startswith("#"): + return [] + elif "=" in arg_line: + # Treat lines with "=" as if they were passed as command-line arguments + key, value = arg_line.split("=", 1) + return ['--' + key.strip(), value.strip()] + elif arg_line.startswith('no-'): + return ['--' + arg_line.strip()] + else: + return arg_line.split() + + +class CustomBooleanAction(argparse.Action): + def __init__(self,option_strings, + dest, + default=None, + required=False, + help=None, + metavar=None): + + _option_strings = [] + for option_string in option_strings: + _option_strings.append(option_string) + + if option_string.startswith('--'): + option_string = '--no-' + option_string[2:] + _option_strings.append(option_string) + + if help is not None and default is not None: + help += f" (default: {default})" + + super().__init__( + option_strings=_option_strings, + dest=dest, + nargs='?', + const=None, + default=default, + required=required, + help=help, + metavar=metavar) + + def __call__(self, parser, namespace, values, option_string=None): + if option_string and option_string.startswith('--no-'): + # Handle the case where the option is negated, e.g., --no-shading-crosshatch + setattr(namespace, self.dest, False) + elif values is None or values.lower() == 'true': + # Handle the cases where the option is provided without a value or explicitly set to 'true' + setattr(namespace, self.dest, True) + elif values.lower() == 'false': + # Handle the case where the option is explicitly set to 'false' + setattr(namespace, self.dest, False) + else: + # assign the target value to the provided input. e.g, --send=21523 + setattr(namespace, self.dest, values) + +class PrintDefaultsAction(argparse.Action): + def __call__(self, parser, namespace, values, option_string=None): + printed = set() + formatted_strings = [ + self.format_argument(action, namespace) + for action in parser._actions + if not isinstance(action, argparse._HelpAction) + and action.help != argparse.SUPPRESS + and (formatted := f'{action.dest}: {action.default}') not in printed and not printed.add(formatted) + ] + print('\n'.join(formatted_strings)) + # parser.exit() + + def format_argument(self, action, namespace): + + if action.dest in ('scale', 'align_x', 'align_y'): + value = parse_alignment(getattr(namespace, action.dest, action.default), reverse=True) + elif action.dest == 'extract_color' and (value := getattr(namespace, action.dest, action.default) ) == None: + value = 'all' + else: + value = getattr(namespace, action.dest, action.default) + return f'{action.dest + ":":<25}{value}' + + +def parse_alignment(arg, enumMode=False, reverse=False): + verbose_mapping = {'none': 'n', 'left': 'l', 'right': 'r', 'center': 'c', 'bottom': 'b', 'top': 't', 'down': 'd', 'fit': 'f'} + enum_mapping = {'n': ALIGN_SCALE_NONE, 'l': ALIGN_LEFT, 'r': ALIGN_RIGHT, 'c': ALIGN_CENTER, 'b': ALIGN_BOTTOM, 't': ALIGN_TOP, 'd': SCALE_DOWN_ONLY, 'f': SCALE_FIT} + if enumMode: return enum_mapping.get(arg, ALIGN_SCALE_NONE) + if reverse: return next((key for key, value in verbose_mapping.items() if value == arg), None) + return verbose_mapping.get(arg.lower(), 'n') if len(arg) > 1 else arg + +def none_or_str(value): + return None if value=='none' else value + + + +class PenAction(argparse.Action): + def __init__(self, PenClass, *args, **kwargs): + super().__init__(*args, **kwargs) + self.Pen = PenClass + def __call__(self, parser, namespace, values, option_string=None): + pens = {} + pen_file = Path(values) + if pen_file.is_file(): + pens = {p.pen: p for line in open(pen_file) if (line_stripped := line.strip()) and (p := self.Pen(line_stripped))} + else: + parser.error(f'Invalid filename provided in {self.dest} \n') + setattr(namespace, self.dest, pens) \ No newline at end of file diff --git a/gcodeplotutils/enums.py b/gcodeplotutils/enums.py new file mode 100644 index 0000000..44d03b4 --- /dev/null +++ b/gcodeplotutils/enums.py @@ -0,0 +1,9 @@ +SCALE_NONE = 0 +SCALE_DOWN_ONLY = 1 +SCALE_FIT = 2 +ALIGN_SCALE_NONE = 0 +ALIGN_BOTTOM = 1 +ALIGN_TOP = 2 +ALIGN_LEFT = ALIGN_BOTTOM +ALIGN_RIGHT = ALIGN_TOP +ALIGN_CENTER = 3 \ No newline at end of file From 9162ab85336de4ba771b84903e9cfcf501263032 Mon Sep 17 00:00:00 2001 From: IridiumIO Date: Sat, 30 Dec 2023 21:03:18 +1000 Subject: [PATCH 13/15] - Breakout argparse functions and classes into separate file - Cleanup unecessary arguments and enable input parity with old "--no" prefixes - Create enums.py so that argparser_c.py can use it too. - Tidy up __main__ section --- .gitignore | 3 +- gcodeplot.py | 291 ++++++++++++++-------------------- gcodeplotutils/argparser_c.py | 116 ++++++++++++++ gcodeplotutils/enums.py | 9 ++ 4 files changed, 246 insertions(+), 173 deletions(-) create mode 100644 gcodeplotutils/argparser_c.py create mode 100644 gcodeplotutils/enums.py diff --git a/.gitignore b/.gitignore index 4ed0ec4..f596c91 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /svgpath/__pycache__/ *.pyc -/test \ No newline at end of file +/test +gcodeplot_orig.py \ No newline at end of file diff --git a/gcodeplot.py b/gcodeplot.py index 211410b..43fd4c1 100755 --- a/gcodeplot.py +++ b/gcodeplot.py @@ -16,16 +16,8 @@ import svgpath.parser as parser import sys import xml.etree.ElementTree as ET - -SCALE_NONE = 0 -SCALE_DOWN_ONLY = 1 -SCALE_FIT = 2 -ALIGN_SCALE_NONE = 0 -ALIGN_BOTTOM = 1 -ALIGN_TOP = 2 -ALIGN_LEFT = ALIGN_BOTTOM -ALIGN_RIGHT = ALIGN_TOP -ALIGN_CENTER = 3 +from gcodeplotutils.enums import * +from gcodeplotutils.argparser_c import cArgumentParser, PrintDefaultsAction, CustomBooleanAction, PenAction, parse_alignment, none_or_str class Plotter(object): def __init__(self, xyMin:tuple=(7,8), xyMax:tuple=(204,178), @@ -703,84 +695,91 @@ def fixComments(plotter, data, comment = ";"): - -class PrintDefaultsAction(argparse.Action): - def __call__(self, parser, namespace, values, option_string=None): - printed = set() - formatted_strings = [ - self.format_argument(action, namespace) - for action in parser._actions - if not isinstance(action, argparse._HelpAction) - and action.help != argparse.SUPPRESS - and (formatted := f'{action.dest}: {action.default}') not in printed and not printed.add(formatted) - ] - print('\n'.join(formatted_strings)) - # parser.exit() - - def format_argument(self, action, namespace): - - if action.dest in ('scale', 'align_x', 'align_y'): - value = parse_alignment(getattr(namespace, action.dest, action.default), reverse=True) - elif action.dest == 'extract_color' and (value := getattr(namespace, action.dest, action.default) ) == None: - value = 'all' - else: - value = getattr(namespace, action.dest, action.default) - return f'{action.dest + ":":<25}{value}' +def parse_svg_file(data): + try: + svgTree = ET.fromstring(data) + return svgTree if 'svg' in svgTree.tag else None + except: + return None -class PenAction(argparse.Action): - def __call__(self, parser, namespace, values, option_string=None): - pens = {} - pen_file = Path(values) - if pen_file.is_file(): - pens = {p.pen: p for line in open(pen_file) if (line_stripped := line.strip()) and (p := Pen(line_stripped))} - else: - parser.error(f'Invalid filename provided in {self.dest} \n') - setattr(namespace, self.dest, pens) +def generate_pen_data(svgTree, data, args, shader:Shader): + penData = {} + + if svgTree is not None: + penData = parseSVG(svgTree, tolerance=args.tolerance, shader=shader, strokeAll=args.stroke_all, pens=args.pens, extractColor=args.extract_color if args.boolean_extract_color else None) + else: + penData = parseHPGL(data, dpi=args.input_dpi) + + penData = removePenBob(penData) + + if not args.allow_repeats: + penData = dedup(penData) + + if args.sort and penData: + penData = {pen: safeSorted(paths, comparison=comparePaths) for pen, paths in penData.items()} + penData = removePenBob(penData) + + if args.optimization_time > 0. and args.direction is None and penData: + penData = {pen: anneal.optimize(paths, timeout=args.optimization_time/2., quiet=args.quiet) for pen, paths in penData.items()} + penData = removePenBob(penData) + + if (args.tool_offset > 0. or args.overcut > 0.) and penData: + if parse_alignment(args.scale, enumMode=True) != SCALE_NONE: + sys.stderr.write("Scaling with tool-offset > 0 will produce unpredictable results.\n") + op = OffsetProcessor(toolOffset=args.tool_offset, overcut=args.overcut, tolerance=args.tolerance) + penData = {pen: op.processPath(paths) for pen, paths in penData.items()} + if args.direction is not None and penData: + penData = {pen: directionalize(paths, args.direction) for pen, paths in penData.items()} + penData = removePenBob(penData) + + if len(penData) > 1 and penData: + sys.stderr.write("Uses the following pens:\n") + for pen in sorted(penData): + sys.stderr.write(describePen(args.pens, pen)+"\n") + + return penData -def parse_alignment(arg, enumMode=False, reverse=False): - verbose_mapping = {'none': 'n', 'left': 'l', 'right': 'r', 'center': 'c', 'bottom': 'b', 'top': 't', 'down': 'd', 'fit': 'f'} - enum_mapping = {'n': ALIGN_SCALE_NONE, 'l': ALIGN_LEFT, 'r': ALIGN_RIGHT, 'c': ALIGN_CENTER, 'b': ALIGN_BOTTOM, 't': ALIGN_TOP, 'd': SCALE_DOWN_ONLY, 'f': SCALE_FIT} - if enumMode: return enum_mapping.get(arg, ALIGN_SCALE_NONE) - if reverse: return next((key for key, value in verbose_mapping.items() if value == arg), None) - return verbose_mapping.get(arg.lower(), 'n') if len(arg) > 1 else arg -def none_or_str(value): - return None if value=='none' else value +def generate_HPGL_or_GCODE(penData, args, plotter): + + if args.hpgl_out and not args.simulation: + res = emitHPGL(penData, pens=args.pens) + else: + align = [parse_alignment(args.align_x, enumMode=True), parse_alignment(args.align_y, enumMode=True)] + res = emitGcode(penData, align=align, scalingMode=parse_alignment(args.scale, enumMode=True), tolerance=args.tolerance, + plotter=plotter, gcodePause=args.gcode_pause, pens=args.pens, pauseAtStart=args.pause_at_start, simulation=args.simulation, quiet=args.quiet) + + if not res: + sys.stderr.write("No points.") + sys.exit(1) + + return res -def parse_arguments(argparser:argparse.ArgumentParser): +def parse_arguments(argparser:cArgumentParser): argparser.add_argument('--dump-options', help='show current settings instead of doing anything', action=PrintDefaultsAction, nargs=0) - argparser.add_argument('-r', '--allow-repeats', dest='deduplicate', help='do not deduplicate paths',action='store_false') + argparser.add_argument('-r', '--allow-repeats', help='do not deduplicate paths', action=CustomBooleanAction, default=False) argparser.add_argument('-f', '--scale', metavar='MODE', choices=['n', 'f', 'd'], default='n', type=parse_alignment, help='scaling option: none(n), fit(f), down-only(d) [default none; other options do not work with tool-offset]') argparser.add_argument('-D', '--input-dpi', metavar='x[,y]', default=(1016., 1016.), help='hpgl dpi', type=lambda s: tuple(map(float, s.split(','))) if ',' in s else (float(s), float(s))) # returns (x,x) if only one number provided, otherwise returns (x,y) argparser.add_argument('-t', '--tolerance', metavar='x', default=0.05, type=float, help='ignore (some) deviations of x millimeters or less [default: %(default)s]') - - group_send = argparser.add_mutually_exclusive_group() - group_send.add_argument('-s', '--send', type=int, metavar='PORT', default=None, help='Send gcode to serial port instead of stdout') - group_send.add_argument('--no-send', dest='send', action='store_const', const=None, help='Set sendport to None') + argparser.add_argument('-s', '--send', metavar='PORT', default=None, action=CustomBooleanAction, help='Send gcode to serial port instead of stdout') argparser.add_argument('-S', '--send-speed', metavar='BAUD', default=115200, help='set baud rate for sending') - argparser.add_argument('--send-and-save', metavar='PORT', default=None, help=argparse.SUPPRESS) - argparser.add_argument('-x', '--align-x', metavar='MODE', choices=['n', 'l', 'r', 'c'], default='l', type=parse_alignment, help='horizontal alignment: none(n), left(l), right(r) or center(c)') argparser.add_argument('-y', '--align-y', metavar='MODE', choices=['n', 'b', 't', 'c'], default='t', type=parse_alignment, help='horizontal alignment: none(n), bottom(b), top(t) or center(c)') - - # PLOTTER INIT - argparser.add_argument('-a', '--area', metavar='x1,y1,x2,y2', default=[7, 8, 204, 178], type=lambda s: list(map(float, s.split(','))), help='gcode print area in millimeters') argparser.add_argument('--min-x', type=float, default=None, help=argparse.SUPPRESS) argparser.add_argument('--min-y', type=float, default=None, help=argparse.SUPPRESS) argparser.add_argument('--max-x', type=float, default=None, help=argparse.SUPPRESS) argparser.add_argument('--max-y', type=float, default=None, help=argparse.SUPPRESS) - argparser.add_argument('-Z', '--lift-delta-z', metavar='Z', default=2.5, type=float, help='amount to lift for pen-up (millimeters)') argparser.add_argument('-z', '--work-z', metavar='Z', default=14.5, type=float, help='z-position for drawing (millimeters)') argparser.add_argument('-V', '--pen-up-speed', metavar='S', default=40, type=float, help='speed for moving with pen up (millimeters/second)') @@ -793,114 +792,72 @@ def parse_arguments(argparser:argparse.ArgumentParser): argparser.add_argument('--init-code', metavar='GCODE', type=none_or_str, default="G00 S1; endstops|G00 E0; no extrusion|G01 S1; endstops|G01 E0; no extrusion|G21; millimeters|G91 G0 F%.1f{{zspeed*60}} Z%.3f{{safe}}; pen park !!Zsafe|G90; absolute|G28 X; home|G28 Y; home|G28 Z; home", help='gcode init commands (separate lines with |)') argparser.add_argument('--end-code', metavar='GCODE', type=none_or_str, default=None, help='Gcode to run at end of task') - argparser.add_argument('-H', '--hpgl-out', action=argparse.BooleanOptionalAction, default=False, help='output is HPGL, not gcode; most options are ignored.') + argparser.add_argument('-P', '--pens', metavar='PENFILE', default={1:Pen('1 (0.,0.) black default')}, action=PenAction, PenClass=Pen, help='read output pens from penfile') argparser.add_argument('-T', '--shading-threshold', metavar='N', default=1.0, type=float, help='darkest grayscale to leave unshaded (decimal, 0. to 1.; set to 0 to turn off SVG shading) [default 1.0]') - - argparser.add_argument('-m', '--shading-lightest', metavar='X', default=3.0, type=float, help='shading spacing for lightest colors (millimeters) [default 3.0]') argparser.add_argument('-M', '--shading-darkest', metavar='X', default=0.5, type=float, help='shading spacing for darkest color (millimeters) [default 0.5]') argparser.add_argument('-A', '--shading-angle', metavar='X', default=45, type=float, help='shading angle (degrees) [default 45]') - argparser.add_argument('-X', '--shading-crosshatch', action=argparse.BooleanOptionalAction, default=False, help='cross hatch shading') - - argparser.add_argument('-L', '--stroke-all', action=argparse.BooleanOptionalAction, default=False, help='stroke even regions specified by SVG to have no stroke') argparser.add_argument('-O', '--shading-avoid-outline', action=argparse.BooleanOptionalAction, default=False, help='avoid going over outline twice when shading') #?Unused + argparser.add_argument('-R', '--extract-color', metavar='C', default=None, type=parser.rgbFromColor, help='extract color (specified in SVG format , e.g., rgb(1,0,0) or #ff0000 or red)') + argparser.add_argument('-L', '--stroke-all', action=argparse.BooleanOptionalAction, default=False, help='stroke even regions specified by SVG to have no stroke') argparser.add_argument('-e', '--direction', metavar='ANGLE', default=None, type=lambda value: None if value.lower() == 'none' else float(value), help='for slanted pens: prefer to draw in given direction (degrees; 0=positive x, 90=positive y, none=no preferred direction) [default none]') argparser.add_argument('-o', '--optimization-time', metavar='T', default=60, type=int, help='max time to spend optimizing (seconds; set to 0 to turn off optimization) [default 60]') argparser.add_argument('-d', '--sort', action=argparse.BooleanOptionalAction, default=False, help='sort paths from inside to outside for cutting [default off]') - - - - # parser.add_argument('-c', '--config-file', metavar='$FILENAME', help='read arguments, one per line, from filename. Prepend the filename with "$" e.g. $"args.txt"') + argparser.add_argument('-w', '--gcode-pause', metavar='CMD', default='@pause', help='gcode pause command [default: @pause]') - argparser.add_argument('-P', '--pens', metavar='PENFILE', default={1:Pen('1 (0.,0.) black default')}, action=PenAction, help='read output pens from penfile') - argparser.add_argument('-U', '--pause-at-start', action=argparse.BooleanOptionalAction, default=False, help='pause at start (can be included without any input file to manually move stuff)') - argparser.add_argument('-R', '--extract-color', metavar='C', default=None, type=parser.rgbFromColor, help='extract color (specified in SVG format , e.g., rgb(1,0,0) or #ff0000 or red)') - + argparser.add_argument('--tool-mode', metavar='MODE', choices=['custom','cut','draw'], default='custom', help=argparse.SUPPRESS) argparser.add_argument('--tool-offset', metavar='X', default=0.0, type=float, help='cutting tool offset (millimeters) [default 0.0]') argparser.add_argument('--overcut', metavar='X', default=0.0, type=float, help='overcut (millimeters) [default 0.0]') - argparser.add_argument('--moonraker', metavar='URL', default=None, help='moonraker url') argparser.add_argument('--moonraker-filename', metavar='FILENAME', default='toolpath.gcode', help='name of uploaded file') argparser.add_argument('--moonraker-autoprint', metavar='TRUE/FALSE', default=False, help='whether to automatically begin the print job after upload') argparser.add_argument('--simulation', metavar='TRUE/FALSE', action=argparse.BooleanOptionalAction, default=False, help=argparse.SUPPRESS) - argparser.add_argument('--tab', dest='quiet', default=False, type=bool, help=argparse.SUPPRESS) - argparser.add_argument('--tool-mode', metavar='MODE', choices=['custom','cut','draw'], default='custom', help=argparse.SUPPRESS) #Inkscape specific boolean parameters argparser.add_argument('--boolean-extract-color', metavar='TRUE/FALSE', type=lambda val: True if val.lower() == 'true' else False, help=argparse.SUPPRESS) argparser.add_argument('--boolean-shading-crosshatch', metavar='TRUE/FALSE', dest='shading_crosshatch', help=argparse.SUPPRESS) argparser.add_argument('--boolean-sort', metavar='TRUE/FALSE', dest='sort', help=argparse.SUPPRESS) + argparser.add_argument('--send-and-save', metavar='PORT', default=False, help=argparse.SUPPRESS) #Could probably roll this into "send" and check if we're in Inkscape at the end of __main__ by using tab/quiet instead + argparser.add_argument('--tab', dest='quiet', default=False, type=bool, help=argparse.SUPPRESS) - return argparser.parse_known_args() - - -def parse_svg_file(data): - try: - svgTree = ET.fromstring(data) - return svgTree if 'svg' in svgTree.tag else None - except: - return None - - - -def generate_pen_data(svgTree, data, args, scalingMode, shader:Shader): - penData = {} + args, positional = argparser.parse_known_args() - if svgTree is not None: - penData = parseSVG(svgTree, tolerance=args.tolerance, shader=shader, strokeAll=args.stroke_all, pens=args.pens, extractColor=args.extract_color if args.boolean_extract_color else None) - else: - penData = parseHPGL(data, dpi=args.input_dpi) - - penData = removePenBob(penData) + # I probably shouldn't have done this. If a port is provided on SEND, use it, + # otherwise check if it was provided on send_and_save, otherwise set SEND to None + # If a port is provided on send_and_save, then it sets SEND to the port, then sets itself to True. + args.send = args.send if str(args.send).isdigit() else args.send_and_save if str(args.send_and_save).isdigit() else None + args.send_and_save = True if str(args.send_and_save).isdigit() else False - if args.deduplicate: - penData = dedup(penData) - - if args.sort and penData: - penData = {pen: safeSorted(paths, comparison=comparePaths) for pen, paths in penData.items()} - penData = removePenBob(penData) - - if args.optimization_time > 0. and args.direction is None and penData: - penData = {pen: anneal.optimize(paths, timeout=args.optimization_time/2., quiet=args.quiet) for pen, paths in penData.items()} - penData = removePenBob(penData) + args.optimization_time = 0 if args.sort else args.optimization_time + args.sort = False if args.optimization_time > 0 else args.sort - if (args.tool_offset > 0. or args.overcut > 0.) and penData: - if scalingMode != SCALE_NONE: - sys.stderr.write("Scaling with tool-offset > 0 will produce unpredictable results.\n") - op = OffsetProcessor(toolOffset=args.tool_offset, overcut=args.overcut, tolerance=args.tolerance) - penData = {pen: op.processPath(paths) for pen, paths in penData.items()} + if args.tool_mode == 'cut': + args.optimization_time = 0 + args.sort = True + args.direction = None + elif args.tool_mode == 'draw': + args.tool_offset = 0. + args.sort = False + + + return args, positional + - if args.direction is not None and penData: - penData = {pen: directionalize(paths, args.direction) for pen, paths in penData.items()} - penData = removePenBob(penData) - - if len(penData) > 1 and penData: - sys.stderr.write("Uses the following pens:\n") - for pen in sorted(penData): - sys.stderr.write(describePen(args.pens, pen)+"\n") - - return penData if __name__ == '__main__': - argparser = argparse.ArgumentParser(prog='Gcode Plot', description='test', fromfile_prefix_chars='$', epilog="You can load options from a text file by passing the filename prefixed with a '$' e.g. [python gcodeplot.py $'args.txt']", formatter_class=argparse.ArgumentDefaultsHelpFormatter) + argparser = cArgumentParser(prog='Gcode Plot', description='test', fromfile_prefix_chars='$', epilog="You can load options from a text file by passing the filename prefixed with a '$' e.g. [python gcodeplot.py $'args.txt']", formatter_class=argparse.ArgumentDefaultsHelpFormatter) args, positional = parse_arguments(argparser) - sendPort = args.send if args.send is not None else args.send_and_save - sendAndSave = args.send_and_save is not None - scalingMode = parse_alignment(args.scale, enumMode=True) - args.optimization_time = 0 if args.sort else args.optimization_time - args.sort = False if args.optimization_time > 0 else args.sort - plotter = Plotter(xyMin=tuple((args.min_x if args.min_x is not None else args.area[0], args.min_y if args.min_y is not None else args.area[1])), xyMax=tuple((args.max_x if args.max_x is not None else args.area[2], args.max_y if args.max_y is not None else args.area[3])), drawSpeed=args.pen_down_speed, @@ -916,73 +873,52 @@ def generate_pen_data(svgTree, data, args, scalingMode, shader:Shader): endCode=args.end_code, comment=args.comment_delimiters) - shader = Shader(unshadedThreshold=args.shading_threshold, + shader = Shader(unshadedThreshold= 0 if args.tool_mode == 'cut' else args.shading_threshold, lightestSpacing=args.shading_lightest, darkestSpacing=args.shading_darkest, angle=args.shading_angle, crossHatch=args.shading_crosshatch) - if args.tool_mode == 'cut': - shader.unshadedThreshold = 0 - args.optimization_time = 0 - args.sort = True - args.direction = None - elif args.tool_mode == 'draw': - args.tool_offset = 0. - args.sort = False - + plotter.updateVariables() - # If no file is provided on the input, assume the intent is to run the init g-code over serial. + # If no input SVG is provided on stdin, assume the intent is to just run the init g-code over serial. if len(positional) == 0: if not args.pause_at_start: argparser.print_help() - if sendPort is None: + if args.send is None: sys.stderr.write("Need to specify --send=port to be able to pause without any file.") sys.exit(1) - sendgcode.sendGcode(port=sendPort, speed=args.send_speed, commands=gcodeHeader(plotter) + [args.gcode_pause], gcodePause=args.gcode_pause, variables=plotter.variables, formulas=plotter.formulas) + sendgcode.sendGcode(port=args.send, speed=args.send_speed, commands=gcodeHeader(plotter) + [args.gcode_pause], gcodePause=args.gcode_pause, variables=plotter.variables, formulas=plotter.formulas) sys.exit(0) - # Otherwise, open the input file + # Otherwise, open the input file... with open(positional[0], 'r') as f: data = f.read() + # Gather the SVG data and generate pen data, then generate the output HPGL/GCode... + # Note the program will exit if HPGL/GCode cannot be created svgTree = parse_svg_file(data) shader.setDrawingDirectionAngle(args.direction) - - penData = generate_pen_data(svgTree, data, args, scalingMode, shader) - - - if args.hpgl_out and not args.simulation: - g = emitHPGL(penData, pens=args.pens) - else: - align = [parse_alignment(args.align_x, enumMode=True), parse_alignment(args.align_y, enumMode=True)] - g = emitGcode(penData, align=align, scalingMode=scalingMode, tolerance=args.tolerance, - plotter=plotter, gcodePause=args.gcode_pause, pens=args.pens, pauseAtStart=args.pause_at_start, simulation=args.simulation, quiet=args.quiet) - - if not g: - sys.stderr.write("No points.") - sys.exit(1) + penData = generate_pen_data(svgTree, data, args, shader) + g = generate_HPGL_or_GCODE(penData, args, plotter) + filtered = '\n'.join(fixComments(plotter, g, comment=plotter.comment)) + '\n' + # "Dump" here refers to whether the output code will be sent to stdout or not. dump = True - - if sendPort is not None and not args.simulation: - dump = sendAndSave + + # If we have a port to send to, and we're not in simulation mode, send either the GCode or HPGL over serial. + # If send_and_save is false, then it means we don't want to save the data (from Inkscape; saving is done by returning the data via stdout) + if args.send is not None and not args.simulation: + dump = args.send_and_save if args.hpgl_out: - sendgcode.sendHPGL(port=sendPort, speed=args.send_speed, commands=g) + sendgcode.sendHPGL(port=args.send, speed=args.send_speed, commands=g) else: - sendgcode.sendGcode(port=sendPort, speed=args.send_speed, commands=g, gcodePause=args.gcode_pause, plotter=plotter, variables=plotter.variables, formulas=plotter.formulas) + sendgcode.sendGcode(port=args.send, speed=args.send_speed, commands=g, gcodePause=args.gcode_pause, plotter=plotter, variables=plotter.variables, formulas=plotter.formulas) - if not dump: - sys.exit(0) - - if args.hpgl_out: - sys.stdout.write(g) - sys.exit(0) - - filtered = '\n'.join(fixComments(plotter, g, comment=plotter.comment)) + '\n' + # If we want to upload to Klipper via Moonraker if args.moonraker != "" and args.moonraker is not None: moonraker = args.moonraker.strip("/") + "/server/files/upload" virtual_file = io.BytesIO(filtered.encode('utf-8')) @@ -991,5 +927,16 @@ def generate_pen_data(svgTree, data, args, scalingMode, shader:Shader): if response.status_code != 201: sys.stderr.write(f"Error uploading file. Status code: {response.status_code}") + # If we don't want to return the file over stdout, we exit here... + if not dump: + sys.exit(0) + + # Otherwise, save the file to stdout if it's HPGL... + if args.hpgl_out: + sys.stdout.write(g) + sys.exit(0) + + # Or format and save to stdout if it's GCode + print('\n'.join(fixComments(plotter, g, comment=plotter.comment))) \ No newline at end of file diff --git a/gcodeplotutils/argparser_c.py b/gcodeplotutils/argparser_c.py new file mode 100644 index 0000000..8061240 --- /dev/null +++ b/gcodeplotutils/argparser_c.py @@ -0,0 +1,116 @@ +#Custom argparse classes for additional function. Allows stripping '#' comments +#from files passed in, and also allows 'arg=value' format instead of 'arg value' +# +#Also allows negatable arguments; --arg=124 --arg=true --arg=false --no-arg + + +import argparse +from pathlib import Path +from .enums import * + +class cArgumentParser(argparse.ArgumentParser): + def convert_arg_line_to_args(self, arg_line): + + if arg_line.startswith("#"): + return [] + elif "=" in arg_line: + # Treat lines with "=" as if they were passed as command-line arguments + key, value = arg_line.split("=", 1) + return ['--' + key.strip(), value.strip()] + elif arg_line.startswith('no-'): + return ['--' + arg_line.strip()] + else: + return arg_line.split() + + +class CustomBooleanAction(argparse.Action): + def __init__(self,option_strings, + dest, + default=None, + required=False, + help=None, + metavar=None): + + _option_strings = [] + for option_string in option_strings: + _option_strings.append(option_string) + + if option_string.startswith('--'): + option_string = '--no-' + option_string[2:] + _option_strings.append(option_string) + + if help is not None and default is not None: + help += f" (default: {default})" + + super().__init__( + option_strings=_option_strings, + dest=dest, + nargs='?', + const=None, + default=default, + required=required, + help=help, + metavar=metavar) + + def __call__(self, parser, namespace, values, option_string=None): + if option_string and option_string.startswith('--no-'): + # Handle the case where the option is negated, e.g., --no-shading-crosshatch + setattr(namespace, self.dest, False) + elif values is None or values.lower() == 'true': + # Handle the cases where the option is provided without a value or explicitly set to 'true' + setattr(namespace, self.dest, True) + elif values.lower() == 'false': + # Handle the case where the option is explicitly set to 'false' + setattr(namespace, self.dest, False) + else: + # assign the target value to the provided input. e.g, --send=21523 + setattr(namespace, self.dest, values) + +class PrintDefaultsAction(argparse.Action): + def __call__(self, parser, namespace, values, option_string=None): + printed = set() + formatted_strings = [ + self.format_argument(action, namespace) + for action in parser._actions + if not isinstance(action, argparse._HelpAction) + and action.help != argparse.SUPPRESS + and (formatted := f'{action.dest}: {action.default}') not in printed and not printed.add(formatted) + ] + print('\n'.join(formatted_strings)) + # parser.exit() + + def format_argument(self, action, namespace): + + if action.dest in ('scale', 'align_x', 'align_y'): + value = parse_alignment(getattr(namespace, action.dest, action.default), reverse=True) + elif action.dest == 'extract_color' and (value := getattr(namespace, action.dest, action.default) ) == None: + value = 'all' + else: + value = getattr(namespace, action.dest, action.default) + return f'{action.dest + ":":<25}{value}' + + +def parse_alignment(arg, enumMode=False, reverse=False): + verbose_mapping = {'none': 'n', 'left': 'l', 'right': 'r', 'center': 'c', 'bottom': 'b', 'top': 't', 'down': 'd', 'fit': 'f'} + enum_mapping = {'n': ALIGN_SCALE_NONE, 'l': ALIGN_LEFT, 'r': ALIGN_RIGHT, 'c': ALIGN_CENTER, 'b': ALIGN_BOTTOM, 't': ALIGN_TOP, 'd': SCALE_DOWN_ONLY, 'f': SCALE_FIT} + if enumMode: return enum_mapping.get(arg, ALIGN_SCALE_NONE) + if reverse: return next((key for key, value in verbose_mapping.items() if value == arg), None) + return verbose_mapping.get(arg.lower(), 'n') if len(arg) > 1 else arg + +def none_or_str(value): + return None if value=='none' else value + + + +class PenAction(argparse.Action): + def __init__(self, PenClass, *args, **kwargs): + super().__init__(*args, **kwargs) + self.Pen = PenClass + def __call__(self, parser, namespace, values, option_string=None): + pens = {} + pen_file = Path(values) + if pen_file.is_file(): + pens = {p.pen: p for line in open(pen_file) if (line_stripped := line.strip()) and (p := self.Pen(line_stripped))} + else: + parser.error(f'Invalid filename provided in {self.dest} \n') + setattr(namespace, self.dest, pens) \ No newline at end of file diff --git a/gcodeplotutils/enums.py b/gcodeplotutils/enums.py new file mode 100644 index 0000000..44d03b4 --- /dev/null +++ b/gcodeplotutils/enums.py @@ -0,0 +1,9 @@ +SCALE_NONE = 0 +SCALE_DOWN_ONLY = 1 +SCALE_FIT = 2 +ALIGN_SCALE_NONE = 0 +ALIGN_BOTTOM = 1 +ALIGN_TOP = 2 +ALIGN_LEFT = ALIGN_BOTTOM +ALIGN_RIGHT = ALIGN_TOP +ALIGN_CENTER = 3 \ No newline at end of file From 46759ba34d6ed4ce585bb9931db0ed113445fdba Mon Sep 17 00:00:00 2001 From: IridiumIO Date: Mon, 21 Apr 2025 18:16:56 +1000 Subject: [PATCH 14/15] Fix argument handling of args.sort and args.optimization_time - Optimization time was broken due to using false-y boolean check rather than properly checking for True/False values --- gcodeplot.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/gcodeplot.py b/gcodeplot.py index 43fd4c1..d618bbe 100755 --- a/gcodeplot.py +++ b/gcodeplot.py @@ -837,8 +837,10 @@ def parse_arguments(argparser:cArgumentParser): args.send = args.send if str(args.send).isdigit() else args.send_and_save if str(args.send_and_save).isdigit() else None args.send_and_save = True if str(args.send_and_save).isdigit() else False - args.optimization_time = 0 if args.sort else args.optimization_time - args.sort = False if args.optimization_time > 0 else args.sort + if args.sort == True: + args.optimization_time = 0 + elif args.optimization_time > 0: + args.sort = False if args.tool_mode == 'cut': args.optimization_time = 0 From e4bdd19234eb6a4ed60bed2a274a73dd235ed4f3 Mon Sep 17 00:00:00 2001 From: IridiumIO Date: Tue, 22 Apr 2025 08:44:44 +1000 Subject: [PATCH 15/15] Adjust argument parsing to support configuration files passed in using the old getopt() style - If both command-line parameters and a config file are used, the command-line parameters will overwrite ones from the config file --- .gitignore | 5 ++++- gcodeplot.py | 41 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index f596c91..e503412 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ /svgpath/__pycache__/ *.pyc /test -gcodeplot_orig.py \ No newline at end of file +gcodeplot_orig.py +gcodeplot.spec +launch.json +gcodeplot.exe diff --git a/gcodeplot.py b/gcodeplot.py index d618bbe..7b72590 100755 --- a/gcodeplot.py +++ b/gcodeplot.py @@ -760,7 +760,26 @@ def generate_HPGL_or_GCODE(penData, args, plotter): def parse_arguments(argparser:cArgumentParser): - + + + # Pre-parse command-line arguments to identify explicitly provided arguments + # This is done to later add the `--config-file` argument to the parser and overwrite config file options with explicitly provided ones + args, unknown = argparser.parse_known_args() + explicit_args = [] + for i, arg in enumerate(unknown): + if arg.startswith('-'): # Check if it's an option + explicit_args.append(arg) + # Include the next variable if valid (accounts for options defined as '--option value' instead of '--option=value') if it is not a flag + if i + 1 < len(unknown) and not unknown[i + 1].startswith('-') and '=' not in unknown[i + 1] and "=" not in unknown[i]: + explicit_args.append(unknown[i + 1]) + + + + ###### ###### + ###### Begin argument parser ###### + ###### ###### + + argparser.add_argument('--config-file', metavar='CONFIG', help='Read options from a configuration file. Options are parsed line by line. Lines starting with "#" are ignored. Explicitly provided options will override config file options.') argparser.add_argument('--dump-options', help='show current settings instead of doing anything', action=PrintDefaultsAction, nargs=0) argparser.add_argument('-r', '--allow-repeats', help='do not deduplicate paths', action=CustomBooleanAction, default=False) @@ -829,8 +848,25 @@ def parse_arguments(argparser:cArgumentParser): argparser.add_argument('--send-and-save', metavar='PORT', default=False, help=argparse.SUPPRESS) #Could probably roll this into "send" and check if we're in Inkscape at the end of __main__ by using tab/quiet instead argparser.add_argument('--tab', dest='quiet', default=False, type=bool, help=argparse.SUPPRESS) + ###### ###### + ###### End of argument parser ###### + ###### ###### + + + args, positional = argparser.parse_known_args() + # If a config file is provided, parse it + if args.config_file: + config_opts = getConfigOpts(args.config_file) + # Convert config options to a flat list of arguments + config_args = [item for sublist in config_opts for item in sublist if item is not None] + # Combine arguments: config_args + explicitly provided arguments + combined_args = config_args + list(explicit_args) + args = argparser.parse_args(combined_args) + + + # I probably shouldn't have done this. If a port is provided on SEND, use it, # otherwise check if it was provided on send_and_save, otherwise set SEND to None # If a port is provided on send_and_save, then it sets SEND to the port, then sets itself to True. @@ -850,7 +886,8 @@ def parse_arguments(argparser:cArgumentParser): args.tool_offset = 0. args.sort = False - + + return args, positional