11 Commits
Author SHA1 Message Date
me 5bf9463085 Merge remote-tracking branch 'origin/rofl' into rofl 2020-11-28 16:16:49 -05:00
me 10e1f91a2f fixed autoscale 2020-11-28 16:15:38 -05:00
arcadeperfect 81d3a5e6f0 Update README.md
typos
2020-08-13 20:26:19 -04:00
arcadeperfect d212fba311 fixed autoscale 2020-03-17 00:37:29 -04:00
arcadeperfect 02e44ac912 fixed autoscale 2020-03-16 17:40:03 -04:00
arcadeperfect 0a74e7b39f general tweaks 2020-03-16 16:01:11 -04:00
arcadeperfect 47be43b1d9 remove venv 2020-03-11 16:14:44 -04:00
arcadeperfect ccaea45ab2 remove venv 2020-03-11 15:43:25 -04:00
arcadeperfect 29d89025cb remove venv 2020-03-11 15:41:12 -04:00
arcadeperfect 1ae3241171 json 2020-01-05 14:09:17 -05:00
arcadeperfect 99d43d5691 dump to json before optimise 2020-01-05 13:13:35 -05:00
10 changed files with 404 additions and 308 deletions

No files matched your search

-6
View File
@@ -1,6 +0,0 @@
__pycache__/config.cpython-313.pyc
__pycache__/*.pyc
artwork/*.svg
gcode/*.gcode
-15
View File
@@ -1,15 +0,0 @@
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 "$@"
+11 -42
View File
@@ -1,58 +1,27 @@
## SVG to Gcode converter for GRBL pen plotter
Forked from vishpat/svg2gcode
Forked from [vishpat/svg2gcode](https://github.com/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
>s2g source.svg target.gcode
_copy 's2g' to your path and run from anywhere_
**Convert all SVG files in `artwork/`**
**There is a brute force path optimisator algarithm. Very slow for large files, can be disabled in config.py**
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.
**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
There are various settings you can change in config.py. Most notably, you must set an upper and lower position for the z axis.
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
* 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 = "$H\nG21 G90 G54\nG10 L2 P1 Z0"
preamble = "G90"
"""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 = 2
zTravel = 4
""" height that the z axis will use to draw """
zDraw = 0
""" drawing feed rate in mm/min """
feed_rate = 1600
""" feed rate """
feed_rate = 80000
""" decimal precision of gcode"""
precision = 2
""" shrink and shift out-of-bed drawings to fit, preserving aspect ratio"""
auto_scale = True
""" scale gcode to fit bed size"""
auto_scale = False
""" 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"
+218 -5
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,8 +19,10 @@ def get_distance(a, b, sq=False):
def get_total_distance(shapes):
total_distance = 0
for previous, current in zip(shapes, shapes[1:]):
total_distance += get_distance(current[0], previous[-1], sq=True)
last = shapes.pop(0)[-1]
for i in shapes:
total_distance += get_distance(i[0], last, sq=True)
return total_distance
@@ -31,8 +33,13 @@ 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]
@@ -52,10 +59,216 @@ def optimise_path(shapes, sq=False):
selection = shape
if reverse:
new_order.append([x for x in reversed(selection)])
new_order.append(list(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
@@ -0,0 +1,108 @@
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
+49 -64
View File
@@ -6,25 +6,22 @@ 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
from optimise import optimise_path, get_total_distance, test_edges
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(path, auto_scale=True):
def get_shapes(svg_file, auto_scale=False):
t1 = dt.now()
svg_shapes = set(['rect', 'circle', 'ellipse', 'line', 'polyline', 'polygon', 'path'])
shapes = []
tree = ET.parse(path)
tree = ET.parse(svg_file)
root = tree.getroot()
pointRatio = 0.352778
@@ -41,17 +38,36 @@ def get_shapes(path, auto_scale=True):
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))
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 "px" in width:
# print("px")
# width.replace("px", "")q
#
# if "px" in height:
# print("px")
# height.replace("px", "")
width = float(width)
height = float(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:
@@ -83,6 +99,11 @@ def get_shapes(path, auto_scale=True):
y = -y + height
if auto_scale:
x *= scale_x
y *= scale_y
if first:
#coords.append((x, -y + height))
coords.append((x, y))
@@ -96,41 +117,7 @@ def get_shapes(path, auto_scale=True):
if first:
first = False
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"
)
shapes.append(coords)
timer(t1, "parsing gcode ")
@@ -145,13 +132,19 @@ 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):
def shapes_2_gcode(shapes, test_boundries = False):
t1 = dt.now()
with open("header.txt") as h:
header = h.read()
commands = [f"{header}", preamble, f'F{feed_rate}']
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"))
for i in shapes:
commands += ['', shape_preamble, ""]
@@ -167,42 +160,33 @@ def shapes_2_gcode(shapes):
commands += [" ",postamble, ""]
commands += ["(home)", f"G0 Z{zTravel}", f"G0 X0 Y0"]
commands += ["(home)", f"G0 {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)
if pre_distance:
print("factor: ", post_distance / pre_distance)
else:
print("factor: not applicable (no travel between shapes)")
print("factor: ", post_distance / pre_distance)
commands = shapes_2_gcode(new_order)
@@ -212,7 +196,7 @@ def main(file_path, output):
commands = shapes_2_gcode(shapes)
write_file(output, commands)
write_gcode(output, commands)
print("done")
@@ -223,8 +207,9 @@ 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
@@ -1,122 +0,0 @@
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
@@ -1,45 +0,0 @@
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()
+10 -1
View File
@@ -4,4 +4,13 @@ def timer(t, label):
duration = dt.now() - t
duration = duration.total_seconds()
print("{} took {}".format(label, duration))
return 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 ")