From c5243865d65f0f4d30c9110f6bcabc252f2ed3b2 Mon Sep 17 00:00:00 2001 From: Wouter Verweirder Date: Fri, 12 Sep 2025 19:25:16 +0200 Subject: [PATCH] vpype and axidraw cli --- pyproject.toml | 4 +- requirements.txt | 9 ++ src/penplotter/nodes.py | 220 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 230 insertions(+), 3 deletions(-) create mode 100644 requirements.txt diff --git a/pyproject.toml b/pyproject.toml index e174ffc..2ee4a68 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,9 @@ license = {text = "MIT license"} requires-python = ">=3.10" classifiers = [] dependencies = [ - + "Pillow>=9.0.0", + "vpype>=1.0.0", + "https://cdn.evilmadscientist.com/dl/ad/public/AxiDraw_API.zip", ] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..477e903 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,9 @@ +# ComfyUI PenPlotter Dependencies +# Image processing library +Pillow>=9.0.0 + +# Vector drawing and plotting library +vpype>=1.0.0 + +# AxiDraw API for pen plotter control +https://cdn.evilmadscientist.com/dl/ad/public/AxiDraw_API.zip \ No newline at end of file diff --git a/src/penplotter/nodes.py b/src/penplotter/nodes.py index 02252da..d2e4025 100644 --- a/src/penplotter/nodes.py +++ b/src/penplotter/nodes.py @@ -131,13 +131,229 @@ class ImageToCenterline: return (f"Error: {str(e)}",) +class OptimizeSVG: + CATEGORY = "Pen Plotter" + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "svg_string": ("STRING", {"multiline": True}), + }, + "optional": { + "fit_to_margins": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 50.0, "step": 0.1}), + "landscape": ("BOOLEAN", {"default": True}), + "linemerge": ("BOOLEAN", {"default": True}), + "tolerance": ("FLOAT", {"default": 0.1, "min": 0.01, "max": 5.0, "step": 0.01}), + "linesort": ("BOOLEAN", {"default": True}), + "reloop": ("BOOLEAN", {"default": True}), + "linesimplify": ("BOOLEAN", {"default": True}), + "page_width": ("FLOAT", {"default": 100.0, "min": 10.0, "max": 500.0, "step": 1.0}), + "page_height": ("FLOAT", {"default": 148.0, "min": 10.0, "max": 500.0, "step": 1.0}), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("optimized_svg",) + FUNCTION = "optimize_svg" + + def run_vpype_command(self, input_path, output_path, fit_to_margins, landscape, linemerge, + tolerance, linesort, reloop, linesimplify, page_width, page_height): + """Run vpype command with specified parameters.""" + try: + # Build the vpype command + cmd = ['vpype', 'read', str(input_path)] + + # Add layout command with fit-to-margins and landscape options + layout_cmd = ['layout', f'--fit-to-margins', f'{fit_to_margins}mm'] + if landscape: + layout_cmd.extend(['--landscape', f'{page_width}x{page_height}mm']) + else: + layout_cmd.append(f'{page_width}x{page_height}mm') + cmd.extend(layout_cmd) + + # Add linemerge if enabled + if linemerge: + cmd.extend(['linemerge', '--tolerance', f'{tolerance}mm']) + + # Add linesort if enabled + if linesort: + cmd.append('linesort') + + # Add reloop if enabled + if reloop: + cmd.append('reloop') + + # Add linesimplify if enabled + if linesimplify: + cmd.append('linesimplify') + + # Add write command with page size + cmd.extend(['write', '--page-size', f'{page_width}x{page_height}mm', str(output_path)]) + + # Execute the command + result = subprocess.run(cmd, capture_output=True, text=True) + + if result.returncode == 0: + return True + else: + print(f"Error running vpype: {result.stderr}") + return False + + except FileNotFoundError: + print("Error: vpype command not found. Please ensure vpype is installed and in your PATH.") + return False + except Exception as e: + print(f"Error running vpype: {e}") + return False + + def optimize_svg(self, svg_string, fit_to_margins=0.0, landscape=True, linemerge=True, + tolerance=0.1, linesort=True, reloop=True, linesimplify=True, + page_width=100.0, page_height=148.0): + """Optimize SVG string using vpype.""" + try: + # Create temporary files + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + input_svg_path = temp_path / "input.svg" + output_svg_path = temp_path / "output.svg" + + # Write input SVG to temporary file + with open(input_svg_path, 'w', encoding='utf-8') as f: + f.write(svg_string) + + # Run vpype command + if not self.run_vpype_command( + input_svg_path, output_svg_path, fit_to_margins, landscape, + linemerge, tolerance, linesort, reloop, linesimplify, + page_width, page_height + ): + return ("Error: Failed to run vpype optimization",) + + # Read the optimized SVG file and return as string + if output_svg_path.exists(): + with open(output_svg_path, 'r', encoding='utf-8') as f: + optimized_svg = f.read() + return (optimized_svg,) + else: + return ("Error: Optimized SVG file was not created",) + + except Exception as e: + return (f"Error: {str(e)}",) + + +class PlotSVG: + CATEGORY = "Pen Plotter" + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "svg_string": ("STRING", {"multiline": True}), + }, + "optional": { + "layer": ("INT", {"default": 2, "min": 1, "max": 8, "step": 1}), + "preview_only": ("BOOLEAN", {"default": False}), + "pen_up_speed": ("INT", {"default": 75, "min": 1, "max": 100, "step": 1}), + "pen_down_speed": ("INT", {"default": 25, "min": 1, "max": 100, "step": 1}), + "pen_up_delay": ("INT", {"default": 0, "min": 0, "max": 5000, "step": 10}), + "pen_down_delay": ("INT", {"default": 0, "min": 0, "max": 5000, "step": 10}), + "copies": ("INT", {"default": 1, "min": 1, "max": 100, "step": 1}), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("status",) + FUNCTION = "plot_svg" + + def run_axicli_command(self, svg_path, layer, preview_only, pen_up_speed, pen_down_speed, + pen_up_delay, pen_down_delay, copies): + """Run axicli command with specified parameters.""" + try: + # Build the axicli command + cmd = ['axicli', str(svg_path)] + + # Add layer parameter + cmd.extend(['-L', str(layer)]) + + # Add preview mode if enabled + if preview_only: + cmd.append('-m') # Preview mode + + # Add speed settings + cmd.extend(['-S', str(pen_up_speed)]) # Pen-up speed + cmd.extend(['-s', str(pen_down_speed)]) # Pen-down speed + + # Add delay settings + if pen_up_delay > 0: + cmd.extend(['-d', str(pen_up_delay)]) # Pen-up delay + if pen_down_delay > 0: + cmd.extend(['-D', str(pen_down_delay)]) # Pen-down delay + + # Add copies + if copies > 1: + cmd.extend(['-C', str(copies)]) + + # Execute the command + result = subprocess.run(cmd, capture_output=True, text=True) + + if result.returncode == 0: + return True, result.stdout + else: + print(f"Error running axicli: {result.stderr}") + return False, result.stderr + + except FileNotFoundError: + error_msg = "Error: axicli command not found. Please ensure AxiDraw software is installed and axicli is in your PATH." + print(error_msg) + return False, error_msg + except Exception as e: + error_msg = f"Error running axicli: {e}" + print(error_msg) + return False, error_msg + + def plot_svg(self, svg_string, layer=2, preview_only=False, pen_up_speed=75, pen_down_speed=25, + pen_up_delay=0, pen_down_delay=0, copies=1): + """Send SVG to plotter using axicli.""" + try: + # Create temporary file for the SVG + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + svg_path = temp_path / "plot.svg" + + # Write SVG to temporary file + with open(svg_path, 'w', encoding='utf-8') as f: + f.write(svg_string) + + # Run axicli command + success, output = self.run_axicli_command( + svg_path, layer, preview_only, pen_up_speed, pen_down_speed, + pen_up_delay, pen_down_delay, copies + ) + + if success: + if preview_only: + return ("Preview completed successfully. Check AxiDraw software for preview.",) + else: + return (f"Plot completed successfully. Copies: {copies}",) + else: + return (f"Error: {output}",) + + except Exception as e: + return (f"Error: {str(e)}",) + + # A dictionary that contains all nodes you want to export with their names # NOTE: names should be globally unique NODE_CLASS_MAPPINGS = { - "ImageToCenterline": ImageToCenterline + "ImageToCenterline": ImageToCenterline, + "OptimizeSVG": OptimizeSVG, + "PlotSVG": PlotSVG } # A dictionary that contains the friendly/humanly readable titles for the nodes NODE_DISPLAY_NAME_MAPPINGS = { - "ImageToCenterline": "Image to Centerline SVG" + "ImageToCenterline": "Image to Centerline SVG", + "OptimizeSVG": "Optimize SVG with vpype", + "PlotSVG": "Plot SVG with AxiDraw" }