vpype and axidraw cli

This commit is contained in:
Wouter Verweirder
2025-09-12 19:25:16 +02:00
parent 18d7288801
commit c5243865d6
3 changed files with 230 additions and 3 deletions
+3 -1
View File
@@ -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",
]
+9
View File
@@ -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
+218 -2
View File
@@ -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"
}