1026 lines
38 KiB
Python
1026 lines
38 KiB
Python
from __future__ import annotations
|
|
|
|
import datetime
|
|
import math
|
|
from typing import Any
|
|
from xml.etree import ElementTree as ET
|
|
|
|
from affine import Affine
|
|
|
|
from boxes.extents import Extents
|
|
|
|
EPS = 1e-4
|
|
PADDING = 10
|
|
|
|
RANDOMIZE_COLORS = False # enable to ease check for continuity of paths
|
|
|
|
|
|
def points_equal(x1, y1, x2, y2):
|
|
return abs(x1 - x2) < EPS and abs(y1 - y2) < EPS
|
|
|
|
|
|
def pdiff(p1, p2):
|
|
x1, y1 = p1
|
|
x2, y2 = p2
|
|
return (x1 - x2, y1 - y2)
|
|
|
|
|
|
class Surface:
|
|
|
|
scale = 1.0
|
|
invert_y = False
|
|
|
|
def __init__(self, fname) -> None:
|
|
self._fname = fname
|
|
self.parts: list[Any] = []
|
|
self._p = self.new_part("default")
|
|
self.count = 0
|
|
|
|
def set_metadata(self, metadata):
|
|
self.metadata = metadata
|
|
|
|
def flush(self):
|
|
pass
|
|
|
|
def finish(self):
|
|
pass
|
|
|
|
def _adjust_coordinates(self):
|
|
extents = self.extents()
|
|
extents.xmin -= PADDING
|
|
extents.ymin -= PADDING
|
|
extents.xmax += PADDING
|
|
extents.ymax += PADDING
|
|
|
|
m = Affine.translation(-extents.xmin, -extents.ymin)
|
|
if self.invert_y:
|
|
m = Affine.scale(self.scale, -self.scale) * m
|
|
m = Affine.translation(0, self.scale*extents.height) * m
|
|
else:
|
|
m = Affine.scale(self.scale, self.scale) * m
|
|
|
|
self.transform(self.scale, m, self.invert_y)
|
|
|
|
return Extents(0, 0, extents.width * self.scale, extents.height * self.scale)
|
|
|
|
def render(self, renderer):
|
|
renderer.init(**self.args)
|
|
for p in self.parts:
|
|
p.render(renderer)
|
|
renderer.finish()
|
|
|
|
def transform(self, f, m, invert_y=False):
|
|
for p in self.parts:
|
|
p.transform(f, m, invert_y)
|
|
|
|
def new_part(self, name="part"):
|
|
if self.parts and len(self.parts[-1].pathes) == 0:
|
|
return self._p
|
|
p = Part(name)
|
|
self.parts.append(p)
|
|
self._p = p
|
|
return p
|
|
|
|
def append(self, *path):
|
|
self.count += 1
|
|
if self.count > 100000:
|
|
raise ValueError("Too many lines")
|
|
self._p.append(*path)
|
|
|
|
def stroke(self, **params):
|
|
return self._p.stroke(**params)
|
|
|
|
def move_to(self, *xy):
|
|
self._p.move_to(*xy)
|
|
|
|
def extents(self):
|
|
if not self.parts:
|
|
return Extents()
|
|
return sum([p.extents() for p in self.parts])
|
|
|
|
|
|
class Part:
|
|
def __init__(self, name) -> None:
|
|
self.pathes: list[Any] = []
|
|
self.path: list[Any] = []
|
|
|
|
def extents(self):
|
|
if not self.pathes:
|
|
return Extents()
|
|
return sum([p.extents() for p in self.pathes])
|
|
|
|
def transform(self, f, m, invert_y=False):
|
|
assert(not self.path)
|
|
for p in self.pathes:
|
|
p.transform(f, m, invert_y)
|
|
|
|
def append(self, *path):
|
|
self.path.append(list(path))
|
|
|
|
def stroke(self, **params):
|
|
if len(self.path) == 0:
|
|
return
|
|
# search for path ending at new start coordinates to append this path to
|
|
xy0 = self.path[0][1:3]
|
|
if (not points_equal(*xy0, *self.path[-1][1:3]) and
|
|
not self.path[0][0] == "T"):
|
|
for p in reversed(self.pathes):
|
|
xy1 = p.path[-1][1:3]
|
|
if points_equal(*xy0, *xy1) and p.params == params:
|
|
p.path.extend(self.path[1:])
|
|
self.path = []
|
|
return p
|
|
p = Path(self.path, params)
|
|
self.pathes.append(p)
|
|
self.path = []
|
|
return p
|
|
|
|
def move_to(self, *xy):
|
|
if len(self.path) == 0:
|
|
self.path.append(["M", *xy])
|
|
elif self.path[-1][0] == "M":
|
|
self.path[-1] = ["M", *xy]
|
|
else:
|
|
xy0 = self.path[-1][1:3]
|
|
if not points_equal(*xy0, *xy):
|
|
self.path.append(["M", *xy])
|
|
|
|
|
|
class Path:
|
|
def __init__(self, path, params) -> None:
|
|
self.path = path
|
|
self.params = params
|
|
|
|
def __repr__(self) -> str:
|
|
l = len(self.path)
|
|
# x1,y1 = self.path[0][1:3]
|
|
if l>0:
|
|
x2, y2 = self.path[-1][1:3]
|
|
return f"Path[{l}] to ({x2:.2f},{y2:.2f})"
|
|
return f"empty Path"
|
|
|
|
def extents(self):
|
|
e = Extents()
|
|
for p in self.path:
|
|
e.add(*p[1:3])
|
|
if p[0] == 'T':
|
|
m, text, params = p[3:]
|
|
h = params['fs']
|
|
l = len(text) * h * 0.7
|
|
align = params.get('align', 'left')
|
|
start, end = {
|
|
'left' : (0, 1),
|
|
'middle' : (-0.5, 0.5),
|
|
'end' : (-1, 0),
|
|
}[align]
|
|
for x in (start*l, end*l):
|
|
for y in (0, h):
|
|
x_, y_ = m * (x, y)
|
|
e.add(x_, y_)
|
|
return e
|
|
|
|
def transform(self, f, m, invert_y=False):
|
|
self.params["lw"] *= f
|
|
for c in self.path:
|
|
C = c[0]
|
|
c[1], c[2] = m * (c[1], c[2])
|
|
if C == 'C':
|
|
c[3], c[4] = m * (c[3], c[4])
|
|
c[5], c[6] = m * (c[5], c[6])
|
|
if C == "T":
|
|
c[3] = m * c[3]
|
|
if invert_y:
|
|
c[3] *= Affine.scale(1, -1)
|
|
|
|
def faster_edges(self, inner_corners):
|
|
if inner_corners == "backarc":
|
|
return
|
|
|
|
for (i, p) in enumerate(self.path):
|
|
if p[0] == "C" and i > 1 and i < len(self.path) - 1:
|
|
if self.path[i - 1][0] == "L" and self.path[i + 1][0] == "L":
|
|
p11 = self.path[i - 2][1:3]
|
|
p12 = self.path[i - 1][1:3]
|
|
p21 = p[1:3]
|
|
p22 = self.path[i + 1][1:3]
|
|
if (((p12[0]-p21[0])**2 + (p12[1]-p21[1])**2) >
|
|
self.params["lw"]**2):
|
|
continue
|
|
lines_intersect, x, y = line_intersection((p11, p12), (p21, p22))
|
|
if lines_intersect:
|
|
self.path[i - 1] = ("L", x, y)
|
|
if inner_corners == "loop":
|
|
self.path[i] = ("C", x, y, *p12, *p21)
|
|
else:
|
|
self.path[i] = ("L", x, y)
|
|
# filter duplicates
|
|
if len(self.path) > 1: # no need to find duplicates if only one element in path
|
|
self.path = [p for n, p in enumerate(self.path) if p != self.path[n-1]]
|
|
|
|
class Context:
|
|
def __init__(self, surface, *al, **ad) -> None:
|
|
self._renderer = self._dwg = surface
|
|
|
|
self._bounds = Extents()
|
|
self._padding = PADDING
|
|
|
|
self._stack: list[Any] = []
|
|
self._m = Affine.translation(0, 0)
|
|
self._xy = (0, 0)
|
|
self._mxy = self._m * self._xy
|
|
self._lw = 0
|
|
self._rgb = (0, 0, 0)
|
|
self._ff = "sans-serif"
|
|
self._fs = 10
|
|
self._last_path = None
|
|
|
|
def _update_bounds_(self, mx, my):
|
|
self._bounds.update(mx, my)
|
|
|
|
def save(self):
|
|
self._stack.append(
|
|
(self._m, self._xy, self._lw, self._rgb, self._mxy, self._last_path)
|
|
)
|
|
self._xy = (0, 0)
|
|
|
|
def restore(self):
|
|
(
|
|
self._m,
|
|
self._xy,
|
|
self._lw,
|
|
self._rgb,
|
|
self._mxy,
|
|
self._last_path,
|
|
) = self._stack.pop()
|
|
|
|
## transformations
|
|
|
|
def translate(self, x, y):
|
|
self._m *= Affine.translation(x, y)
|
|
self._xy = (0, 0)
|
|
|
|
def scale(self, sx, sy):
|
|
self._m *= Affine.scale(sx, sy)
|
|
|
|
def rotate(self, r):
|
|
self._m *= Affine.rotation(180 * r / math.pi)
|
|
|
|
def set_line_width(self, lw):
|
|
self._lw = lw
|
|
|
|
def set_source_rgb(self, r, g, b):
|
|
self._rgb = (r, g, b)
|
|
|
|
## path methods
|
|
|
|
def _line_to(self, x, y):
|
|
self._add_move()
|
|
x1, y1 = self._mxy
|
|
self._xy = x, y
|
|
x2, y2 = self._mxy = self._m * self._xy
|
|
if not points_equal(x1, y1, x2, y2):
|
|
self._dwg.append("L", x2, y2)
|
|
|
|
def _add_move(self):
|
|
self._dwg.move_to(*self._mxy)
|
|
|
|
def move_to(self, x, y):
|
|
self._xy = (x, y)
|
|
self._mxy = self._m * self._xy
|
|
|
|
def line_to(self, x, y):
|
|
self._line_to(x, y)
|
|
|
|
def _arc(self, xc, yc, radius, angle1, angle2, direction):
|
|
if abs(angle1 - angle2) < EPS or radius < EPS:
|
|
return
|
|
x1, y1 = radius * math.cos(angle1) + xc, radius * math.sin(angle1) + yc
|
|
x4, y4 = radius * math.cos(angle2) + xc, radius * math.sin(angle2) + yc
|
|
|
|
# XXX direction seems not needed for small arcs
|
|
ax = x1 - xc
|
|
ay = y1 - yc
|
|
bx = x4 - xc
|
|
by = y4 - yc
|
|
q1 = ax * ax + ay * ay
|
|
q2 = q1 + ax * bx + ay * by
|
|
k2 = 4/3 * ((2 * q1 * q2)**0.5 - q2) / (ax * by - ay * bx)
|
|
|
|
x2 = xc + ax - k2 * ay
|
|
y2 = yc + ay + k2 * ax
|
|
x3 = xc + bx + k2 * by
|
|
y3 = yc + by - k2 * bx
|
|
|
|
mx1, my1 = self._m * (x1, y1)
|
|
mx2, my2 = self._m * (x2, y2)
|
|
mx3, my3 = self._m * (x3, y3)
|
|
mx4, my4 = self._m * (x4, y4)
|
|
mxc, myc = self._m * (xc, yc)
|
|
|
|
self._add_move()
|
|
self._dwg.append("C", mx4, my4, mx2, my2, mx3, my3)
|
|
self._xy = (x4, y4)
|
|
self._mxy = (mx4, my4)
|
|
|
|
def arc(self, xc, yc, radius, angle1, angle2):
|
|
self._arc(xc, yc, radius, angle1, angle2, 1)
|
|
|
|
def arc_negative(self, xc, yc, radius, angle1, angle2):
|
|
self._arc(xc, yc, radius, angle1, angle2, -1)
|
|
|
|
def curve_to(self, x1, y1, x2, y2, x3, y3):
|
|
# mx0,my0 = self._m*self._xy
|
|
mx1, my1 = self._m * (x1, y1)
|
|
mx2, my2 = self._m * (x2, y2)
|
|
mx3, my3 = self._m * (x3, y3)
|
|
self._add_move()
|
|
self._dwg.append("C", mx3, my3, mx1, my1, mx2, my2) # destination first!
|
|
self._xy = (x3, y3)
|
|
self._mxy = (mx3, my3)
|
|
|
|
def stroke(self):
|
|
# print('stroke stack-level=',len(self._stack),'lastpath=',self._last_path,)
|
|
self._last_path = self._dwg.stroke(rgb=self._rgb, lw=self._lw)
|
|
self._xy = (0, 0)
|
|
|
|
def fill(self):
|
|
self._xy = (0, 0)
|
|
raise NotImplementedError()
|
|
|
|
def set_font(self, style, bold=False, italic=False):
|
|
if style not in ("serif", "sans-serif", "monospaced"):
|
|
raise ValueError("Unknown font style")
|
|
self._ff = (style, bold, italic)
|
|
|
|
def set_font_size(self, fs):
|
|
self._fs = fs
|
|
|
|
def show_text(self, text, **args):
|
|
params = {"ff": self._ff, "fs": self._fs, "lw": self._lw, "rgb": self._rgb}
|
|
params.update(args)
|
|
mx0, my0 = self._m * self._xy
|
|
m = self._m
|
|
self._dwg.append("T", mx0, my0, m, text, params)
|
|
|
|
def text_extents(self, text):
|
|
fs = self._fs
|
|
# XXX ugly hack! Fix Boxes.text() !
|
|
return (0, 0, 0.6 * fs * len(text), 0.65 * fs, fs * 0.1, 0)
|
|
|
|
def rectangle(self, x, y, width, height):
|
|
|
|
# todo: better check for empty path?
|
|
self.stroke()
|
|
|
|
self.move_to(x, y)
|
|
self.line_to(x + width, y)
|
|
self.line_to(x + width, y + height)
|
|
self.line_to(x, y + height)
|
|
self.line_to(x, y)
|
|
self.stroke()
|
|
|
|
def get_current_point(self):
|
|
return self._xy
|
|
|
|
def flush(self):
|
|
pass
|
|
# todo: check, if needed
|
|
# self.stroke()
|
|
|
|
## additional methods
|
|
def new_part(self):
|
|
self._dwg.new_part()
|
|
|
|
|
|
class SVGSurface(Surface):
|
|
|
|
invert_y = True
|
|
|
|
fonts = {
|
|
'serif' : 'TimesNewRoman, "Times New Roman", Times, Baskerville, Georgia, serif',
|
|
'sans-serif' : '"Helvetica Neue", Helvetica, Arial, sans-serif',
|
|
'monospaced' : '"Courier New", Courier, "Lucida Sans Typewriter"'
|
|
}
|
|
|
|
def _addTag(self, parent, tag, text, first=False):
|
|
if first:
|
|
t = ET.Element(tag)
|
|
else:
|
|
t = ET.SubElement(parent, tag)
|
|
t.text = text
|
|
t.tail = '\n'
|
|
if first:
|
|
parent.insert(0, t)
|
|
return t
|
|
|
|
def _add_metadata(self, root):
|
|
md = self.metadata
|
|
|
|
# Add Inkscape style rdf meta data
|
|
root.set("xmlns:dc", "http://purl.org/dc/elements/1.1/")
|
|
root.set("xmlns:cc", "http://creativecommons.org/ns#")
|
|
root.set("xmlns:rdf","http://www.w3.org/1999/02/22-rdf-syntax-ns#")
|
|
|
|
title = "{group} - {name}".format(**md)
|
|
date = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
|
|
m = self._addTag(root, "metadata", '\n', True)
|
|
r = ET.SubElement(m, 'rdf:RDF')
|
|
w = ET.SubElement(r, 'cc:Work')
|
|
w.text = '\n'
|
|
|
|
self._addTag(w, 'dc:title', title)
|
|
self._addTag(w, 'dc:date', date)
|
|
|
|
if "url" in md and md["url"]:
|
|
self._addTag(w, 'dc:source', md["url"])
|
|
self._addTag(w, 'dc:source', md["url_short"])
|
|
else:
|
|
self._addTag(w, 'dc:source', md["cli"])
|
|
|
|
desc = md["short_description"] or ""
|
|
if "description" in md and md["description"]:
|
|
desc += "\n\n" + md["description"]
|
|
desc += "\n\nCreated with Boxes.py (https://festi.info/boxes.py)\n"
|
|
desc += "Command line: %s\n" % md["cli"]
|
|
desc += "Command line short: %s\n" % md["cli_short"]
|
|
if md["url"]:
|
|
desc += "Url: %s\n" % md["url"]
|
|
desc += "Url short: %s\n" % md["url_short"]
|
|
desc += "SettingsUrl: %s\n" % md["url"].replace("&render=1", "")
|
|
desc += "SettingsUrl short: %s\n" % md["url_short"].replace("&render=1", "")
|
|
self._addTag(w, 'dc:description', desc)
|
|
|
|
# title
|
|
self._addTag(root, "title", md["name"], True)
|
|
|
|
# Add XML comment
|
|
txt = """
|
|
{name} - {short_description}
|
|
""".format(**md)
|
|
if md["description"]:
|
|
txt += """
|
|
|
|
{description}
|
|
|
|
""".format(**md)
|
|
txt += """
|
|
Created with Boxes.py (https://festi.info/boxes.py)
|
|
Creation date: {date}
|
|
""".format(date=date, **md)
|
|
|
|
txt += "Command line (remove spaces between dashes): %s\n" % md["cli_short"]
|
|
|
|
if md["url"]:
|
|
txt += "Url: %s\n" % md["url"]
|
|
txt += "Url short: %s\n" % md["url_short"]
|
|
txt += "SettingsUrl: %s\n" % md["url"].replace("&render=1", "")
|
|
txt += "SettingsUrl short: %s\n" % md["url_short"].replace("&render=1", "")
|
|
m = ET.Comment(txt.replace("--", "- -").replace("--", "- -")) # ----
|
|
m.tail = '\n'
|
|
root.insert(0, m)
|
|
|
|
def finish(self, inner_corners="loop"):
|
|
extents = self._adjust_coordinates()
|
|
w = extents.width * self.scale
|
|
h = extents.height * self.scale
|
|
|
|
|
|
nsmap = {
|
|
"dc": "http://purl.org/dc/elements/1.1/",
|
|
"cc": "http://creativecommons.org/ns#",
|
|
"rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
|
|
"svg": "http://www.w3.org/2000/svg",
|
|
"xlink": "http://www.w3.org/1999/xlink",
|
|
"inkscape": "http://www.inkscape.org/namespaces/inkscape",
|
|
}
|
|
ET.register_namespace("", "http://www.w3.org/2000/svg")
|
|
ET.register_namespace("xlink", "http://www.w3.org/1999/xlink")
|
|
svg = ET.Element('svg', width=f"{w:.2f}mm", height=f"{h:.2f}mm",
|
|
viewBox=f"0.0 0.0 {w:.2f} {h:.2f}",
|
|
xmlns="http://www.w3.org/2000/svg")
|
|
for name, value in nsmap.items():
|
|
svg.set(f"xmlns:{name}", value)
|
|
svg.text = "\n"
|
|
tree = ET.ElementTree(svg)
|
|
|
|
self._add_metadata(svg)
|
|
|
|
for i, part in enumerate(self.parts):
|
|
if not part.pathes:
|
|
continue
|
|
g = ET.SubElement(svg, "g", id=f"p-{i}",
|
|
style="fill:none;stroke-linecap:round;stroke-linejoin:round;")
|
|
g.text = "\n "
|
|
g.tail = "\n"
|
|
for j, path in enumerate(part.pathes):
|
|
p = []
|
|
x, y = 0, 0
|
|
start = None
|
|
last = None
|
|
path.faster_edges(inner_corners)
|
|
for c in path.path:
|
|
x0, y0 = x, y
|
|
C, x, y = c[0:3]
|
|
if C == "M":
|
|
if start and points_equal(start[1], start[2],
|
|
last[1], last[2]):
|
|
p.append("Z")
|
|
start = c
|
|
p.append(f"M {x:.3f} {y:.3f}")
|
|
elif C == "L":
|
|
if abs(x - x0) < EPS:
|
|
p.append(f"V {y:.3f}")
|
|
elif abs(y - y0) < EPS:
|
|
p.append(f"H {x:.3f}")
|
|
else:
|
|
p.append(f"L {x:.3f} {y:.3f}")
|
|
elif C == "C":
|
|
x1, y1, x2, y2 = c[3:]
|
|
p.append(
|
|
f"C {x1:.3f} {y1:.3f} {x2:.3f} {y2:.3f} {x:.3f} {y:.3f}"
|
|
)
|
|
elif C == "T":
|
|
m, text, params = c[3:]
|
|
m = m * Affine.translation(0, -params['fs'])
|
|
tm = " ".join(f"{m[i]:.3f}" for i in (0, 3, 1, 4, 2, 5))
|
|
font, bold, italic = params['ff']
|
|
fontweight = ("normal", "bold")[bool(bold)]
|
|
fontstyle = ("normal", "italic")[bool(italic)]
|
|
|
|
style = f"font-family: {font} ; font-weight: {fontweight}; font-style: {fontstyle}; fill: {rgb_to_svg_color(*params['rgb'])}"
|
|
t = ET.SubElement(g, "text",
|
|
#x=f"{x:.3f}", y=f"{y:.3f}",
|
|
transform=f"matrix( {tm} )",
|
|
style=style)
|
|
t.text = text
|
|
t.set("font-size", f"{params['fs']}px")
|
|
t.set("text-anchor", params.get('align', 'left'))
|
|
t.set("dominant-baseline", 'hanging')
|
|
else:
|
|
print("Unknown", c)
|
|
|
|
last = c
|
|
|
|
if start and start is not last and \
|
|
points_equal(start[1], start[2], last[1], last[2]):
|
|
p.append("Z")
|
|
color = (
|
|
random_svg_color()
|
|
if RANDOMIZE_COLORS
|
|
else rgb_to_svg_color(*path.params["rgb"])
|
|
)
|
|
if p and p[-1][0] == "M":
|
|
p.pop()
|
|
if p: # might be empty if only contains text
|
|
t = ET.SubElement(g, "path", d=" ".join(p), stroke=color)
|
|
t.set("stroke-width", f'{path.params["lw"]:.2f}')
|
|
t.tail = "\n "
|
|
t.tail = "\n"
|
|
tree.write(open(self._fname, "wb"), encoding="utf-8", xml_declaration=True, method="xml")
|
|
|
|
class PSSurface(Surface):
|
|
|
|
scale = 72 / 25.4 # 72 dpi
|
|
|
|
fonts = {
|
|
('serif', False, False) : 'Times-Roman',
|
|
('serif', False, True) : 'Times-Italic',
|
|
('serif', True, False) : 'Times-Bold',
|
|
('serif', True, True) : 'Times-BoldItalic',
|
|
('sans-serif', False, False) : 'Helvetica',
|
|
('sans-serif', False, True) : 'Helvetica-Oblique',
|
|
('sans-serif', True, False) : 'Helvetica-Bold',
|
|
('sans-serif', True, True) : 'Helvetica-BoldOblique',
|
|
('monospaced', False, False) : 'Courier',
|
|
('monospaced', False, True) : 'Courier-Oblique',
|
|
('monospaced', True, False) : 'Courier-Bold',
|
|
('monospaced', True, True) : 'Courier-BoldOblique',
|
|
}
|
|
|
|
def _metadata(self):
|
|
md = self.metadata
|
|
|
|
desc = ""
|
|
desc += "%%Title: Boxes.py - {group} - {name}\n".format(**md)
|
|
desc += f'%%CreationDate: {datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}\n'
|
|
desc += f'%%Keywords: boxes.py, laser, laser cutter\n'
|
|
desc += f'%%Creator: {md.get("url") or md["cli"]}\n'
|
|
desc += "%%CreatedBy: Boxes.py (https://festi.info/boxes.py)\n"
|
|
for line in (md["short_description"] or "").split("\n"):
|
|
desc += "%% %s\n" % line
|
|
desc += "%\n"
|
|
if "description" in md and md["description"]:
|
|
desc += "%\n"
|
|
for line in md["description"].split("\n"):
|
|
desc += "%% %s\n" % line
|
|
desc += "%\n"
|
|
|
|
desc += "%% Command line: %s\n" % md["cli"]
|
|
desc += "%% Command line short: %s\n" % md["cli_short"]
|
|
if md["url"]:
|
|
desc += f'%%Url: {md["url"]}\n'
|
|
desc += f'%%Url short: {md["url_short"]}\n'
|
|
desc += f'%%SettingsUrl: {md["url"].replace("&render=1", "")}\n'
|
|
desc += f'%%SettingsUrl short: {md["url_short"].replace("&render=1", "")}\n'
|
|
return desc
|
|
|
|
def finish(self, inner_corners="loop"):
|
|
|
|
extents = self._adjust_coordinates()
|
|
w = extents.width
|
|
h = extents.height
|
|
|
|
f = open(self._fname, "w", encoding="latin1", errors="replace")
|
|
|
|
f.write(f"""%!PS-Adobe-2.0 EPSF-2.0
|
|
%%BoundingBox: 0 0 {w:.0f} {h:.0f}
|
|
{self._metadata()}
|
|
%%EndComments
|
|
|
|
1 setlinecap
|
|
1 setlinejoin
|
|
0.0 0.0 0.0 setrgbcolor
|
|
""")
|
|
f.write("""
|
|
/ReEncode { % inFont outFont encoding | -
|
|
/MyEncoding exch def
|
|
exch findfont
|
|
dup length dict
|
|
begin
|
|
{def} forall
|
|
/Encoding MyEncoding def
|
|
currentdict
|
|
end
|
|
definefont
|
|
} def
|
|
|
|
""")
|
|
for font in self.fonts.values():
|
|
f.write(f"/{font} /{font}-Latin1 ISOLatin1Encoding ReEncode\n")
|
|
# f.write(f"%%DocumentMedia: \d+x\d+mm ((\d+) (\d+)) 0 \("
|
|
# dwg['width']=f'{w:.2f}mm'
|
|
# dwg['height']=f'{h:.2f}mm'
|
|
|
|
for i, part in enumerate(self.parts):
|
|
if not part.pathes:
|
|
continue
|
|
for j, path in enumerate(part.pathes):
|
|
p = []
|
|
x, y = 0, 0
|
|
path.faster_edges(inner_corners)
|
|
|
|
for c in path.path:
|
|
x0, y0 = x, y
|
|
C, x, y = c[0:3]
|
|
if C == "M":
|
|
p.append(f"{x:.3f} {y:.3f} moveto")
|
|
elif C == "L":
|
|
p.append(f"{x:.3f} {y:.3f} lineto")
|
|
elif C == "C":
|
|
x1, y1, x2, y2 = c[3:]
|
|
p.append(
|
|
f"{x1:.3f} {y1:.3f} {x2:.3f} {y2:.3f} {x:.3f} {y:.3f} curveto"
|
|
)
|
|
elif C == "T":
|
|
m, text, params = c[3:]
|
|
tm = " ".join(f"{m[i]:.3f}" for i in (0, 3, 1, 4, 2, 5))
|
|
text = text.replace("(", "r\(").replace(")", r"\)")
|
|
color = " ".join(f"{c:.2f}" for c in params["rgb"])
|
|
align = params.get('align', 'left')
|
|
f.write(f"/{self.fonts[params['ff']]}-Latin1 findfont\n")
|
|
f.write(f"{params['fs']} scalefont\n")
|
|
f.write("setfont\n")
|
|
#f.write(f"currentfont /Encoding ISOLatin1Encoding put\n")
|
|
f.write(f"{color} setrgbcolor\n")
|
|
f.write("matrix currentmatrix") # save current matrix
|
|
f.write(f"[ {tm} ] concat\n")
|
|
if align == "left":
|
|
f.write(f"0.0\n")
|
|
else:
|
|
f.write(f"({text}) stringwidth pop ")
|
|
if align == "middle":
|
|
f.write(f"-0.5 mul\n")
|
|
else: # end
|
|
f.write(f"neg\n")
|
|
# offset y by descender
|
|
f.write("currentfont dup /FontBBox get 1 get \n")
|
|
f.write("exch /FontMatrix get 3 get mul neg moveto \n")
|
|
|
|
f.write(f"({text}) show\n") # text created by dup above
|
|
f.write("setmatrix\n\n") # restore matrix
|
|
else:
|
|
print("Unknown", c)
|
|
color = (
|
|
random_svg_color()
|
|
if RANDOMIZE_COLORS
|
|
else rgb_to_svg_color(*path.params["rgb"])
|
|
)
|
|
if p: # todo: might be empty since text is not implemented yet
|
|
color = " ".join(f"{c:.2f}" for c in path.params["rgb"])
|
|
f.write("newpath\n")
|
|
f.write("\n".join(p))
|
|
f.write("\n")
|
|
f.write(f"{path.params['lw']} setlinewidth\n")
|
|
f.write(f"{color} setrgbcolor\n")
|
|
f.write("stroke\n\n")
|
|
f.write(
|
|
"""
|
|
showpage
|
|
%%Trailer
|
|
%%EOF
|
|
"""
|
|
)
|
|
f.close()
|
|
|
|
class LBRN2Surface(Surface):
|
|
|
|
|
|
invert_y = False
|
|
dbg = False
|
|
|
|
fonts = {
|
|
'serif' : 'Times New Roman',
|
|
'sans-serif' : 'Arial',
|
|
'monospaced' : 'Courier New'
|
|
}
|
|
|
|
lbrn2_colors=[
|
|
0, # Colors.OUTER_CUT (BLACK) --> Lightburn C00 (black)
|
|
1, # Colors.INNER_CUT (BLUE) --> Lightburn C01 (blue)
|
|
3, # Colors.ETCHING (GREEN) --> Lightburn C02 (green)
|
|
6, # Colors.ETCHING_DEEP (CYAN) --> Lightburn C06 (cyan)
|
|
30, # Colors.ANNOTATIONS (RED) --> Lightburn T1
|
|
7, # Colors.OUTER_CUT (MAGENTA) --> Lightburn C07 (magenta)
|
|
4, # Colors.OUTER_CUT (YELLOW) --> Lightburn C04 (yellow)
|
|
8, # Colors.OUTER_CUT (WHITE) --> Lightburn C08 (grey)
|
|
]
|
|
|
|
def finish(self, inner_corners="loop"):
|
|
if self.dbg: print("LBRN2 save")
|
|
extents = self._adjust_coordinates()
|
|
w = extents.width * self.scale
|
|
h = extents.height * self.scale
|
|
|
|
svg = ET.Element('LightBurnProject', AppVersion="1.0.06", FormatVersion="1", MaterialHeight="0", MirrorX="False", MirrorY="False")
|
|
svg.text = "\n"
|
|
num = 0
|
|
txtOffset = {}
|
|
|
|
tree = ET.ElementTree(svg)
|
|
if self.dbg: print ("8", num)
|
|
|
|
cs = ET.SubElement(svg, "CutSetting", Type="Cut")
|
|
index = ET.SubElement(cs, "index", Value="3") # green layer (ETCHING)
|
|
name = ET.SubElement(cs, "name", Value="Etch")
|
|
priority = ET.SubElement(cs, "priority", Value="0") # is cut first
|
|
|
|
cs = ET.SubElement(svg, "CutSetting", Type="Cut")
|
|
index = ET.SubElement(cs, "index", Value="6") # cyan layer (ETCHING_DEEP)
|
|
name = ET.SubElement(cs, "name", Value="Deep Etch")
|
|
priority = ET.SubElement(cs, "priority", Value="1") # is cut second
|
|
|
|
cs = ET.SubElement(svg, "CutSetting", Type="Cut")
|
|
index = ET.SubElement(cs, "index", Value="7") # magenta layer (MAGENTA)
|
|
name = ET.SubElement(cs, "name", Value="C07")
|
|
priority = ET.SubElement(cs, "priority", Value="2") # is cut third
|
|
|
|
cs = ET.SubElement(svg, "CutSetting", Type="Cut")
|
|
index = ET.SubElement(cs, "index", Value="4") # yellow layer (YELLOW)
|
|
name = ET.SubElement(cs, "name", Value="C04")
|
|
priority = ET.SubElement(cs, "priority", Value="3") # is cut third
|
|
|
|
cs = ET.SubElement(svg, "CutSetting", Type="Cut")
|
|
index = ET.SubElement(cs, "index", Value="8") # grey layer (WHITE)
|
|
name = ET.SubElement(cs, "name", Value="C08")
|
|
priority = ET.SubElement(cs, "priority", Value="4") # is cut fourth
|
|
|
|
cs = ET.SubElement(svg, "CutSetting", Type="Cut")
|
|
index = ET.SubElement(cs, "index", Value="1") # blue layer (INNER_CUT)
|
|
name = ET.SubElement(cs, "name", Value="Inner Cut")
|
|
priority = ET.SubElement(cs, "priority", Value="5") # is cut fifth
|
|
|
|
cs = ET.SubElement(svg, "CutSetting", Type="Cut")
|
|
index = ET.SubElement(cs, "index", Value="0") # black layer (OUTER_CUT)
|
|
name = ET.SubElement(cs, "name", Value="Outer Cut")
|
|
priority = ET.SubElement(cs, "priority", Value="6") # is cut sixth
|
|
|
|
cs = ET.SubElement(svg, "CutSetting", Type="Tool")
|
|
index = ET.SubElement(cs, "index", Value="30") # T1 layer (ANNOTATIONS)
|
|
name = ET.SubElement(cs, "name", Value="T1") # tool layer do not support names
|
|
priority = ET.SubElement(cs, "priority", Value="7") # is not cut at all
|
|
|
|
for i, part in enumerate(self.parts):
|
|
if self.dbg: print ("7", num)
|
|
if not part.pathes:
|
|
continue
|
|
gp = ET.SubElement(svg, "Shape", Type="Group")
|
|
gp.text = "\n "
|
|
gp.tail = "\n"
|
|
children = ET.SubElement(gp, "Children")
|
|
children.text = "\n "
|
|
children.tail = "\n"
|
|
|
|
for j, path in enumerate(part.pathes):
|
|
myColor = self.lbrn2_colors[4*int(path.params["rgb"][0])+2*int(path.params["rgb"][1])+int(path.params["rgb"][2])]
|
|
|
|
p = []
|
|
x, y = 0, 0
|
|
C = ""
|
|
start = None
|
|
last = None
|
|
path.faster_edges(inner_corners)
|
|
num = 0
|
|
cnt = 1
|
|
ende = len(path.path)-1
|
|
if self.dbg:
|
|
for c in path.path:
|
|
print ("6",num, c)
|
|
num += 1
|
|
num = 0
|
|
|
|
c = path.path[num]
|
|
C, x, y = c[0:3]
|
|
if self.dbg: print("ende:" ,ende)
|
|
while num < ende or (C == "T" and num <= ende): #len(path.path):
|
|
if self.dbg: print ("0", num)
|
|
c = path.path[num]
|
|
if self.dbg: print("first: ", num, c)
|
|
|
|
C, x, y = c[0:3]
|
|
if C == "M":
|
|
if self.dbg: print ("1", num)
|
|
sh = ET.SubElement(children, "Shape", Type="Path", CutIndex=str(myColor))
|
|
sh.text = "\n "
|
|
sh.tail = "\n"
|
|
vl = ET.SubElement(sh, "VertList")
|
|
vl.text = f"V{x:.3f} {y:.3f}c0x1c1x1"
|
|
vl.tail = "\n"
|
|
pl = ET.SubElement(sh, "PrimList")
|
|
pl.text = ""#f"L{cnt} {cnt+1}"
|
|
pl.tail = "\n"
|
|
start = c
|
|
x0, y0 = x, y
|
|
# do something with M
|
|
done = False
|
|
bspline = False
|
|
while done == False and num < ende: #len(path.path):
|
|
num += 1
|
|
c = path.path[num]
|
|
if self.dbg: print ("next: ",num, c)
|
|
C, x, y = c[0:3]
|
|
if C == "M":
|
|
if start and points_equal(start[1], start[2], x, y):
|
|
pl.text = "LineClosed"
|
|
start = c
|
|
cnt = 1
|
|
if self.dbg: print ("next, because M")
|
|
done = True
|
|
elif C == "T":
|
|
if self.dbg: print ("next, because T")
|
|
done = True
|
|
else:
|
|
if C == "L":
|
|
vl.text+=(f"V{x:.3f} {y:.3f}c0x1c1x1")
|
|
pl.text += f"L{cnt-1} {cnt}"
|
|
cnt +=1
|
|
elif C == "C":
|
|
x1, y1, x2, y2 = c[3:]
|
|
if self.dbg: print ("C: ",x0, y0, x1, y1, x, y, x2, y2)
|
|
vl.text+=(f"V{x0:.3f} {y0:.3f}c0x{(x1):.3f}c0y{(y1):.3f}c1x1V{x:.3f} {y:.3f}c0x1c1x{(x2):.3f}c1y{(y2):.3f}")
|
|
pl.text += f"L{cnt-1} {cnt}B{cnt} {cnt+1}"
|
|
cnt +=2
|
|
bspline = True
|
|
else:
|
|
print("unknown", c)
|
|
if done == False:
|
|
x0, y0 = x, y
|
|
|
|
if start and points_equal(start[1], start[2], x0, y0):
|
|
if bspline == False:
|
|
pl.text = "LineClosed"
|
|
start = c
|
|
if self.dbg: print ("2", num)
|
|
elif C == "T":
|
|
cnt = 1
|
|
#C = ""
|
|
if self.dbg: print ("3", num)
|
|
m, text, params = c[3:]
|
|
m = m * Affine.translation(0, params['fs'])
|
|
if self.dbg: print ("T: ",x, y, c)
|
|
num += 1
|
|
font, bold, italic = params['ff']
|
|
if params.get('font', 'Arial')=='Arial':
|
|
f = self.fonts[font]
|
|
else:
|
|
f = params.get('font', 'Arial')
|
|
fontColor = self.lbrn2_colors[4*int(params["rgb"][0])+2*int(params["rgb"][1])+int(params["rgb"][2])]
|
|
|
|
#alignment can be left|middle|end
|
|
if params.get('align', 'left')=='middle':
|
|
hor = '1'
|
|
else:
|
|
if params.get('align', 'left')=='end':
|
|
hor = '2'
|
|
else:
|
|
hor = '0'
|
|
ver = 1 # vertical is always bottom, text is shifted in box class
|
|
|
|
pos = text.find('%')
|
|
offs = 0
|
|
if pos >- 1:
|
|
if self.dbg: print ("p: ", pos, text[pos+1:pos+3])
|
|
texttype = '2'
|
|
if self.dbg: print("l ", len(text[pos+1:pos+3]))
|
|
if text[pos+1:pos+2].isnumeric():
|
|
if self.dbg: print ("t0", text[pos+1:pos+3])
|
|
if text[pos+1:pos+3].isnumeric() and len(text[pos+1:pos+3]) == 2:
|
|
if self.dbg: print ("t1")
|
|
if text[pos:pos+3] in txtOffset:
|
|
if self.dbg: print ("t2")
|
|
offs = txtOffset[text[pos:pos+3]] + 1
|
|
else:
|
|
if self.dbg: print ("t3")
|
|
offs = 0
|
|
txtOffset[text[pos:pos+3]] = offs
|
|
else:
|
|
if self.dbg: print ("t4")
|
|
if text[pos:pos+2] in txtOffset:
|
|
if self.dbg: print ("t5")
|
|
offs = txtOffset[text[pos:pos+2]] + 1
|
|
else:
|
|
offs = 0
|
|
if self.dbg: print ("t6")
|
|
txtOffset[text[pos:pos+2]] = offs
|
|
else:
|
|
if self.dbg: print ("t7")
|
|
texttype = '0'
|
|
else:
|
|
texttype = '0'
|
|
if self.dbg: print ("t8")
|
|
if self.dbg: print ("o: ", text, txtOffset, offs)
|
|
|
|
if not text:
|
|
if self.dbg: print ("T: text with empty string - ",x, y, c)
|
|
else:
|
|
sh = ET.SubElement(children, "Shape", Type="Text", CutIndex=str(fontColor), Font=f"{f}", H=f"{(params['fs']*1.75*0.6086434):.3f}", Str=f"{text}", Bold=f"{'1' if bold else '0'}", Italic=f"{'1' if italic else '0'}", Ah=f"{str(hor)}", Av=f"{str(ver)}", Eval=f"{texttype}", VariableOffset=f"{str(offs)}") # 1mm = 1.75 Lightburn H units
|
|
sh.text = "\n "
|
|
sh.tail = "\n"
|
|
xf = ET.SubElement(sh, "XForm")
|
|
xf.text = " ".join(f"{m[i]:.3f}" for i in (0, 3, 1, 4, 2, 5))
|
|
xf.tail = "\n"
|
|
else:
|
|
if self.dbg: print ("4", num)
|
|
print ("next, because not M")
|
|
num += 1
|
|
|
|
url = self.metadata["url"].replace("&render=1", "") # remove render argument to get web form again
|
|
|
|
pl = ET.SubElement(svg, "Notes", ShowOnLoad="1", Notes="File created by Boxes.py script, programmed by Florian Festi.\nLightburn output by Klaus Steinhammer.\n\nURL with settings:\n" + str(url))
|
|
pl.text = ""
|
|
pl.tail = "\n"
|
|
|
|
if self.dbg: print ("5", num)
|
|
tree.write(open(self._fname, "wb"), encoding="utf-8", xml_declaration=True, method="xml")
|
|
from random import random
|
|
|
|
|
|
def random_svg_color():
|
|
r, g, b = random(), random(), random()
|
|
return f"rgb({r*255:.0f},{g*255:.0f},{b*255:.0f})"
|
|
|
|
|
|
def rgb_to_svg_color(r, g, b):
|
|
return f"rgb({r*255:.0f},{g*255:.0f},{b*255:.0f})"
|
|
|
|
|
|
def line_intersection(line1, line2):
|
|
|
|
xdiff = (line1[0][0] - line1[1][0], line2[0][0] - line2[1][0])
|
|
ydiff = (line1[0][1] - line1[1][1], line2[0][1] - line2[1][1])
|
|
|
|
def det(a, b):
|
|
return a[0] * b[1] - a[1] * b[0]
|
|
|
|
div = det(xdiff, ydiff)
|
|
if div == 0:
|
|
# todo: deal with parallel line intersection / overlay
|
|
return False, None, None
|
|
|
|
d = (det(*line1), det(*line2))
|
|
x = det(d, xdiff) / div
|
|
y = det(d, ydiff) / div
|
|
|
|
on_segments = (
|
|
(x + EPS >= min(line1[0][0], line1[1][0])),
|
|
(x + EPS >= min(line2[0][0], line2[1][0])),
|
|
(x - EPS <= max(line1[0][0], line1[1][0])),
|
|
(x - EPS <= max(line2[0][0], line2[1][0])),
|
|
(y + EPS >= min(line1[0][1], line1[1][1])),
|
|
(y + EPS >= min(line2[0][1], line2[1][1])),
|
|
(y - EPS <= max(line1[0][1], line1[1][1])),
|
|
(y - EPS <= max(line2[0][1], line2[1][1])),
|
|
)
|
|
|
|
return min(on_segments), x, y
|