Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5bf9463085 | ||
|
|
10e1f91a2f | ||
|
|
81d3a5e6f0 | ||
|
|
d212fba311 | ||
|
|
02e44ac912 | ||
|
|
0a74e7b39f | ||
|
|
47be43b1d9 | ||
|
|
ccaea45ab2 | ||
|
|
29d89025cb | ||
|
|
1ae3241171 | ||
|
|
99d43d5691 |
No files matched your search
@@ -1,6 +0,0 @@
|
||||
__pycache__/config.cpython-313.pyc
|
||||
|
||||
__pycache__/*.pyc
|
||||
artwork/*.svg
|
||||
gcode/*.gcode
|
||||
|
||||
@@ -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 "$@"
|
||||
@@ -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
|
||||
@@ -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
@@ -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}")
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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 ")
|
||||
Reference in new issue
Block a user