10 Commits
Author SHA1 Message Date
kolkman c1ada9a60e recursion limit and downscale 2026-11-06 08:12:29 +01:00
kolkman d74c8d57b4 minore converter improvements 2026-11-04 16:18:29 +01:00
arcadeperfect d34118520e Update README.md 2020-08-20 16:44:21 -04:00
Alexander Harding 11d475e00f Removed venv 2020-01-15 09:04:21 -05:00
arcadeperfect 6267b2738a Merge branch 'master' of https://github.com/arcadeperfect/svg2gcode_grbl 2020-01-05 17:34:52 -05:00
arcadeperfect 9278ec95a2 fixed width / height float conversion bug 2020-01-05 17:09:15 -05:00
arcadeperfect 5d437e56f2 Update README.md 2020-01-04 19:51:10 -05:00
arcadeperfect 2e9e535c24 Update README.md 2020-01-04 17:34:09 -05:00
arcadeperfect 16bfb2b6df Update README.md 2020-01-04 17:16:35 -05:00
arcadeperfect e579545d28 Update README.md 2020-01-04 17:12:51 -05:00
10 changed files with 308 additions and 404 deletions

No files matched your search

+6
View File
@@ -0,0 +1,6 @@
__pycache__/config.cpython-313.pyc
__pycache__/*.pyc
artwork/*.svg
gcode/*.gcode
+15
View File
@@ -0,0 +1,15 @@
PYTHON ?= python3
ARTWORK_DIR ?= artwork
GCODE_DIR ?= gcode
SVG_FILES := $(wildcard $(ARTWORK_DIR)/*.svg)
GCODE_FILES := $(patsubst $(ARTWORK_DIR)/%.svg,$(GCODE_DIR)/%.gcode,$(SVG_FILES))
.PHONY: all
all: $(GCODE_FILES)
$(GCODE_DIR)/%.gcode: $(ARTWORK_DIR)/%.svg config.py svg2gcode.py | $(GCODE_DIR)
$(PYTHON) convert.py "$<" "$@"
$(GCODE_DIR):
mkdir -p "$@"
+42 -11
View File
@@ -1,27 +1,58 @@
Forked from vishpat/svg2gcode
## SVG to Gcode converter for GRBL pen plotter
https://github.com/vishpat/svg2gcode
Forked from [vishpat/svg2gcode](https://github.com/vishpat/svg2gcode)
I found that the plugins for inkscape and illustrator would crash when I tried to convert SVG's with many thousands of lines to gcode.
I found that the plugins for inkscape and illustrator would crash when I tried to convert SVG's with many thousands of lines to gcode.
So I modified this to generate gcode for GRBL based pen plotter
**Usage as cli:**
_python convert.py source.svg destination.gcode_
>python convert.py source.svg destination.gcode
**or in bash**
_copy 's2g' to your path and run from anywhere_
copy 's2g' to your path and run from anywhere
>s2g source.svg target.gcode
**There is a brute force path optimisator algarithm. Very slow for large files, can be disabled in config.py**
**Convert all SVG files in `artwork/`**
**illustrator exports svg's in points** regardless of your document settings
if units are set to points in the config file, we convert to mm on the fly
Run `make` from the repository root. Each `artwork/name.svg` is converted to
`gcode/name.gcode`. Only files that are newer than their corresponding output
are converted; changes to `config.py` or `svg2gcode.py` also rebuild all generated G-code. The
directories and Python command can be overridden, for example:
`make ARTWORK_DIR=my-art GCODE_DIR=out PYTHON=python3`.
**There is a brute force path optimisator algorithm. Very slow for large files, can be disabled in config.py**
Travel-distance measurements preserve all drawing shapes. Single-shape drawings
have zero inter-shape travel; SVGs with no drawable shapes are rejected.
Conversion raises Python's recursion limit to at least 100,000 as a temporary
workaround for long SVG paths. Larger paths may still exceed this limit, and
deep recursion can exhaust the stack on some Python versions and platforms.
**Illustrator exports in points, not mm** regardless of your document settings.
If units are set to points in the config file, we convert to mm on the fly.
With `auto_scale = True` (the default), drawings outside
`0 <= X <= bed_max_x` or `0 <= Y <= bed_max_y` are automatically fitted using
their actual coordinate bounds after unit conversion and SVG transformations.
Fitting preserves aspect ratio, never enlarges drawings, and shifts fitted
drawings to start at X0/Y0. Drawings already in range remain unchanged.
Set `auto_scale = False` to reject out-of-range drawings instead. The error
reports the offending coordinate and bed limits; no output is created or
overwritten on a size-check failure.
These are work-coordinate limits, not a check of controller offsets or Z travel.
There are various settings you can change in config.py. Most notably, you must set an upper and lower position for the z axis.
zTravel is the height it will move to before traveling to the next stroke
zDraw should be the height at which the pen will make contact with the paper
The `preamble` is emitted after the header and before the feed rate and drawing
moves. It currently homes with `$H`, selects millimeters, absolute positioning
and G54, then resets only the G54 Z offset to zero. X/Y offsets are preserved.
The controller or sender must support `$H` within a job. Regenerate existing
G-code with `make` and upload the new files to apply this startup sequence.
* zTravel is the height it will move to before traveling to the next stroke
* zDraw should be the height at which the pen will make contact with the paper
* zDraw should be the height at which the pen will make contact with the paper
+8 -8
View File
@@ -1,5 +1,5 @@
"""G-code emitted at the start of processing the SVG file"""
preamble = "G90"
preamble = "$H\nG21 G90 G54\nG10 L2 P1 Z0"
"""G-code emitted at the end of processing the SVG file"""
@@ -28,20 +28,20 @@ value is greater than 0.1
smoothness = 0.02
""" height that the z axis will use to travel between strokes """
zTravel = 4
zTravel = 2
""" height that the z axis will use to draw """
zDraw = 0
""" feed rate """
feed_rate = 80000
""" drawing feed rate in mm/min """
feed_rate = 1600
""" decimal precision of gcode"""
precision = 2
""" scale gcode to fit bed size"""
auto_scale = False
""" shrink and shift out-of-bed drawings to fit, preserving aspect ratio"""
auto_scale = True
""" optimize path - slow for large files"""
optimise = True
@@ -50,5 +50,5 @@ optimise = True
illustrator exports svg's in points, not mm
set to "mm" if you don't want to convert to mm
"""
units = "points"
#units = "mm"
#units = "points"
units = "mm"
+5 -218
View File
@@ -1,7 +1,7 @@
from math import sqrt
from datetime import datetime as dt
from utils import *
import json
def get_distance(a, b, sq=False):
x1, y1 = a[0], a[1]
@@ -19,10 +19,8 @@ def get_distance(a, b, sq=False):
def get_total_distance(shapes):
total_distance = 0
last = shapes.pop(0)[-1]
for i in shapes:
total_distance += get_distance(i[0], last, sq=True)
for previous, current in zip(shapes, shapes[1:]):
total_distance += get_distance(current[0], previous[-1], sq=True)
return total_distance
@@ -33,13 +31,8 @@ def optimise_path(shapes, sq=False):
new_order = []
new_order.append(shapes.pop(0))
l = len(shapes)
lp = 0
print(lp)
while len(new_order) <= l:
p = round((1-(len(shapes) / l))*100)
if not p == lp:
print(p)
lp = p
shortest = float("Inf")
last = new_order[-1][-1]
@@ -59,216 +52,10 @@ def optimise_path(shapes, sq=False):
selection = shape
if reverse:
new_order.append(list(reversed(selection)))
#new_order.append([x for x in reversed(selection)])
new_order.append([x for x in reversed(selection)])
else:
new_order.append(selection)
shapes.remove(selection)
timer(t1, "optimizing ")
return new_order
def test_edges(shapes):
m = get_min_max(shapes)
xMin = m[0]
yMin = m[1]
xMax = m[2]
yMax = m[3]
boundry = [ [xMin, yMin], [xMin, yMax], [xMax, yMax], [xMax, yMin], xMin, yMin]
return boundry
# def scale_shapes(shapes, x_scale, y_scale):
#
# for i in shapes:
# for j in i:
# shapes[i][j][0] = shapes[i][j][0] * x_scale
# shapes[i][j][1] = shapes[i][j][1] * y_scale
#
# return shapes
# def lerp(old_min, new_min, old_max, new_max, value):
#
# OldRange = (old_max - old_min)
# NewRange = (new_max - new_min)
# return (((value - old_min) * NewRange) / OldRange) + new_min
def get_min_max(shapes):
xMin, yMin = float("inf"), float("inf")
xMax, yMax = 0, 0
for i in shapes:
for j in i:
xMin = min(xMin, j[0])
xMax = max(xMax, j[0])
yMin = min(yMin, j[1])
yMax = max(yMax, j[1])
print(f"min is {xMin}, {yMin}")
print(f"max is {xMax}, {yMax}")
return (xMin, yMin), (xMax, yMax)
def auto_scale(shapes,bed_x, bed_y):
def scale(old_min_xy, old_max_xy, bed_xy, value_xy):
offset_x = old_min_xy[0]
offset_y = old_min_xy[1]
old_max_offsetted_x = old_max_xy[0] - offset_x
old_max_offsetted_y = old_max_xy[1] - offset_y
scale_x = bed_xy[0] / old_max_offsetted_x
scale_y = bed_xy[1] / old_max_offsetted_y
scale = min(scale_x, scale_y)
x = value_xy[0] - offset_x
y = value_xy[1] - offset_y
x *= scale
y *= scale
return x, y
min_max = get_min_max(shapes)
for x, i in enumerate(shapes):
for y, j in enumerate(i):
shapes[x][y] = scale(min_max[0], min_max[1], (bed_x, bed_y), shapes[x][y])
return shapes
#
#
# min_max = get_min_max(shapes)
# old_min = min(min_max[0], min_max[1])
# old_max = max(min_max[2], min_max[3])
# new_min = 0
# new_max = max(bed_x, bed_y)
#
# print(f"old min {old_min}")
# print(f"old max {old_max}")
# print(f"new_min {new_min}")
# print(f"new_max {new_max}")
#
# # print(f"")
#
# # print(shapes)
# for x, i in enumerate(shapes):
#
# for y, j in enumerate(i):
# for z, k in enumerate(j):
# this = shapes[x][y][z]
# this_lerp = lerp(old_min, new_min, old_max, new_max, this)
# # print(f"{this:03} --> {this_lerp:03}")
# shapes[x][y][z] = this_lerp
#
# return shapes
def concatenate(shapes, threshold):
print("concat")
new_shapes = []
new_shape = []
for x, i in enumerate(shapes):
current_start = shapes[x][0]
previous_end = shapes[x-1][-1]
current_shape = i
if len(new_shape) == 0:
new_shape = current_shape
if get_distance(current_start, previous_end, True) < threshold:
new_shape += current_shape
else:
new_shapes.append(new_shape)
new_shape = []
print(f"{len(shapes)} became {len(new_shapes)} shapes")
return new_shapes
def compare_lines(line1, line2, threshold): #return true if lines are too similar
count = 0
def check(list): #return true if all values are below threshold
for i in list:
# print(abs(i))
if abs(i) >= threshold:
# print(abs(i))
return False
return True
l1_x1 = line1[0][0]
l1_y1 = line1[0][1]
l1_x2 = line1[1][0]
l1_y2 = line1[1][1]
l2_x1 = line2[0][0]
l2_y1 = line2[0][1]
l2_x2 = line2[1][0]
l2_y2 = line2[1][1]
d1 = l1_x1 - l2_x1
d2 = l1_y1 - l2_y1
d3 = l1_x2 - l2_x2
d4 = l1_y2 - l2_y2
d5 = l1_x1 - l2_x2
d6 = l1_y1 - l2_y2
d7 = l1_x2 - l2_x1
d8 = l1_x2 - l2_y2
c1 = check([d1, d2, d3, d4])
c2 = check([d5, d6, d7, d8])
if c1 or c2:
return True
count += 1
else:
return False
def remove_redundant_lines(shapes, threshold):
count = 0
first = True
prev_points = shapes[0][0]
prev_line = ()
for shape in shapes:
for points in shape:
if first:
first = False
continue
line = (prev_points, points)
if not prev_line:
prev_line = line
continue
if compare_lines(prev_line, line, threshold):
count += 1
prev_points = points
prev_line = line
print(f"redundant lines: {count}")
-108
View File
@@ -1,108 +0,0 @@
from optimise import optimise_path, auto_scale, concatenate, remove_redundant_lines
from svg2gcode import get_shapes, shapes_2_gcode
from os.path import join
from json import dump as jdump, load as jload
from utils import *
# def write(output_file_name, data):
# with open(output_file_name, 'w+') as output_file:
# output_file.write(data)
def dump(outfile, data):
with open(outfile, 'w+') as o:
jdump(data, o)
def read(infile):
with open(infile) as i:
return jload(i)
proj_location = "/bish/projects/plots/noiseField"
svg_location = join(proj_location, "svg")
json_location = join(proj_location, "json")
gcode_location = join(proj_location, "gcode")
#
# svg_file = "test_shapes.svg"
svg_file = "1.svg"
# svg_file = "square.svg"
svg_path = join(svg_location, svg_file)
file_name = svg_file.split(".")[0]
shapes_file = f"{file_name}_shapes.json"
shapes_path = join(json_location, shapes_file)
optimised_shapes_file = f"{file_name}_optimised.json"
optimised_shapes_path = join(json_location, optimised_shapes_file)
concatenated_shapes_file = f"{file_name}_optimised.json"
concatenated_shapes_path = join(json_location, concatenated_shapes_file)
scaled_shapes_file = f"{file_name}_scaled.json"
scaled_shapes_path = join(json_location, scaled_shapes_file)
version = 9
gcode_output_file = f"{file_name}_v{version}.gcode"
gcode_output_path = join(gcode_location, gcode_output_file)
print(proj_location)
print(svg_location)
print(json_location)
print(gcode_location)
print(svg_path)
print(shapes_path)
print(optimised_shapes_path)
print(scaled_shapes_path)
print(concatenated_shapes_path)
print(gcode_output_path)
# auto_scale = False
#
# shapes = get_shapes(svg_path) #parse svg
# dump(shapes_path, shapes) #dump parsed svg to json
current = read(shapes_path)
# tempCurrent = []
# for x,i in enumerate(current):
# if x%10 == 0:
# tempCurrent.append(i)
# current = tempCurrent
#
# new_order = optimise_path(current) #optimise json shapes
# dump(optimised_shapes_path, new_order) #dunmp optimised shapes
# current = read(optimised_shapes_path)
# jnew_order = read(optimised_shapes_path) #read optimised shapes
#
#flip xy
# for a,i in enumerate(current):
# for b,j in enumerate(i):
#
# c = current[a][b]
# c.reverse()
# current[a][b] = c
# print(current[a][b])
scaled_shapes = auto_scale(current, 205, 268)
new_maxX = 0
new_maxY = 0
for i in scaled_shapes:
new_maxX = max(new_maxX, max(i[0][0], i[-1][0]))
new_maxY = max(new_maxY, max(i[0][-1], i[-1][-1]))
print("new max!!!!!!")
print(new_maxX, new_maxY)
current = scaled_shapes
# concat_shapes = concatenate(current, 0.35)
# current = concat_shapes
# remove_reundant = remove_redundant_lines(current, 0.4)
#
commands = shapes_2_gcode(current, test_boundries=False) #generate gcode commands
write_gcode(gcode_output_path, commands) #write gcode file
+64 -49
View File
@@ -6,22 +6,25 @@ import shapes as shapes_pkg
from shapes import point_generator
from config import *
import re
from math import isfinite
from datetime import datetime as dt
from optimise import optimise_path, get_total_distance, test_edges
from optimise import optimise_path, get_total_distance
from utils import *
debug = False
# todo fix total distance algorithm
# todo can we guess the units?
# todo add manual scale option
# todo add z rise option
# todo why do i need to flip?
def get_shapes(svg_file, auto_scale=False):
def get_shapes(path, auto_scale=True):
t1 = dt.now()
svg_shapes = set(['rect', 'circle', 'ellipse', 'line', 'polyline', 'polygon', 'path'])
shapes = []
tree = ET.parse(svg_file)
tree = ET.parse(path)
root = tree.getroot()
pointRatio = 0.352778
@@ -38,36 +41,17 @@ def get_shapes(svg_file, auto_scale=False):
print("Unable to get width and height for the svg")
sys.exit(1)
width = float(re.sub("[^0-9]", "", width))
height = float(re.sub("[^0-9]", "", height))
# width = float(re.sub("[^0-9]", "", width))
# height = float(re.sub("[^0-9]", "", height))
# if "px" in width:
# print("px")
# width.replace("px", "")q
#
# if "px" in height:
# print("px")
# height.replace("px", "")
width = float(width)
height = float(height)
width = float(re.findall(r"[-+]?\d*\.\d+|\d+", width)[0])
height = float(re.findall(r"[-+]?\d*\.\d+|\d+", height)[0])
print("width / height ", width, height)
if units == "points":
width *= pointRatio
height *= pointRatio
if auto_scale:
print("\nauto scaling")
scale_x = bed_max_x / max(width, height)
scale_y = bed_max_y / max(width, height)
scale_x = min(scale_x, scale_y)
scale_y = scale_x
print("width / height ", width, height)
print("scale factor ", scale_x, "\n")
for elem in root.iter():
try:
@@ -99,11 +83,6 @@ def get_shapes(svg_file, auto_scale=False):
y = -y + height
if auto_scale:
x *= scale_x
y *= scale_y
if first:
#coords.append((x, -y + height))
coords.append((x, y))
@@ -117,7 +96,41 @@ def get_shapes(svg_file, auto_scale=False):
if first:
first = False
shapes.append(coords)
if coords:
shapes.append(coords)
if not (isfinite(bed_max_x) and isfinite(bed_max_y)
and bed_max_x > 0 and bed_max_y > 0):
raise ValueError("Bed dimensions must be finite and greater than zero")
points = [point for shape in shapes for point in shape]
if any(not (isfinite(x) and isfinite(y)) for x, y in points):
raise ValueError(f"{path}: drawing coordinates must be finite")
if auto_scale and points:
min_x = min(x for x, y in points)
max_x = max(x for x, y in points)
min_y = min(y for x, y in points)
max_y = max(y for x, y in points)
if min_x < 0 or max_x > bed_max_x or min_y < 0 or max_y > bed_max_y:
span_x = max_x - min_x
span_y = max_y - min_y
scale = min(1.0, bed_max_x / span_x if span_x else 1.0,
bed_max_y / span_y if span_y else 1.0)
print("auto fitting: scale", scale, "origin", (min_x, min_y))
# Bound floating-point roundoff at the fitted upper edges.
shapes = [[(min(bed_max_x, (x - min_x) * scale),
min(bed_max_y, (y - min_y) * scale))
for x, y in shape] for shape in shapes]
for shape in shapes:
for x, y in shape:
if not (0 <= x <= bed_max_x and 0 <= y <= bed_max_y):
raise ValueError(
f"{path}: drawing coordinate X{x:.6f} Y{y:.6f} mm "
f"is outside bed limits X[0, {bed_max_x}] "
f"Y[0, {bed_max_y}] mm"
)
timer(t1, "parsing gcode ")
@@ -132,19 +145,13 @@ def g_string(x, y, z=False, prefix="G1", p=3):
return f"{prefix} X{x:.{p}f} Y{y:.{p}f}"
def shapes_2_gcode(shapes, test_boundries = False):
def shapes_2_gcode(shapes):
t1 = dt.now()
with open("header.txt") as h:
header = h.read()
commands = [f"{header}", f'F{feed_rate}']
if test_boundries:
# commands.append("(boundry check)")
for i in test_edges(shapes):
commands.append(g_string(i[0],i[1], zTravel, "G0"))
commands = [f"{header}", preamble, f'F{feed_rate}']
for i in shapes:
commands += ['', shape_preamble, ""]
@@ -160,33 +167,42 @@ def shapes_2_gcode(shapes, test_boundries = False):
commands += [" ",postamble, ""]
commands += ["(home)", f"G0 {zTravel}", f"G0 X0 Y0"]
commands += ["(home)", f"G0 Z{zTravel}", f"G0 X0 Y0"]
timer(t1, "shapes_2_gcode ")
return commands
def write_file(output, commands):
t1 = dt.now()
with open(output, 'w+') as output_file:
for i in commands:
output_file.write(i + "\n")
timer(t1, "writing file ")
def main(file_path, output):
sys.setrecursionlimit(max(sys.getrecursionlimit(), 100000))
shapes = get_shapes(file_path, auto_scale)
if not shapes:
raise ValueError(f"{file_path}: no drawable SVG shapes found")
if optimise:
pre_distance = get_total_distance(shapes)
print("unoptimised distance: ", get_total_distance(shapes))
new_order = optimise_path(shapes)
post_distance = get_total_distance(new_order)
print("unoptimised distance: ", pre_distance)
print("optimised distance ", post_distance)
print("factor: ", post_distance / pre_distance)
if pre_distance:
print("factor: ", post_distance / pre_distance)
else:
print("factor: not applicable (no travel between shapes)")
commands = shapes_2_gcode(new_order)
@@ -196,7 +212,7 @@ def main(file_path, output):
commands = shapes_2_gcode(shapes)
write_gcode(output, commands)
write_file(output, commands)
print("done")
@@ -207,9 +223,8 @@ if __name__ == "__main__":
# file_path = "./svg/medium_example.svg"
file_path = "./svg/text.svg"
# file_path = "./svg/example.svg"
# file_path = "./svg/lines.svg"
output = "./gcode_optimised/1.gcode"
main(file_path, output)
+122
View File
@@ -0,0 +1,122 @@
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
import svg2gcode
class DrawingBoundsTests(unittest.TestCase):
def setUp(self):
self.directory = tempfile.TemporaryDirectory()
self.addCleanup(self.directory.cleanup)
self.source = Path(self.directory.name) / "drawing.svg"
self.output = Path(self.directory.name) / "drawing.gcode"
for name, value in (
("bed_max_x", 200),
("bed_max_y", 280),
("units", "mm"),
("auto_scale", False),
):
patcher = patch.object(svg2gcode, name, value)
patcher.start()
self.addCleanup(patcher.stop)
def write_line(self, x1, y1, x2, y2, width=200, height=280, transform=""):
self.source.write_text(
f'<svg xmlns="http://www.w3.org/2000/svg" '
f'width="{width}" height="{height}">'
f'<line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" '
f'transform="{transform}"/></svg>'
)
def test_exact_boundaries_are_allowed(self):
self.write_line(0, 0, 200, 280)
shapes = svg2gcode.get_shapes(self.source, False)
self.assertIn((0, 280), shapes[0])
self.assertIn((200, 0), shapes[0])
def test_each_boundary_is_enforced(self):
for x, y in ((-0.0001, 100), (200.0001, 100),
(100, -0.0001), (100, 280.0001)):
with self.subTest(x=x, y=y):
self.write_line(10, 10, x, y)
with self.assertRaisesRegex(ValueError, "outside bed limits"):
svg2gcode.get_shapes(self.source, False)
def test_transformed_coordinates_are_checked(self):
self.write_line(10, 10, 20, 20, transform="translate(200, 0)")
with self.assertRaisesRegex(ValueError, "outside bed limits"):
svg2gcode.get_shapes(self.source, False)
def test_points_are_converted_before_checking(self):
self.write_line(0, 0, 500, 500, width=500, height=500)
with patch.object(svg2gcode, "units", "points"):
shapes = svg2gcode.get_shapes(self.source, False)
self.assertAlmostEqual(shapes[0][-1][0], 500 * 0.352778)
def test_auto_scaling_is_applied_before_checking(self):
self.write_line(0, 0, 400, 560, width=400, height=560)
shapes = svg2gcode.get_shapes(self.source, True)
self.assertTrue(all(0 <= x <= 200 and 0 <= y <= 280
for shape in shapes for x, y in shape))
self.assertEqual(shapes[0], [(0, 280), (200, 0)])
def test_fitting_preserves_in_bounds_coordinates(self):
self.write_line(10, 10, 20, 20)
self.assertEqual(svg2gcode.get_shapes(self.source, True),
svg2gcode.get_shapes(self.source, False))
def test_negative_coordinates_are_shifted_and_scaled(self):
self.write_line(-100, -100, 300, 300, height=200)
shapes = svg2gcode.get_shapes(self.source, True)
self.assertEqual(shapes[0], [(0, 200), (200, 0)])
def test_horizontal_vertical_and_single_point_drawings(self):
for coords, expected in (
((-100, 100, 300, 100), [(0, 0), (200, 0)]),
((100, -100, 100, 300), [(0, 280), (0, 0)]),
((300, 100, 300, 100), [(0, 0)]),
):
with self.subTest(coords=coords):
self.write_line(*coords)
self.assertEqual(svg2gcode.get_shapes(self.source, True)[0],
expected)
def test_fitting_uses_drawing_not_canvas_dimensions(self):
self.write_line(10, 300, 20, 350, width=500, height=500)
self.assertEqual(svg2gcode.get_shapes(self.source, True)[0],
[(10, 200), (20, 150)])
def test_fitting_transformed_coordinates(self):
self.write_line(10, 10, 20, 20, transform="translate(200, 0)")
shapes = svg2gcode.get_shapes(self.source, True)
self.assertTrue(all(0 <= x <= 200 and 0 <= y <= 280
for shape in shapes for x, y in shape))
def test_invalid_bed_dimensions_are_rejected(self):
self.write_line(10, 10, 20, 20)
for dimension in (0, -1, float("inf")):
with patch.object(svg2gcode, "bed_max_x", dimension):
with self.assertRaisesRegex(ValueError, "Bed dimensions"):
svg2gcode.get_shapes(self.source, True)
def test_canvas_size_alone_does_not_reject_in_bounds_drawing(self):
self.write_line(10, 300, 20, 350, width=500, height=500)
self.assertTrue(svg2gcode.get_shapes(self.source, False))
def test_failure_does_not_create_or_overwrite_output(self):
self.write_line(10, 10, 201, 20)
with patch.object(svg2gcode, "optimise_path") as optimise_path:
with self.assertRaisesRegex(ValueError, "X201.000000"):
svg2gcode.main(self.source, self.output)
self.assertFalse(self.output.exists())
self.output.write_text("existing output")
with self.assertRaises(ValueError):
svg2gcode.main(self.source, self.output)
self.assertEqual(self.output.read_text(), "existing output")
optimise_path.assert_not_called()
if __name__ == "__main__":
unittest.main()
+45
View File
@@ -0,0 +1,45 @@
import copy
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
import svg2gcode
from optimise import get_total_distance
class OptimisationTests(unittest.TestCase):
def test_distance_does_not_remove_shapes(self):
shapes = [[(0, 0), (1, 0)], [(4, 0), (5, 0)], [(5, 4)]]
original = copy.deepcopy(shapes)
self.assertEqual(get_total_distance(shapes), 7)
self.assertEqual(get_total_distance(shapes), 7)
self.assertEqual(shapes, original)
def test_empty_and_single_shape_distance(self):
self.assertEqual(get_total_distance([]), 0)
self.assertEqual(get_total_distance([[(0, 0), (1, 1)]]), 0)
def test_single_shape_conversion_retains_all_moves(self):
with tempfile.TemporaryDirectory() as directory:
output = Path(directory) / "drawing.gcode"
shapes = [[(10, 20), (11, 21), (12, 22)]]
with patch.object(svg2gcode, "get_shapes", return_value=shapes), \
patch.object(svg2gcode, "optimise", True):
svg2gcode.main("drawing.svg", output)
text = output.read_text()
for x, y in ((10, 20), (11, 21), (12, 22)):
self.assertIn(f"G1 X{x:.3f} Y{y:.3f}", text)
self.assertTrue(text.endswith("G0 Z2\nG0 X0 Y0\n"))
def test_no_shapes_is_an_explicit_error_without_output(self):
with tempfile.TemporaryDirectory() as directory:
output = Path(directory) / "drawing.gcode"
with patch.object(svg2gcode, "get_shapes", return_value=[]):
with self.assertRaisesRegex(ValueError, "no drawable SVG shapes"):
svg2gcode.main("empty.svg", output)
self.assertFalse(output.exists())
if __name__ == "__main__":
unittest.main()
+1 -10
View File
@@ -4,13 +4,4 @@ def timer(t, label):
duration = dt.now() - t
duration = duration.total_seconds()
print("{} took {}".format(label, duration))
return duration
def write_gcode(output_file_name, data):
t1 = dt.now()
with open(output_file_name, 'w+') as output_file:
for i in data:
output_file.write(i + "\n")
print(f"writing to {output_file_name}")
timer(t1, "writing file ")
return duration