diff --git a/extensions/fablabchemnitz/box_maker_elliptical_box/box_maker_elliptical_box.inx b/extensions/fablabchemnitz/box_maker_elliptical_box/box_maker_elliptical_box.inx
new file mode 100644
index 0000000..b013913
--- /dev/null
+++ b/extensions/fablabchemnitz/box_maker_elliptical_box/box_maker_elliptical_box.inx
@@ -0,0 +1,35 @@
+
+
+ Box Maker - Elliptical Box
+ fablabchemnitz.de.box_maker_elliptical_box
+
+
+
+
+
+
+
+
+ 3.0
+ 30.0
+ 50.0
+ 40.0
+ 1.5
+ false
+ 4
+ 90
+ false
+ false
+ false
+
+ all
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/box_maker_elliptical_box/box_maker_elliptical_box.py b/extensions/fablabchemnitz/box_maker_elliptical_box/box_maker_elliptical_box.py
new file mode 100644
index 0000000..a264b4a
--- /dev/null
+++ b/extensions/fablabchemnitz/box_maker_elliptical_box/box_maker_elliptical_box.py
@@ -0,0 +1,278 @@
+#!/usr/bin/env python3
+
+from inkscape_helper.Coordinate import Coordinate
+import inkscape_helper.Effect as eff
+import inkscape_helper.SVG as svg
+from inkscape_helper.Ellipse import Ellipse
+
+from inkscape_helper.Line import Line
+from inkscape_helper.EllipticArc import EllipticArc
+
+from math import *
+import inkex
+
+#Note: keep in mind that SVG coordinates start in the top-left corner i.e. with an inverted y-axis
+
+# first define some SVG primitives
+greenStyle = svg.green_style
+
+def _makeCurvedSurface(topLeft, w, h, cutSpacing, hCutCount, thickness, parent, invertNotches = False, centralRib = False):
+ width = Coordinate(w, 0)
+ height = Coordinate(0, h)
+ wCutCount = int(floor(w / cutSpacing))
+ if wCutCount % 2 == 0:
+ wCutCount += 1 # make sure we have an odd number of cuts
+ xCutDist = w / wCutCount
+ xSpacing = Coordinate(xCutDist, 0)
+ ySpacing = Coordinate(0, cutSpacing)
+ cut = height / hCutCount - ySpacing
+ plateThickness = Coordinate(0, thickness)
+ notchEdges = []
+ topHCuts = []
+ bottomHCuts = []
+
+ p = svg.Path()
+
+ for cutIndex in range(wCutCount):
+ if (cutIndex % 2 == 1) != invertNotches: # make a notch here
+ inset = plateThickness
+ else:
+ inset = Coordinate(0, 0)
+
+ # A-column of cuts
+ aColStart = topLeft + xSpacing * cutIndex
+ notchEdges.append((aColStart - topLeft).x)
+
+ if cutIndex > 0: # no cuts at x == 0
+ p.move_to(aColStart, True)
+ p.line_to(cut / 2)
+
+ for j in range(hCutCount - 1):
+ pos = aColStart + cut / 2 + ySpacing + (cut + ySpacing) * j
+ p.move_to(pos, True)
+ p.line_to(cut)
+
+ p.move_to(aColStart + height - cut / 2, True)
+ p.line_to(cut / 2)
+
+
+ # B-column of cuts, offset by half the cut length; these cuts run in the opposite direction
+ bColStart = topLeft + xSpacing * cutIndex + xSpacing / 2
+ for j in reversed(range(hCutCount)):
+ end = bColStart + ySpacing / 2 + (cut + ySpacing) * j
+ start = end + cut
+ if centralRib and hCutCount % 2 == 0 and cutIndex % 2 == 1:
+ holeTopLeft = start + (ySpacing - plateThickness - xSpacing) / 2
+ if j == hCutCount // 2 - 1:
+ start -= plateThickness / 2
+ p.move_to(holeTopLeft + plateThickness + xSpacing, True)
+ p.line_to(-xSpacing)
+
+ p.move_to(holeTopLeft, True)
+ p.line_to(xSpacing)
+
+ elif j == hCutCount // 2:
+ end += plateThickness / 2
+ if j == 0: # first row
+ end += inset
+ elif j == hCutCount - 1: # last row
+ start -= inset
+ p.move_to(start, True)
+ p.line_to(end, True)
+
+ #horizontal cuts (should be done last)
+ topHCuts.append((aColStart + inset, aColStart + inset + xSpacing))
+ bottomHCuts.append((aColStart + height - inset, aColStart + height - inset + xSpacing))
+
+ # draw the outline
+ for c in reversed(bottomHCuts):
+ p.move_to(c[1], True)
+ p.line_to(c[0], True)
+
+ p.move_to(topLeft + height, True)
+ p.line_to(-height)
+
+ for c in topHCuts:
+ p.move_to(c[0], True)
+ p.line_to(c[1], True)
+
+ p.move_to(topLeft + width, True)
+ p.line_to(height)
+
+
+ group = svg.group(parent)
+ p.path(group)
+
+ notchEdges.append(w)
+ return notchEdges
+
+def _makeNotchedEllipse(center, ellipse, start_theta, thickness, notches, parent, invertNotches):
+ start_theta += pi # rotate 180 degrees to put the lid on the topside
+
+ ell_radius = Coordinate(ellipse.x_radius, ellipse.y_radius)
+ ell_radius_t = ell_radius + Coordinate(thickness, thickness)
+
+ theta = ellipse.theta_from_dist(start_theta, notches[0])
+ ell_point = center + ellipse.coordinate_at_theta(theta)
+ prev_offset = ellipse.tangent(theta) * thickness
+
+ p = svg.Path()
+ p.move_to(ell_point, absolute=True)
+
+ for n in range(len(notches) - 1):
+ theta = ellipse.theta_from_dist(start_theta, notches[n + 1])
+ ell_point = center + ellipse.coordinate_at_theta(theta)
+ notch_offset = ellipse.tangent(theta) * thickness
+ notch_point = ell_point + notch_offset
+
+ if (n % 2 == 0) != invertNotches:
+ p.arc_to(ell_radius, ell_point, absolute=True)
+ prev_offset = notch_offset
+
+ else:
+ p.line_to(prev_offset)
+ p.arc_to(ell_radius_t, notch_point, absolute=True)
+ p.line_to(-notch_offset)
+
+ p.path(parent)
+
+
+
+class EllipticalBox(eff.Effect):
+ """
+ Creates a new layer with the drawings for a parametrically generaded box.
+ """
+ def __init__(self):
+ options = [
+ ['unit', str, 'mm', 'Unit, one of: cm, mm, in, ft, ...'],
+ ['thickness', float, '3.0', 'Material thickness'],
+ ['width', float, '100', 'Box width'],
+ ['height', float, '100', 'Box height'],
+ ['depth', float, '100', 'Box depth'],
+ ['cut_dist', float, '1.5', 'Distance between cuts on the wrap around. Note that this value will change slightly to evenly fill up the available space.'],
+ ['auto_cut_dist', inkex.Boolean, 'false', 'Automatically set the cut distance based on the curvature.'], # in progress
+ ['cut_nr', int, '3', 'Number of cuts across the depth of the box.'],
+ ['lid_angle', float, '120', 'Angle that forms the lid (in degrees, measured from centerpoint of the ellipse)'],
+ ['body_ribcount', int, '0', 'Number of ribs in the body'],
+ ['lid_ribcount', int, '0', 'Number of ribs in the lid'],
+ ['invert_lid_notches', inkex.Boolean, 'false', 'Invert the notch pattern on the lid (keeps the lid from sliding sideways)'],
+ ['central_rib_lid', inkex.Boolean, 'false', 'Create a central rib in the lid'],
+ ['central_rib_body', inkex.Boolean, 'false', 'Create a central rib in the body']
+ ]
+ eff.Effect.__init__(self, options)
+
+
+ def effect(self):
+ """
+ Draws as basic elliptical box, based on provided parameters
+ """
+
+ # input sanity check
+ error = False
+ if min(self.options.height, self.options.width, self.options.depth) == 0:
+ eff.errormsg('Error: Dimensions must be non zero')
+ error = True
+
+ if self.options.cut_nr < 1:
+ eff.errormsg('Error: Number of cuts should be at least 1')
+ error = True
+
+ if (self.options.central_rib_lid or self.options.central_rib_body) and self.options.cut_nr % 2 == 1:
+ eff.errormsg('Error: Central rib is only valid with an even number of cuts')
+ error = True
+
+ if self.options.unit not in self.knownUnits:
+ eff.errormsg('Error: unknown unit. '+ self.options.unit)
+ error = True
+
+ if error:
+ exit()
+
+
+ # convert units
+ unit = self.options.unit
+ H = self.svg.unittouu(str(self.options.height) + unit)
+ W = self.svg.unittouu(str(self.options.width) + unit)
+ D = self.svg.unittouu(str(self.options.depth) + unit)
+ thickness = self.svg.unittouu(str(self.options.thickness) + unit)
+ cutSpacing = self.svg.unittouu(str(self.options.cut_dist) + unit)
+ cutNr = self.options.cut_nr
+
+ doc_root = self.document.getroot()
+ layer = svg.layer(doc_root, 'Elliptical Box')
+
+ ell = Ellipse(W/2, H/2)
+
+ #body and lid
+ lidAngleRad = self.options.lid_angle * 2 * pi / 360
+ lid_start_theta = ell.theta_at_angle(pi / 2 - lidAngleRad / 2)
+ lid_end_theta = ell.theta_at_angle(pi / 2 + lidAngleRad / 2)
+
+ lidLength = ell.dist_from_theta(lid_start_theta, lid_end_theta)
+ bodyLength = ell.dist_from_theta(lid_end_theta, lid_start_theta)
+
+ # do not put elements right at the edge of the page
+ xMargin = 3
+ yMargin = 3
+
+ bottom_grp = svg.group(layer)
+ top_grp = svg.group(layer)
+
+ bodyNotches = _makeCurvedSurface(Coordinate(xMargin, yMargin), bodyLength, D, cutSpacing, cutNr,
+ thickness, bottom_grp, False, self.options.central_rib_body)
+ lidNotches = _makeCurvedSurface(Coordinate(xMargin, D + 2 * yMargin), lidLength, D, cutSpacing, cutNr,
+ thickness, top_grp, not self.options.invert_lid_notches,
+ self.options.central_rib_lid)
+
+ # create elliptical sides
+ sidesGrp = svg.group(layer)
+
+ elCenter = Coordinate(xMargin + thickness + W / 2, 2 * D + H / 2 + thickness + 3 * yMargin)
+
+ # indicate the division between body and lid
+ p = svg.Path()
+ if self.options.invert_lid_notches:
+ p.move_to(elCenter + ell.coordinate_at_theta(lid_start_theta + pi), True)
+ p.line_to(elCenter, True)
+ p.line_to(elCenter + ell.coordinate_at_theta(lid_end_theta + pi), True)
+
+ else:
+ angleA = ell.theta_from_dist(lid_start_theta, lidNotches[1])
+ angleB = ell.theta_from_dist(lid_start_theta, lidNotches[-2])
+
+ p.move_to(elCenter + ell.coordinate_at_theta(angleA + pi), True)
+ p.line_to(elCenter, True)
+ p.line_to(elCenter + ell.coordinate_at_theta(angleB + pi), True)
+
+ _makeNotchedEllipse(elCenter, ell, lid_end_theta, thickness, bodyNotches, sidesGrp, False)
+ _makeNotchedEllipse(elCenter, ell, lid_start_theta, thickness, lidNotches, sidesGrp,
+ not self.options.invert_lid_notches)
+
+ p.path(sidesGrp, greenStyle)
+
+ # ribs
+ if self.options.central_rib_lid or self.options.central_rib_body:
+ innerRibCenter = Coordinate(xMargin + thickness + W / 2, 2 * D + 1.5 * (H + 2 *thickness) + 4 * yMargin)
+ innerRibGrp = svg.group(layer)
+
+ outerRibCenter = Coordinate(2 * xMargin + 1.5 * (W + 2 * thickness),
+ 2 * D + 1.5 * (H + 2 * thickness) + 4 * yMargin)
+ outerRibGrp = svg.group(layer)
+
+
+ if self.options.central_rib_lid:
+ _makeNotchedEllipse(innerRibCenter, ell, lid_start_theta, thickness, lidNotches, innerRibGrp, False)
+ _makeNotchedEllipse(outerRibCenter, ell, lid_start_theta, thickness, lidNotches, outerRibGrp, True)
+
+ if self.options.central_rib_body:
+ spacer = Coordinate(0, 10)
+ _makeNotchedEllipse(innerRibCenter + spacer, ell, lid_end_theta, thickness, bodyNotches, innerRibGrp, False)
+ _makeNotchedEllipse(outerRibCenter + spacer, ell, lid_end_theta, thickness, bodyNotches, outerRibGrp, True)
+
+ if self.options.central_rib_lid or self.options.central_rib_body:
+ svg.text(sidesGrp, elCenter, 'side (duplicate this)')
+ svg.text(innerRibGrp, innerRibCenter, 'inside rib')
+ svg.text(outerRibGrp, outerRibCenter, 'outside rib')
+
+if __name__ == '__main__':
+ EllipticalBox().run()
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/box_maker_elliptical_box/inkscape_helper/BezierCurve.py b/extensions/fablabchemnitz/box_maker_elliptical_box/inkscape_helper/BezierCurve.py
new file mode 100644
index 0000000..7423eb0
--- /dev/null
+++ b/extensions/fablabchemnitz/box_maker_elliptical_box/inkscape_helper/BezierCurve.py
@@ -0,0 +1,92 @@
+from __future__ import division
+
+from PathSegment import *
+from math import hypot
+
+class BezierCurve(PathSegment):
+ nr_points = 10
+ def __init__(self, P): # number of points is limited to 3 or 4
+
+ if len(P) == 3: # quadratic
+ self.B = lambda t : (1 - t)**2 * P[0] + 2 * (1 - t) * t * P[1] + t**2 * P[2]
+ self.Bd = lambda t : 2 * (1 - t) * (P[1] - P[0]) + 2 * t * (P[2] - P[1])
+ self.Bdd = lambda t : 2 * (P[2] - 2 * P[1] + P[0])
+ elif len(P) == 4: #cubic
+ self.B = lambda t : (1 - t)**3 * P[0] + 3 * (1 - t)**2 * t * P[1] + 3 * (1 - t) * t**2 * P[2] + t**3 * P[3]
+ self.Bd = lambda t : 3 * (1 - t)**2 * (P[1] - P[0]) + 6 * (1 - t) * t * (P[2] - P[1]) + 3 * t**2 * (P[3] - P[2])
+ self.Bdd = lambda t : 6 * (1 - t) * (P[2] - 2 * P[1] + P[0]) + 6 * t * (P[3] - 2 * P[2] + P[1])
+
+ self.tangent = lambda t : self.Bd(t)
+ # self.curvature = lambda t : (Bd(t).x * Bdd(t).y - Bd(t).y * Bdd(t).x) / hypot(Bd(t).x, Bd(t).y)**3
+
+
+ self.distances = [0] # cumulative distances for each 't'
+ prev_pt = self.B(0)
+ for i in range(self.nr_points):
+ t = (i + 1) / self.nr_points
+ pt = self.B(t)
+ self.distances.append(self.distances[-1] + hypot(prev_pt.x - pt.x, prev_pt.y - pt.y))
+ prev_pt = pt
+ self._length = self.distances[-1]
+
+ def curvature(self, t):
+ n = self.Bd(t).x * self.Bdd(t).y - self.Bd(t).y * self.Bdd(t).x
+ d = hypot(self.Bd(t).x, self.Bd(t).y)**3
+ if d == 0:
+ return n * float('inf')
+ else:
+ return n / d
+
+ @classmethod
+ def quadratic(cls, start, c, end):
+ bezier = cls()
+
+ @classmethod
+ def cubic(cls, start, c1, c2, end):
+ bezier = cls()
+
+ def __make_eq__(self):
+ pass
+
+ @property
+ def length(self):
+ return self._length
+
+ def subdivide(self, part_length, start_offset=0):
+ nr_parts = int((self.length - start_offset) // part_length)
+ k_o = start_offset / self.length
+ k2t = lambda k : k_o + k * part_length / self.length
+ points = [self.pathpoint_at_t(k2t(k)) for k in range(nr_parts + 1)]
+ return(points, self.length - points[-1].c_dist)
+
+
+ def pathpoint_at_t(self, t):
+ """pathpoint on the curve from t=0 to point at t."""
+ step = 1 / self.nr_points
+ pt_idx = int(t / step)
+ length = self.distances[pt_idx]
+ ip_fact = (t - pt_idx * step) / step
+
+ if ip_fact > 0 and t < 1: # not a perfect match, need to interpolate
+ length += ip_fact * (self.distances[pt_idx + 1] - self.distances[pt_idx])
+
+ return PathPoint(t, self.B(t), self.tangent(t), self.curvature(t), length)
+
+
+ def t_at_length(self, length):
+ """interpolated t where the curve is at the given length"""
+ if length == self.length:
+ return 1
+ i_small = 0
+ i_big = self.nr_points + 1
+
+ while i_big - i_small > 1: # binary search
+ i_half = i_small + (i_big - i_small) // 2
+ if self.distances[i_half] <= length:
+ i_small = i_half
+ else:
+ i_big = i_half
+
+ small_dist = self.distances[i_small]
+ return i_small / self.nr_points + (length - small_dist) * (self.distances[i_big] - small_dist) # interpolated length
+
diff --git a/extensions/fablabchemnitz/box_maker_elliptical_box/inkscape_helper/Coordinate.py b/extensions/fablabchemnitz/box_maker_elliptical_box/inkscape_helper/Coordinate.py
new file mode 100644
index 0000000..298d157
--- /dev/null
+++ b/extensions/fablabchemnitz/box_maker_elliptical_box/inkscape_helper/Coordinate.py
@@ -0,0 +1,104 @@
+from math import *
+
+def inner_product(a, b):
+ return a.x * b.x + a.y * b.y
+
+class Coordinate(object):
+ """
+ Basic (x, y) coordinate class (or should it be called vector?) which allows some simple operations.
+ """
+ def __init__(self, x, y):
+ self.x = float(x)
+ self.y = float(y)
+
+ #polar coordinates
+ @property
+ def t(self):
+ angle = atan2(self.y, self.x)
+ if angle < 0:
+ angle += pi * 2
+ return angle
+
+ @t.setter
+ def t(self, value):
+ length = self.r
+ self.x = cos(value) * length
+ self.y = sin(value) * length
+
+
+ @property
+ def r(self):
+ return hypot(self.x, self.y)
+
+ @r.setter
+ def r(self, value):
+ angle = self.t
+ self.x = cos(angle) * value
+ self.y = sin(angle) * value
+
+ def __repr__(self):
+ return self.__str__()
+
+ def __str__(self):
+ return "(%f, %f)" % (self.x, self.y)
+
+ def __eq__(self, other):
+ return self.x == other.x and self.y == other.y
+
+ def __add__(self, other):
+ return Coordinate(self.x + other.x, self.y + other.y)
+
+ def __sub__(self, other):
+ return Coordinate(self.x - other.x, self.y - other.y)
+
+ def __mul__(self, factor):
+ return Coordinate(self.x * factor, self.y * factor)
+
+ def __neg__(self):
+ return Coordinate(-self.x, -self.y)
+
+ def __rmul__(self, other):
+ return self * other
+
+ def __div__(self, quotient):
+ return Coordinate(self.x / quotient, self.y / quotient)
+
+ def __truediv__(self, quotient):
+ return self.__div__(quotient)
+
+ def dot(self, other):
+ """dot product"""
+ return self.x * other.x + self.y * other.y
+
+ def cross_norm(self, other):
+ """"the norm of the cross product"""
+ self.x * other.y - self.y * other.x
+
+ def close_enough_to(self, other, limit=1E-9):
+ return (self - other).r < limit
+
+
+class IntersectionError(ValueError):
+ """Raised when two lines do not intersect."""
+
+def on_segment(pt, start, end):
+ """Check if pt is between start and end. The three points are presumed to be collinear."""
+ pt -= start
+ end -= start
+ ex, ey = end.x, end.y
+ px, py = pt.x, pt.y
+ px *= cmp(ex, 0)
+ py *= cmp(ey, 0)
+ return px >= 0 and px <= abs(ex) and py >= 0 and py <= abs(ey)
+
+def intersection (s1, e1, s2, e2, on_segments = True):
+ D = (s1.x - e1.x) * (s2.y - e2.y) - (s1.y - e1.y) * (s2.x - e2.x)
+ if D == 0:
+ raise IntersectionError("Lines from {s1} to {e1} and {s2} to {e2} are parallel")
+ N1 = s1.x * e1.y - s1.y * e1.x
+ N2 = s2.x * e2.y - s2.y * e2.x
+ I = ((s2 - e2) * N1 - (s1 - e1) * N2) / D
+
+ if on_segments and not (on_segment(I, s1, e1) and on_segment(I, s2, e2)):
+ raise IntersectionError("Intersection {0} is not on line segments [{1} -> {2}] [{3} -> {4}]".format(I, s1, e1, s2, e2))
+ return I
diff --git a/extensions/fablabchemnitz/box_maker_elliptical_box/inkscape_helper/Effect.py b/extensions/fablabchemnitz/box_maker_elliptical_box/inkscape_helper/Effect.py
new file mode 100644
index 0000000..e38a4f6
--- /dev/null
+++ b/extensions/fablabchemnitz/box_maker_elliptical_box/inkscape_helper/Effect.py
@@ -0,0 +1,36 @@
+import inkex
+
+errormsg = inkex.errormsg
+debug = inkex.debug
+
+class Effect(inkex.Effect):
+ """
+ Provides some extra features to inkex.Effect:
+ - Allows you to pass a list of options in stead of setting them one by one
+ - acces to unittouu() that is compatible between Inkscape versions 0.48 and 0.91
+ """
+ def __init__(self, options=None):
+ inkex.Effect.__init__(self)
+ self.knownUnits = ['in', 'pt', 'px', 'mm', 'cm', 'm', 'km', 'pc', 'yd', 'ft']
+
+ if options != None:
+ for opt in options:
+ if len(opt) == 2:
+ self.arg_parser.add_argument('--' + opt[0], type = opt[1])
+ else:
+ self.arg_parser.add_argument('--' + opt[0], type = opt[1],default = opt[2], help = opt[3])
+
+ try:
+ inkex.Effect.unittouu # unitouu has moved since Inkscape 0.91
+ except AttributeError:
+ try:
+ def unittouu(self, unit):
+ return inkex.unittouu(unit)
+ except AttributeError:
+ pass
+
+ def effect(self):
+ """
+
+ """
+ pass
diff --git a/extensions/fablabchemnitz/box_maker_elliptical_box/inkscape_helper/Ellipse.py b/extensions/fablabchemnitz/box_maker_elliptical_box/inkscape_helper/Ellipse.py
new file mode 100644
index 0000000..3e0370e
--- /dev/null
+++ b/extensions/fablabchemnitz/box_maker_elliptical_box/inkscape_helper/Ellipse.py
@@ -0,0 +1,157 @@
+from __future__ import division
+from math import *
+from inkscape_helper.Coordinate import Coordinate
+
+class Ellipse(object):
+ """Used as a base class for EllipticArc."""
+ nr_points = 1024 #used for piecewise linear circumference calculation (ellipse circumference is tricky to calculate)
+ # approximate circumfere: c = pi * (3 * (a + b) - sqrt(10 * a * b + 3 * (a ** 2 + b ** 2)))
+
+ def __init__(self, x_radius, y_radius):
+ self.y_radius = y_radius
+ self.x_radius = x_radius
+ self.distances = [0]
+ theta = 0
+ self.angle_step = 2 * pi / self.nr_points
+
+ for i in range(self.nr_points):
+ prev_dist = self.distances[-1]
+ prev_coord = self.coordinate_at_theta(theta)
+ theta += self.angle_step
+ x, y = x_radius * cos(theta), y_radius * sin(theta)
+ self.distances.append(prev_dist + hypot(prev_coord.x - x, prev_coord.y - y))
+
+ @property
+ def circumference(self):
+ return self.distances[-1]
+
+ def curvature(self, theta):
+ c = self.coordinate_at_theta(theta)
+ return (self.x_radius*self.y_radius)/((cos(theta)**2*self.y_radius**2 + sin(theta)**2*self.x_radius**2)**(3/2))
+
+ def tangent(self, theta):
+ angle = self.theta_at_angle(theta)
+ return Coordinate(cos(angle), sin(angle))
+
+ def coordinate_at_theta(self, theta):
+ """Coordinate of the point at theta."""
+ return Coordinate(self.x_radius * cos(theta), self.y_radius * sin(theta))
+
+ def dist_from_theta(self, theta_start, theta_end):
+ """Distance accross the surface from point at angle theta_end to point at angle theta_end. Measured in positive (CCW) sense."""
+ #print 'thetas ', theta_start, theta_end # TODO: figure out why are there so many with same start and end?
+ # make sure thetas are between 0 and 2 * pi
+ theta_start %= 2 * pi
+ theta_end %= 2 * pi
+ i1 = int(theta_start / self.angle_step)
+ p1 = theta_start % self.angle_step
+ l1 = self.distances[i1 + 1] - self.distances[i1]
+ i2 = int(theta_end / self.angle_step)
+ p2 = theta_end % self.angle_step
+ l2 = self.distances[i2 + 1] - self.distances[i2]
+ if theta_start <= theta_end:
+ len = self.distances[i2] - self.distances[i1] + l2 * p2 - l1 * p1
+ else:
+ len = self.circumference + self.distances[i2] - self.distances[i1]
+ return len
+
+ def theta_from_dist(self, theta_start, dist):
+ """Returns the angle that you get when starting at theta_start and moving a distance (dist) in CCW direction"""
+ si = int(theta_start / self.angle_step) % self.nr_points
+ p = theta_start % self.angle_step
+
+ piece_length = self.distances[si + 1] - self.distances[si]
+ start_dist = self.distances[si] + p * piece_length
+ end_dist = dist + start_dist
+
+ if end_dist > self.circumference: # wrap around zero angle
+ end_dist -= self.circumference
+
+ min_idx = 0
+ max_idx = self.nr_points
+ while max_idx - min_idx > 1: # binary search
+ half_idx = min_idx + (max_idx - min_idx) // 2
+ if self.distances[half_idx] < end_dist:
+ min_idx = half_idx
+ else:
+ max_idx = half_idx
+ step_dist = self.distances[max_idx] - self.distances[min_idx]
+ return (min_idx + (end_dist - self.distances[min_idx]) / step_dist) * self.angle_step
+
+ def theta_at_angle(self, angle):
+ cf = 0
+ if angle > pi / 2:
+ cf = pi
+ if angle > 3 * pi / 2:
+ cf = 2 * pi
+ return atan(self.x_radius/self.y_radius * tan(angle)) + cf
+
+ def skewTransform(self, l, a2, b2):
+ x0 = a2**2
+ x1 = b2**2
+ x2 = l**2
+ x3 = x0*x2
+ x4 = x0 - x1 + x3
+ x5 = 2*a2*b2
+ x6 = x0 + x1 + x3
+ x7 = sqrt((-x5 + x6)*(x5 + x6))
+ x9 = 1/(x4 - x7)
+ x10 = x6 - x7
+ x11 = l*x10
+ x12 = b2**4
+ x13 = 4*x12
+ x14 = x10**2
+ x15 = 4*x1
+ x16 = sqrt(-x10*x15 + x13 + x14*x2 + x14)
+ x17 = 2*atan(x9*(x11 - x16))
+ x18 = sqrt(2)
+ x19 = sqrt(x10)
+ x20 = b2*x18*x19/2
+ x21 = x0/2
+ x22 = x1/2
+ x23 = x2*x21
+ x24 = x21 - x22 + x23
+ x25 = x7/2
+ x27 = 1/(x24 - x25)
+ x28 = x21 + x22 + x23
+ x29 = x28 - x25
+ x30 = l*x29
+ x31 = x14/4
+ x32 = 2*x1
+ x33 = sqrt(x12 + x2*x31 - x29*x32 + x31)
+ x34 = 2*atan(x27*(x30 - x33))
+ x35 = x20*sqrt(1/(-x1*cos(x34)**2 + x29))*sin(x34)
+ x36 = x18/2
+ x37 = -x19*x36
+ x39 = 2*atan(x9*(x11 + x16))
+ x40 = 2*atan(x27*(x30 + x33))
+ x41 = x20*sqrt(1/(-x1*cos(x40)**2 + x29))*sin(x40)
+ x42 = 1/(x4 + x7)
+ x43 = x6 + x7
+ x44 = l*x43
+ x45 = x43**2
+ x46 = sqrt(x13 - x15*x43 + x2*x45 + x45)
+ x47 = 2*atan(x42*(x44 - x46))
+ x48 = sqrt(x43)
+ x49 = b2*x18*x48/2
+ x50 = 1/(x24 + x25)
+ x51 = x25 + x28
+ x52 = l*x51
+ x53 = x45/4
+ x54 = sqrt(x12 + x2*x53 - x32*x51 + x53)
+ x55 = 2*atan(x50*(x52 - x54))
+ x56 = x49*sqrt(1/(-x1*cos(x55)**2 + x51))*sin(x55)
+ x57 = -x36*x48
+ x59 = 2*atan(x42*(x44 + x46))
+ x60 = 2*atan(x50*(x52 + x54))
+ x61 = x49*sqrt(1/(-x1*cos(x60)**2 + x51))*sin(x60)
+
+ #solutions (alpha, a1, b1)
+ (x17, -x35, x19*x36)
+ (x17, x35, x37)
+ (x39, -x41, x19*x36)
+ (x39, x41, x37)
+ (x47, -x56, x36*x48)
+ (x47, x56, x57)
+ (x59, -x61, x36*x48)
+ (x59, x61, x57)
diff --git a/extensions/fablabchemnitz/box_maker_elliptical_box/inkscape_helper/EllipticArc.py b/extensions/fablabchemnitz/box_maker_elliptical_box/inkscape_helper/EllipticArc.py
new file mode 100644
index 0000000..0353d11
--- /dev/null
+++ b/extensions/fablabchemnitz/box_maker_elliptical_box/inkscape_helper/EllipticArc.py
@@ -0,0 +1,125 @@
+from inkscape_helper.PathSegment import *
+from inkscape_helper.Coordinate import Coordinate
+from inkscape_helper.Ellipse import Ellipse
+
+from math import sqrt, pi
+import copy
+
+
+class EllipticArc(PathSegment):
+
+ ell_dict = {}
+
+ def __init__(self, start, end, rx, ry, axis_rot, pos_dir=True, large_arc=False):
+ self.rx = rx
+ self.ry = ry
+ # calculate ellipse center
+ # the center is on two ellipses one with its center at the start point, the other at the end point
+ # for simplicity take the one ellipse at the origin and the other with offset (tx, ty),
+ # find the center and translate back to the original offset at the end
+ axis_rot *= pi / 180 # convert to radians
+ # start and end are mutable objects, copy to avoid modifying them
+ r_start = copy.copy(start)
+ r_end = copy.copy(end)
+ r_start.t -= axis_rot
+ r_end.t -= axis_rot
+ end_o = r_end - r_start # offset end vector
+
+ tx = end_o.x
+ ty = end_o.y
+
+ # some helper variables for the intersection points
+ # used sympy to come up with the equations
+ ff = (rx**2*ty**2 + ry**2*tx**2)
+ cx = rx**2*ry*tx*ty**2 + ry**3*tx**3
+ cy = rx*ty*ff
+ sx = rx*ty*sqrt(4*rx**4*ry**2*ty**2 - rx**4*ty**4 + 4*rx**2*ry**4*tx**2 - 2*rx**2*ry**2*tx**2*ty**2 - ry**4*tx**4)
+ sy = ry*tx*sqrt(-ff*(-4*rx**2*ry**2 + rx**2*ty**2 + ry**2*tx**2))
+
+ # intersection points
+ c1 = Coordinate((cx - sx) / (2*ry*ff), (cy + sy) / (2*rx*ff))
+ c2 = Coordinate((cx + sx) / (2*ry*ff), (cy - sy) / (2*rx*ff))
+
+ if end_o.cross_norm(c1 - r_start) < 0: # c1 is to the left of end_o
+ left = c1
+ right = c2
+ else:
+ left = c2
+ right = c1
+
+ if pos_dir != large_arc: #center should be on the left of end_o
+ center_o = left
+ else: #center should be on the right of end_o
+ center_o = right
+
+ #re-use ellipses with same rx, ry to save some memory
+ if (rx, ry) in self.ell_dict:
+ self.ellipse = self.ell_dict[(rx, ry)]
+ else:
+ self.ellipse = Ellipse(rx, ry)
+ self.ell_dict[(rx, ry)] = self.ellipse
+
+ self.start = start
+ self.end = end
+ self.axis_rot = axis_rot
+ self.pos_dir = pos_dir
+ self.large_arc = large_arc
+ self.start_theta = self.ellipse.theta_at_angle((-center_o).t)
+ self.end_theta = self.ellipse.theta_at_angle((end_o - center_o).t)
+
+ # translate center back to original offset
+ center_o.t += axis_rot
+ self.center = center_o + start
+
+ @property
+ def length(self):
+ return self.ellipse.dist_from_theta(self.start_theta, self.end_theta)
+
+ def t_to_theta(self, t):
+ """convert t (always between 0 and 1) to angle theta"""
+ start = self.start_theta
+ end = self.end_theta
+
+ if self.pos_dir and end < start:
+ end += 2 * pi
+
+ if not self.pos_dir and start < end:
+ end -= 2 * pi
+ arc_size = end - start
+
+ return (start + (end - start) * t) % (2 * pi)
+
+ def theta_to_t(self, theta):
+ full_arc_size = (self.end_theta - self.start_theta + 2 * pi) % (2 * pi)
+ theta_arc_size = (theta - self.start_theta + 2 * pi) % (2 * pi)
+ return theta_arc_size / full_arc_size
+
+ def curvature(self, t):
+ theta = self.t_to_theta(t)
+ return self.ellipse.curvature(theta)
+
+ def tangent(self, t):
+ theta = self.t_to_theta(t)
+ return self.ellipse.tangent(theta)
+
+ def t_at_length(self, length):
+ """interpolated t where the curve is at the given length"""
+ theta = self.ellipse.theta_from_dist(length, self.start_theta)
+ return self.theta_to_t(theta)
+
+ def length_at_t(self, t):
+ return self.ellipse.dist_from_theta(self.start_theta, self.t_to_theta(t))
+
+ def pathpoint_at_t(self, t):
+ """pathpoint on the curve from t=0 to point at t."""
+ centered = self.ellipse.coordinate_at_theta(self.t_to_theta(t))
+ centered.t += self.axis_rot
+ return PathPoint(t, centered + self.center, self.tangent(t), self.curvature(t), self.length_at_t(t))
+
+ # identical to Bezier code
+ def subdivide(self, part_length, start_offset=0):
+ nr_parts = int((self.length - start_offset) // part_length)
+ k_o = start_offset / self.length
+ k2t = lambda k : k_o + k * part_length / self.length
+ points = [self.pathpoint_at_t(k2t(k)) for k in range(nr_parts + 1)]
+ return(points, self.length - points[-1].c_dist)
diff --git a/extensions/fablabchemnitz/box_maker_elliptical_box/inkscape_helper/Line.py b/extensions/fablabchemnitz/box_maker_elliptical_box/inkscape_helper/Line.py
new file mode 100644
index 0000000..a64a88d
--- /dev/null
+++ b/extensions/fablabchemnitz/box_maker_elliptical_box/inkscape_helper/Line.py
@@ -0,0 +1,25 @@
+from inkscape_helper.PathSegment import *
+
+class Line(PathSegment):
+
+ def __init__(self, start, end):
+ self.start = start
+ self.end = end
+ self.pp = lambda t : PathPoint(t, self.start + t * (self.end - self.start), self.end - self.start, 0, t * self.length)
+
+ @property
+ def length(self):
+ return (self.end - self.start).r
+
+ def subdivide(self, part_length, start_offset=0): # note: start_offset should be smaller than part_length
+ nr_parts = int((self.length - start_offset) // part_length)
+ k_o = start_offset / self.length
+ k2t = lambda k : k_o + k * part_length / self.length
+ points = [self.pp(k2t(k)) for k in range(nr_parts + 1)]
+ return(points, self.length - points[-1].c_dist)
+
+ def pathpoint_at_t(self, t):
+ return self.pp(t)
+
+ def t_at_length(self, length):
+ return length / self.length
diff --git a/extensions/fablabchemnitz/box_maker_elliptical_box/inkscape_helper/Matrix.py b/extensions/fablabchemnitz/box_maker_elliptical_box/inkscape_helper/Matrix.py
new file mode 100644
index 0000000..188ef27
--- /dev/null
+++ b/extensions/fablabchemnitz/box_maker_elliptical_box/inkscape_helper/Matrix.py
@@ -0,0 +1,49 @@
+import copy
+
+class Matrix(object):
+ """
+ Matrix class with some basic matrix operations
+ """
+ def __init__(self, array):
+ columns = len(array[0])
+ for r in array[1:]: #make sure each row has same number of columns
+ assert len(r) == columns
+
+ self.array = copy.copy(array)
+ self.rows = len(array)
+ self.columns = columns
+
+ def __repr__(self):
+ return self.__str__()
+
+ def __str__(self):
+ a = ['[' + ', '.join([str(i) for i in r]) + ']' for r in self.array]
+ return '[\n' + ',\n'.join(a) + '\n]'
+
+ def minor(self, row, col):
+ return Matrix([[self[r][c] for c in range(self.columns) if c != col] for r in range(self.rows) if r != row])
+
+ def det(self):
+ if self.rows != self.columns:
+ raise TypeError, 'Can only calculate determinant for a square matrix'
+ if self.rows == 1:
+ return self[0][0]
+ if self.rows == 2:
+ return self[0][0] * self[1][1] - self[0][1] * self[1][0]
+ det = 0
+ for i in range(self.columns):
+ det += (-1)**i * self.array[0][i] * self.minor(0, i).det()
+ return det
+
+ def __getitem__(self, index):
+ return self.array[index]
+
+ def __add__(self, other):
+ if self.rows != other.rows or self.columns != other.columns:
+ raise TypeError, 'Both matrices should have equal dimensions. Is ({} x {}) and ({} x {}).'.format(self.rows, self.columns, other.rows, other.columns)
+ return Matrix([[self[r][c] + other[r][c] for c in range(self.columns)] for r in range(self.rows)])
+
+ def __mul__(self, other):
+ if self.columns != other.rows:
+ raise TypeError, 'Left matrix should have same number of columns as right matrix has rows. Is ({} x {}) and ({} x {}).'.format(self.rows, self.columns, other.rows, other.columns)
+ return Matrix([[sum([self[r][i] * other[i][c] for i in range(self.columns)]) for c in range(other.columns)] for r in range(self.rows)])
diff --git a/extensions/fablabchemnitz/box_maker_elliptical_box/inkscape_helper/PathSegment.py b/extensions/fablabchemnitz/box_maker_elliptical_box/inkscape_helper/PathSegment.py
new file mode 100644
index 0000000..38673ab
--- /dev/null
+++ b/extensions/fablabchemnitz/box_maker_elliptical_box/inkscape_helper/PathSegment.py
@@ -0,0 +1,29 @@
+from collections import namedtuple
+
+PathPoint = namedtuple('PathPoint', 't coord tangent curvature c_dist')
+
+class PathSegment(object):
+
+ def __init__(self):
+ raise NotImplementedError
+
+ @property
+ def lenth(self):
+ raise NotImplementedError
+
+ def subdivide(self, part_length):
+ raise NotImplementedError
+
+ def pathpoint_at_t(self, t):
+ raise NotImplementedError
+
+ def t_at_length(self, length):
+ raise NotImplementedError
+
+ # also need:
+
+ # find a way do do curvature dependent spacing
+ # - based on deviation from a standard radius?
+ # - or ratio between thickness and curvature?
+ #def point_at_distance(d):
+ # pass
diff --git a/extensions/fablabchemnitz/box_maker_elliptical_box/inkscape_helper/SVG.py b/extensions/fablabchemnitz/box_maker_elliptical_box/inkscape_helper/SVG.py
new file mode 100644
index 0000000..e3a300f
--- /dev/null
+++ b/extensions/fablabchemnitz/box_maker_elliptical_box/inkscape_helper/SVG.py
@@ -0,0 +1,93 @@
+import inkex
+import simplestyle
+from lxml import etree
+
+def _format_1st(command, is_absolute):
+ """Small helper function for the Path class"""
+ return command.upper() if is_absolute else command.lower()
+
+default_style = str(inkex.Style(
+ {'stroke': '#000000',
+ 'stroke-width': '0.1',
+ 'fill': 'none'
+ }))
+
+red_style = str(inkex.Style(
+ {'stroke': '#FF0000',
+ 'stroke-width': '0.1',
+ 'fill': 'none'
+ }))
+
+green_style = str(inkex.Style(
+ {'stroke': '#00FF00',
+ 'stroke-width': '0.1',
+ 'fill': 'none'
+ }))
+
+blue_style = str(inkex.Style(
+ {'stroke': '#0000FF',
+ 'stroke-width': '0.1',
+ 'fill': 'none'
+ }))
+
+def layer(parent, layer_name):
+ layer = etree.SubElement(parent, 'g')
+ layer.set(inkex.addNS('label', 'inkscape'), layer_name)
+ layer.set(inkex.addNS('groupmode', 'inkscape'), 'layer')
+ return layer
+
+def group(parent):
+ return etree.SubElement(parent, 'g')
+
+def text(parent, coordinate, txt, style=default_style):
+ text = etree.Element(inkex.addNS('text', 'svg'))
+ text.text = txt
+ text.set('x', str(coordinate.x))
+ text.set('y', str(coordinate.y))
+ style = {'text-align': 'center', 'text-anchor': 'middle'}
+ text.set('style', str(inkex.Style(style)))
+ parent.append(text)
+
+class Path(object):
+ """
+ Generates SVG paths
+ """
+ def __init__(self):
+ self.nodes = []
+
+ def move_to(self, coord, absolute=False):
+ self.nodes.append("{0} {1} {2}".format(_format_1st('m', absolute), coord.x, coord.y))
+
+ def line_to(self, coord, absolute=False):
+ self.nodes.append("{0} {1} {2}".format(_format_1st('l', absolute), coord.x, coord.y))
+
+ def h_line_to(self, dist, absolute=False):
+ self.nodes.append("{0} {1}".format(_format_1st('h', absolute), dist))
+
+ def v_line_to(self, dist, absolute=False):
+ self.nodes.append("{0} {1}".format(_format_1st('v', absolute), dist))
+
+ def arc_to(self, radius, coord, rotation=0, pos_sweep=True, large_arc=False, absolute=False):
+ self.nodes.append("{0} {1} {2} {3} {4} {5} {6} {7}"
+ .format(_format_1st('a', absolute), radius.x, radius.y, rotation,
+ 1 if large_arc else 0, 1 if pos_sweep else 0, coord.x, coord.y))
+
+ def close(self):
+ self.nodes.append('z')
+
+ def path(self, parent, style=default_style):
+ attribs = {'style': style,
+ 'd': ' '.join(self.nodes)}
+ etree.SubElement(parent, inkex.addNS('path', 'svg'), attribs)
+
+ def curve(self, parent, segments, style, closed=True):
+ pathStr = ' '.join(segments)
+ if closed:
+ pathStr += ' z'
+ attributes = {
+ 'style': style,
+ 'd': pathStr}
+ etree.SubElement(parent, inkex.addNS('path', 'svg'), attributes)
+
+ def remove_last(self):
+ self.nodes.pop()
diff --git a/extensions/fablabchemnitz/box_maker_elliptical_box/inkscape_helper/__init__.py b/extensions/fablabchemnitz/box_maker_elliptical_box/inkscape_helper/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/extensions/fablabchemnitz/box_maker_elliptical_box/meta.json b/extensions/fablabchemnitz/box_maker_elliptical_box/meta.json
new file mode 100644
index 0000000..1fd1d7b
--- /dev/null
+++ b/extensions/fablabchemnitz/box_maker_elliptical_box/meta.json
@@ -0,0 +1,21 @@
+[
+ {
+ "name": "Box Maker - Elliptical Box",
+ "id": "fablabchemnitz.de.box_maker_elliptical_box",
+ "path": "box_maker_elliptical_box",
+ "dependent_extensions": null,
+ "original_name": " Elliptical Box Maker",
+ "original_id": "be.fablab-leuven.inkscape.elliptical_box",
+ "license": "MIT License",
+ "license_url": "https://github.com/BvdP/elliptical-box-maker/blob/master/LICENSE",
+ "comment": "",
+ "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/box_maker_elliptical_box",
+ "fork_url": "https://github.com/BvdP/elliptical-box-maker",
+ "documentation_url": "https://stadtfabrikanten.org/display/IFM/Box+Maker+-+Elliptical+Box",
+ "inkscape_gallery_url": null,
+ "main_authors": [
+ "github.com/BvdP",
+ "github.com/eridur-de"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/box_maker_lasercut_box/box_maker_lasercut_box.inx b/extensions/fablabchemnitz/box_maker_lasercut_box/box_maker_lasercut_box.inx
new file mode 100644
index 0000000..2150c90
--- /dev/null
+++ b/extensions/fablabchemnitz/box_maker_lasercut_box/box_maker_lasercut_box.inx
@@ -0,0 +1,89 @@
+
+
+ Box Maker - Lasercut Box
+ fablabchemnitz.de.box_maker_lasercut_box
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 50.0
+ 30.0
+ 15
+ 3.0
+ 11
+ 11
+ 6
+ true
+ true
+
+
+
+ 0.0
+ false
+
+
+
+ false
+
+
+
+
+
+
+
+ true
+
+
+
+ all
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/box_maker_lasercut_box/box_maker_lasercut_box.py b/extensions/fablabchemnitz/box_maker_lasercut_box/box_maker_lasercut_box.py
new file mode 100644
index 0000000..8415c24
--- /dev/null
+++ b/extensions/fablabchemnitz/box_maker_lasercut_box/box_maker_lasercut_box.py
@@ -0,0 +1,473 @@
+#!/usr/bin/env python3
+'''
+Copyright (C)2011 Mark Schafer
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+'''
+
+# Build a tabbed box for lasercutting with tight fit, and minimal material use options.
+# User defines:
+# - internal or external dimensions,
+# - number of tabs,
+# - amount lost to laser (kerf),
+# - include corner cubes or not,
+# - dimples, or perfect fit (accounting for kerf).
+# If zero kerf - will be perfectly packed for minimal laser cuts and material size.
+
+### Todo
+# add option to pack multiple boxes (if zero kerf) - new tab maybe?
+# add option for little circles at sharp corners for acrylic
+# annotations: - add overlap value as markup - Ponoko annotation color
+# choose colours from a dictionary
+
+### Versions
+# 0.1 February 2011 - basic lasercut box with dimples etc
+# 0.2 changes to unittouu for Inkscape 0.91
+# 0.3 Option to avoid half-sized tabs at corners.
+
+__version__ = "0.3"
+
+import inkex
+from inkex.paths import Path
+from lxml import etree
+
+class BoxMakerLasercutBox(inkex.EffectExtension):
+
+ def add_arguments(self, pars):
+ pars.add_argument("-i", "--int_ext", type = inkex.Boolean, default=False, help="Are the Dimensions for External or Internal sizing.")
+ pars.add_argument("-x", "--width", type=float, default=50.0, help="The Box Width - in the X dimension")
+ pars.add_argument("-y", "--height", type=float, default=30.0, help="The Box Height - in the Y dimension")
+ pars.add_argument("-z", "--depth", type=float, default=15.0, help="The Box Depth - in the Z dimension")
+ pars.add_argument("-t", "--thickness", type=float, default=3.0, help="Material Thickness - critical to know")
+ pars.add_argument("-u", "--units", default="cm", help="The unit of the box dimensions")
+ pars.add_argument("-c", "--corners", type = inkex.Boolean, default=True, help="The corner cubes can be removed for a different look")
+ pars.add_argument("-H", "--halftabs", type = inkex.Boolean, default=True, help="Start/End with half-sized tabs - Avoid with very small tabs")
+ pars.add_argument("-p", "--ntab_W", type=int, default=11, help="Number of tabs in Width")
+ pars.add_argument("-q", "--ntab_H", type=int, default=11, help="Number of tabs in Height")
+ pars.add_argument("-r", "--ntab_D", type=int, default=6, help="Number of tabs in Depth")
+ pars.add_argument("-k", "--kerf_size", type=float,default=0.0, help="Kerf size - amount lost to laser for this material. 0 = loose fit")
+ pars.add_argument("-d", "--dimples", type=inkex.Boolean, default=False, help="Add dimples for press fitting wooden materials")
+ pars.add_argument("-s", "--dstyle", type=inkex.Boolean, default=False, help="Dimples can be triangles(cheaper) or half rounds(better)")
+ pars.add_argument("-g", "--linewidth", type=inkex.Boolean, default=False, help="Use the kerf value as the drawn line width")
+ pars.add_argument("-j", "--annotation", type=inkex.Boolean, default=True, help="Show Kerf value as annotation")
+ #dummy for the doc tab - which is named
+ pars.add_argument("--tab", default="use", help="The selected UI-tab when OK was pressed")
+ #internal useful variables
+
+ def annotation(self, x, y, text):
+ """ Draw text at this location
+ - change to path
+ - use annotation color """
+ pass
+
+
+ def thickness_line(self, dimple, vert_horiz, pos_neg):
+ """ called to draw dimples (also draws simple lines if no dimple)
+ - pos_neg is 1, -1 for direction
+ - vert_horiz is v or h """
+ if dimple and self.kerf > 0.0: # we need a dimple
+ # size is radius = kerf
+ # short line, half circle, short line
+ #[ 'C', [x1,y1, x2,y2, x,y] ] x1 is first handle, x2 is second
+ lines = []
+ radius = self.kerf
+ if self.thick - 2 * radius < 0.2: # correct for large dimples(kerf) on small thicknesses
+ radius = (self.thick - 0.2) / 2
+ short = 0.1
+ else:
+ short = self.thick/2 - radius
+ if vert_horiz == 'v': # vertical line
+ # first short line
+ lines.append(['v', [pos_neg*short]])
+ # half circle
+ if pos_neg == 1: # only the DH_sides need reversed tabs to interlock
+ if self.dimple_tri:
+ lines.append(['l', [radius, pos_neg*radius]])
+ lines.append(['l', [-radius, pos_neg*radius]])
+ else:
+ lines.append(['c', [radius, 0, radius, pos_neg*2*radius, 0, pos_neg*2*radius]])
+ else:
+ if self.dimple_tri:
+ lines.append(['l', [-radius, pos_neg*radius]])
+ lines.append(['l', [radius, pos_neg*radius]])
+ else:
+ lines.append(['c', [-radius, 0, -radius, pos_neg*2*radius, 0, pos_neg*2*radius]])
+ # last short line
+ lines.append(['v', [pos_neg*short]])
+ else: # horizontal line
+ # first short line
+ lines.append(['h', [pos_neg*short]])
+ # half circle
+ if self.dimple_tri:
+ lines.append(['l', [pos_neg*radius, radius]])
+ lines.append(['l', [pos_neg*radius, -radius]])
+ else:
+ lines.append(['c', [0, radius, pos_neg*2*radius, radius, pos_neg*2*radius, 0]])
+ # last short line
+ lines.append(['h', [pos_neg*short]])
+ return lines
+
+ # No dimple - so much easier
+ else: # return a straight v or h line same as thickness
+ if vert_horiz == 'v':
+ return [ ['v', [pos_neg*self.thick]] ]
+ else:
+ return [ ['h', [pos_neg*self.thick]] ]
+
+
+ def draw_WH_lid(self, startx, starty, masktop=False):
+ """ Return an SVG path for the top or bottom of box
+ - the Width * Height dimension """
+ line_path = []
+ line_path.append(['M', [startx, starty]])
+ # top row of tabs
+ if masktop and self.kerf ==0.0: # don't draw top for packing with no extra cuts
+ line_path.append(['m', [self.boxW, 0]])
+ else:
+ if not self.ht: line_path.append(['l', [self.boxW/self.Wtabs/4 - self.pf/2, 0]])
+ for i in range(int(self.Wtabs)):
+ line_path.append(['h', [self.boxW/self.Wtabs/4 - self.pf/2]])
+ #line_path.append(['v', [0, -thick]]) # replaced with dimpled version
+ for l in self.thickness_line(self.dimple, 'v', -1):
+ line_path.append(l)
+ line_path.append(['h', [self.boxW/self.Wtabs/2 + self.pf]])
+ line_path.append(['v', [self.thick]])
+ line_path.append(['h', [self.boxW/self.Wtabs/4 - self.pf/2]])
+ if not self.ht: line_path.append(['l', [self.boxW/self.Wtabs/4 - self.pf/2, 0]])
+ # right hand vertical drop
+ if not self.ht: line_path.append(['l', [0, self.boxH/self.Htabs/4 - self.pf/2]])
+ for i in range(int(self.Htabs)):
+ line_path.append(['v', [self.boxH/self.Htabs/4 - self.pf/2]])
+ line_path.append(['h', [self.thick]])
+ line_path.append(['v', [self.boxH/self.Htabs/2 + self.pf]])
+ #line_path.append(['h', [-thick, 0]]) # replaced with dimpled version
+ for l in self.thickness_line(self.dimple, 'h', -1):
+ line_path.append(l)
+ line_path.append(['v', [self.boxH/self.Htabs/4 - self.pf/2]])
+ if not self.ht: line_path.append(['l', [0, self.boxH/self.Htabs/4 - self.pf/2]])
+ # bottom row (in reverse)
+ if not self.ht: line_path.append(['l', [-self.boxW/self.Wtabs/4 + self.pf/2, 0]])
+ for i in range(int(self.Wtabs)):
+ line_path.append(['h', [-self.boxW/self.Wtabs/4 + self.pf/2]])
+ line_path.append(['v', [self.thick]])
+ line_path.append(['h', [-self.boxW/self.Wtabs/2 - self.pf]])
+ #line_path.append(['v', [0, -thick]]) # replaced with dimpled version
+ for l in self.thickness_line(self.dimple, 'v', -1):
+ line_path.append(l)
+ line_path.append(['h', [-self.boxW/self.Wtabs/4 + self.pf/2]])
+ if not self.ht: line_path.append(['l', [-self.boxW/self.Wtabs/4 + self.pf/2, 0]])
+ # up the left hand side
+ if not self.ht: line_path.append(['l', [0, -self.boxH/self.Htabs/4 + self.pf/2]])
+ for i in range(int(self.Htabs)):
+ line_path.append(['v', [-self.boxH/self.Htabs/4 + self.pf/2]])
+ #line_path.append(['h', [-thick, 0]]) # replaced with dimpled version
+ for l in self.thickness_line(self.dimple, 'h', -1):
+ line_path.append(l)
+ line_path.append(['v', [-self.boxH/self.Htabs/2 - self.pf]])
+ line_path.append(['h', [self.thick]])
+ line_path.append(['v', [-self.boxH/self.Htabs/4 + self.pf/2]])
+ if not self.ht: line_path.append(['l', [0, -self.boxH/self.Htabs/4 + self.pf/2]])
+ return line_path
+
+
+ def draw_WD_side(self, startx, starty, mask=False, corners=True):
+ """ Return an SVG path for the long side of box
+ - the Width * Depth dimension """
+ # Draw side of the box (placed below the lid)
+ line_path = []
+ # top row of tabs
+ if corners:
+ line_path.append(['M', [startx - self.thick, starty]])
+ line_path.append(['v', [-self.thick]])
+ line_path.append(['h', [self.thick]])
+ else:
+ line_path.append(['M', [startx, starty]])
+ line_path.append(['v', [-self.thick]])
+ #
+ if self.kerf > 0.0: # if fit perfectly - don't draw double line
+ if not self.ht: line_path.append(['l', [self.boxW/self.Wtabs/4 + self.pf/2, 0]])
+ for i in range(int(self.Wtabs)):
+ line_path.append(['h', [self.boxW/self.Wtabs/4 + self.pf/2]])
+ line_path.append(['v', [self.thick]])
+ line_path.append(['h', [self.boxW/self.Wtabs/2 - self.pf]])
+ #line_path.append(['v', [0, -thick]]) # replaced with dimpled version
+ for l in self.thickness_line(self.dimple, 'v', -1):
+ line_path.append(l)
+ line_path.append(['h', [self.boxW/self.Wtabs/4 + self.pf/2]])
+ if not self.ht: line_path.append(['l', [self.boxW/self.Wtabs/4 + self.pf/2, 0]])
+ if corners: line_path.append(['h', [self.thick]])
+ else: # move to skipped drawn lines
+ if corners:
+ line_path.append(['m', [self.boxW + self.thick, 0]])
+ else:
+ line_path.append(['m', [self.boxW, 0]])
+ #
+ line_path.append(['v', [self.thick]])
+ if not corners: line_path.append(['h', [self.thick]])
+ # RHS
+ if not self.ht: line_path.append(['l', [0, self.boxD/self.Dtabs/4 + self.pf/2]])
+ for i in range(int(self.Dtabs)):
+ line_path.append(['v', [self.boxD/self.Dtabs/4 + self.pf/2]])
+ #line_path.append(['h', [-thick, 0]]) # replaced with dimpled version
+ for l in self.thickness_line(self.dimple, 'h', -1):
+ line_path.append(l)
+ line_path.append(['v', [self.boxD/self.Dtabs/2 - self.pf]])
+ line_path.append(['h', [self.thick]])
+ line_path.append(['v', [self.boxD/self.Dtabs/4 + self.pf/2]])
+ if not self.ht: line_path.append(['l', [0, self.boxD/self.Dtabs/4 + self.pf/2]])
+ #
+ if corners:
+ line_path.append(['v', [self.thick]])
+ line_path.append(['h', [-self.thick]])
+ else:
+ line_path.append(['h', [-self.thick]])
+ line_path.append(['v', [self.thick]])
+ # base
+ if not self.ht: line_path.append(['l', [-self.boxW/self.Wtabs/4 - self.pf/2, 0]])
+ for i in range(int(self.Wtabs)):
+ line_path.append(['h', [-self.boxW/self.Wtabs/4 - self.pf/2]])
+ #line_path.append(['v', [0, -thick]]) # replaced with dimpled version
+ for l in self.thickness_line(self.dimple, 'v', -1):
+ line_path.append(l)
+ line_path.append(['h', [-self.boxW/self.Wtabs/2 + self.pf]])
+ line_path.append(['v', [self.thick]])
+ line_path.append(['h', [-self.boxW/self.Wtabs/4 - self.pf/2]])
+ if not self.ht: line_path.append(['l', [-self.boxW/self.Wtabs/4 - self.pf/2, 0]])
+ #
+ if corners:
+ line_path.append(['h', [-self.thick]])
+ line_path.append(['v', [-self.thick]])
+ else:
+ line_path.append(['v', [-self.thick]])
+ line_path.append(['h', [-self.thick]])
+ # LHS
+ if not self.ht: line_path.append(['l', [0, -self.boxD/self.Dtabs/4 - self.pf/2]])
+ for i in range(int(self.Dtabs)):
+ line_path.append(['v', [-self.boxD/self.Dtabs/4 - self.pf/2]])
+ line_path.append(['h', [self.thick]])
+ line_path.append(['v', [-self.boxD/self.Dtabs/2 + self.pf]])
+ #line_path.append(['h', [-thick, 0]]) # replaced with dimpled version
+ for l in self.thickness_line(self.dimple, 'h', -1):
+ line_path.append(l)
+ line_path.append(['v', [-self.boxD/self.Dtabs/4 - self.pf/2]])
+ if not self.ht: line_path.append(['l', [0, -self.boxD/self.Dtabs/4 - self.pf/2]])
+ #
+ if not corners: line_path.append(['h', [self.thick]])
+ return line_path
+
+
+ def draw_HD_side(self, startx, starty, corners, mask=False):
+ """ Return an SVG path for the short side of box
+ - the Height * Depth dimension """
+ line_path = []
+ # top row of tabs
+ line_path.append(['M', [startx, starty]])
+ if not(mask and corners and self.kerf == 0.0):
+ line_path.append(['h', [self.thick]])
+ else:
+ line_path.append(['m', [self.thick, 0]])
+ if not self.ht: line_path.append(['l', [self.boxD/self.Dtabs/4 - self.pf/2, 0]])
+ for i in range(int(self.Dtabs)):
+ line_path.append(['h', [self.boxD/self.Dtabs/4 - self.pf/2]])
+ line_path.append(['v', [-self.thick]])
+ line_path.append(['h', [self.boxD/self.Dtabs/2 + self.pf]])
+ #line_path.append(['v', [0, thick]]) # replaced with dimpled version
+ for l in self.thickness_line(self.dimple, 'v', 1):
+ line_path.append(l)
+ line_path.append(['h', [self.boxD/self.Dtabs/4 - self.pf/2]])
+ if not self.ht: line_path.append(['l', [self.boxD/self.Dtabs/4 - self.pf/2, 0]])
+ line_path.append(['h', [self.thick]])
+ #
+ if not self.ht: line_path.append(['l', [0, self.boxH/self.Htabs/4 + self.pf/2]])
+ for i in range(int(self.Htabs)):
+ line_path.append(['v', [self.boxH/self.Htabs/4 + self.pf/2]])
+ #line_path.append(['h', [-thick, 0]]) # replaced with dimpled version
+ for l in self.thickness_line(self.dimple, 'h', -1):
+ line_path.append(l)
+ line_path.append(['v', [self.boxH/self.Htabs/2 - self.pf]])
+ line_path.append(['h', [self.thick]])
+ line_path.append(['v', [self.boxH/self.Htabs/4 + self.pf/2]])
+ if not self.ht: line_path.append(['l', [0, self.boxH/self.Htabs/4 + self.pf/2]])
+ line_path.append(['h', [-self.thick]])
+ #
+ if not self.ht: line_path.append(['l', [-self.boxD/self.Dtabs/4 + self.pf/2, 0]])
+ for i in range(int(self.Dtabs)):
+ line_path.append(['h', [-self.boxD/self.Dtabs/4 + self.pf/2]])
+ #line_path.append(['v', [0, thick]]) # replaced with dimpled version
+ for l in self.thickness_line(self.dimple, 'v', 1): # this is the weird +1 instead of -1 dimple
+ line_path.append(l)
+ line_path.append(['h', [-self.boxD/self.Dtabs/2 - self.pf]])
+ line_path.append(['v', [-self.thick]])
+ line_path.append(['h', [-self.boxD/self.Dtabs/4 + self.pf/2]])
+ if not self.ht: line_path.append(['l', [-self.boxD/self.Dtabs/4 + self.pf/2, 0]])
+ line_path.append(['h', [-self.thick]])
+ #
+ if self.kerf > 0.0: # if fit perfectly - don't draw double line
+ if not self.ht: line_path.append(['l', [0, -self.boxH/self.Htabs/4 - self.pf/2]])
+ for i in range(int(self.Htabs)):
+ line_path.append(['v', [-self.boxH/self.Htabs/4 - self.pf/2]])
+ line_path.append(['h', [self.thick]])
+ line_path.append(['v', [-self.boxH/self.Htabs/2 + self.pf]])
+ #line_path.append(['h', [-thick, 0]]) # replaced with dimpled version
+ for l in self.thickness_line(self.dimple, 'h', -1):
+ line_path.append(l)
+ line_path.append(['v', [-self.boxH/self.Htabs/4 - self.pf/2]])
+ if not self.ht: line_path.append(['l', [0, -self.boxH/self.Htabs/4 - self.pf/2]])
+ return line_path
+
+ ###--------------------------------------------
+ ### The main function called by the inkscape UI
+ def effect(self):
+
+ self.stroke_width = self.svg.unittouu('1px') #default for visiblity
+ self.line_style = {'stroke': '#0000FF', # Ponoko blue
+ 'fill': 'none',
+ 'stroke-width': self.stroke_width,
+ 'stroke-linecap': 'butt',
+ 'stroke-linejoin': 'miter'}
+
+
+ # document dimensions (for centering)
+ docW = self.svg.unittouu(self.document.getroot().get('width'))
+ docH = self.svg.unittouu(self.document.getroot().get('height'))
+ # extract fields from UI
+ self.boxW = self.svg.unittouu(str(self.options.width) + self.options.units)
+ self.boxH = self.svg.unittouu(str(self.options.height) + self.options.units)
+ self.boxD = self.svg.unittouu(str(self.options.depth) + self.options.units)
+ self.thick = self.svg.unittouu(str(self.options.thickness) + self.options.units)
+ self.kerf = self.svg.unittouu(str(self.options.kerf_size) + self.options.units)
+ if self.kerf < 0.01: self.kerf = 0.0 # snap to 0 for UI error when setting spinner to 0.0
+ self.Wtabs = self.options.ntab_W
+ self.Htabs = self.options.ntab_H
+ self.Dtabs = self.options.ntab_D
+ self.dimple = self.options.dimples
+ line_width = self.options.linewidth
+ corners = self.options.corners
+ self.dimple_tri = self.options.dstyle
+ self.annotation = self.options.annotation
+ self.ht = self.options.halftabs
+ if not self.ht:
+ self.Wtabs += 0.5
+ self.Htabs += 0.5
+ self.Dtabs += 0.5
+
+ # Correct for thickness in dimensions
+ if self.options.int_ext: # external so add thickness
+ self.boxW -= self.thick*2
+ self.boxH -= self.thick*2
+ self.boxD -= self.thick*2
+ # adjust for laser kerf (precise measurement)
+ self.boxW += self.kerf
+ self.boxH += self.kerf
+ self.boxD += self.kerf
+
+ # Precise fit or dimples (if kerf > 0.0)
+ if self.dimple == False: # and kerf > 0.0:
+ self.pf = self.kerf
+ else:
+ self.pf = 0.0
+
+ # set the stroke width and line style
+ sw = self.kerf
+ if self.kerf == 0.0: sw = self.stroke_width
+ ls = self.line_style
+ if line_width: # user wants drawn line width to be same as kerf size
+ ls['stroke-width'] = sw
+ line_style = str(inkex.Style(ls))
+
+ ###---------------------------
+ ### create the inkscape object
+ box_id = self.svg.get_unique_id('box')
+ self.box = g = etree.SubElement(self.svg.get_current_layer(), 'g', {'id':box_id})
+
+ #Set local position for drawing (will transform to center of doc at end)
+ lower_pos = 0
+ left_pos = 0
+ # Draw Lid (using SVG path definitions)
+ line_path = self.draw_WH_lid(left_pos, lower_pos)
+ # Add to scene
+ line_atts = { 'style':line_style, 'id':box_id+'-lid', 'd':str(Path(line_path)) }
+ etree.SubElement(g, inkex.addNS('path','svg'), line_atts)
+
+ # draw the side of the box directly below
+ if self.kerf > 0.0:
+ lower_pos += self.boxH + (3*self.thick)
+ else: # kerf = 0 so don't draw extra lines and fit perfectly
+ lower_pos += self.boxH + self.thick # at lower edge of lid
+ left_pos += 0
+ # Draw side of the box (placed below the lid)
+ line_path = self.draw_WD_side(left_pos, lower_pos, corners=corners)
+ # Add to scene
+ line_atts = { 'style':line_style, 'id':box_id+'-longside1', 'd':str(Path(line_path)) }
+ etree.SubElement(g, inkex.addNS('path','svg'), line_atts)
+
+ # draw the bottom of the box directly below
+ if self.kerf > 0.0:
+ lower_pos += self.boxD + (3*self.thick)
+ else: # kerf = 0 so don't draw extra lines and fit perfectly
+ lower_pos += self.boxD + self.thick # at lower edge
+ left_pos += 0
+ # Draw base of the box
+ line_path = self.draw_WH_lid(left_pos, lower_pos, True)
+ # Add to scene
+ line_atts = { 'style':line_style, 'id':box_id+'-base', 'd':str(Path(line_path)) }
+ etree.SubElement(g, inkex.addNS('path','svg'), line_atts)
+
+ # draw the second side of the box directly below
+ if self.kerf > 0.0:
+ lower_pos += self.boxH + (3*self.thick)
+ else: # kerf = 0 so don't draw extra lines and fit perfectly
+ lower_pos += self.boxH + self.thick # at lower edge of lid
+ left_pos += 0
+ # Draw side of the box (placed below the lid)
+ line_path = self.draw_WD_side(left_pos, lower_pos, corners=corners)
+ # Add to scene
+ line_atts = { 'style':line_style, 'id':box_id+'-longside2', 'd':str(Path(line_path)) }
+ etree.SubElement(g, inkex.addNS('path','svg'), line_atts)
+
+ # draw next on RHS of lid
+ if self.kerf > 0.0:
+ left_pos += self.boxW + (2*self.thick) # adequate space (could be a param for separation when kerf > 0)
+ else:
+ left_pos += self.boxW # right at right edge of lid
+ lower_pos = 0
+ # Side of the box (placed next to the lid)
+ line_path = self.draw_HD_side(left_pos, lower_pos, corners)
+ # Add to scene
+ line_atts = { 'style':line_style, 'id':box_id+'-endface2', 'd':str(Path(line_path)) }
+ etree.SubElement(g, inkex.addNS('path','svg'), line_atts)
+
+ # draw next on RHS of base
+ if self.kerf > 0.0:
+ lower_pos += self.boxH + self.boxD + 6*self.thick
+ else:
+ lower_pos += self.boxH +self.boxD + 2*self.thick
+ # Side of the box (placed next to the lid)
+ line_path = self.draw_HD_side(left_pos, lower_pos, corners, True)
+ # Add to scene
+ line_atts = { 'style':line_style, 'id':box_id+'-endface1', 'd':str(Path(line_path)) }
+ etree.SubElement(g, inkex.addNS('path','svg'), line_atts)
+
+
+ ###----------------------------------------
+ # Transform entire drawing to center of doc
+ lower_pos += self.boxH*2 + self.boxD*2 + 2*self.thick
+ left_pos += self.boxH + 2*self.thick
+ g.set( 'transform', 'translate(%f,%f)' % ( (docW-left_pos)/2, (docH-lower_pos)/2))
+
+ # The implementation algorithm has added intermediate short lines and doubled up when using h,v with extra zeros
+ #self.thin(g) # remove short straight lines
+
+if __name__ == '__main__':
+ BoxMakerLasercutBox().run()
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/box_maker_lasercut_box/meta.json b/extensions/fablabchemnitz/box_maker_lasercut_box/meta.json
new file mode 100644
index 0000000..33df582
--- /dev/null
+++ b/extensions/fablabchemnitz/box_maker_lasercut_box/meta.json
@@ -0,0 +1,22 @@
+[
+ {
+ "name": "Box Maker - Lasercut Box",
+ "id": "fablabchemnitz.de.box_maker_lasercut_box",
+ "path": "box_maker_lasercut_box",
+ "dependent_extensions": null,
+ "original_name": "Lasercut Box",
+ "original_id": "org.inkscape.LasercutBox",
+ "license": "GNU GPL v2",
+ "license_url": "https://github.com/Neon22/inkscape-LasercutBox/blob/master/LICENSE",
+ "comment": "",
+ "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/box_maker_lasercut_box",
+ "fork_url": "https://github.com/Neon22/inkscape-LasercutBox",
+ "documentation_url": "https://stadtfabrikanten.org/display/IFM/Box+Maker+-+Lasercut+Box",
+ "inkscape_gallery_url": null,
+ "main_authors": [
+ "github.com/Neon22",
+ "github.com/jnweiger",
+ "github.com/eridur-de"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/box_maker_path_to_flex/.gitignore b/extensions/fablabchemnitz/box_maker_path_to_flex/.gitignore
new file mode 100644
index 0000000..e394608
--- /dev/null
+++ b/extensions/fablabchemnitz/box_maker_path_to_flex/.gitignore
@@ -0,0 +1 @@
+/DebugPath2Flex.txt
diff --git a/extensions/fablabchemnitz/box_maker_path_to_flex/box_maker_path_to_flex.inx b/extensions/fablabchemnitz/box_maker_path_to_flex/box_maker_path_to_flex.inx
new file mode 100644
index 0000000..dbed152
--- /dev/null
+++ b/extensions/fablabchemnitz/box_maker_path_to_flex/box_maker_path_to_flex.inx
@@ -0,0 +1,29 @@
+
+
+ Box Maker - Path To Flex
+ fablabchemnitz.de.box_maker_path_to_flex
+
+
+
+
+
+
+
+
+ 3.0
+ 50.0
+ 2
+ 1000.0
+ true
+
+ all
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/box_maker_path_to_flex/box_maker_path_to_flex.py b/extensions/fablabchemnitz/box_maker_path_to_flex/box_maker_path_to_flex.py
new file mode 100644
index 0000000..50e1ea8
--- /dev/null
+++ b/extensions/fablabchemnitz/box_maker_path_to_flex/box_maker_path_to_flex.py
@@ -0,0 +1,1354 @@
+#!/usr/bin/env python3
+
+
+# paths2flex.py
+
+# This is an Inkscape extension to generate boxes with sides as flex which follow a path selected in inkscape
+# The Inkscape objects must first be converted to paths (Path > Object to Path).
+# Some paths may not work well -- if the curves are too small for example.
+
+# Written by Thierry Houdoin (thierry@fablab-lannion.org), december 2018
+# This work is largely inspred from path2openSCAD.py, written by Daniel C. Newman
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+import math
+import os.path
+import inkex
+import re
+from lxml import etree
+from inkex import bezier
+from inkex.paths import Path, CubicSuperPath
+
+DEFAULT_WIDTH = 100
+DEFAULT_HEIGHT = 100
+
+objStyle = str(inkex.Style(
+ {'stroke': '#000000',
+ 'stroke-width': 0.1,
+ 'fill': 'none'
+ }))
+
+objStyleStart = str(inkex.Style(
+ {'stroke': '#FF0000',
+ 'stroke-width': 0.1,
+ 'fill': 'none'
+ }))
+
+
+class inkcape_draw_cartesian:
+ def __init__(self, Offset, group):
+ self.offsetX = Offset[0]
+ self.offsetY = Offset[1]
+ self.Path = ''
+ self.group = group
+
+ def MoveTo(self, x, y):
+ #Retourne chaine de caractères donnant la position du point avec des coordonnées cartesiennes
+ self.Path += ' M ' + str(round(x-self.offsetX, 3)) + ',' + str(round(y-self.offsetY, 3))
+
+ def LineTo(self, x, y):
+ #Retourne chaine de caractères donnant la position du point avec des coordonnées cartesiennes
+ self.Path += ' L ' + str(round(x-self.offsetX, 3)) + ',' + str(round(y-self.offsetY, 3))
+
+ def Line(self, x1, y1, x2, y2):
+ #Retourne chaine de caractères donnant la position du point avec des coordonnées cartesiennes
+ self.Path += ' M ' + str(round(x1-self.offsetX, 3)) + ',' + str(round(y1-self.offsetY, 3)) + ' L ' + str(round(x2-self.offsetX, 3)) + ',' + str(round(y2-self.offsetY, 3))
+
+ def GenPath(self):
+ line_attribs = {'style': objStyle, 'd': self.Path}
+ etree.SubElement(self.group, inkex.addNS('path', 'svg'), line_attribs)
+
+ def GenPathStart(self):
+ line_attribs = {'style': objStyleStart, 'd': self.Path}
+ etree.SubElement(self.group, inkex.addNS('path', 'svg'), line_attribs)
+
+
+class Line:
+ def __init__(self, a, b, c):
+ self.a = a
+ self.b = b
+ self.c = c
+ def __str__(self):
+ return "Line a="+str(self.a)+" b="+str(self.b)+" c="+str(self.c)
+
+ def Intersect(self, Line2):
+ ''' Return the point which is at the intersection between the two lines
+ '''
+ det = Line2.a * self.b - self.a*Line2.b;
+ if abs(det) < 1e-6: # Line are parallel, return None
+ return None
+ return ((Line2.b*self.c - Line2.c*self.b)/det, (self.a*Line2.c - Line2.a*self.c)/det)
+
+ def square_line_distance(self, pt):
+ '''
+ Compute the distance between point and line
+ Distance between point and line is (a * pt.x + b * pt.y + c)*(a * pt.x + b * pt.y + c)/(a*a + b*b)
+ '''
+ return (self.a * pt[0] + self.b * pt[1] + self.c)*(self.a * pt[0]+ self.b * pt[1] + self.c)/(self.a*self.a + self.b*self.b)
+
+class Segment(Line):
+ def __init__(self, A, B):
+ self.xA = A[0]
+ self.xB = B[0]
+ self.yA = A[1]
+ self.yB = B[1]
+ self.xm = min(self.xA, self.xB)
+ self.xM = max(self.xA, self.xB)
+ self.ym = min(self.yA, self.yB)
+ self.yM = max(self.yA, self.yB)
+ Line.__init__(self, A[1] - B[1], B[0] - A[0], A[0] * B[1] - B[0] * A[1])
+
+ def __str__(self):
+ return "Segment "+str([A,B])+ " a="+str(self.a)+" b="+str(self.b)+" c="+str(self.c)
+
+ def InSegment(self, Pt):
+ if Pt[0] < self.xm or Pt[0] > self.xM:
+ return 0 # Impossible lower than xmin or greater than xMax
+ if Pt[1] < self.ym or Pt[1] > self.yM:
+ return 0 # Impossible lower than ymin or greater than yMax
+ return 1
+
+ def __str__(self):
+ return "Seg"+str([(self.xA, self.yA), (self.xB, self.yB)])+" Line a="+str(self.a)+" b="+str(self.b)+" c="+str(self.c)
+
+def pointInBBox(pt, bbox):
+
+ '''
+ Determine if the point pt=[x, y] lies on or within the bounding
+ box bbox=[xmin, xmax, ymin, ymax].
+ '''
+
+ # if (x < xmin) or (x > xmax) or (y < ymin) or (y > ymax)
+ if (pt[0] < bbox[0]) or (pt[0] > bbox[1]) or \
+ (pt[1] < bbox[2]) or (pt[1] > bbox[3]):
+ return False
+ else:
+ return True
+
+def bboxInBBox(bbox1, bbox2):
+
+ '''
+ Determine if the bounding box bbox1 lies on or within the
+ bounding box bbox2. NOTE: we do not test for strict enclosure.
+
+ Structure of the bounding boxes is
+
+ bbox1 = [ xmin1, xmax1, ymin1, ymax1 ]
+ bbox2 = [ xmin2, xmax2, ymin2, ymax2 ]
+ '''
+
+ # if (xmin1 < xmin2) or (xmax1 > xmax2) or (ymin1 < ymin2) or (ymax1 > ymax2)
+
+ if (bbox1[0] < bbox2[0]) or (bbox1[1] > bbox2[1]) or \
+ (bbox1[2] < bbox2[2]) or (bbox1[3] > bbox2[3]):
+ return False
+ else:
+ return True
+
+def pointInPoly(p, poly, bbox=None):
+
+ '''
+ Use a ray casting algorithm to see if the point p = [x, y] lies within
+ the polygon poly = [[x1,y1],[x2,y2],...]. Returns True if the point
+ is within poly, lies on an edge of poly, or is a vertex of poly.
+ '''
+
+ if (p is None) or (poly is None):
+ return False
+
+ # Check to see if the point lies outside the polygon's bounding box
+ if not bbox is None:
+ if not pointInBBox(p, bbox):
+ return False
+
+ # Check to see if the point is a vertex
+ if p in poly:
+ return True
+
+ # Handle a boundary case associated with the point
+ # lying on a horizontal edge of the polygon
+ x = p[0]
+ y = p[1]
+ p1 = poly[0]
+ p2 = poly[1]
+ for i in range(len(poly)):
+ if i != 0:
+ p1 = poly[i-1]
+ p2 = poly[i]
+ if (y == p1[1]) and (p1[1] == p2[1]) and \
+ (x > min(p1[0], p2[0])) and (x < max(p1[0], p2[0])):
+ return True
+
+ n = len(poly)
+ inside = False
+
+ p1_x,p1_y = poly[0]
+ for i in range(n + 1):
+ p2_x,p2_y = poly[i % n]
+ if y > min(p1_y, p2_y):
+ if y <= max(p1_y, p2_y):
+ if x <= max(p1_x, p2_x):
+ if p1_y != p2_y:
+ intersect = p1_x + (y - p1_y) * (p2_x - p1_x) / (p2_y - p1_y)
+ if x <= intersect:
+ inside = not inside
+ else:
+ inside = not inside
+ p1_x,p1_y = p2_x,p2_y
+
+ return inside
+
+def polyInPoly(poly1, bbox1, poly2, bbox2):
+
+ '''
+ Determine if polygon poly2 = [[x1,y1],[x2,y2],...]
+ contains polygon poly1.
+
+ The bounding box information, bbox=[xmin, xmax, ymin, ymax]
+ is optional. When supplied it can be used to perform rejections.
+ Note that one bounding box containing another is not sufficient
+ to imply that one polygon contains another. It's necessary, but
+ not sufficient.
+ '''
+
+ # See if poly1's bboundin box is NOT contained by poly2's bounding box
+ # if it isn't, then poly1 cannot be contained by poly2.
+
+ if (not bbox1 is None) and (not bbox2 is None):
+ if not bboxInBBox(bbox1, bbox2):
+ return False
+
+ # To see if poly1 is contained by poly2, we need to ensure that each
+ # vertex of poly1 lies on or within poly2
+
+ for p in poly1:
+ if not pointInPoly(p, poly2, bbox2):
+ return False
+
+ # Looks like poly1 is contained on or in Poly2
+
+ return True
+
+def subdivideCubicPath(sp, flat, i=1):
+
+ '''
+ [ Lifted from eggbot.py with impunity ]
+
+ Break up a bezier curve into smaller curves, each of which
+ is approximately a straight line within a given tolerance
+ (the "smoothness" defined by [flat]).
+
+ This is a modified version of cspsubdiv.cspsubdiv(): rewritten
+ because recursion-depth errors on complicated line segments
+ could occur with cspsubdiv.cspsubdiv().
+ '''
+
+ while True:
+ while True:
+ if i >= len(sp):
+ return
+
+ p0 = sp[i - 1][1]
+ p1 = sp[i - 1][2]
+ p2 = sp[i][0]
+ p3 = sp[i][1]
+
+ b = (p0, p1, p2, p3)
+
+ if bezier.maxdist(b) > flat:
+ break
+
+ i += 1
+
+ one, two = bezier.beziersplitatt(b, 0.5)
+ sp[i - 1][2] = one[1]
+ sp[i][0] = two[2]
+ p = [one[2], one[3], two[1]]
+ sp[i:1] = [p]
+
+# Second degree equation solver.
+# Return a tuple with the two real solutions, raise an error if there is no real solution
+def Solve2nd(a, b, c):
+ delta = b**2 - 4*a*c
+ if (delta < 0):
+ print("No real solution")
+ return none
+ x1 = (-b + math.sqrt(delta))/(2*a)
+ x2 = (-b - math.sqrt(delta))/(2*a)
+ return (x1, x2)
+
+# Compute distance between two points
+def distance2points(x0, y0, x1, y1):
+ return math.hypot(x0-x1,y0-y1)
+
+class BoxMakerPathToFlex(inkex.EffectExtension):
+
+ def add_arguments(self, pars):
+ self.knownUnits = ['in', 'pt', 'px', 'mm', 'cm', 'm', 'km', 'pc', 'yd', 'ft']
+ pars.add_argument('--unit', default = 'mm', help = 'Unit, should be one of ')
+ pars.add_argument('--thickness', type = float, default = '3.0', help = 'Material thickness')
+ pars.add_argument('--zc', type = float, default = '50.0', help = 'Flex height')
+ pars.add_argument('--notch_interval', type = int, default = '2', help = 'Interval between notches')
+ pars.add_argument('--max_size_flex', type = float, default = '1000.0', help = 'Max size of a single band of flex, above this limit it will be cut')
+ pars.add_argument('--Mode_Debug', type = inkex.Boolean, default = 'false', help = 'Output Debug information in file')
+
+ # Dictionary of paths we will construct. It's keyed by the SVG node
+ # it came from. Such keying isn't too useful in this specific case,
+ # but it can be useful in other applications when you actually want
+ # to go back and update the SVG document
+ self.paths = {}
+
+ self.flexnotch = []
+ # Debug Output file
+ self.fDebug = None
+
+ # Dictionary of warnings issued. This to prevent from warning
+ # multiple times about the same problem
+ self.warnings = {}
+
+ #Get bounding rectangle
+ self.xmin, self.xmax = (1.0E70, -1.0E70)
+ self.ymin, self.ymax = (1.0E70, -1.0E70)
+ self.cx = float(DEFAULT_WIDTH) / 2.0
+ self.cy = float(DEFAULT_HEIGHT) / 2.0
+
+ def unittouu(self, unit):
+ return inkex.unittouu(unit)
+
+ def DebugMsg(self, s):
+ if self.fDebug:
+ self.fDebug.write(s)
+
+ # Generate long vertical lines for flex
+ # Parameters : StartX, StartY, size, nunmber of lines and +1 if lines goes up and -1 down
+ def GenLinesFlex(self, StartX, StartY, Size, nLine, UpDown, path):
+ for i in range(nLine):
+ path.Line(StartX, StartY, StartX, StartY + UpDown*Size)
+ self.DebugMsg("GenLinesFlex from "+str((StartX, StartY))+" to "+str((StartX, StartY + UpDown*Size))+'\n')
+ StartY += UpDown*(Size+2)
+
+
+ # Generate the path link to a flex step
+ #
+ def generate_step_flex(self, step, size_notch, ShortMark, LongMark, nMark, index):
+ path = inkcape_draw_cartesian(self.OffsetFlex, self.group)
+ #External part of the notch, fraction of total notch
+ notch_useful = 2.0 / (self.notchesInterval + 2)
+ # First, link towards next step
+ # Line from ((step+1)*size_notch, 0) to ((step+0.5)*size_notch, 0
+ path.Line((step+1)*size_notch, 0, (step+notch_useful)*size_notch, 0)
+ if self.flexnotch[index] == 0:
+ ShortMark = 0
+ # Then ShortLine from ((step+notch_useful)*size_notch, ShortMark) towards ((step+notch_useful)*size_notch, -Thickness)
+ path.Line((step+notch_useful)*size_notch, ShortMark,(step+notch_useful)*size_notch, -self.thickness)
+ # Then notch
+ path.LineTo(step*size_notch, -self.thickness)
+ # Then short mark towards other side (step*size_notch, shortmark)
+ path.LineTo(step*size_notch, ShortMark)
+ if ShortMark != 0: #Only if there is flex
+ # Then line towards center
+ self.GenLinesFlex(step*size_notch, ShortMark + 2, (self.height - 2*ShortMark - 2.0)/(nMark-1) - 2.0, nMark-1, 1, path)
+ # Then notch
+ path.Line(step*size_notch, self.height - ShortMark, step*size_notch, self.height + self.thickness)
+ path.LineTo((step+notch_useful)*size_notch, self.height + self.thickness)
+ path.LineTo((step+notch_useful)*size_notch, self.height - ShortMark)
+ if ShortMark != 0:
+ #Then nMark-1 Lines
+ self.GenLinesFlex((step+notch_useful)*size_notch, self.height - ShortMark - 2, (self.height - 2*ShortMark - 2.0)/(nMark-1) - 2.0, nMark-1, -1, path)
+ #Then Long lines internal to notch
+ self.GenLinesFlex((step+notch_useful/2)*size_notch, 1 - self.thickness, (self.height + 2.0*self.thickness)/nMark - 2, nMark, 1, path)
+ # link towards next One
+ path.Line((step+notch_useful)*size_notch, self.height, (step+1)*size_notch, self.height)
+ if ShortMark != 0:
+ # notchesInterval *nMark Long lines up to next notch or 2 shorts and nMark-1 long
+ i = 1
+ while i < self.notchesInterval:
+ pos = (i + 2.0) / (self.notchesInterval + 2.0)
+ if i % 2 :
+ #odd draw from bottom to top, nMark lines
+ self.GenLinesFlex((step+pos)*size_notch, self.height - 1, self.height /nMark - 2.0, nMark, -1, path)
+ else:
+ # even draw from top to bottom nMark+1 lines, 2 short and nMark-1 Long
+ path.Line((step+pos)*size_notch, 3, (step+pos)*size_notch, ShortMark)
+ self.GenLinesFlex((step+pos)*size_notch, ShortMark + 2, (self.height - 2*ShortMark - 2.0)/(nMark-1) - 2.0, nMark-1, 1, path)
+ path.Line((step+pos)*size_notch, self.height - ShortMark, (step+pos)*size_notch, self.height - 3)
+ i += 1
+ # Write path to inkscape
+ path.GenPath()
+
+ def GenerateStartFlex(self, size_notch, ShortMark, LongMark, nMark, index):
+ '''
+ Draw the start pattern
+ The notch is only 1 mm wide, to enable putting both start and end notch in the same hole in the cover
+ '''
+ path = inkcape_draw_cartesian(self.OffsetFlex, self.group)
+ #External part of the notch, fraction of total notch
+ notch_useful = 1.0 / (self.notchesInterval + 2)
+ notch_in = self.notchesInterval / (self.notchesInterval + 2.0)
+ # First, link towards next step
+ # Line from (, 0) to 0, 0
+ path.Line(-notch_in*size_notch, 0, 0, 0)
+ if self.flexnotch[index] == 0:
+ ShortMark = 0
+ # Then ShortLine from (-notch_in*size_notch, ShortMark) towards -notch_in*size_notch, Thickness)
+ path.Line(-notch_in*size_notch, ShortMark, -notch_in*size_notch, -self.thickness)
+ # Then notch (beware, only size_notch/4 here)
+ path.LineTo((notch_useful-1)*size_notch, -self.thickness)
+ # Then edge, full length
+ path.LineTo((notch_useful-1)*size_notch, self.height+self.thickness)
+ # Then notch
+ path.LineTo(-notch_in*size_notch, self.height + self.thickness)
+ path.LineTo(-notch_in*size_notch, self.height - ShortMark + 1)
+ if ShortMark != 0:
+ #Then nMark - 1 Lines
+ self.GenLinesFlex(-notch_in*size_notch, self.height - ShortMark - 2, (self.height - 2*ShortMark - 2.0)/(nMark-1) - 2.0, nMark-1, -1, path)
+ # link towards next One
+ path.Line(-notch_in*size_notch, self.height, 0, self.height)
+ if ShortMark != 0:
+ # notchesInterval *nMark Long lines up to next notch or 2 shorts and nMark-1 long
+ i = 1
+ while i < self.notchesInterval:
+ pos = (i - self.notchesInterval) / (self.notchesInterval + 2.0)
+ if i % 2 :
+ #odd draw from bottom to top, nMark lines
+ self.GenLinesFlex(pos*size_notch, self.height - 1, self.height /nMark - 2.0, nMark, -1, path)
+ else:
+ # even draw from top to bottom nMark+1 lines, 2 short and nMark-1 Long
+ path.Line(pos*size_notch, 3, pos*size_notch, ShortMark)
+ self.GenLinesFlex(pos*size_notch, ShortMark + 2, (self.height - 2*ShortMark - 2.0)/(nMark-1) - 2.0, nMark-1, 1, path)
+ path.Line(pos*size_notch, self.height - ShortMark, pos*size_notch, self.height - 3)
+ i += 1
+ path.GenPath()
+
+
+ def GenerateEndFlex(self, step, size_notch, ShortMark, LongMark, nMark, index):
+ path = inkcape_draw_cartesian(self.OffsetFlex, self.group)
+ delta_notch = 1.0 / (self.notchesInterval + 2.0)
+ if self.flexnotch[index] == 0:
+ ShortMark = 0
+ # ShortLine from (step*size_notch, ShortMark) towards step*size_notch, -Thickness)
+ path.Line(step*size_notch, ShortMark, step*size_notch, -self.thickness)
+ # Then notch (beware, only 1mm here)
+ path.LineTo((step+delta_notch)*size_notch, -self.thickness)
+ # Then edge, full length
+ path.LineTo((step+delta_notch)*size_notch, self.height+self.thickness)
+ # Then notch
+ path.LineTo(step*size_notch, self.height + self.thickness)
+ path.LineTo(step*size_notch, self.height - ShortMark)
+ if ShortMark != 0:
+ #Then nMark - 1 Lines
+ self.GenLinesFlex(step*size_notch, self.height - ShortMark - 2, (self.height - 2*ShortMark - 2.0)/(nMark-1) - 2.0, nMark-1, -1, path)
+ path.GenPath()
+
+
+ def GenFlex(self, parent, num_notch, size_notch, xOffset, yOffset):
+ group = etree.SubElement(parent, 'g')
+ self.group = group
+ #Compute number of vertical lines. Each long mark should be at most 50mm long to avoid failures
+ nMark = int(self.height / 50) + 1
+ nMark = max(nMark, 2) # At least 2 marks
+ #Then compute number of flex bands
+ FlexLength = num_notch * size_notch
+ nb_flex_band = int (FlexLength // self.max_flex_size) + 1
+ notch_per_band = num_notch / nb_flex_band + 1
+ self.DebugMsg("Generate flex structure with "+str(nb_flex_band)+" bands, "+str(num_notch)+" notches, offset ="+str((xOffset, yOffset))+'\n')
+ #Sizes of short and long lines to make flex
+ LongMark = (self.height / nMark) - 2.0 #Long Mark equally divide the height
+ ShortMark = LongMark/2 # And short mark should lay at center of long marks
+ idx_notch = 0
+ while num_notch > 0:
+ self.OffsetFlex = (xOffset, yOffset)
+ self.GenerateStartFlex(size_notch, ShortMark, LongMark, nMark, idx_notch)
+ idx_notch += 1
+ notch = 0
+ if notch_per_band > num_notch:
+ notch_per_band = num_notch #for the last one
+ while notch < notch_per_band - 1:
+ self.generate_step_flex(notch, size_notch, ShortMark, LongMark, nMark, idx_notch)
+ notch += 1
+ idx_notch += 1
+ num_notch -= notch_per_band
+ if num_notch == 0:
+ self.GenerateEndFlex(notch, size_notch, ShortMark, LongMark, nMark, 0)
+ else:
+ self.GenerateEndFlex(notch, size_notch, ShortMark, LongMark, nMark, idx_notch)
+ xOffset -= size_notch * notch_per_band + 10
+
+ def getPathVertices(self, path, node=None):
+
+ '''
+ Decompose the path data from an SVG element into individual
+ subpaths, each subpath consisting of absolute move to and line
+ to coordinates. Place these coordinates into a list of polygon
+ vertices.
+ '''
+ self.DebugMsg("Entering getPathVertices, len="+str(len(path))+"\n")
+ if (not path) or (len(path) == 0):
+ # Nothing to do
+ return None
+
+ # parsePath() may raise an exception. This is okay
+ simple_path = Path(path).to_arrays()
+ if (not simple_path) or (len(simple_path) == 0):
+ # Path must have been devoid of any real content
+ return None
+ self.DebugMsg("After parsePath in getPathVertices, len="+str(len(simple_path))+"\n")
+ self.DebugMsg(" Path = "+str(simple_path)+'\n')
+
+ # Get a cubic super path
+ cubic_super_path = CubicSuperPath(simple_path)
+ if (not cubic_super_path) or (len(cubic_super_path) == 0):
+ # Probably never happens, but...
+ return None
+ self.DebugMsg("After CubicSuperPath in getPathVertices, len="+str(len(cubic_super_path))+"\n")
+
+
+ # Now traverse the cubic super path
+ subpath_list = []
+ subpath_vertices = []
+ index_sp = 0
+ for sp in cubic_super_path:
+
+ # We've started a new subpath
+ # See if there is a prior subpath and whether we should keep it
+ self.DebugMsg("Processing SubPath"+str(index_sp)+" SubPath List len="+str(len(subpath_list))+" Vertices list length="+str(len(subpath_vertices)) +"\n")
+
+ if len(subpath_vertices):
+ subpath_list.append(subpath_vertices)
+
+ subpath_vertices = []
+ self.DebugMsg("Before subdivideCubicPath len="+str(len(sp)) +"\n")
+ self.DebugMsg(" Bsp="+str(sp)+'\n')
+ subdivideCubicPath(sp, 0.1)
+ self.DebugMsg("After subdivideCubicPath len="+str(len(sp)) +"\n")
+ self.DebugMsg(" Asp="+str(sp)+'\n')
+
+ # Note the first point of the subpath
+ first_point = sp[0][1]
+ subpath_vertices.append(first_point)
+ sp_xmin = first_point[0]
+ sp_xmax = first_point[0]
+ sp_ymin = first_point[1]
+ sp_ymax = first_point[1]
+
+ n = len(sp)
+
+ # Traverse each point of the subpath
+ for csp in sp[1:n]:
+
+ # Append the vertex to our list of vertices
+ pt = csp[1]
+ subpath_vertices.append(pt)
+ #self.DebugMsg("Append subpath_vertice '"+str(pt)+"len="+str(len(subpath_vertices)) +"\n")
+
+
+ # Track the bounding box of this subpath
+ if pt[0] < sp_xmin:
+ sp_xmin = pt[0]
+ elif pt[0] > sp_xmax:
+ sp_xmax = pt[0]
+ if pt[1] < sp_ymin:
+ sp_ymin = pt[1]
+ elif pt[1] > sp_ymax:
+ sp_ymax = pt[1]
+
+ # Track the bounding box of the overall drawing
+ # This is used for centering the polygons in OpenSCAD around the (x,y) origin
+ if sp_xmin < self.xmin:
+ self.xmin = sp_xmin
+ if sp_xmax > self.xmax:
+ self.xmax = sp_xmax
+ if sp_ymin < self.ymin:
+ self.ymin = sp_ymin
+ if sp_ymax > self.ymax:
+ self.ymax = sp_ymax
+
+ # Handle the final subpath
+ if len(subpath_vertices):
+ subpath_list.append(subpath_vertices)
+
+ if len(subpath_list) > 0:
+ self.paths[node] = subpath_list
+
+ '''
+ self.DebugMsg("After getPathVertices\n")
+ index_i = 0
+ for i in self.paths[node]:
+ index_j = 0
+ for j in i:
+ self.DebugMsg('Path '+str(index_i)+" élément "+str(index_j)+" = "+str(j)+'\n')
+ index_j += 1
+ index_i += 1
+ '''
+
+ def DistanceOnPath(self, p, pt, index):
+ '''
+ Return the distances before and after the point pt on the polygon p
+ The point pt is in the segment index of p, that is between p[index] and p[index+1]
+ '''
+ i = 0
+ before = 0
+ after = 0
+ while i < index:
+ # First walk through polygon up to p[index]
+ before += distance2points(p[i+1][0], p[i+1][1], p[i][0], p[i][1])
+ i += 1
+ #For the segment index compute the part before and after
+ before += distance2points(pt[0], pt[1], p[index][0], p[index][1])
+ after += distance2points(pt[0], pt[1], p[index+1][0], p[index+1][1])
+ i = index + 1
+ while i < len(p)-1:
+ after += distance2points(p[i+1][0], p[i+1][1], p[i][0], p[i][1])
+ i += 1
+ return (before, after)
+
+ # Compute position of next notch.
+ # Next notch will be on the path p, and at a distance notch_size from previous point
+ # Return new index in path p
+ def compute_next_notch(self, notch_points, p, Angles_p, last_index_in_p, notch_size):
+ index_notch = len(notch_points)
+ # Coordinates of last notch
+ Ox = notch_points[index_notch - 1][0]
+ Oy = notch_points[index_notch - 1][1]
+ CurAngle = Angles_p[last_index_in_p-1]
+ #self.DebugMsg("Enter cnn:last_index_in_p="+str(last_index_in_p)+" CurAngle="+str(round(CurAngle*180/math.pi))+" Segment="+str((p[last_index_in_p-1], p[last_index_in_p]))+" Length="+str(distance2points(p[last_index_in_p-1][0], p[last_index_in_p-1][1], p[last_index_in_p][0], p[last_index_in_p][1]))+"\n")
+ DeltaAngle = 0
+ while last_index_in_p < (len(p) - 1) and distance2points(Ox, Oy, p[last_index_in_p][0], p[last_index_in_p][1]) < notch_size + DeltaAngle*self.thickness/2.0:
+ Diff_angle = Angles_p[last_index_in_p] - CurAngle
+ if Diff_angle > math.pi:
+ Diff_angle -= 2*math.pi
+ elif Diff_angle < -math.pi:
+ Diff_angle += 2*math.pi
+ Diff_angle = abs(Diff_angle)
+ DeltaAngle += Diff_angle
+ CurAngle = Angles_p[last_index_in_p]
+ #self.DebugMsg("cnn:last_index_in_p="+str(last_index_in_p)+" Angle="+str(round(Angles_p[last_index_in_p]*180/math.pi))+" Diff_angle="+str(round(Diff_angle*180/math.pi))+" DeltaAngle="+str(round(DeltaAngle*180/math.pi))+" Distance="+str(distance2points(Ox, Oy, p[last_index_in_p][0], p[last_index_in_p][1]))+"/"+str(notch_size + DeltaAngle*self.thickness/2.0)+"\n")
+ last_index_in_p += 1 # Go to next point in polygon
+
+
+ # Starting point for the line x0, y0 is p[last_index_in_p-1]
+ x0 = p[last_index_in_p-1][0]
+ y0 = p[last_index_in_p-1][1]
+ # End point for the line x1, y1 is p[last_index_in_p]
+ x1 = p[last_index_in_p][0]
+ y1 = p[last_index_in_p][1]
+
+ Distance_notch = notch_size + DeltaAngle*self.thickness/2.0
+ #self.DebugMsg(" compute_next_notch("+str(index_notch)+") Use Segment="+str(last_index_in_p)+" DeltaAngle="+str(round(DeltaAngle*180/math.pi))+"°, notch_size="+str(notch_size)+" Distance_notch="+str(Distance_notch)+'\n')
+
+ # The actual notch position will be on the line between last_index_in_p-1 and last_index_in_p and at a distance Distance_notch of Ox,Oy
+ # The intersection of a line and a circle could be computed as a second degree equation in a general case
+ # Specific case, when segment is vertical
+ if abs(x1-x0) <0.001:
+ # easy case, x= x0 so y = sqrt(d2 - x*x)
+ solx1 = x0
+ solx2 = x0
+ soly1 = Oy + math.sqrt(Distance_notch**2 - (x0 - Ox)**2)
+ soly2 = Oy - math.sqrt(Distance_notch**2 - (x0 - Ox)**2)
+ else:
+ Slope = (y1 - y0) / (x1 - x0)
+ # The actual notch position will be on the line between last_index_in_p-1 and last_index_in_p and at a distance notch size of Ox,Oy
+ # The intersection of a line and a circle could be computed as a second degree equation
+ # The coefficients of this equation are computed below
+ a = 1.0 + Slope**2
+ b = 2*Slope*y0 - 2*Slope**2*x0 - 2*Ox - 2*Slope*Oy
+ c = Slope**2*x0**2 + y0**2 -2*Slope*x0*y0 + 2*Slope*x0*Oy - 2*y0*Oy + Ox**2 + Oy**2 - Distance_notch**2
+ solx1, solx2 = Solve2nd(a, b, c)
+ soly1 = y0 + Slope*(solx1-x0)
+ soly2 = y0 + Slope*(solx2-x0)
+ # Now keep the point which is between (x0,y0) and (x1, y1)
+ # The distance between (x1,y1) and the "good" solution will be lower than the distance between (x0,y0) and (x1,y1)
+ distance1 = distance2points(x1, y1, solx1, soly1)
+ distance2 = distance2points(x1, y1, solx2, soly2)
+ if distance1 < distance2:
+ #Keep solx1
+ solx = solx1
+ soly = soly1
+ else:
+ #Keep solx2
+ solx = solx2
+ soly = soly2
+ notch_points.append((solx, soly, last_index_in_p-1))
+ if abs(distance2points(solx, soly, Ox, Oy) - Distance_notch) > 1:
+ #Problem
+ self.DebugMsg("Problem in compute_next_notch: x0,y0 ="+str((x0,y0))+" x1,y1="+str((x1,y1))+'\n')
+ self.DebugMsg("Len(p)="+str(len(p))+'\n')
+ self.DebugMsg("Slope="+str(Slope)+'\n')
+ self.DebugMsg("solx1="+str(solx1)+" soly1="+str(soly1)+" soly1="+str(solx2)+" soly1="+str(soly2)+'\n')
+ self.DebugMsg(str(index_notch)+": Adding new point ("+str(solx)+","+ str(soly) + "), distance is "+ str(distance2points(solx, soly, Ox, Oy))+ " New index in path :"+str(last_index_in_p)+'\n')
+
+ #self.DebugMsg(str(index_notch)+": Adding new point ("+str(solx)+","+ str(soly) + "), distance is "+ str(distance2points(solx, soly, Ox, Oy))+ " New index in path :"+str(last_index_in_p)+'\n')
+ return last_index_in_p
+
+ def DrawPoly(self, p, parent):
+ group = etree.SubElement(parent, 'g')
+ Newpath = inkcape_draw_cartesian((self.xmin - self.xmax - 10, 0), group)
+ self.DebugMsg('DrawPoly First element (0) : '+str(p[0])+ ' Call MoveTo('+ str(p[0][0])+','+str(p[0][1])+'\n')
+ Newpath.MoveTo(p[0][0], p[0][1])
+ n = len(p)
+ index = 1
+ for point in p[1:n]:
+ Newpath.LineTo(point[0], point[1])
+ index += 1
+ Newpath.GenPath()
+
+ def Simplify(self, poly, max_error):
+ '''
+ Simplify the polygon, remove vertices which are aligned or too close from others
+ The parameter give the max error, below this threshold, points will be removed
+ return the simplified polygon, which is modified in place
+ '''
+ #First point
+ LastIdx = 0
+ limit = max_error * max_error #Square because distance will be square !
+ i = 1
+ while i < len(poly)-1:
+ #Build segment between Vertex[i-1] and Vertex[i+1]
+ Seg = Segment(poly[LastIdx], poly[i+1])
+ #self.DebugMsg("Pt["+str(i)+"]/"+str(len(poly))+" ="+str(poly[i])+" Segment="+str(Seg)+"\n")
+ # Compute square of distance between Vertex[i] and Segment
+ dis_square = Seg.square_line_distance(poly[i])
+ if dis_square < max_error:
+ # Too close, remove this point
+ poly.pop(i) #and do NOT increment index
+ #self.DebugMsg("Simplify, removing pt "+str(i)+"="+str(poly[i])+" in Segment : "+str(Seg)+" now "+str(len(poly))+" vertices\n")
+ else:
+ LastIdx = i
+ i += 1 #Increment index
+ # No need to process last point, it should NOT be modified and stay equal to first one
+ return poly
+
+ def MakePolyCCW(self, p):
+ '''
+ Take for polygon as input and make it counter clockwise.
+ If already CCW, just return the polygon, if not reverse it
+ To determine if polygon is CCW, compute area. If > 0 the polygon is CCW
+ '''
+ area = 0
+ for i in range(len(p)-1):
+ area += p[i][0]*p[i+1][1] - p[i+1][0]*p[i][1]
+ self.DebugMsg("poly area = "+str(area/2)+"\n")
+ if area < 0:
+ # Polygon is cloackwise, reverse
+ p.reverse()
+ self.DebugMsg("Polygon was clockwise, reverse it\n")
+ return p
+
+ def ComputeAngles(self, p):
+ '''
+ Compute a list with angles of all edges of the polygon
+ Return this list
+ '''
+ angles = []
+ for i in range(len(p)-1):
+ a = math.atan2(p[i+1][1] - p[i][1], p[i+1][0] - p[i][0])
+ angles.append(a)
+ # Last value is not defined as Pt n-1 = Pt 0, set it to angle[0]
+ angles.append(angles[0])
+ return angles
+
+ def writeModifiedPath(self, node, parent):
+ '''
+ Take the paths (polygons) computed from previous step and generate
+ 1) The input path with notches
+ 2) The flex structure associated with the path with notches (same length and number of notches)
+ '''
+ path = self.paths[node]
+ if (path is None) or (len(path) == 0):
+ return
+ self.DebugMsg('Enter writeModifiedPath, node='+str(node)+' '+str(len(path))+' paths, global Offset'+str((self.xmin - self.xmax - 10, 0))+'\n')
+
+ # First, if there are several paths, checks if one path is included in the first one.
+ # If not exchange such as the first one is the bigger one.
+ # All paths which are not the first one will have notches reverted to be outside the polygon instead of inside the polygon.
+ # On the finbal paths, these notches will always be inside the form.
+ if len(path) > 1:
+ OrderPathModified = True
+ # Arrange paths such as greater one is first, all others
+ while OrderPathModified:
+ OrderPathModified = False
+ for i in range(1, len(path)):
+ if polyInPoly(path[i], None, path[0], None):
+ self.DebugMsg("Path "+str(i)+" is included in path 0\n")
+ elif polyInPoly(path[0], None, path[i], None):
+ self.DebugMsg("Path "+str(i)+" contains path 0, exchange\n")
+ path[0], path[i] = path[i], path[0]
+ OrderPathModified = True
+
+ index_path = 0
+ xFlexOffset = self.xmin - 2*self.xmax - 20
+ yFlexOffset = self.height - self.ymax - 10
+ for p in path:
+ self.DebugMsg('Processing Path, '+str(index_path)+" Len(path)="+str(len(p))+'\n')
+ self.DebugMsg('p='+str(p)+'\n')
+ reverse_notch = False
+ if index_path > 0 and polyInPoly(p, None, path[0], None):
+ reverse_notch = True # For included path, reverse notches
+ #Simplify path, remove unnecessary vertices
+ p = self.Simplify(p, 0.1)
+ self.DebugMsg("---After simplification, path has "+str(len(p))+" vertices\n")
+ #Ensure that polygon is counter clockwise
+ p = self.MakePolyCCW(p)
+ self.DrawPoly(p, parent)
+ #Now compute path length. Path length is the sum of length of edges
+ length_path = 0
+ n = len(p)
+ index = 1
+ while index < n:
+ length_path += math.hypot((p[index][0] - p[index-1][0]), (p[index][1] - p[index-1][1]))
+ index += 1
+
+ angles = self.ComputeAngles(p)
+ # compute the sum of angles difference and check that it is 2*pi
+ SumAngle = 0.0
+ for i in range(len(p)-1):
+ Delta_angle = angles[i+1] - angles[i]
+ if Delta_angle > math.pi:
+ Delta_angle -= 2*math.pi
+ elif Delta_angle < -math.pi:
+ Delta_angle += 2*math.pi
+ Delta_angle = abs(Delta_angle)
+ self.DebugMsg("idx="+str(i)+" Angle1 ="+str(round(angles[i]*180/math.pi,3))+" Angle 2="+str(round(angles[i+1]*180/math.pi,3))+" Delta angle="+str(round(Delta_angle*180/math.pi, 3))+"°\n")
+ SumAngle += Delta_angle
+ self.DebugMsg("Sum of angles="+str(SumAngle*180/math.pi)+"°\n")
+
+ # Flex length will be path length - thickness*SumAngle/2 to keep flex aligned on the shortest path
+ flex_length = length_path - self.thickness*SumAngle/2
+
+ self.DebugMsg('Path length ='+str(length_path)+" Flex length ="+str(flex_length)+" Difference="+str(length_path-flex_length)+'\n')
+
+ #Default notch size is notchesInterval + 2mm
+ #Actual notch size will be adjusted to match the length
+ notch_number = int(round(flex_length / (self.notchesInterval + 2), 0))
+ notch_size = flex_length / notch_number
+ self.DebugMsg('Number of notches ='+str(notch_number)+' ideal notch size =' + str(round(notch_size,3)) +'\n')
+
+
+ # Compute position of the points on the path that will become notches
+ # Starting at 0, each point will be at distance actual_notch_size from the previous one, at least on one side of the notch (the one with the smallest distance)
+ # On the path (middle line) the actual distance will be notch_size + thickness*delta_angle/2 where delta angle is the difference between the angle at starting point and end point
+ # As notches are not aligned to vertices, the actual length of the path will be different from the computed one (lower in fact)
+ # To avoid a last notch too small, we will repeat the process until the size of the last notch is OK (less than .1mm error)
+ # Use an algorithm which corrects the notch_size by computing previous length of the last notch
+
+ nb_try = 0
+ size_last_notch = 0
+ oldSize = 0
+ BestDifference = 9999999
+ BestNotchSize = notch_size
+ mode_linear = False
+ delta_notch = -0.01 #In most cases, should reduce notch size
+ while nb_try < 100:
+ notch_points = [ (p[0][0], p[0][1], 0) ] # Build a list of tuples with corrdinates (x,y) and offset within polygon which is 0 the the starting point
+ index = 1 # Notch index
+ last_index_in_p = 1 # Start at 1, index 0 is the current one
+ self.DebugMsg("Pass "+str(nb_try)+" First point ("+str(p[0][0])+","+ str(p[0][1]) + ' notch_size='+str(notch_size)+'\n')
+ while index < notch_number:
+ #Compute next notch point and append it to the list
+ last_index_in_p = self.compute_next_notch(notch_points, p, angles, last_index_in_p, notch_size)
+ #before, after = self.DistanceOnPath(p, notch_points[index], last_index_in_p-1)
+ #self.DebugMsg(" Notch "+str(index)+" placed in "+str(notch_points[index])+" distance before ="+str(before)+" after="+str(after)+" total="+str(before+after)+'\n')
+ index += 1
+ size_last_notch = distance2points(p[n-1][0], p[n-1][1], notch_points[index-1][0], notch_points[index-1][1])
+ self.DebugMsg("Last notch size :"+str(size_last_notch)+'\n')
+ if abs(notch_size - size_last_notch) < BestDifference:
+ BestNotchSize = notch_size
+ BestDifference = abs(notch_size - size_last_notch)
+ if abs(notch_size - size_last_notch) <= 0.1:
+ break
+ # Change size_notch, cut small part in each notch
+ # The 0.5 factor is used to avoid non convergent series (too short then too long...)
+ if mode_linear:
+ if notch_size > size_last_notch and delta_notch > 0:
+ delta_notch -= delta_notch*0.99
+ elif notch_size < size_last_notch and delta_notch < 0:
+ delta_notch -= delta_notch*0.99
+ notch_size += delta_notch
+ self.DebugMsg("Linear mode, changing delta_notch size :"+str(delta_notch)+" --> notch_size="+str(notch_size)+'\n')
+ else:
+ if notch_size > size_last_notch and delta_notch > 0:
+ delta_notch = -0.5*delta_notch
+ self.DebugMsg("Changing delta_notch size :"+str(delta_notch)+'\n')
+ elif notch_size < size_last_notch and delta_notch < 0:
+ delta_notch = -0.5*delta_notch
+ self.DebugMsg("Changing delta_notch size :"+str(delta_notch)+'\n')
+ notch_size += delta_notch
+ if abs(delta_notch) < 0.002:
+ mode_linear = True
+
+ # Change size_notch, cut small part in each notch
+ oldSize = notch_size
+ # The 0.5 factor is used to avoid non convergent series (too short then too long...)
+ notch_size -= 0.5*(notch_size - size_last_notch)/notch_number
+ nb_try += 1
+
+ if nb_try >= 100:
+ self.DebugMsg("Algorithm doesn't converge, use best results :"+str(BestNotchSize)+" which gave last notch size difference "+str(BestDifference)+'\n')
+ notch_size = BestNotchSize
+
+ # Now draw the actual notches
+ group = etree.SubElement(parent, 'g')
+ # First draw a start line which will help to position flex.
+ Startpath = inkcape_draw_cartesian(((self.xmin - self.xmax - 10), 0), group)
+ index_in_p = notch_points[0][2]
+ AngleSlope = math.atan2(p[index_in_p+1][1] - p[index_in_p][1], p[index_in_p+1][0] - p[index_in_p][0])
+ #Now compute both ends of the notch,
+ AngleOrtho = AngleSlope + math.pi/2
+ Line_Start = (notch_points[0][0] + self.thickness/2*math.cos(AngleOrtho), notch_points[0][1] + self.thickness/2*math.sin(AngleOrtho))
+ Line_End = (notch_points[0][0] - self.thickness/2*math.cos(AngleOrtho), notch_points[0][1] - self.thickness/2*math.sin(AngleOrtho))
+ self.DebugMsg("Start line Start"+str(Line_Start)+" End("+str(Line_End)+" Start inside "+str(pointInPoly(Line_Start, p))+ " End inside :"+str(pointInPoly(Line_End, p))+'\n')
+ #Notch End should be inside the path and Notch Start outside... If not reverse
+ if pointInPoly(Line_Start, p):
+ Line_Start, Line_End = Line_End, Line_Start
+ AngleOrtho += math.pi
+ elif not pointInPoly(Line_End, p):
+ #Specific case, neither one is in Polygon (Open path ?), take the lowest Y as Line_End
+ if Line_End[1] > Line_Start[0]:
+ Line_Start, Line_End = Line_End, Line_Start
+ AngleOrtho += math.pi
+ #Now compute a new Start, inside the polygon Start = 3*End - 2*Start
+ newLine_Start = (3*Line_End[0] - 2*Line_Start[0], 3*Line_End[1] - 2*Line_Start[1])
+ Startpath.MoveTo(newLine_Start[0], newLine_Start[1])
+ Startpath.LineTo(Line_End[0], Line_End[1])
+ self.DebugMsg("Draw StartLine start from "+str((newLine_Start[0], newLine_Start[1]))+" to "+str((Line_End[0], Line_End[1]))+'\n')
+ Startpath.GenPathStart()
+
+ #Then draw the notches
+ Newpath = inkcape_draw_cartesian(((self.xmin - self.xmax - 10), 0), group)
+ self.DebugMsg("Generate path with "+str(notch_number)+" notches, offset ="+str(((self.xmin - self.xmax - 10), 0))+'\n')
+ isClosed = distance2points(p[n-1][0], p[n-1][1], p[0][0], p[0][1]) < 0.1
+ # Each notch is a tuple with (X, Y, index_in_p). index_in_p will be used to compute slope of line of the notch
+ # The notch will be thickness long, and there will be a part 'inside' the path and a part 'outside' the path
+ # The longest part will be outside
+ index = 0
+ NX0 = 0
+ NX1 = 0
+ NX2 = 0
+ NX3 = 0
+ NY0 = 0
+ NY1 = 0
+ NY2 = 0
+ NY3 = 0
+ N_Angle = 0
+ Notch_Pos = []
+ while index < notch_number:
+ # Line slope of the path at notch point is
+ index_in_p = notch_points[index][2]
+ N_Angle = angles[index_in_p]
+ AngleSlope = math.atan2(p[index_in_p+1][1] - p[index_in_p][1], p[index_in_p+1][0] - p[index_in_p][0])
+ self.DebugMsg("Draw notch "+str(index)+" Slope is "+str(AngleSlope*180/math.pi)+'\n')
+ self.DebugMsg("Ref="+str(notch_points[index])+'\n')
+ self.DebugMsg("Path points:"+str((p[index_in_p][0], p[index_in_p][1]))+', '+ str((p[index_in_p+1][0], p[index_in_p+1][1]))+'\n')
+ #Now compute both ends of the notch,
+ AngleOrtho = AngleSlope + math.pi/2
+ Notch_Start = (notch_points[index][0] + self.thickness/2*math.cos(AngleOrtho), notch_points[index][1] + self.thickness/2*math.sin(AngleOrtho))
+ Notch_End = (notch_points[index][0] - self.thickness/2*math.cos(AngleOrtho), notch_points[index][1] - self.thickness/2*math.sin(AngleOrtho))
+ self.DebugMsg("Notch "+str(index)+": Start"+str(Notch_Start)+" End("+str(Notch_End)+" Start inside "+str(pointInPoly(Notch_Start, p))+ " End inside :"+str(pointInPoly(Notch_End, p))+'\n')
+ #Notch End should be inside the path and Notch Start outside... If not reverse
+ if pointInPoly(Notch_Start, p):
+ Notch_Start, Notch_End = Notch_End, Notch_Start
+ AngleOrtho += math.pi
+ elif not pointInPoly(Notch_End, p):
+ #Specific case, neither one is in Polygon (Open path ?), take the lowest Y as Notch_End
+ if Notch_End[1] > Notch_Start[0]:
+ Notch_Start, Notch_End = Notch_End, Notch_Start
+ AngleOrtho += math.pi
+ #if should reverse notches, do it now
+ if reverse_notch:
+ Notch_Start, Notch_End = Notch_End, Notch_Start
+ AngleOrtho += math.pi
+ if AngleOrtho > 2*math.pi:
+ AngleOrtho -= 2*math.pi
+ ln = 2.0
+ if index == 0:
+ Newpath.MoveTo(Notch_Start[0], Notch_Start[1])
+ first = (Notch_Start[0], Notch_Start[1])
+ if not isClosed:
+ ln = 1.0 # Actual, different Notch size for the first one when open path
+ else:
+ Newpath.LineTo(Notch_Start[0], Notch_Start[1])
+ if not isClosed and index == notch_number - 1:
+ ln = 1.0
+ self.DebugMsg("LineTo starting point from :"+str((x,y))+" to "+str((Notch_Start[0], Notch_Start[1]))+" Length ="+str(distance2points(x, y, Notch_Start[0], Notch_Start[1]))+'\n')
+ Newpath.LineTo(Notch_End[0], Notch_End[1])
+ NX0 = Notch_Start[0]
+ NY0 = Notch_Start[1]
+ NX1 = Notch_End[0]
+ NY1 = Notch_End[1]
+ self.DebugMsg("Draw notch_1 start from "+str((Notch_Start[0], Notch_Start[1]))+" to "+str((Notch_End[0], Notch_End[1]))+'Center is '+str(((Notch_Start[0]+Notch_End[0])/2, (Notch_Start[1]+Notch_End[1])/2))+'\n')
+ #Now draw a line parallel to the path, which is notch_size*(2/(notchesInterval+2)) long. Internal part of the notch
+ x = Notch_End[0] + (notch_size*ln)/(self.notchesInterval+ln)*math.cos(AngleSlope)
+ y = Notch_End[1] + (notch_size*ln)/(self.notchesInterval+ln)*math.sin(AngleSlope)
+ Newpath.LineTo(x, y)
+ NX2 = x
+ NY2 = y
+ self.DebugMsg("Draw notch_2 to "+str((x, y))+'\n')
+ #Then a line orthogonal, which is thickness long, reverse from first one
+ x = x + self.thickness*math.cos(AngleOrtho)
+ y = y + self.thickness*math.sin(AngleOrtho)
+ Newpath.LineTo(x, y)
+ NX3 = x
+ NY3 = y
+ self.DebugMsg("Draw notch_3 to "+str((x, y))+'\n')
+ Notch_Pos.append((NX0, NY0, NX1, NY1, NX2, NY2, NX3, NY3, N_Angle))
+ # No need to draw the last segment, it will be drawn when starting the next notch
+ index += 1
+ #And the last one if the path is closed
+ if isClosed:
+ self.DebugMsg("Path is closed, draw line to start point "+str((p[0][0], p[0][1]))+'\n')
+ Newpath.LineTo(first[0], first[1])
+ else:
+ self.DebugMsg("Path is open\n")
+ Newpath.GenPath()
+ # Analyze notches for debugging purpose
+ for i in range(len(Notch_Pos)):
+ self.DebugMsg("Notch "+str(i)+" Pos="+str(Notch_Pos[i])+" Angle="+str(round(Notch_Pos[i][8]*180/math.pi))+"\n")
+ if (i > 0):
+ self.DebugMsg(" FromLast Notch N3-N0="+str(distance2points(Notch_Pos[i-1][6], Notch_Pos[i-1][7], Notch_Pos[i][0], Notch_Pos[i][1]))+"\n")
+ self.DebugMsg(" Distances: N0-N3="+str(distance2points(Notch_Pos[i][0], Notch_Pos[i][1], Notch_Pos[i][6], Notch_Pos[i][7]))+" N1-N2="+str(distance2points(Notch_Pos[i][2], Notch_Pos[i][3], Notch_Pos[i][4], Notch_Pos[i][5]))+"\n")
+ # For each notch determine if we need flex or not. Flex is only needed if there is some curves
+ # So if notch[i]-1 notch[i] notch[i+1] are aligned, no need to generate flex in i-1 and i
+ for index in range(notch_number):
+ self.flexnotch.append(1) # By default all notches need flex
+ index = 1
+ while index < notch_number-1:
+ det = (notch_points[index+1][0]- notch_points[index-1][0])*(notch_points[index][1] - notch_points[index-1][1]) - (notch_points[index+1][1] - notch_points[index-1][1])*(notch_points[index][0] - notch_points[index-1][0])
+ self.DebugMsg("Notch "+str(index)+": det="+str(det))
+ if abs(det) < 0.1: # My threhold to be adjusted
+ self.flexnotch[index-1] = 0 # No need for flex for this one and the following
+ self.flexnotch[index] = 0
+ self.DebugMsg(" no flex in notch "+str(index-1)+" and "+str(index))
+ index += 1
+ self.DebugMsg("\n")
+ # For the last one try notch_number - 2, notch_number - 1 and 0
+ det = (notch_points[0][0]- notch_points[notch_number - 2][0])*(notch_points[notch_number - 1][1] - notch_points[notch_number - 2][1]) - (notch_points[0][1] - notch_points[notch_number - 2][1])*(notch_points[notch_number - 1][0] - notch_points[notch_number - 2][0])
+ if abs(det) < 0.1: # My threhold to be adjusted
+ self.flexnotch[notch_number-2] = 0 # No need for flex for this one and the following
+ self.flexnotch[notch_number-1] = 0
+ # and the first one with notch_number - 1, 0 and 1
+ det = (notch_points[1][0]- notch_points[notch_number-1][0])*(notch_points[0][1] - notch_points[notch_number-1][1]) - (notch_points[1][1] - notch_points[notch_number-1][1])*(notch_points[0][0] - notch_points[notch_number-1][0])
+ if abs(det) < 0.1: # My threhold to be adjusted
+ self.flexnotch[notch_number-1] = 0 # No need for flex for this one and the following
+ self.flexnotch[0] = 0
+ self.DebugMsg("FlexNotch ="+str(self.flexnotch)+"\n")
+ # Generate Associated flex
+ self.GenFlex(parent, notch_number, notch_size, xFlexOffset, yFlexOffset)
+ yFlexOffset -= self.height + 10
+ index_path += 1
+
+
+ def recursivelyTraverseSvg(self, aNodeList):
+
+ '''
+ [ This too is largely lifted from eggbot.py and path2openscad.py ]
+
+ Recursively walk the SVG document, building polygon vertex lists
+ for each graphical element we support.
+
+ Rendered SVG elements:
+ , , , , , ,
+ Except for path, all elements are first converted into a path the processed
+
+ Supported SVG elements:
+
+
+ Ignored SVG elements:
+ , , , , ,
+ processing directives
+
+ All other SVG elements trigger an error (including )
+ '''
+
+ for node in aNodeList:
+
+ self.DebugMsg("Node type :" + node.tag + '\n')
+ if node.tag == inkex.addNS('g', 'svg') or node.tag == 'g':
+ self.DebugMsg("Group detected, recursive call\n")
+ self.recursivelyTraverseSvg(node)
+
+ elif node.tag == inkex.addNS('path', 'svg'):
+ self.DebugMsg("Path detected, ")
+ path_data = node.get('d')
+ if path_data:
+ self.getPathVertices(path_data, node)
+ else:
+ self.DebugMsg("NO path data present\n")
+
+ elif node.tag == inkex.addNS('rect', 'svg') or node.tag == 'rect':
+
+ # Create a path with the outline of the rectangle
+ x = float(node.get('x'))
+ y = float(node.get('y'))
+ if (not x) or (not y):
+ pass
+ w = float(node.get('width', '0'))
+ h = float(node.get('height', '0'))
+
+ self.DebugMsg('Rectangle X='+ str(x)+',Y='+str(y)+', W='+str(w)+' H='+str(h)+'\n')
+
+ a = []
+ a.append(['M', [x, y]])
+ a.append(['l', [w, 0]])
+ a.append(['l', [0, h]])
+ a.append(['l', [-w, 0]])
+ a.append(['Z', []])
+ self.getPathVertices(str(Path(a)), node)
+ elif node.tag == inkex.addNS('line', 'svg') or node.tag == 'line':
+
+ # Convert
+ #
+ #
+
+ x1 = float(node.get('x1'))
+ y1 = float(node.get('y1'))
+ x2 = float(node.get('x2'))
+ y2 = float(node.get('y2'))
+ self.DebugMsg('Line X1='+ str(x1)+',Y1='+str(y1)+', X2='+str(x2)+' Y2='+str(y2)+'\n')
+
+ if (not x1) or (not y1) or (not x2) or (not y2):
+ pass
+ a = []
+ a.append(['M', [x1, y1]])
+ a.append(['L', [x2, y2]])
+ self.getPathVertices(str(Path(a)), node)
+
+ elif node.tag == inkex.addNS('polyline', 'svg') or node.tag == 'polyline':
+
+ # Convert
+ #
+ #
+ #
+ # to
+ #
+ #
+ #
+ # Note: we ignore polylines with no points
+
+ pl = node.get('points', '').strip()
+ if pl == '':
+ pass
+
+ pa = pl.split()
+ d = "".join(["M " + pa[i] if i == 0 else " L " + pa[i] for i in range(0, len(pa))])
+ self.DebugMsg('PolyLine :'+ d +'\n')
+
+
+ self.getPathVertices(d, node)
+
+ elif node.tag == inkex.addNS('polygon', 'svg') or node.tag == 'polygon':
+
+ # Convert
+ #
+ #
+ #
+ # to
+ #
+ #
+ #
+ # Note: we ignore polygons with no points
+
+ pl = node.get('points', '').strip()
+ if pl == '':
+ pass
+
+ pa = pl.split()
+ d = "".join(["M " + pa[i] if i == 0 else " L " + pa[i] for i in range(0, len(pa))])
+ d += " Z"
+ self.DebugMsg('Polygon :'+ d +'\n')
+ self.getPathVertices(d, node)
+
+ elif node.tag == inkex.addNS('ellipse', 'svg') or \
+ node.tag == 'ellipse' or \
+ node.tag == inkex.addNS('circle', 'svg') or \
+ node.tag == 'circle':
+
+ # Convert circles and ellipses to a path with two 180 degree arcs.
+ # In general (an ellipse), we convert
+ #
+ #
+ #
+ # to
+ #
+ #
+ #
+ # where
+ #
+ # X1 = CX - RX
+ # X2 = CX + RX
+ #
+ # Note: ellipses or circles with a radius attribute of value 0 are ignored
+
+ if node.tag == inkex.addNS('ellipse', 'svg') or node.tag == 'ellipse':
+ rx = float(node.get('rx', '0'))
+ ry = float(node.get('ry', '0'))
+ else:
+ rx = float(node.get('r', '0'))
+ ry = rx
+ if rx == 0 or ry == 0:
+ pass
+
+ cx = float(node.get('cx', '0'))
+ cy = float(node.get('cy', '0'))
+ x1 = cx - rx
+ x2 = cx + rx
+ d = 'M %f,%f ' % (x1, cy) + \
+ 'A %f,%f ' % (rx, ry) + \
+ '0 1 0 %f,%f ' % (x2, cy) + \
+ 'A %f,%f ' % (rx, ry) + \
+ '0 1 0 %f,%f' % (x1, cy)
+ self.DebugMsg('Arc :'+ d +'\n')
+ self.getPathVertices(d, node)
+
+ elif node.tag == inkex.addNS('pattern', 'svg') or node.tag == 'pattern':
+
+ pass
+
+ elif node.tag == inkex.addNS('metadata', 'svg') or node.tag == 'metadata':
+
+ pass
+
+ elif node.tag == inkex.addNS('defs', 'svg') or node.tag == 'defs':
+
+ pass
+
+ elif node.tag == inkex.addNS('desc', 'svg') or node.tag == 'desc':
+
+ pass
+
+ elif node.tag == inkex.addNS('namedview', 'sodipodi') or node.tag == 'namedview':
+
+ pass
+
+ elif node.tag == inkex.addNS('eggbot', 'svg') or node.tag == 'eggbot':
+
+ pass
+
+ elif node.tag == inkex.addNS('text', 'svg') or node.tag == 'text':
+
+ inkex.errormsg('Warning: unable to draw text, please convert it to a path first.')
+
+ pass
+
+ elif node.tag == inkex.addNS('title', 'svg') or node.tag == 'title':
+
+ pass
+
+ elif node.tag == inkex.addNS('image', 'svg') or node.tag == 'image':
+
+ if not self.warnings.has_key('image'):
+ inkex.errormsg(gettext.gettext('Warning: unable to draw bitmap images; ' +
+ 'please convert them to line art first. Consider using the "Trace bitmap..." ' +
+ 'tool of the "Path" menu. Mac users please note that some X11 settings may ' +
+ 'cause cut-and-paste operations to paste in bitmap copies.'))
+ self.warnings['image'] = 1
+ pass
+
+ elif node.tag == inkex.addNS('pattern', 'svg') or node.tag == 'pattern':
+
+ pass
+
+ elif node.tag == inkex.addNS('radialGradient', 'svg') or node.tag == 'radialGradient':
+
+ # Similar to pattern
+ pass
+
+ elif node.tag == inkex.addNS('linearGradient', 'svg') or node.tag == 'linearGradient':
+
+ # Similar in pattern
+ pass
+
+ elif node.tag == inkex.addNS('style', 'svg') or node.tag == 'style':
+
+ # This is a reference to an external style sheet and not the value
+ # of a style attribute to be inherited by child elements
+ pass
+
+ elif node.tag == inkex.addNS('cursor', 'svg') or node.tag == 'cursor':
+
+ pass
+
+ elif node.tag == inkex.addNS('color-profile', 'svg') or node.tag == 'color-profile':
+
+ # Gamma curves, color temp, etc. are not relevant to single color output
+ pass
+
+ elif not isinstance(node.tag, basestring):
+
+ # This is likely an XML processing instruction such as an XML
+ # comment. lxml uses a function reference for such node tags
+ # and as such the node tag is likely not a printable string.
+ # Further, converting it to a printable string likely won't
+ # be very useful.
+
+ pass
+
+ else:
+
+ inkex.errormsg('Warning: unable to draw object <%s>, please convert it to a path first.' % node.tag)
+ pass
+
+
+ def effect(self):
+
+ # convert units
+ unit = self.options.unit
+ self.thickness = self.svg.unittouu(str(self.options.thickness) + unit)
+ self.height = self.svg.unittouu(str(self.options.zc) + unit)
+ self.max_flex_size = self.svg.unittouu(str(self.options.max_size_flex) + unit)
+ self.notchesInterval = int(self.options.notch_interval)
+
+ svg = self.document.getroot()
+ docWidth = self.svg.unittouu(svg.get('width'))
+ docHeigh = self.svg.unittouu(svg.attrib['height'])
+
+ # Open Debug file if requested
+ if self.options.Mode_Debug:
+ try:
+ self.fDebug = open('DebugPath2Flex.txt', 'w')
+ except IOError:
+ print ('cannot open debug output file')
+ self.DebugMsg("Start processing\n")
+
+
+ # First traverse the document (or selected items), reducing
+ # everything to line segments. If working on a selection,
+ # then determine the selection's bounding box in the process.
+ # (Actually, we just need to know it's extrema on the x-axis.)
+
+ # Traverse the selected objects
+ for id in self.options.ids:
+ self.recursivelyTraverseSvg([self.svg.selected[id]])
+ # Determine the center of the drawing's bounding box
+ self.cx = self.xmin + (self.xmax - self.xmin) / 2.0
+ self.cy = self.ymin + (self.ymax - self.ymin) / 2.0
+
+ layer = etree.SubElement(svg, 'g')
+ layer.set(inkex.addNS('label', 'inkscape'), 'Flex_Path')
+ layer.set(inkex.addNS('groupmode', 'inkscape'), 'layer')
+
+ # For each path, build a polygon with notches and the corresponding flex.
+ for key in self.paths:
+ self.writeModifiedPath(key, layer)
+
+ if self.fDebug:
+ self.fDebug.close()
+
+if __name__ == '__main__':
+ BoxMakerPathToFlex().run()
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/box_maker_path_to_flex/meta.json b/extensions/fablabchemnitz/box_maker_path_to_flex/meta.json
new file mode 100644
index 0000000..a455390
--- /dev/null
+++ b/extensions/fablabchemnitz/box_maker_path_to_flex/meta.json
@@ -0,0 +1,21 @@
+[
+ {
+ "name": "Box Maker - Path To Flex",
+ "id": "fablabchemnitz.de.box_maker_path_to_flex",
+ "path": "box_maker_path_to_flex",
+ "dependent_extensions": null,
+ "original_name": "Paths To Flex",
+ "original_id": "fr.fablab-lannion.inkscape.Path2Flex",
+ "license": "GNU GPL v3",
+ "license_url": "https://github.com/thierry7100/Path2flex/blob/master/LICENSE",
+ "comment": "ported to Inkscape v1 by Mario Voigt",
+ "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/box_maker_path_to_flex",
+ "fork_url": "https://github.com/thierry7100/Path2flex",
+ "documentation_url": "https://stadtfabrikanten.org/display/IFM/Box+Maker+-+Path+To+Flex",
+ "inkscape_gallery_url": null,
+ "main_authors": [
+ "github.com/thierry7100",
+ "github.com/eridur-de"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/jpeg_export/jpeg_export.inx b/extensions/fablabchemnitz/jpeg_export/jpeg_export.inx
new file mode 100644
index 0000000..a170302
--- /dev/null
+++ b/extensions/fablabchemnitz/jpeg_export/jpeg_export.inx
@@ -0,0 +1,30 @@
+
+
+ JPEG Export
+ fablabchemnitz.de.jpeg_export
+ C:\Users\
+
+ 100
+ 90
+ true
+ true
+
+
+
+ all
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/jpeg_export/jpeg_export.py b/extensions/fablabchemnitz/jpeg_export/jpeg_export.py
new file mode 100644
index 0000000..ba4b445
--- /dev/null
+++ b/extensions/fablabchemnitz/jpeg_export/jpeg_export.py
@@ -0,0 +1,201 @@
+#!/usr/bin/env python3
+
+#This program is free software: you can redistribute it and/or modify
+#it under the terms of the GNU General Public License as published by
+#the Free Software Foundation, either version 3 of the License, or
+#(at your option) any later version.
+#
+#This program is distributed in the hope that it will be useful,
+#but WITHOUT ANY WARRANTY; without even the implied warranty of
+#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+#GNU General Public License for more details.
+#
+#You should have received a copy of the GNU General Public License
+#along with this program. If not, see .
+
+# Author: Giacomo Mirabassi
+# Version: 0.2
+
+import os
+import re
+import subprocess
+import math
+import inkex
+import shutil
+
+inkex.localization.localize
+
+class JPEGExport(inkex.EffectExtension):
+
+ def add_arguments(self, pars):
+ pars.add_argument("--path", default="")
+ pars.add_argument("--bgcol", default="#ffffff")
+ pars.add_argument("--quality",type=int, default="90")
+ pars.add_argument("--density", type=int, default="90")
+ pars.add_argument("--page", type=inkex.Boolean, default=False)
+ pars.add_argument("--fast", type=inkex.Boolean, default=True)
+
+ def effect(self):
+ """get selected item coords and call command line command to export as a png"""
+ # The user must supply a directory to export:
+ if not self.options.path:
+ inkex.errormsg(_('Please indicate a file name and path to export the jpg.'))
+ exit()
+ if not os.path.basename(self.options.path):
+ inkex.errormsg(_('Please indicate a file name.'))
+ exit()
+ if not os.path.dirname(self.options.path):
+ inkex.errormsg(_('Please indicate a directory other than your system\'s base directory.'))
+ exit()
+
+ # Test if the directory exists and filename is valid:
+ filebase = os.path.dirname(self.options.path)
+ if not os.path.exists(filebase):
+ inkex.errormsg(_('The directory "%s" does not exist.') % filebase)
+ exit()
+ filename = os.path.splitext(os.path.basename(self.options.path))
+ filename_base = filename[0]
+ filename_ending = filename[1]
+ if self.get_valid_filename(filename_base) != filename_base:
+ inkex.errormsg(_('The file name "%s" is invalid.') % filename_base)
+ return
+ if filename_ending != 'jpg' or filename_ending != 'jpeg':
+ filename_ending = 'jpg'
+ outfile = os.path.join(filebase, filename_base + '.' + filename_ending)
+
+ shutil.copy(self.options.input_file, self.options.input_file + ".svg") #make a file copy with file ending to suppress import warnings
+ curfile = self.options.input_file + ".svg"
+ #inkex.utils.debug("curfile:" + curfile)
+
+ # Test if color is valid
+ _rgbhexstring = re.compile(r'#[a-fA-F0-9]{6}$')
+ if not _rgbhexstring.match(self.options.bgcol):
+ inkex.errormsg(_('Please indicate the background color like this: \"#abc123\" or leave the field empty for white.'))
+ exit()
+
+ bgcol = self.options.bgcol
+
+ if self.options.page == False:
+ if len(self.svg.selected) == 0:
+ inkex.errormsg(_('Please select something.'))
+ exit()
+
+ coords=self.processSelected()
+ self.exportArea(int(coords[0]),int(coords[1]),int(coords[2]),int(coords[3]),curfile,outfile,bgcol)
+
+ elif self.options.page == True:
+ self.exportPage(curfile,outfile,bgcol)
+
+ def processSelected(self):
+ """Iterate trough nodes and find the bounding coordinates of the selected area"""
+ startx=None
+ starty=None
+ endx=None
+ endy=None
+ nodelist=[]
+ root=self.document.getroot();
+ toty=self.svg.unittouu(root.attrib['height'])
+ scale = self.svg.unittouu('1px')
+ props=['x', 'y', 'width', 'height']
+
+ for id in self.svg.selected:
+ if self.options.fast == True:
+ nodelist.append(self.svg.getElementById(id))
+ else: # uses command line
+ rawprops=[]
+ for prop in props:
+ command=("inkscape", "--query-id", id, "--query-"+prop, self.options.input_file)
+ proc=subprocess.Popen(command,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
+ proc.wait()
+ rawprops.append(math.ceil(self.svg.unittouu(proc.stdout.read())))
+ proc.stdout.close()
+ proc.stderr.close()
+ nodeEndX = rawprops[0] + rawprops[2]
+ nodeStartY = toty - rawprops[1] - rawprops[3]
+ nodeEndY = toty - rawprops[1]
+
+ if rawprops[0] < startx or startx is None:
+ startx = rawprops[0]
+
+ if nodeStartY < starty or starty is None:
+ starty = nodeStartY
+
+ if nodeEndX > endx or endx is None:
+ endx = nodeEndX
+
+ if nodeEndY > endy or endy is None:
+ endy = nodeEndY
+
+
+ if self.options.fast == True:
+ bbox = sum([node.bounding_box() for node in nodelist], None)
+ #inkex.utils.debug(bbox) - see transform.py
+ '''
+ width = property(lambda self: self.x.size)
+ height = property(lambda self: self.y.size)
+ top = property(lambda self: self.y.minimum)
+ left = property(lambda self: self.x.minimum)
+ bottom = property(lambda self: self.y.maximum)
+ right = property(lambda self: self.x.maximum)
+ center_x = property(lambda self: self.x.center)
+ center_y = property(lambda self: self.y.center)
+ '''
+ startx = math.ceil(bbox.left)
+ endx = math.ceil(bbox.right)
+ h = -bbox.top + bbox.bottom
+ starty = toty - math.ceil(bbox.top) -h
+ endy = toty - math.ceil(bbox.top)
+
+ coords = [startx / scale, starty / scale, endx / scale, endy / scale]
+ return coords
+
+ def exportArea(self, x0, y0, x1, y1, curfile, outfile, bgcol):
+ tmp = self.getTmpPath()
+ command="inkscape --export-area %s:%s:%s:%s -d %s --export-filename \"%sjpinkexp.png\" -b \"%s\" \"%s\"" % (x0, y0, x1, y1, self.options.density, tmp, bgcol, curfile)
+ p = subprocess.Popen(command, shell=True)
+ return_code = p.wait()
+ self.tojpeg(outfile)
+ #inkex.utils.debug("command:" + command)
+ #inkex.utils.debug("Errorcode:" + str(return_code))
+
+ def exportPage(self, curfile, outfile, bgcol):
+ tmp = self.getTmpPath()
+ command = "inkscape --export-area-drawing -d %s --export-filename \"%sjpinkexp.png\" -b \"%s\" \"%s\"" % (self.options.density, tmp, bgcol, curfile)
+ p = subprocess.Popen(command, shell=True)
+ return_code = p.wait()
+ self.tojpeg(outfile)
+ #inkex.utils.debug("command:" + command)
+ #inkex.utils.debug("Errorcode:" + str(return_code))
+
+ def tojpeg(self, outfile):
+ tmp = self.getTmpPath()
+ if os.name == 'nt':
+ outfile = outfile.replace("\\","\\\\")
+ # set the ImageMagick command to run based on what's installed
+ if shutil.which('magick'):
+ command = "magick \"%sjpinkexp.png\" -sampling-factor 4:4:4 -strip -interlace JPEG -colorspace RGB -quality %s -density %s \"%s\" " % (tmp, self.options.quality, self.options.density, outfile)
+ # inkex.utils.debug(command)
+ elif shutil.which('convert'):
+ command = "convert \"%sjpinkexp.png\" -sampling-factor 4:4:4 -strip -interlace JPEG -colorspace RGB -quality %s -density %s \"%s\" " % (tmp, self.options.quality, self.options.density, outfile)
+ # inkex.utils.debug(command)
+ else:
+ inkex.errormsg(_('ImageMagick does not appear to be installed.'))
+ exit()
+ p = subprocess.Popen(command, shell=True)
+ return_code = p.wait()
+ #inkex.utils.debug("command:" + command)
+ #inkex.utils.debug("Errorcode:" + str(return_code))
+
+ def getTmpPath(self):
+ """Define the temporary folder path depending on the operating system"""
+ if os.name == 'nt':
+ return os.getenv('TEMP') + '\\'
+ else:
+ return '/tmp/'
+
+ def get_valid_filename(self, s):
+ s = str(s).strip().replace(" ", "_")
+ return re.sub(r"(?u)[^-\w.]", "", s)
+
+if __name__ == '__main__':
+ JPEGExport().run()
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/jpeg_export/meta.json b/extensions/fablabchemnitz/jpeg_export/meta.json
new file mode 100644
index 0000000..8b3ec9d
--- /dev/null
+++ b/extensions/fablabchemnitz/jpeg_export/meta.json
@@ -0,0 +1,21 @@
+[
+ {
+ "name": "JPEG Export",
+ "id": "fablabchemnitz.de.jpeg_export",
+ "path": "jpeg_export",
+ "dependent_extensions": null,
+ "original_name": "JPEG Export",
+ "original_id": "id.giac.export.jpg",
+ "license": "GNU GPL v3",
+ "license_url": "https://github.com/giacmir/Inkscape-JPEG-export-extension/blob/master/jpegexport.py",
+ "comment": "ported to Inkscape v1 by Mario Voigt",
+ "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/jpeg_export",
+ "fork_url": "https://github.com/giacmir/Inkscape-JPEG-export-extension",
+ "documentation_url": "https://stadtfabrikanten.org/display/IFM/JPEG+Export",
+ "inkscape_gallery_url": null,
+ "main_authors": [
+ "github.com/giacmir",
+ "github.com/eridur-de"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/laserdraw_export/laserdraw_export.inx b/extensions/fablabchemnitz/laserdraw_export/laserdraw_export.inx
new file mode 100644
index 0000000..c6b39dc
--- /dev/null
+++ b/extensions/fablabchemnitz/laserdraw_export/laserdraw_export.inx
@@ -0,0 +1,38 @@
+
+
+ LaserDraw Export (lyz)
+ fablabchemnitz.de.laserdraw_export.lyz
+
+
+
+
+
+
+
+
+
+ 1000
+ 2.0
+ false
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/laserdraw_export/lyz_bezmisc.py b/extensions/fablabchemnitz/laserdraw_export/lyz_bezmisc.py
new file mode 100644
index 0000000..b062bc3
--- /dev/null
+++ b/extensions/fablabchemnitz/laserdraw_export/lyz_bezmisc.py
@@ -0,0 +1,286 @@
+#!/usr/bin/env python3
+'''
+Copyright (C) 2010 Nick Drobchenko, nick@cnc-club.ru
+Copyright (C) 2005 Aaron Spike, aaron@ekips.org
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+'''
+
+import math
+import cmath
+
+def rootWrapper(a,b,c,d):
+ if a:
+ # Monics formula see http://en.wikipedia.org/wiki/Cubic_function#Monic_formula_of_roots
+ a,b,c = (b/a, c/a, d/a)
+ m = 2.0*a**3 - 9.0*a*b + 27.0*c
+ k = a**2 - 3.0*b
+ n = m**2 - 4.0*k**3
+ w1 = -.5 + .5*cmath.sqrt(-3.0)
+ w2 = -.5 - .5*cmath.sqrt(-3.0)
+ if n < 0:
+ m1 = pow(complex((m+cmath.sqrt(n))/2),1./3)
+ n1 = pow(complex((m-cmath.sqrt(n))/2),1./3)
+ else:
+ if m+math.sqrt(n) < 0:
+ m1 = -pow(-(m+math.sqrt(n))/2,1./3)
+ else:
+ m1 = pow((m+math.sqrt(n))/2,1./3)
+ if m-math.sqrt(n) < 0:
+ n1 = -pow(-(m-math.sqrt(n))/2,1./3)
+ else:
+ n1 = pow((m-math.sqrt(n))/2,1./3)
+ x1 = -1./3 * (a + m1 + n1)
+ x2 = -1./3 * (a + w1*m1 + w2*n1)
+ x3 = -1./3 * (a + w2*m1 + w1*n1)
+ return (x1,x2,x3)
+ elif b:
+ det=c**2.0-4.0*b*d
+ if det:
+ return (-c+cmath.sqrt(det))/(2.0*b),(-c-cmath.sqrt(det))/(2.0*b)
+ else:
+ return -c/(2.0*b),
+ elif c:
+ return 1.0*(-d/c),
+ return ()
+
+def bezierparameterize(xxx_todo_changeme):
+ #parametric bezier
+ ((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)) = xxx_todo_changeme
+ x0=bx0
+ y0=by0
+ cx=3*(bx1-x0)
+ bx=3*(bx2-bx1)-cx
+ ax=bx3-x0-cx-bx
+ cy=3*(by1-y0)
+ by=3*(by2-by1)-cy
+ ay=by3-y0-cy-by
+
+ return ax,ay,bx,by,cx,cy,x0,y0
+ #ax,ay,bx,by,cx,cy,x0,y0=bezierparameterize(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)))
+
+def linebezierintersect(xxx_todo_changeme1, xxx_todo_changeme2):
+ #parametric line
+ ((lx1,ly1),(lx2,ly2)) = xxx_todo_changeme1
+ ((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)) = xxx_todo_changeme2
+ dd=lx1
+ cc=lx2-lx1
+ bb=ly1
+ aa=ly2-ly1
+
+ if aa:
+ coef1=cc/aa
+ coef2=1
+ else:
+ coef1=1
+ coef2=aa/cc
+
+ ax,ay,bx,by,cx,cy,x0,y0=bezierparameterize(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)))
+ #cubic intersection coefficients
+ a=coef1*ay-coef2*ax
+ b=coef1*by-coef2*bx
+ c=coef1*cy-coef2*cx
+ d=coef1*(y0-bb)-coef2*(x0-dd)
+
+ roots = rootWrapper(a,b,c,d)
+ retval = []
+ for i in roots:
+ if type(i) is complex and i.imag==0:
+ i = i.real
+ if type(i) is not complex and 0<=i<=1:
+ retval.append(bezierpointatt(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)),i))
+ return retval
+
+def bezierpointatt(xxx_todo_changeme3,t):
+ ((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)) = xxx_todo_changeme3
+ ax,ay,bx,by,cx,cy,x0,y0=bezierparameterize(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)))
+ x=ax*(t**3)+bx*(t**2)+cx*t+x0
+ y=ay*(t**3)+by*(t**2)+cy*t+y0
+ return x,y
+
+def bezierslopeatt(xxx_todo_changeme4,t):
+ ((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)) = xxx_todo_changeme4
+ ax,ay,bx,by,cx,cy,x0,y0=bezierparameterize(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)))
+ dx=3*ax*(t**2)+2*bx*t+cx
+ dy=3*ay*(t**2)+2*by*t+cy
+ return dx,dy
+
+def beziertatslope(xxx_todo_changeme5, xxx_todo_changeme6):
+ ((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)) = xxx_todo_changeme5
+ (dy,dx) = xxx_todo_changeme6
+ ax,ay,bx,by,cx,cy,x0,y0=bezierparameterize(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)))
+ #quadratic coefficents of slope formula
+ if dx:
+ slope = 1.0*(dy/dx)
+ a=3*ay-3*ax*slope
+ b=2*by-2*bx*slope
+ c=cy-cx*slope
+ elif dy:
+ slope = 1.0*(dx/dy)
+ a=3*ax-3*ay*slope
+ b=2*bx-2*by*slope
+ c=cx-cy*slope
+ else:
+ return []
+
+ roots = rootWrapper(0,a,b,c)
+ retval = []
+ for i in roots:
+ if type(i) is complex and i.imag==0:
+ i = i.real
+ if type(i) is not complex and 0<=i<=1:
+ retval.append(i)
+ return retval
+
+def tpoint(xxx_todo_changeme7, xxx_todo_changeme8,t):
+ (x1,y1) = xxx_todo_changeme7
+ (x2,y2) = xxx_todo_changeme8
+ return x1+t*(x2-x1),y1+t*(y2-y1)
+def beziersplitatt(xxx_todo_changeme9,t):
+ ((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)) = xxx_todo_changeme9
+ m1=tpoint((bx0,by0),(bx1,by1),t)
+ m2=tpoint((bx1,by1),(bx2,by2),t)
+ m3=tpoint((bx2,by2),(bx3,by3),t)
+ m4=tpoint(m1,m2,t)
+ m5=tpoint(m2,m3,t)
+ m=tpoint(m4,m5,t)
+
+ return ((bx0,by0),m1,m4,m),(m,m5,m3,(bx3,by3))
+
+'''
+Approximating the arc length of a bezier curve
+according to
+
+if:
+ L1 = |P0 P1| +|P1 P2| +|P2 P3|
+ L0 = |P0 P3|
+then:
+ L = 1/2*L0 + 1/2*L1
+ ERR = L1-L0
+ERR approaches 0 as the number of subdivisions (m) increases
+ 2^-4m
+
+Reference:
+Jens Gravesen
+"Adaptive subdivision and the length of Bezier curves"
+mat-report no. 1992-10, Mathematical Institute, The Technical
+University of Denmark.
+'''
+def pointdistance(xxx_todo_changeme10, xxx_todo_changeme11):
+ (x1,y1) = xxx_todo_changeme10
+ (x2,y2) = xxx_todo_changeme11
+ return math.sqrt(((x2 - x1) ** 2) + ((y2 - y1) ** 2))
+def Gravesen_addifclose(b, len, error = 0.001):
+ box = 0
+ for i in range(1,4):
+ box += pointdistance(b[i-1], b[i])
+ chord = pointdistance(b[0], b[3])
+ if (box - chord) > error:
+ first, second = beziersplitatt(b, 0.5)
+ Gravesen_addifclose(first, len, error)
+ Gravesen_addifclose(second, len, error)
+ else:
+ len[0] += (box / 2.0) + (chord / 2.0)
+def bezierlengthGravesen(b, error = 0.001):
+ len = [0]
+ Gravesen_addifclose(b, len, error)
+ return len[0]
+
+# balf = Bezier Arc Length Function
+balfax,balfbx,balfcx,balfay,balfby,balfcy = 0,0,0,0,0,0
+def balf(t):
+ retval = (balfax*(t**2) + balfbx*t + balfcx)**2 + (balfay*(t**2) + balfby*t + balfcy)**2
+ return math.sqrt(retval)
+
+def Simpson(f, a, b, n_limit, tolerance):
+ n = 2
+ multiplier = (b - a)/6.0
+ endsum = f(a) + f(b)
+ interval = (b - a)/2.0
+ asum = 0.0
+ bsum = f(a + interval)
+ est1 = multiplier * (endsum + (2.0 * asum) + (4.0 * bsum))
+ est0 = 2.0 * est1
+ #print multiplier, endsum, interval, asum, bsum, est1, est0
+ while n < n_limit and abs(est1 - est0) > tolerance:
+ n *= 2
+ multiplier /= 2.0
+ interval /= 2.0
+ asum += bsum
+ bsum = 0.0
+ est0 = est1
+ for i in range(1, n, 2):
+ bsum += f(a + (i * interval))
+ est1 = multiplier * (endsum + (2.0 * asum) + (4.0 * bsum))
+ #print multiplier, endsum, interval, asum, bsum, est1, est0
+ return est1
+
+def bezierlengthSimpson(xxx_todo_changeme12, tolerance = 0.001):
+ ((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)) = xxx_todo_changeme12
+ global balfax,balfbx,balfcx,balfay,balfby,balfcy
+ ax,ay,bx,by,cx,cy,x0,y0=bezierparameterize(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)))
+ balfax,balfbx,balfcx,balfay,balfby,balfcy = 3*ax,2*bx,cx,3*ay,2*by,cy
+ return Simpson(balf, 0.0, 1.0, 4096, tolerance)
+
+def beziertatlength(xxx_todo_changeme13, l = 0.5, tolerance = 0.001):
+ ((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)) = xxx_todo_changeme13
+ global balfax,balfbx,balfcx,balfay,balfby,balfcy
+ ax,ay,bx,by,cx,cy,x0,y0=bezierparameterize(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)))
+ balfax,balfbx,balfcx,balfay,balfby,balfcy = 3*ax,2*bx,cx,3*ay,2*by,cy
+ t = 1.0
+ tdiv = t
+ curlen = Simpson(balf, 0.0, t, 4096, tolerance)
+ targetlen = l * curlen
+ diff = curlen - targetlen
+ while abs(diff) > tolerance:
+ tdiv /= 2.0
+ if diff < 0:
+ t += tdiv
+ else:
+ t -= tdiv
+ curlen = Simpson(balf, 0.0, t, 4096, tolerance)
+ diff = curlen - targetlen
+ return t
+
+#default bezier length method
+bezierlength = bezierlengthSimpson
+
+if __name__ == '__main__':
+ import timing
+ #print linebezierintersect(((,),(,)),((,),(,),(,),(,)))
+ #print linebezierintersect(((0,1),(0,-1)),((-1,0),(-.5,0),(.5,0),(1,0)))
+ tol = 0.00000001
+ curves = [((0,0),(1,5),(4,5),(5,5)),
+ ((0,0),(0,0),(5,0),(10,0)),
+ ((0,0),(0,0),(5,1),(10,0)),
+ ((-10,0),(0,0),(10,0),(10,10)),
+ ((15,10),(0,0),(10,0),(-5,10))]
+ '''
+ for curve in curves:
+ timing.start()
+ g = bezierlengthGravesen(curve,tol)
+ timing.finish()
+ gt = timing.micro()
+
+ timing.start()
+ s = bezierlengthSimpson(curve,tol)
+ timing.finish()
+ st = timing.micro()
+
+ print g, gt
+ print s, st
+ '''
+ for curve in curves:
+ print(beziertatlength(curve,0.5))
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/laserdraw_export/lyz_cspsubdiv.py b/extensions/fablabchemnitz/laserdraw_export/lyz_cspsubdiv.py
new file mode 100644
index 0000000..dc857ab
--- /dev/null
+++ b/extensions/fablabchemnitz/laserdraw_export/lyz_cspsubdiv.py
@@ -0,0 +1,35 @@
+#!/usr/bin/env python3
+from lyz_bezmisc import *
+from lyz_ffgeom import *
+
+def maxdist(xxx_todo_changeme):
+ ((p0x,p0y),(p1x,p1y),(p2x,p2y),(p3x,p3y)) = xxx_todo_changeme
+ p0 = Point(p0x,p0y)
+ p1 = Point(p1x,p1y)
+ p2 = Point(p2x,p2y)
+ p3 = Point(p3x,p3y)
+
+ s1 = Segment(p0,p3)
+ return max(s1.distanceToPoint(p1),s1.distanceToPoint(p2))
+
+def cspsubdiv(csp,flat):
+ for sp in csp:
+ subdiv(sp,flat)
+
+def subdiv(sp,flat,i=1):
+ while i < len(sp):
+ p0 = sp[i-1][1]
+ p1 = sp[i-1][2]
+ p2 = sp[i][0]
+ p3 = sp[i][1]
+
+ b = (p0,p1,p2,p3)
+ m = maxdist(b)
+ if m <= flat:
+ i += 1
+ else:
+ one, two = beziersplitatt(b,0.5)
+ sp[i-1][2] = one[1]
+ sp[i][0] = two[2]
+ p = [one[2],one[3],two[1]]
+ sp[i:1] = [p]
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/laserdraw_export/lyz_cubicsuperpath.py b/extensions/fablabchemnitz/laserdraw_export/lyz_cubicsuperpath.py
new file mode 100644
index 0000000..a07974b
--- /dev/null
+++ b/extensions/fablabchemnitz/laserdraw_export/lyz_cubicsuperpath.py
@@ -0,0 +1,166 @@
+#!/usr/bin/env python3
+"""
+cubicsuperpath.py
+
+Copyright (C) 2005 Aaron Spike, aaron@ekips.org
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+"""
+import lyz_simplepath as simplepath
+from math import *
+
+def matprod(mlist):
+ prod=mlist[0]
+ for m in mlist[1:]:
+ a00=prod[0][0]*m[0][0]+prod[0][1]*m[1][0]
+ a01=prod[0][0]*m[0][1]+prod[0][1]*m[1][1]
+ a10=prod[1][0]*m[0][0]+prod[1][1]*m[1][0]
+ a11=prod[1][0]*m[0][1]+prod[1][1]*m[1][1]
+ prod=[[a00,a01],[a10,a11]]
+ return prod
+def rotmat(teta):
+ return [[cos(teta),-sin(teta)],[sin(teta),cos(teta)]]
+def applymat(mat, pt):
+ x=mat[0][0]*pt[0]+mat[0][1]*pt[1]
+ y=mat[1][0]*pt[0]+mat[1][1]*pt[1]
+ pt[0]=x
+ pt[1]=y
+def norm(pt):
+ return sqrt(pt[0]*pt[0]+pt[1]*pt[1])
+
+def ArcToPath(p1,params):
+ A=p1[:]
+ rx,ry,teta,longflag,sweepflag,x2,y2=params[:]
+ teta = teta*pi/180.0
+ B=[x2,y2]
+ if rx==0 or ry==0 or A==B:
+ return([[A[:],A[:],A[:]],[B[:],B[:],B[:]]])
+ mat=matprod((rotmat(teta),[[1/rx,0],[0,1/ry]],rotmat(-teta)))
+ applymat(mat, A)
+ applymat(mat, B)
+ k=[-(B[1]-A[1]),B[0]-A[0]]
+ d=k[0]*k[0]+k[1]*k[1]
+ k[0]/=sqrt(d)
+ k[1]/=sqrt(d)
+ d=sqrt(max(0,1-d/4))
+ if longflag==sweepflag:
+ d*=-1
+ O=[(B[0]+A[0])/2+d*k[0],(B[1]+A[1])/2+d*k[1]]
+ OA=[A[0]-O[0],A[1]-O[1]]
+ OB=[B[0]-O[0],B[1]-O[1]]
+ start=acos(OA[0]/norm(OA))
+ if OA[1]<0:
+ start*=-1
+ end=acos(OB[0]/norm(OB))
+ if OB[1]<0:
+ end*=-1
+
+ if sweepflag and start>end:
+ end +=2*pi
+ if (not sweepflag) and start 0.0:
+ if rx==0.0 or ry==0.0:
+ rx = max(rx,ry)
+ ry = rx
+ #msg = "rx = %f ry = %f " %(rx,ry)
+ #inkex.errormsg(msg)
+ L1 = "M %f,%f %f,%f " %(x+rx , y , x+width-rx , y )
+ C1 = "A %f,%f 0 0 1 %f,%f" %(rx , ry , x+width , y+ry )
+ L2 = "M %f,%f %f,%f " %(x+width , y+ry , x+width , y+height-ry)
+ C2 = "A %f,%f 0 0 1 %f,%f" %(rx , ry , x+width-rx , y+height )
+ L3 = "M %f,%f %f,%f " %(x+width-rx , y+height , x+rx , y+height )
+ C3 = "A %f,%f 0 0 1 %f,%f" %(rx , ry , x , y+height-ry)
+ L4 = "M %f,%f %f,%f " %(x , y+height-ry, x , y+ry )
+ C4 = "A %f,%f 0 0 1 %f,%f" %(rx , ry , x+rx , y )
+ d = L1 + C1 + L2 + C2 + L3 + C3 + L4 + C4
+ else:
+ d = "M %f,%f %f,%f %f,%f %f,%f Z" %(x,y, x+width,y, x+width,y+height, x,y+height)
+ p = cubicsuperpath.parsePath(d)
+
+ elif node.tag == inkex.addNS('circle','svg'):
+ cx = float(node.get('cx') )
+ cy = float(node.get('cy'))
+ r = float(node.get('r'))
+ d = "M %f,%f A %f,%f 0 0 1 %f,%f %f,%f 0 0 1 %f,%f %f,%f 0 0 1 %f,%f %f,%f 0 0 1 %f,%f Z" %(cx+r,cy, r,r,cx,cy+r, r,r,cx-r,cy, r,r,cx,cy-r, r,r,cx+r,cy)
+ p = cubicsuperpath.parsePath(d)
+
+ elif node.tag == inkex.addNS('ellipse','svg'):
+ cx = float(node.get('cx'))
+ cy = float(node.get('cy'))
+ rx = float(node.get('rx'))
+ ry = float(node.get('ry'))
+ d = "M %f,%f A %f,%f 0 0 1 %f,%f %f,%f 0 0 1 %f,%f %f,%f 0 0 1 %f,%f %f,%f 0 0 1 %f,%f Z" %(cx+rx,cy, rx,ry,cx,cy+ry, rx,ry,cx-rx,cy, rx,ry,cx,cy-ry, rx,ry,cx+rx,cy)
+ p = cubicsuperpath.parsePath(d)
+ else:
+ return
+ trans = node.get('transform')
+ if trans:
+ mat = simpletransform.composeTransform(mat, simpletransform.parseTransform(trans))
+ simpletransform.applyTransformToPath(mat, p)
+
+ ###################################################
+ ## Break Curves down into small lines
+ ###################################################
+ f = self.flatness
+ is_flat = 0
+ while is_flat < 1:
+ try:
+ cspsubdiv.cspsubdiv(p, f)
+ is_flat = 1
+ except IndexError:
+ break
+ except:
+ f += 0.1
+ if f>2 :
+ break
+ #something has gone very wrong.
+ ###################################################
+ for sub in p:
+ for i in range(len(sub)-1):
+ s = sub[i]
+ e = sub[i+1]
+ self.dxf_line([s[1],e[1]],0.025,rgb,path_id)
+
+
+ def process_clone(self, node):
+ trans = node.get('transform')
+ x = node.get('x')
+ y = node.get('y')
+ mat = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]
+ if trans:
+ mat = simpletransform.composeTransform(mat, simpletransform.parseTransform(trans))
+ if x:
+ mat = simpletransform.composeTransform(mat, [[1.0, 0.0, float(x)], [0.0, 1.0, 0.0]])
+ if y:
+ mat = simpletransform.composeTransform(mat, [[1.0, 0.0, 0.0], [0.0, 1.0, float(y)]])
+ # push transform
+ if trans or x or y:
+ self.groupmat.append(simpletransform.composeTransform(self.groupmat[-1], mat))
+ # get referenced node
+ refid = node.get(inkex.addNS('href','xlink'))
+ refnode = self.getElementById(refid[1:])
+ if refnode is not None:
+ if refnode.tag == inkex.addNS('g','svg'):
+ self.process_group(refnode)
+ elif refnode.tag == inkex.addNS('use', 'svg'):
+ self.process_clone(refnode)
+ else:
+ self.process_shape(refnode, self.groupmat[-1])
+ # pop transform
+ if trans or x or y:
+ self.groupmat.pop()
+
+ def process_group(self, group):
+ if group.get(inkex.addNS('groupmode', 'inkscape')) == 'layer':
+ style = group.get('style')
+ if style:
+ style = simplestyle.parseStyle(style)
+ if style.has_key('display'):
+ if style['display'] == 'none' and self.options.layer_option and self.options.layer_option=='visible':
+ return
+ layer = group.get(inkex.addNS('label', 'inkscape'))
+
+ layer = layer.replace(' ', '_')
+ if layer in self.layers:
+ self.layer = layer
+ trans = group.get('transform')
+ if trans:
+ self.groupmat.append(simpletransform.composeTransform(self.groupmat[-1], simpletransform.parseTransform(trans)))
+ for node in group:
+ if node.tag == inkex.addNS('g','svg'):
+ self.process_group(node)
+ elif node.tag == inkex.addNS('use', 'svg'):
+ self.process_clone(node)
+ else:
+ self.process_shape(node, self.groupmat[-1])
+ if trans:
+ self.groupmat.pop()
+
+
+ def Make_PNG(self):
+ #create OS temp folder
+ tmp_dir = tempfile.mkdtemp()
+ svg_temp_file = os.path.join(tmp_dir, "LYZimage.svg")
+ png_temp_file = os.path.join(tmp_dir, "LYZpngdata.png")
+
+ dpi = "%d" %(self.options.resolution)
+ if self.inkscape_version >= 100:
+ cmd = [ "inkscape", self.png_area, "--export-dpi", dpi, \
+ "--export-background","rgb(255, 255, 255)","--export-background-opacity", \
+ "255" ,"--export-type=png", "--export-filename=%s" %(png_temp_file), svg_temp_file ]
+ else:
+ cmd = [ "inkscape", self.png_area, "--export-dpi", dpi, \
+ "--export-background","rgb(255, 255, 255)","--export-background-opacity", \
+ "255" ,"--export-png", png_temp_file, svg_temp_file ]
+
+ if (self.cut_select=="raster") or (self.cut_select=="all") or (self.cut_select=="zip"):
+ self.document.write(svg_temp_file)
+ #cmd = [ "inkscape", self.png_area, "--export-dpi", dpi, "--export-background","rgb(255, 255, 255)","--export-background-opacity", "255" ,"--export-png", png_temp_file, svg_temp_file ]
+ #p = Popen(cmd, stdout=PIPE, stderr=PIPE)
+ #stdout, stderr = p.communicate()
+ run_external(cmd, self.timout)
+ else:
+ shutil.copyfile(sys.argv[-1], svg_temp_file)
+ #cmd = [ "inkscape", self.png_area, "--export-dpi", dpi, "--export-background","rgb(255, 255, 255)","--export-background-opacity", "255" ,"--export-png", png_temp_file, svg_temp_file ]
+ #p = Popen(cmd, stdout=PIPE, stderr=PIPE)
+ #stdout, stderr = p.communicate()
+ run_external(cmd, self.timout)
+ try:
+ with open(png_temp_file, 'rb') as f:
+ self.PNG_DATA = f.read()
+ except:
+ inkex.errormsg("PNG generation timed out.\nTry saving again.\n\n")
+
+ #Delete the temp folder and any files
+ shutil.rmtree(tmp_dir)
+
+ def unit2mm(self, string):
+ # Returns mm given a string representation of units in another system
+ # a dictionary of unit to user unit conversion factors
+ uuconv = {'in': 25.4,
+ 'pt': 25.4/72.0,
+ 'px': 25.4/self.inkscape_dpi,
+ 'mm': 1.0,
+ 'cm': 10.0,
+ 'm' : 1000.0,
+ 'km': 1000.0*1000.0,
+ 'pc': 25.4/6.0,
+ 'yd': 25.4*12*3,
+ 'ft': 25.4*12}
+
+ unit = re.compile('(%s)$' % '|'.join(uuconv.keys()))
+ param = re.compile(r'(([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?)')
+
+ p = param.match(string)
+ u = unit.search(string)
+ if p:
+ retval = float(p.string[p.start():p.end()])
+ else:
+ inkex.errormsg("Size was not determined returning zero value")
+ retval = 0.0
+ if u:
+ retunit = u.string[u.start():u.end()]
+ else:
+ inkex.errormsg("units not understood assuming px at %d dpi" %(self.inkscape_dpi))
+ retunit = 'px'
+
+ try:
+ return retval * uuconv[retunit]
+ except KeyError:
+ return retval
+
+ def effect(self):
+ msg = ""
+ #area_select = self.options.area_select # "page_area", "object_area"
+ area_select = "page_area"
+ self.cut_select = self.options.cut_select # "vector_red", "vector_blue", "raster", "all", "image", "Zip"
+ self.margin = self.options.margin # float value
+ #self.inkscape_dpi = self.options.inkscape_dpi # float value
+ self.inkscape_version = self.options.inkscape_version # float value
+ self.txt2paths = self.options.txt2paths # boolean Value
+
+
+ if self.inkscape_version > 91:
+ self.inkscape_dpi = 96
+ else:
+ self.inkscape_dpi = 90
+
+ if (self.txt2paths):
+ #create OS temp folder
+ tmp_dir = tempfile.mkdtemp()
+ txt2path_file = os.path.join(tmp_dir, "txt2path.svg")
+ if self.inkscape_version >= 100:
+ cmd = [ "inkscape", "--export-text-to-path","--export-plain-svg", "--export-filename=%s" %(txt2path_file), sys.argv[-1] ]
+ else:
+ cmd = [ "inkscape", "--export-text-to-path","--export-plain-svg",txt2path_file, sys.argv[-1] ]
+
+ run_external(cmd, self.timout)
+ self.document.parse(txt2path_file)
+ #Delete the temp folder and any files
+ shutil.rmtree(tmp_dir)
+
+ h_uu = self.unittouu(self.document.getroot().xpath('@height', namespaces=inkex.NSS)[0])
+ w_uu = self.unittouu(self.document.getroot().xpath('@width' , namespaces=inkex.NSS)[0])
+
+ h_mm = self.unit2mm(self.document.getroot().xpath('@height', namespaces=inkex.NSS)[0])
+ w_mm = self.unit2mm(self.document.getroot().xpath('@width', namespaces=inkex.NSS)[0])
+
+ try:
+ view_box_str = self.document.getroot().xpath('@viewBox', namespaces=inkex.NSS)[0]
+ view_box_list = view_box_str.split(' ')
+ Wpix = float(view_box_list[2])
+ Hpix = float(view_box_list[3])
+ scale = h_mm/Hpix
+ Dx = float(view_box_list[0]) / ( float(view_box_list[2])/w_mm )
+ Dy = float(view_box_list[1]) / ( float(view_box_list[3])/h_mm )
+ except:
+ #inkex.errormsg("Using Default Inkscape Scale")
+ scale = 25.4/self.inkscape_dpi
+ Dx = 0
+ Dy = 0
+
+ for node in self.document.getroot().xpath('//svg:g', namespaces=inkex.NSS):
+ if node.get(inkex.addNS('groupmode', 'inkscape')) == 'layer':
+ layer = node.get(inkex.addNS('label', 'inkscape'))
+ self.layernames.append(layer.lower())
+ # if self.options.layer_name and self.options.layer_option and self.options.layer_option=='name' and not layer.lower() in self.options.layer_name:
+ # continue
+ layer = layer.replace(' ', '_')
+ if layer and not layer in self.layers:
+ self.layers.append(layer)
+
+ #self.groupmat = [[[scale, 0.0, 0.0], [0.0, -scale, h_mm]]]
+ self.groupmat = [[[scale, 0.0, 0.0-Dx],
+ [0.0 , -scale, h_mm+Dy]]]
+ #doc = self.document.getroot()
+ self.process_group(self.document.getroot())
+ #################################################
+
+ # msg = msg + self.getDocumentUnit() + "\n"
+ # msg = msg + "scale = %f\n" %(scale)
+ msg = msg + "Height(mm)= %f\n" %(h_mm)
+ msg = msg + "Width (mm)= %f\n" %(w_mm)
+ # msg = msg + "h_uu = %f\n" %(h_uu)
+ # msg = msg + "w_uu = %f\n" %(w_uu)
+
+ #inkex.errormsg(msg)
+
+ if (area_select=="object_area"):
+ self.png_area = "--export-area-drawing"
+ xmin= self.lines[0][0]+0.0
+ xmax= self.lines[0][0]+0.0
+ ymin= self.lines[0][1]+0.0
+ ymax= self.lines[0][1]+0.0
+ for line in self.lines:
+ x1= line[0]
+ y1= line[1]
+ x2= line[2]
+ y2= line[3]
+ xmin = min(min(xmin,x1),x2)
+ ymin = min(min(ymin,y1),y2)
+ xmax = max(max(xmax,x1),x2)
+ ymax = max(max(ymax,y1),y2)
+ else:
+ self.png_area = "--export-area-page"
+ xmin= 0.0
+ xmax= w_mm
+ ymin= -h_mm
+ ymax= 0.0
+
+ self.Xsize=xmax-xmin
+ self.Ysize=ymax-ymin
+ Xcenter=(xmax+xmin)/2.0
+ Ycenter=(ymax+ymin)/2.0
+ for ii in range(len(self.lines)):
+ self.lines[ii][0] = self.lines[ii][0]-Xcenter
+ self.lines[ii][1] = self.lines[ii][1]-Ycenter
+ self.lines[ii][2] = self.lines[ii][2]-Xcenter
+ self.lines[ii][3] = self.lines[ii][3]-Ycenter
+
+ if (self.cut_select=="raster") or \
+ (self.cut_select=="all" ) or \
+ (self.cut_select=="image" ) or \
+ (self.cut_select=="zip" ):
+ self.Make_PNG()
+
+LYZExport().affect()
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/laserdraw_export/lyz_export_zip.inx b/extensions/fablabchemnitz/laserdraw_export/lyz_export_zip.inx
new file mode 100644
index 0000000..407c9e3
--- /dev/null
+++ b/extensions/fablabchemnitz/laserdraw_export/lyz_export_zip.inx
@@ -0,0 +1,45 @@
+
+
+ LaserDraw Export (zip)
+ fablabchemnitz.de.laserdraw_export.zip
+
+
+
+ 1000
+ 2.0
+ false
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/laserdraw_export/lyz_ffgeom.py b/extensions/fablabchemnitz/laserdraw_export/lyz_ffgeom.py
new file mode 100644
index 0000000..28ef03a
--- /dev/null
+++ b/extensions/fablabchemnitz/laserdraw_export/lyz_ffgeom.py
@@ -0,0 +1,138 @@
+#!/usr/bin/env python3
+"""
+ ffgeom.py
+ Copyright (C) 2005 Aaron Cyril Spike, aaron@ekips.org
+
+ This file is part of FretFind 2-D.
+
+ FretFind 2-D is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ FretFind 2-D is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with FretFind 2-D; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+"""
+import math
+try:
+ NaN = float('NaN')
+except ValueError:
+ PosInf = 1e300000
+ NaN = PosInf/PosInf
+
+class Point:
+ precision = 5
+ def __init__(self, x, y):
+ self.__coordinates = {'x' : float(x), 'y' : float(y)}
+ def __getitem__(self, key):
+ return self.__coordinates[key]
+ def __setitem__(self, key, value):
+ self.__coordinates[key] = float(value)
+ def __repr__(self):
+ return '(%s, %s)' % (round(self['x'],self.precision),round(self['y'],self.precision))
+ def copy(self):
+ return Point(self['x'],self['y'])
+ def translate(self, x, y):
+ self['x'] += x
+ self['y'] += y
+ def move(self, x, y):
+ self['x'] = float(x)
+ self['y'] = float(y)
+
+class Segment:
+ def __init__(self, e0, e1):
+ self.__endpoints = [e0, e1]
+ def __getitem__(self, key):
+ return self.__endpoints[key]
+ def __setitem__(self, key, value):
+ self.__endpoints[key] = value
+ def __repr__(self):
+ return repr(self.__endpoints)
+ def copy(self):
+ return Segment(self[0],self[1])
+ def translate(self, x, y):
+ self[0].translate(x,y)
+ self[1].translate(x,y)
+ def move(self,e0,e1):
+ self[0] = e0
+ self[1] = e1
+ def delta_x(self):
+ return self[1]['x'] - self[0]['x']
+ def delta_y(self):
+ return self[1]['y'] - self[0]['y']
+ #alias functions
+ run = delta_x
+ rise = delta_y
+ def slope(self):
+ if self.delta_x() != 0:
+ return self.delta_x() / self.delta_y()
+ return NaN
+ def intercept(self):
+ if self.delta_x() != 0:
+ return self[1]['y'] - (self[0]['x'] * self.slope())
+ return NaN
+ def distanceToPoint(self, p):
+ s2 = Segment(self[0],p)
+ c1 = dot(s2,self)
+ if c1 <= 0:
+ return Segment(p,self[0]).length()
+ c2 = dot(self,self)
+ if c2 <= c1:
+ return Segment(p,self[1]).length()
+ return self.perpDistanceToPoint(p)
+ def perpDistanceToPoint(self, p):
+ len = self.length()
+ if len == 0: return NaN
+ return math.fabs(((self[1]['x'] - self[0]['x']) * (self[0]['y'] - p['y'])) - \
+ ((self[0]['x'] - p['x']) * (self[1]['y'] - self[0]['y']))) / len
+ def angle(self):
+ return math.pi * (math.atan2(self.delta_y(), self.delta_x())) / 180
+ def length(self):
+ return math.sqrt((self.delta_x() ** 2) + (self.delta_y() ** 2))
+ def pointAtLength(self, len):
+ if self.length() == 0: return Point(NaN, NaN)
+ ratio = len / self.length()
+ x = self[0]['x'] + (ratio * self.delta_x())
+ y = self[0]['y'] + (ratio * self.delta_y())
+ return Point(x, y)
+ def pointAtRatio(self, ratio):
+ if self.length() == 0: return Point(NaN, NaN)
+ x = self[0]['x'] + (ratio * self.delta_x())
+ y = self[0]['y'] + (ratio * self.delta_y())
+ return Point(x, y)
+ def createParallel(self, p):
+ return Segment(Point(p['x'] + self.delta_x(), p['y'] + self.delta_y()), p)
+ def intersect(self, s):
+ return intersectSegments(self, s)
+
+def intersectSegments(s1, s2):
+ x1 = s1[0]['x']
+ x2 = s1[1]['x']
+ x3 = s2[0]['x']
+ x4 = s2[1]['x']
+
+ y1 = s1[0]['y']
+ y2 = s1[1]['y']
+ y3 = s2[0]['y']
+ y4 = s2[1]['y']
+
+ denom = ((y4 - y3) * (x2 - x1)) - ((x4 - x3) * (y2 - y1))
+ num1 = ((x4 - x3) * (y1 - y3)) - ((y4 - y3) * (x1 - x3))
+ num2 = ((x2 - x1) * (y1 - y3)) - ((y2 - y1) * (x1 - x3))
+
+ num = num1
+
+ if denom != 0:
+ x = x1 + ((num / denom) * (x2 - x1))
+ y = y1 + ((num / denom) * (y2 - y1))
+ return Point(x, y)
+ return Point(NaN, NaN)
+
+def dot(s1, s2):
+ return s1.delta_x() * s2.delta_x() + s1.delta_y() * s2.delta_y()
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/laserdraw_export/lyz_inkex.py b/extensions/fablabchemnitz/laserdraw_export/lyz_inkex.py
new file mode 100644
index 0000000..7fb4c82
--- /dev/null
+++ b/extensions/fablabchemnitz/laserdraw_export/lyz_inkex.py
@@ -0,0 +1,399 @@
+#!/usr/bin/env python3
+"""
+inkex.py
+A helper module for creating Inkscape extensions
+
+Copyright (C) 2005,2010 Aaron Spike and contributors
+
+Contributors:
+ Aurelio A. Heckert
+ Bulia Byak
+ Nicolas Dufour, nicoduf@yahoo.fr
+ Peter J. R. Moulder
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+"""
+import copy
+import gettext
+import optparse
+import os
+import random
+import re
+import sys
+from math import *
+from lxml import etree
+
+# a dictionary of all of the xmlns prefixes in a standard inkscape doc
+NSS = {
+u'sodipodi' :u'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd',
+u'cc' :u'http://creativecommons.org/ns#',
+u'ccOLD' :u'http://web.resource.org/cc/',
+u'svg' :u'http://www.w3.org/2000/svg',
+u'dc' :u'http://purl.org/dc/elements/1.1/',
+u'rdf' :u'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
+u'inkscape' :u'http://www.inkscape.org/namespaces/inkscape',
+u'xlink' :u'http://www.w3.org/1999/xlink',
+u'xml' :u'http://www.w3.org/XML/1998/namespace'
+}
+
+
+def localize():
+ domain = 'inkscape'
+ if sys.platform.startswith('win'):
+ import locale
+ current_locale, encoding = locale.getdefaultlocale()
+ os.environ['LANG'] = current_locale
+ try:
+ localdir = os.environ['INKSCAPE_LOCALEDIR']
+ trans = gettext.translation(domain, localdir, [current_locale], fallback=True)
+ except KeyError:
+ trans = gettext.translation(domain, fallback=True)
+ elif sys.platform.startswith('darwin'):
+ try:
+ localdir = os.environ['INKSCAPE_LOCALEDIR']
+ trans = gettext.translation(domain, localdir, fallback=True)
+ except KeyError:
+ try:
+ localdir = os.environ['PACKAGE_LOCALE_DIR']
+ trans = gettext.translation(domain, localdir, fallback=True)
+ except KeyError:
+ trans = gettext.translation(domain, fallback=True)
+ else:
+ try:
+ localdir = os.environ['PACKAGE_LOCALE_DIR']
+ trans = gettext.translation(domain, localdir, fallback=True)
+ except KeyError:
+ trans = gettext.translation(domain, fallback=True)
+ #sys.stderr.write(str(localdir) + "\n")
+ trans.install()
+
+
+def debug(what):
+ sys.stderr.write(str(what) + "\n")
+ return what
+
+
+def errormsg(msg):
+ """Intended for end-user-visible error messages.
+
+ (Currently just writes to stderr with an appended newline, but could do
+ something better in future: e.g. could add markup to distinguish error
+ messages from status messages or debugging output.)
+
+ Note that this should always be combined with translation:
+
+ import inkex
+ ...
+ inkex.errormsg("This extension requires two selected paths.")
+ """
+ #if isinstance(msg, unicode):
+ # sys.stderr.write(msg.encode("utf-8") + "\n")
+ #else:
+ # sys.stderr.write((unicode(msg, "utf-8", errors='replace') + "\n").encode("utf-8"))
+ print(msg)
+
+def are_near_relative(a, b, eps):
+ return (a-b <= a*eps) and (a-b >= -a*eps)
+
+
+def check_inkbool(option, opt, value):
+ if str(value).capitalize() == 'True':
+ return True
+ elif str(value).capitalize() == 'False':
+ return False
+ else:
+ raise optparse.OptionValueError("option %s: invalid inkbool value: %s" % (opt, value))
+
+
+def addNS(tag, ns=None):
+ val = tag
+ if ns is not None and len(ns) > 0 and ns in NSS and len(tag) > 0 and tag[0] != '{':
+ val = "{%s}%s" % (NSS[ns], tag)
+ return val
+
+
+class InkOption(optparse.Option):
+ TYPES = optparse.Option.TYPES + ("inkbool",)
+ TYPE_CHECKER = copy.copy(optparse.Option.TYPE_CHECKER)
+ TYPE_CHECKER["inkbool"] = check_inkbool
+
+
+class Effect:
+ """A class for creating Inkscape SVG Effects"""
+
+ def __init__(self, *args, **kwargs):
+ self.document = None
+ self.original_document = None
+ self.ctx = None
+ self.selected = {}
+ self.doc_ids = {}
+ self.options = None
+ self.args = None
+ self.OptionParser = optparse.OptionParser(usage="usage: %prog [options] SVGfile", option_class=InkOption)
+ self.OptionParser.add_option("--id", dest="ids", default=[], help="id attribute of object to manipulate")
+ self.OptionParser.add_option("--selected-nodes", dest="selected_nodes", default=[], help="id:subpath:position of selected nodes, if any")
+ # TODO write a parser for this
+
+ def effect(self):
+ """Apply some effects on the document. Extensions subclassing Effect
+ must override this function and define the transformations
+ in it."""
+ pass
+
+ def getoptions(self,args=sys.argv[1:]):
+ """Collect command line arguments"""
+ self.options, self.args = self.OptionParser.parse_args(args)
+
+ def parse(self, filename=None, encoding=None):
+ """Parse document in specified file or on stdin"""
+
+ # First try to open the file from the function argument
+ if filename is not None:
+ try:
+ stream = open(filename, 'r')
+ except IOError:
+ errormsg("Unable to open specified file: %s" % filename)
+ sys.exit()
+
+ # If it wasn't specified, try to open the file specified as
+ # an object member
+ elif self.svg_file is not None:
+ try:
+ stream = open(self.svg_file, 'r')
+ except IOError:
+ errormsg("Unable to open object member file: %s" % self.svg_file)
+ sys.exit()
+
+ # Finally, if the filename was not specified anywhere, use
+ # standard input stream
+ else:
+ stream = sys.stdin
+
+ if encoding == None:
+ p = etree.XMLParser(huge_tree=True, recover=True)
+ else:
+ p = etree.XMLParser(huge_tree=True, recover=True, encoding=encoding)
+
+ self.document = etree.parse(stream, parser=p)
+ self.original_document = copy.deepcopy(self.document)
+ stream.close()
+
+ # defines view_center in terms of document units
+ def getposinlayer(self):
+ #defaults
+ self.current_layer = self.document.getroot()
+ self.view_center = (0.0, 0.0)
+
+ layerattr = self.document.xpath('//sodipodi:namedview/@inkscape:current-layer', namespaces=NSS)
+ if layerattr:
+ layername = layerattr[0]
+ layer = self.document.xpath('//svg:g[@id="%s"]' % layername, namespaces=NSS)
+ if layer:
+ self.current_layer = layer[0]
+
+ xattr = self.document.xpath('//sodipodi:namedview/@inkscape:cx', namespaces=NSS)
+ yattr = self.document.xpath('//sodipodi:namedview/@inkscape:cy', namespaces=NSS)
+ if xattr and yattr:
+ x = self.unittouu(xattr[0] + 'px')
+ y = self.unittouu(yattr[0] + 'px')
+ doc_height = self.unittouu(self.getDocumentHeight())
+ if x and y:
+ self.view_center = (float(x), doc_height - float(y))
+ # FIXME: y-coordinate flip, eliminate it when it's gone in Inkscape
+
+ def getselected(self):
+ """Collect selected nodes"""
+ for i in self.options.ids:
+ path = '//*[@id="%s"]' % i
+ for node in self.document.xpath(path, namespaces=NSS):
+ self.selected[i] = node
+
+ def getElementById(self, id):
+ path = '//*[@id="%s"]' % id
+ el_list = self.document.xpath(path, namespaces=NSS)
+ if el_list:
+ return el_list[0]
+ else:
+ return None
+
+ def getParentNode(self, node):
+ for parent in self.document.getiterator():
+ if node in parent.getchildren():
+ return parent
+
+ def getdocids(self):
+ docIdNodes = self.document.xpath('//@id', namespaces=NSS)
+ for m in docIdNodes:
+ self.doc_ids[m] = 1
+
+ def getNamedView(self):
+ return self.document.xpath('//sodipodi:namedview', namespaces=NSS)[0]
+
+ def createGuide(self, posX, posY, angle):
+ atts = {
+ 'position': str(posX)+','+str(posY),
+ 'orientation': str(sin(radians(angle)))+','+str(-cos(radians(angle)))
+ }
+ guide = etree.SubElement(
+ self.getNamedView(),
+ addNS('guide','sodipodi'), atts)
+ return guide
+
+ def output(self):
+ """Serialize document into XML on stdout"""
+ original = etree.tostring(self.original_document)
+ result = etree.tostring(self.document)
+ if original != result:
+ self.document.write(sys.stdout)
+
+ def affect(self, args=sys.argv[1:], output=True):
+ """Affect an SVG document with a callback effect"""
+ self.svg_file = args[-1]
+ localize()
+ self.getoptions(args)
+ self.parse()
+ self.getposinlayer()
+ self.getselected()
+ self.getdocids()
+ self.effect()
+ if output:
+ self.output()
+
+ def uniqueId(self, old_id, make_new_id=True):
+ new_id = old_id
+ if make_new_id:
+ while new_id in self.doc_ids:
+ new_id += random.choice('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ')
+ self.doc_ids[new_id] = 1
+ return new_id
+
+ def xpathSingle(self, path):
+ try:
+ retval = self.document.xpath(path, namespaces=NSS)[0]
+ except:
+ errormsg("No matching node for expression: %s" % path)
+ retval = None
+ return retval
+
+ # a dictionary of unit to user unit conversion factors
+ __uuconv = {'in': 96.0, 'pt': 1.33333333333, 'px': 1.0, 'mm': 3.77952755913, 'cm': 37.7952755913,
+ 'm': 3779.52755913, 'km': 3779527.55913, 'pc': 16.0, 'yd': 3456.0, 'ft': 1152.0}
+
+ # Fault tolerance for lazily defined SVG
+ def getDocumentWidth(self):
+ width = self.document.getroot().get('width')
+ if width:
+ return width
+ else:
+ viewbox = self.document.getroot().get('viewBox')
+ if viewbox:
+ return viewbox.split()[2]
+ else:
+ return '0'
+
+ # Fault tolerance for lazily defined SVG
+ def getDocumentHeight(self):
+ """Returns a string corresponding to the height of the document, as
+ defined in the SVG file. If it is not defined, returns the height
+ as defined by the viewBox attribute. If viewBox is not defined,
+ returns the string '0'."""
+ height = self.document.getroot().get('height')
+ if height:
+ return height
+ else:
+ viewbox = self.document.getroot().get('viewBox')
+ if viewbox:
+ return viewbox.split()[3]
+ else:
+ return '0'
+
+ def getDocumentUnit(self):
+ """Returns the unit used for in the SVG document.
+ In the case the SVG document lacks an attribute that explicitly
+ defines what units are used for SVG coordinates, it tries to calculate
+ the unit from the SVG width and viewBox attributes.
+ Defaults to 'px' units."""
+ svgunit = 'px' # default to pixels
+
+ svgwidth = self.getDocumentWidth()
+ viewboxstr = self.document.getroot().get('viewBox')
+ if viewboxstr:
+ unitmatch = re.compile('(%s)$' % '|'.join(self.__uuconv.keys()))
+ param = re.compile(r'(([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?)')
+
+ p = param.match(svgwidth)
+ u = unitmatch.search(svgwidth)
+
+ width = 100 # default
+ viewboxwidth = 100 # default
+ svgwidthunit = 'px' # default assume 'px' unit
+ if p:
+ width = float(p.string[p.start():p.end()])
+ else:
+ errormsg("SVG Width not set correctly! Assuming width = 100")
+ if u:
+ svgwidthunit = u.string[u.start():u.end()]
+
+ viewboxnumbers = []
+ for t in viewboxstr.split():
+ try:
+ viewboxnumbers.append(float(t))
+ except ValueError:
+ pass
+ if len(viewboxnumbers) == 4: # check for correct number of numbers
+ viewboxwidth = viewboxnumbers[2]
+
+ svgunitfactor = self.__uuconv[svgwidthunit] * width / viewboxwidth
+
+ # try to find the svgunitfactor in the list of units known. If we don't find something, ...
+ eps = 0.01 # allow 1% error in factor
+ for key in self.__uuconv:
+ if are_near_relative(self.__uuconv[key], svgunitfactor, eps):
+ # found match!
+ svgunit = key
+
+ return svgunit
+
+ def unittouu(self, string):
+ """Returns userunits given a string representation of units in another system"""
+ unit = re.compile('(%s)$' % '|'.join(self.__uuconv.keys()))
+ param = re.compile(r'(([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?)')
+
+ p = param.match(string)
+ u = unit.search(string)
+ if p:
+ retval = float(p.string[p.start():p.end()])
+ else:
+ retval = 0.0
+ if u:
+ try:
+ return retval * (self.__uuconv[u.string[u.start():u.end()]] / self.__uuconv[self.getDocumentUnit()])
+ except KeyError:
+ pass
+ else: # default assume 'px' unit
+ return retval / self.__uuconv[self.getDocumentUnit()]
+
+ return retval
+
+ def uutounit(self, val, unit):
+ return val / (self.__uuconv[unit] / self.__uuconv[self.getDocumentUnit()])
+
+ def addDocumentUnit(self, value):
+ """Add document unit when no unit is specified in the string """
+ try:
+ float(value)
+ return value + self.getDocumentUnit()
+ except ValueError:
+ return value
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/laserdraw_export/lyz_library.py b/extensions/fablabchemnitz/laserdraw_export/lyz_library.py
new file mode 100644
index 0000000..3ec4b53
--- /dev/null
+++ b/extensions/fablabchemnitz/laserdraw_export/lyz_library.py
@@ -0,0 +1,441 @@
+#!/usr/bin/env python3
+'''
+This script reads and writes Laser Draw (LaserDRW) LYZ files.
+
+File history:
+0.1 Initial code (2/5/2017)
+
+Copyright (C) 2017 Scorch www.scorchworks.com
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+'''
+
+import struct
+import sys
+import os
+from time import time
+from shutil import copyfile
+
+##############################################################################
+def show(byte_in):
+ print("%03d" %ord(byte_in))
+
+
+def possible_values(loc,len,type,bf):
+ cur= bf.tell()
+ vals=""
+ if type=='d' or type=='Q' or type=='>d' or type=='>Q':
+ tl=8
+ elif type=='i' or type=='f' or type=='l' or type=='L' or type=='I':
+ tl=4
+ elif type=='>i' or type=='>f' or type=='>l' or type=='>L' or type=='>I':
+ tl=4
+ elif type=='h' or type=='H' or type=='>h' or type=='>H':
+ tl=2
+
+ for i in range(tl):
+ for j in range(i,(len-tl+1),tl):
+ bf.seek(loc+j)
+ vals = vals + "\t"+ str( struct.unpack(type,bf.read(tl))[0] )
+ vals = vals+"\n"
+ bf.seek(cur)
+ return vals
+##############################################################################
+
+
+class LYZ_CLASS:
+ def __init__(self):
+ # DEFINE HEADER FIELDS
+ self.header_fields = []
+
+ self.header_data = []
+ self.feature_list = []
+ self.left_over = ""
+ self.EOF = ""
+
+ ########################## Description ,location,length,type,default value
+ self.header_fields.append(["EXTENSION" , 9999999, 4, 't', ".LYZ" ]) #0
+ self.header_fields.append(["LENGTH" , 9999999, 4, 'i', 221 ]) #1
+ self.header_fields.append(["N FEATURES" , 9999999, 4, 'i', 0 ]) #2
+ self.header_fields.append(["?A(4)" , 9999999, 4, 'i', 0 ]) #3
+ self.header_fields.append(["CREATOR" , 9999999, 50, 't',"Creater: LaserDraw.exe(Lihuiyu software Co., Ltd.)"]) #4
+ self.header_fields.append(["?B(14)" , 9999999, 14, 'z',[0,0,0,0,0,0,0,0,0,2,0,0,0,128] ]) #5
+ self.header_fields.append(["DESC" , 9999999, 37, 't',"Description: LaserDraw Graphics File."]) #6
+ self.header_fields.append(["?C(1)" , 9999999, 1, 'z', [0] ]) #7
+ self.header_fields.append(["?D(1)" , 9999999, 1, 'z', [0] ]) #8
+ self.header_fields.append(["Time(8)" , 9999999, 8, 'z', [0,0,0,0,0,0,0,0] ]) #9
+ self.header_fields.append(["TIME(8)" , 9999999, 8, 'z', [0,0,0,0,0,0,0,0] ]) #10
+ self.header_fields.append(["?G(8)" , 9999999, 4, 'z', [0,0,0,0] ]) #11
+ self.header_fields.append(["?H(8)" , 9999999, 4, 'z', [0,0,0,0] ]) #12
+ self.header_fields.append(["?I(18)" , 9999999, 18, 'z', [176,8,210,125,0,65,206,0,0,19,17,126,0,36,35,23,0,2] ]) #13
+ self.header_fields.append(["OFFSET" , 9999999, 8, 'd', 0.0 ]) #14 #was 84
+ self.header_fields.append(["X SIZE" , 9999999, 8, 'd', 42.0 ]) #15
+ self.header_fields.append(["Y SIZE" , 9999999, 8, 'd', 42.0 ]) #16
+ self.header_fields.append(["BORDER1" , 9999999, 8, 'd', 1.0 ]) #17
+ self.header_fields.append(["BORDER2" , 9999999, 8, 'd', 1.0 ]) #18
+ self.header_fields.append(["BORDER3" , 9999999, 8, 'd', 1.0 ]) #19
+ self.header_fields.append(["BORDER4" , 9999999, 8, 'd', 1.0 ]) #20
+
+ # DEFINE FEATURE FIELDS
+ self.feature_fields=[]
+ ########################## Description ,location,length,type,default value
+ self.feature_fields.append(["?a(4)" , 9999999, 4, 'i', 0 ]) #0
+ self.feature_fields.append(["SHAPE TYPE", 9999999, 1, 'b', 10 ]) #1
+ #SHAPE TYPE NUMBERS
+ #0, circle
+ #1 square
+ #2 Square Rounded Corners
+ #3 Square Bevel Corners
+ #4 triangle
+ #5 diamond
+ #8 Star
+ #10 line
+ #12 PNG
+ #22 line text
+ self.feature_fields.append(["AC Density", 9999999, 4, 'z', [75,0,0,0] ]) #2 [ACdensity,color 0 or 8, ?,?]
+ self.feature_fields.append(["?b(1)" , 9999999, 1, 'z', [134] ]) #3 #solid fill 134
+ self.feature_fields.append(["AC cnt" , 9999999, 1, 'z', [2] ]) #4 This needs to be 2 for lines
+ self.feature_fields.append(["?c(1)" , 9999999, 1, 'z', [0] ]) #5
+ self.feature_fields.append(["?d(1)" , 9999999, 1, 'z', [6] ]) #6
+ self.feature_fields.append(["?e(3)" , 9999999, 3, 'z', [0 ,0 ,0 ] ]) #7
+ self.feature_fields.append(["?f(4)" , 9999999, 4, 'i', 16 ]) #8
+ self.feature_fields.append(["ZOOM" , 9999999, 8, 'd', 96 ]) #9
+ self.feature_fields.append(["?g(8)" , 9999999, 8, 'd', 0 ]) #10
+ self.feature_fields.append(["?h(8)" , 9999999, 8, 'd', 0 ]) #11
+ self.feature_fields.append(["?i(8)" , 9999999, 8, 'd', 0 ]) #12
+ self.feature_fields.append(["?j(8)" , 9999999, 8, 'd', 0 ]) #13
+ self.feature_fields.append(["X cent Loc", 9999999, 8, 'd', 0 ]) #14 To the Right of the center of the laser area
+ self.feature_fields.append(["Y cent Loc", 9999999, 8, 'd', 0 ]) #15 Down from the center of the laser area
+ self.feature_fields.append(["Width" , 9999999, 8, 'd', 0 ]) #16
+ self.feature_fields.append(["Height" , 9999999, 8, 'd', 0 ]) #17
+ self.feature_fields.append(["Pen Width" , 9999999, 8, 'd', 0.025 ]) #18
+ self.feature_fields.append(["AC Line" , 9999999, 8, 'd', 0.127 ]) #19
+ self.feature_fields.append(["Rot(deg)" , 9999999, 8, 'd', 0 ]) #20
+ self.feature_fields.append(["Corner Rad", 9999999, 8, 'd', 0 ]) #21
+ self.feature_fields.append(["?k(8)" , 9999999, 8, 'd', 0 ]) #22
+ self.feature_fields.append(["?l(8)" , 9999999, 8, 'd', 0 ]) #23
+ self.feature_fields.append(["?m(8)" , 9999999, 8, 'd', 0 ]) #24
+ self.feature_fields.append(["?n(4)" , 9999999, 4, 'i', 4 ]) #25
+ self.feature_fields.append(["?o(4)" , 9999999, 4, 'i', 0 ]) #26
+ self.feature_fields.append(["?p(4)" , 9999999, 4, 'z', [0 ,0 ,0 ,0 ] ]) #27
+ self.feature_fields.append(["?q(4)" , 9999999, 4, 'z', [255,255,255,0 ] ]) #28
+ self.feature_fields.append(["?r(4)" , 9999999, 4, 'z', [0 ,0 ,0 ,0 ] ]) #29
+ self.feature_fields.append(["string_len", 9999999, 4, 'i', 0 ]) #30
+ self.feature_fields.append(["filename" , 9999999, 4, 'x', "\000" ]) #31
+ self.feature_fields.append(["?u(4)" , 9999999, 4, 'z', [0 ,0 ,0 ,0 ] ]) #32
+ self.feature_fields.append(["ACtexture1", 9999999, 4, 'z', [255,255,255,255] ]) #33 [0 ,0 ,0 ,0 ]
+ self.feature_fields.append(["ACtexture2", 9999999, 4, 'z', [0 ,0 ,0 ,0 ] ]) #34
+ self.feature_fields.append(["?v(4)" , 9999999, 4, 'z', [0 ,0 ,0 ,0 ] ]) #35
+ self.feature_fields.append(["?w(4)" , 9999999, 4, 'z', [2 ,0 ,0 ,0 ] ]) #36
+ self.feature_fields.append(["data length", 9999999, 4, 'i', 2 ]) #37 needs to be 2 for line
+
+ self.feature_appendix = []
+ for i in range(13):
+ self.feature_appendix.append([])
+ ## Appendix values for Line
+ self.feature_appendix[10].append(["line X1", 9999999, 4, 'i', -10000 ]) #position as 1000*value
+ self.feature_appendix[10].append(["line Y1", 9999999, 4, 'i', -10000 ]) #position as 1000*value
+ self.feature_appendix[10].append(["line X2", 9999999, 4, 'i', 10000 ]) #position as 1000*value
+ self.feature_appendix[10].append(["line Y2", 9999999, 4, 'i', 10000 ]) #position as 1000*value
+ self.feature_appendix[10].append(["lineEND", 9999999, 4, 'i', 0 ])
+ ##Appendix values for PNG
+ self.feature_appendix[12].append(["PNGdata", 9999999, 0, 't', "" ])
+ self.feature_appendix[12].append(["PNGend" , 9999999, 8, 'z', [0,0,0,0,0,0,0,0] ])
+
+
+ def lyz_read(self,loc,len,type,bf):
+ #try:
+ if 1==1:
+ #bf.seek(loc)
+ if type=='t':
+ data = bf.read(len)
+ elif type == 'z':
+ data = []
+ for i in range(len):
+ data.append(ord(bf.read(1)))
+ elif type == 'x':
+ data = ""
+ for i in range(0,len,4):
+ data_temp = bf.read(4)
+ data = data + data_temp[0]
+ else:
+ data = struct.unpack(type, bf.read(len))[0]
+ return data
+ #except:
+ # print("Error Reading data (lyz_read)")
+ # return []
+
+
+ def lyz_write(self,data,type,bf):
+ #print("type,data: ",type,data)
+ if type=='t':
+ #print("data:",data)
+ #bf.write(data)
+ try:
+ bf.write(data)
+ except:
+ bf.write(data.encode())
+ elif type == 'z':
+ for i in range(len(data)):
+ #bf.write(chr(data[i]))
+ bf.write(struct.pack('B',data[i]))
+ elif type == 'x':
+ for char in data:
+ bf.write(char.encode())
+ bf.write(struct.pack('B',0))
+ bf.write(struct.pack('B',0))
+ bf.write(struct.pack('B',0))
+ else:
+ bf.write(struct.pack(type,data))
+
+ def read_header(self,f):
+ self.header_data=[]
+ for line in self.header_fields:
+ #pos = line[1]
+ len = line[2]
+ typ = line[3]
+ self.header_data.append(self.lyz_read(None,len,typ,f))
+
+
+ def read_feature(self,f):
+ feature_data=[]
+ for i in range(len(self.feature_fields)):
+ length = self.feature_fields[i][2]
+ typ = self.feature_fields[i][3]
+
+ if i==31 and feature_data[1]==12:
+ string_length = feature_data[-1]*4
+ feature_data.append(self.lyz_read(None,string_length,typ,f))
+ else:
+ feature_data.append(self.lyz_read(None,length,typ,f))
+
+ #if i==30 and feature_data[1]==12:
+ # self.feature_fields[i+1][2] = feature_data[-1]*4
+
+
+ feat_type = feature_data[1]
+ if feat_type==10 or feat_type==12:
+ for i in range(len(self.feature_appendix[feat_type])):
+ if feat_type==12 and i==0:
+ length = feature_data[-1]
+ else:
+ length = self.feature_appendix[feat_type][i][2]
+ typ = self.feature_appendix[feat_type][i][3]
+ feature_data.append(self.lyz_read(None,length,typ,f))
+ return feature_data
+
+
+ def setup_new_header(self):
+ self.header_data=[]
+ for line in self.header_fields:
+ data = line[4]
+ self.header_data.append(data)
+
+
+ def add_line(self,x1,y1,x2,y2,Pen_Width=.025):
+ feature_data=[]
+ for line in self.feature_fields:
+ data = line[4]
+ feature_data.append(data)
+ feature_data.append(int(x1*1000.0))
+ feature_data.append(int(y1*1000.0))
+ feature_data.append(int(x2*1000.0))
+ feature_data.append(int(y2*1000.0))
+ feature_data.append(0)
+ feature_data[1]=10 #set type to line
+ feature_data[4]=[2] #Not sure what this is for lines but it needs to be 2
+ feature_data[18]=Pen_Width
+
+ self.header_data[2]=self.header_data[2]+1
+ self.feature_list.append(feature_data)
+
+ def add_png(self,PNG_DATA,Xsixe,Ysize):
+ filename="filename"
+ feature_data=[]
+ for line in self.feature_fields:
+ data = line[4]
+ feature_data.append(data)
+ feature_data.append(PNG_DATA)
+ feature_data.append([0,0,0,0,0,0,0,0])
+ feature_data[1] = 12 # set type to PNG
+ feature_data[3] = [150]
+ feature_data[2] = [75, 4, 0, 144]
+ feature_data[4] = [0] # Number of Anti-Counterfeit lines
+ feature_data[6] = [12] # if this is not set to [12] the image does not get passed to the engrave window
+ feature_data[16] = Xsixe # set PNG width
+ feature_data[17] = Ysize # set PNG height
+ feature_data[18] = 1.0
+ feature_data[26] = 16777215
+ feature_data[30]= len(filename) # set filename length
+ feature_data[31]= filename # set filename
+ feature_data[33]=[0 ,0 ,0 ,0 ]
+ feature_data[34]=[255,255,255,255]
+ feature_data[36]=[226, 29, 5, 175]
+ feature_data[37]= len(PNG_DATA) # set PNG data length
+
+ self.header_data[2]=self.header_data[2]+1
+ self.feature_list.append(feature_data)
+
+ def set_size(self,Xsize,Ysize):
+ self.header_data[15]=Xsize
+ self.header_data[16]=Ysize
+
+ def set_margin(self,margin):
+ self.header_data[17]=margin/2
+ self.header_data[18]=margin/2
+ self.header_data[19]=margin/2
+ self.header_data[20]=margin/2
+
+ def find_PNG(self,f):
+ self.PNGstart = -1
+ self.PNGend = -1
+ f.seek(0)
+ loc=0
+ flag = True
+ while flag:
+ byte=f.read(1)
+ if byte=="":
+ flag=False
+ if byte =="P":
+ if byte =="N":
+ if byte =="G":
+ self.PNGstart = f.tell()-4
+ if byte =="E":
+ if byte =="N":
+ if byte =="D":
+ self.PNGend = f.tell()+4
+ flag = False
+ f.seek(0)
+
+
+ def read_file(self, file_name):
+ with open(file_name, "rb") as f:
+ self.find_PNG(f)
+ PNGlen = self.PNGend-self.PNGstart
+ self.png_message = "PNGlen: ",PNGlen
+ self.read_header(f)
+ for i in range(self.header_data[2]):
+ data = self.read_feature(f)
+ self.feature_list.append(data)
+
+ self.left_over = f.read( self.header_data[1]-4-f.tell() )
+
+ self.EOF = ""
+ byte = f.read(1)
+ while byte!="":
+ self.EOF=self.EOF+byte
+ byte = f.read(1)
+ #print(possible_values(200+217,348-200,'d',f))
+
+
+ def write_file(self, file_name):
+ with open(file_name, "wb") as f:
+ for i in range(len(self.header_fields)):
+ typ = self.header_fields[i][3]
+ data = self.header_data[i]
+ self.lyz_write(data,typ,f)
+
+ for j in range(len(self.feature_list)):
+ for i in range(len(self.feature_fields)):
+ typ = self.feature_fields[i][3]
+ data = self.feature_list[j][i]
+ #print(j,i," typ,data: ",typ,data)
+ self.lyz_write(data,typ,f)
+
+ feat_type=self.feature_list[j][1]
+ if feat_type==10 or feat_type==12:
+ appendix_data=[]
+ for i in range(len(self.feature_appendix[feat_type])):
+ typ = self.feature_appendix[feat_type][i][3]
+ data = self.feature_list[j][i+len(self.feature_fields)] #appendix_data
+ #print(j,i," typ,data: "typ,data)
+ self.lyz_write(data,typ,f)
+
+ f.write("@EOF".encode())
+ length=f.tell()
+ f.seek(4)
+ f.write(struct.pack('i',length))
+
+
+ def print_header(self):
+ print("\nHEADER DATA:")
+ print("--------------------")
+ for i in range(len(self.header_data)):
+ print("%11s : " %(self.header_fields[i][0]),self.header_data[i])
+
+
+ def print_features(self):
+ for i in range(len(self.feature_list)):
+ print("\nFEATURE #%d:" %(i+1))
+ print("--------------------")
+ feature = self.feature_list[i]
+ for j in range(len(self.feature_fields)):
+ try:
+ print("%11s : " %(self.feature_fields[j][0]),feature[j])
+ except:
+ print("error")
+ feat_type = feature[1]
+ if feat_type==10 or feat_type==12:
+ print("---LINE COORDS---")
+ for j in range(len(self.feature_appendix[feat_type])):
+ jj = j+len(self.feature_fields)
+ if feat_type==12 and jj==38:
+ print("%11s : " %(self.feature_appendix[feat_type][j][0]),"....")
+ else:
+ print("%11s : " %(self.feature_appendix[feat_type][j][0]),feature[jj])
+ print("--------------------")
+
+
+
+if __name__ == "__main__":
+ ###############################
+ try:
+ file_name = sys.argv[1]
+ print("input: ",file_name)
+ except:
+ file_name = ""
+ ###############################
+ try:
+ file_out = sys.argv[2]
+ print("output: ",file_name)
+ except:
+ file_out = ""
+ ###############################
+
+ if file_name=="test":
+ LYZ=LYZ_CLASS()
+ LYZ.setup_new_header()
+ #image_file = "squigles.png"
+ #image_file = "drawing_mod.png"
+ image_file = "temp.png"
+ with open(image_file, 'rb') as f:
+ PNG_DATA = f.read()
+ LYZ.add_png(PNG_DATA,20,20)
+
+ LYZ.add_line(5,5,-10,-10,0.025)
+ #LYZ.print_header()
+ #LYZ.print_features()
+ LYZ.write_file("test.lyz")
+ else:
+ if file_name!="":
+ LYZ=LYZ_CLASS()
+ LYZ.read_file(file_name)
+ LYZ.print_header()
+ LYZ.print_features()
+ print("LEFTOVER :", LYZ.left_over)
+ print("EOF :",LYZ.EOF)
+
+ if file_out!="":
+ LYZ.write_file(file_out)
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/laserdraw_export/lyz_simplepath.py b/extensions/fablabchemnitz/laserdraw_export/lyz_simplepath.py
new file mode 100644
index 0000000..bce11d2
--- /dev/null
+++ b/extensions/fablabchemnitz/laserdraw_export/lyz_simplepath.py
@@ -0,0 +1,209 @@
+#!/usr/bin/env python3
+"""
+simplepath.py
+functions for digesting paths into a simple list structure
+
+Copyright (C) 2005 Aaron Spike, aaron@ekips.org
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+"""
+import re, math
+
+def lexPath(d):
+ """
+ returns and iterator that breaks path data
+ identifies command and parameter tokens
+ """
+ offset = 0
+ length = len(d)
+ delim = re.compile(r'[ \t\r\n,]+')
+ command = re.compile(r'[MLHVCSQTAZmlhvcsqtaz]')
+ parameter = re.compile(r'(([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?)')
+ while 1:
+ m = delim.match(d, offset)
+ if m:
+ offset = m.end()
+ if offset >= length:
+ break
+ m = command.match(d, offset)
+ if m:
+ yield [d[offset:m.end()], True]
+ offset = m.end()
+ continue
+ m = parameter.match(d, offset)
+ if m:
+ yield [d[offset:m.end()], False]
+ offset = m.end()
+ continue
+ #TODO: create new exception
+ raise Exception('Invalid path data!')
+'''
+pathdefs = {commandfamily:
+ [
+ implicitnext,
+ #params,
+ [casts,cast,cast],
+ [coord type,x,y,0]
+ ]}
+'''
+pathdefs = {
+ 'M':['L', 2, [float, float], ['x','y']],
+ 'L':['L', 2, [float, float], ['x','y']],
+ 'H':['H', 1, [float], ['x']],
+ 'V':['V', 1, [float], ['y']],
+ 'C':['C', 6, [float, float, float, float, float, float], ['x','y','x','y','x','y']],
+ 'S':['S', 4, [float, float, float, float], ['x','y','x','y']],
+ 'Q':['Q', 4, [float, float, float, float], ['x','y','x','y']],
+ 'T':['T', 2, [float, float], ['x','y']],
+ 'A':['A', 7, [float, float, float, int, int, float, float], ['r','r','a',0,'s','x','y']],
+ 'Z':['L', 0, [], []]
+ }
+def parsePath(d):
+ """
+ Parse SVG path and return an array of segments.
+ Removes all shorthand notation.
+ Converts coordinates to absolute.
+ """
+ retval = []
+ lexer = lexPath(d)
+
+ pen = (0.0,0.0)
+ subPathStart = pen
+ lastControl = pen
+ lastCommand = ''
+
+ while 1:
+ try:
+ token, isCommand = next(lexer)
+ except StopIteration:
+ break
+ params = []
+ needParam = True
+ if isCommand:
+ if not lastCommand and token.upper() != 'M':
+ raise Exception('Invalid path, must begin with moveto.')
+ else:
+ command = token
+ else:
+ #command was omited
+ #use last command's implicit next command
+ needParam = False
+ if lastCommand:
+ if lastCommand.isupper():
+ command = pathdefs[lastCommand][0]
+ else:
+ command = pathdefs[lastCommand.upper()][0].lower()
+ else:
+ raise Exception('Invalid path, no initial command.')
+ numParams = pathdefs[command.upper()][1]
+ while numParams > 0:
+ if needParam:
+ try:
+ token, isCommand = next(lexer)
+ if isCommand:
+ raise Exception('Invalid number of parameters')
+ except StopIteration:
+ raise Exception('Unexpected end of path')
+ cast = pathdefs[command.upper()][2][-numParams]
+ param = cast(token)
+ if command.islower():
+ if pathdefs[command.upper()][3][-numParams]=='x':
+ param += pen[0]
+ elif pathdefs[command.upper()][3][-numParams]=='y':
+ param += pen[1]
+ params.append(param)
+ needParam = True
+ numParams -= 1
+ #segment is now absolute so
+ outputCommand = command.upper()
+
+ #Flesh out shortcut notation
+ if outputCommand in ('H','V'):
+ if outputCommand == 'H':
+ params.append(pen[1])
+ if outputCommand == 'V':
+ params.insert(0,pen[0])
+ outputCommand = 'L'
+ if outputCommand in ('S','T'):
+ params.insert(0,pen[1]+(pen[1]-lastControl[1]))
+ params.insert(0,pen[0]+(pen[0]-lastControl[0]))
+ if outputCommand == 'S':
+ outputCommand = 'C'
+ if outputCommand == 'T':
+ outputCommand = 'Q'
+
+ #current values become "last" values
+ if outputCommand == 'M':
+ subPathStart = tuple(params[0:2])
+ pen = subPathStart
+ if outputCommand == 'Z':
+ pen = subPathStart
+ else:
+ pen = tuple(params[-2:])
+
+ if outputCommand in ('Q','C'):
+ lastControl = tuple(params[-4:-2])
+ else:
+ lastControl = pen
+ lastCommand = command
+
+ retval.append([outputCommand,params])
+ return retval
+
+def formatPath(a):
+ """Format SVG path data from an array"""
+ return "".join([cmd + " ".join([str(p) for p in params]) for cmd, params in a])
+
+def translatePath(p, x, y):
+ for cmd,params in p:
+ defs = pathdefs[cmd]
+ for i in range(defs[1]):
+ if defs[3][i] == 'x':
+ params[i] += x
+ elif defs[3][i] == 'y':
+ params[i] += y
+
+def scalePath(p, x, y):
+ for cmd,params in p:
+ defs = pathdefs[cmd]
+ for i in range(defs[1]):
+ if defs[3][i] == 'x':
+ params[i] *= x
+ elif defs[3][i] == 'y':
+ params[i] *= y
+ elif defs[3][i] == 'r': # radius parameter
+ params[i] *= x
+ elif defs[3][i] == 's': # sweep-flag parameter
+ if x*y < 0:
+ params[i] = 1 - params[i]
+ elif defs[3][i] == 'a': # x-axis-rotation angle
+ if y < 0:
+ params[i] = - params[i]
+
+def rotatePath(p, a, cx = 0, cy = 0):
+ if a == 0:
+ return p
+ for cmd,params in p:
+ defs = pathdefs[cmd]
+ for i in range(defs[1]):
+ if defs[3][i] == 'x':
+ x = params[i] - cx
+ y = params[i + 1] - cy
+ r = math.sqrt((x**2) + (y**2))
+ if r != 0:
+ theta = math.atan2(y, x) + a
+ params[i] = (r * math.cos(theta)) + cx
+ params[i + 1] = (r * math.sin(theta)) + cy
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/laserdraw_export/lyz_simplestyle.py b/extensions/fablabchemnitz/laserdraw_export/lyz_simplestyle.py
new file mode 100644
index 0000000..f648545
--- /dev/null
+++ b/extensions/fablabchemnitz/laserdraw_export/lyz_simplestyle.py
@@ -0,0 +1,242 @@
+#!/usr/bin/env python3
+"""
+simplestyle.py
+Two simple functions for working with inline css
+and some color handling on top.
+
+Copyright (C) 2005 Aaron Spike, aaron@ekips.org
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+"""
+
+svgcolors={
+ 'aliceblue':'#f0f8ff',
+ 'antiquewhite':'#faebd7',
+ 'aqua':'#00ffff',
+ 'aquamarine':'#7fffd4',
+ 'azure':'#f0ffff',
+ 'beige':'#f5f5dc',
+ 'bisque':'#ffe4c4',
+ 'black':'#000000',
+ 'blanchedalmond':'#ffebcd',
+ 'blue':'#0000ff',
+ 'blueviolet':'#8a2be2',
+ 'brown':'#a52a2a',
+ 'burlywood':'#deb887',
+ 'cadetblue':'#5f9ea0',
+ 'chartreuse':'#7fff00',
+ 'chocolate':'#d2691e',
+ 'coral':'#ff7f50',
+ 'cornflowerblue':'#6495ed',
+ 'cornsilk':'#fff8dc',
+ 'crimson':'#dc143c',
+ 'cyan':'#00ffff',
+ 'darkblue':'#00008b',
+ 'darkcyan':'#008b8b',
+ 'darkgoldenrod':'#b8860b',
+ 'darkgray':'#a9a9a9',
+ 'darkgreen':'#006400',
+ 'darkgrey':'#a9a9a9',
+ 'darkkhaki':'#bdb76b',
+ 'darkmagenta':'#8b008b',
+ 'darkolivegreen':'#556b2f',
+ 'darkorange':'#ff8c00',
+ 'darkorchid':'#9932cc',
+ 'darkred':'#8b0000',
+ 'darksalmon':'#e9967a',
+ 'darkseagreen':'#8fbc8f',
+ 'darkslateblue':'#483d8b',
+ 'darkslategray':'#2f4f4f',
+ 'darkslategrey':'#2f4f4f',
+ 'darkturquoise':'#00ced1',
+ 'darkviolet':'#9400d3',
+ 'deeppink':'#ff1493',
+ 'deepskyblue':'#00bfff',
+ 'dimgray':'#696969',
+ 'dimgrey':'#696969',
+ 'dodgerblue':'#1e90ff',
+ 'firebrick':'#b22222',
+ 'floralwhite':'#fffaf0',
+ 'forestgreen':'#228b22',
+ 'fuchsia':'#ff00ff',
+ 'gainsboro':'#dcdcdc',
+ 'ghostwhite':'#f8f8ff',
+ 'gold':'#ffd700',
+ 'goldenrod':'#daa520',
+ 'gray':'#808080',
+ 'grey':'#808080',
+ 'green':'#008000',
+ 'greenyellow':'#adff2f',
+ 'honeydew':'#f0fff0',
+ 'hotpink':'#ff69b4',
+ 'indianred':'#cd5c5c',
+ 'indigo':'#4b0082',
+ 'ivory':'#fffff0',
+ 'khaki':'#f0e68c',
+ 'lavender':'#e6e6fa',
+ 'lavenderblush':'#fff0f5',
+ 'lawngreen':'#7cfc00',
+ 'lemonchiffon':'#fffacd',
+ 'lightblue':'#add8e6',
+ 'lightcoral':'#f08080',
+ 'lightcyan':'#e0ffff',
+ 'lightgoldenrodyellow':'#fafad2',
+ 'lightgray':'#d3d3d3',
+ 'lightgreen':'#90ee90',
+ 'lightgrey':'#d3d3d3',
+ 'lightpink':'#ffb6c1',
+ 'lightsalmon':'#ffa07a',
+ 'lightseagreen':'#20b2aa',
+ 'lightskyblue':'#87cefa',
+ 'lightslategray':'#778899',
+ 'lightslategrey':'#778899',
+ 'lightsteelblue':'#b0c4de',
+ 'lightyellow':'#ffffe0',
+ 'lime':'#00ff00',
+ 'limegreen':'#32cd32',
+ 'linen':'#faf0e6',
+ 'magenta':'#ff00ff',
+ 'maroon':'#800000',
+ 'mediumaquamarine':'#66cdaa',
+ 'mediumblue':'#0000cd',
+ 'mediumorchid':'#ba55d3',
+ 'mediumpurple':'#9370db',
+ 'mediumseagreen':'#3cb371',
+ 'mediumslateblue':'#7b68ee',
+ 'mediumspringgreen':'#00fa9a',
+ 'mediumturquoise':'#48d1cc',
+ 'mediumvioletred':'#c71585',
+ 'midnightblue':'#191970',
+ 'mintcream':'#f5fffa',
+ 'mistyrose':'#ffe4e1',
+ 'moccasin':'#ffe4b5',
+ 'navajowhite':'#ffdead',
+ 'navy':'#000080',
+ 'oldlace':'#fdf5e6',
+ 'olive':'#808000',
+ 'olivedrab':'#6b8e23',
+ 'orange':'#ffa500',
+ 'orangered':'#ff4500',
+ 'orchid':'#da70d6',
+ 'palegoldenrod':'#eee8aa',
+ 'palegreen':'#98fb98',
+ 'paleturquoise':'#afeeee',
+ 'palevioletred':'#db7093',
+ 'papayawhip':'#ffefd5',
+ 'peachpuff':'#ffdab9',
+ 'peru':'#cd853f',
+ 'pink':'#ffc0cb',
+ 'plum':'#dda0dd',
+ 'powderblue':'#b0e0e6',
+ 'purple':'#800080',
+ 'rebeccapurple':'#663399',
+ 'red':'#ff0000',
+ 'rosybrown':'#bc8f8f',
+ 'royalblue':'#4169e1',
+ 'saddlebrown':'#8b4513',
+ 'salmon':'#fa8072',
+ 'sandybrown':'#f4a460',
+ 'seagreen':'#2e8b57',
+ 'seashell':'#fff5ee',
+ 'sienna':'#a0522d',
+ 'silver':'#c0c0c0',
+ 'skyblue':'#87ceeb',
+ 'slateblue':'#6a5acd',
+ 'slategray':'#708090',
+ 'slategrey':'#708090',
+ 'snow':'#fffafa',
+ 'springgreen':'#00ff7f',
+ 'steelblue':'#4682b4',
+ 'tan':'#d2b48c',
+ 'teal':'#008080',
+ 'thistle':'#d8bfd8',
+ 'tomato':'#ff6347',
+ 'turquoise':'#40e0d0',
+ 'violet':'#ee82ee',
+ 'wheat':'#f5deb3',
+ 'white':'#ffffff',
+ 'whitesmoke':'#f5f5f5',
+ 'yellow':'#ffff00',
+ 'yellowgreen':'#9acd32'
+}
+
+def parseStyle(s):
+ """Create a dictionary from the value of an inline style attribute"""
+ if s is None:
+ return {}
+ else:
+ return dict([[x.strip() for x in i.split(":")] for i in s.split(";") if len(i.strip())])
+
+def formatStyle(a):
+ """Format an inline style attribute from a dictionary"""
+ return ";".join([att+":"+str(val) for att,val in a.iteritems()])
+
+def isColor(c):
+ """Determine if its a color we can use. If not, leave it unchanged."""
+ if c.startswith('#') and (len(c)==4 or len(c)==7):
+ return True
+ if c.lower() in svgcolors.keys():
+ return True
+ #might be "none" or some undefined color constant or rgb()
+ #however, rgb() shouldnt occur at this point
+ return False
+
+def parseColor(c):
+ """Creates a rgb int array"""
+ tmp = svgcolors.get(c.lower())
+ if tmp is not None:
+ c = tmp
+ elif c.startswith('#') and len(c)==4:
+ c='#'+c[1:2]+c[1:2]+c[2:3]+c[2:3]+c[3:]+c[3:]
+ elif c.startswith('rgb('):
+ # remove the rgb(...) stuff
+ tmp = c.strip()[4:-1]
+ numbers = [number.strip() for number in tmp.split(',')]
+ converted_numbers = []
+ if len(numbers) == 3:
+ for num in numbers:
+ if num.endswith(r'%'):
+ converted_numbers.append(int(float(num[0:-1])*255/100))
+ else:
+ converted_numbers.append(int(num))
+ return tuple(converted_numbers)
+ else:
+ return (0,0,0)
+ try:
+ r=int(c[1:3],16)
+ g=int(c[3:5],16)
+ b=int(c[5:],16)
+ except:
+ # unknown color ...
+ # Return a default color. Maybe not the best thing to do but probably
+ # better than raising an exception.
+ return(0,0,0)
+ return (r,g,b)
+
+def formatColoria(a):
+ """int array to #rrggbb"""
+ return '#%02x%02x%02x' % (a[0],a[1],a[2])
+
+def formatColorfa(a):
+ """float array to #rrggbb"""
+ return '#%02x%02x%02x' % (int(round(a[0]*255)),int(round(a[1]*255)),int(round(a[2]*255)))
+
+def formatColor3i(r,g,b):
+ """3 ints to #rrggbb"""
+ return '#%02x%02x%02x' % (r,g,b)
+
+def formatColor3f(r,g,b):
+ """3 floats to #rrggbb"""
+ return '#%02x%02x%02x' % (int(round(r*255)),int(round(g*255)),int(round(b*255)))
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/laserdraw_export/lyz_simpletransform.py b/extensions/fablabchemnitz/laserdraw_export/lyz_simpletransform.py
new file mode 100644
index 0000000..70c49ec
--- /dev/null
+++ b/extensions/fablabchemnitz/laserdraw_export/lyz_simpletransform.py
@@ -0,0 +1,259 @@
+#!/usr/bin/env python3
+'''
+Copyright (C) 2006 Jean-Francois Barraud, barraud@math.univ-lille1.fr
+Copyright (C) 2010 Alvin Penner, penner@vaxxine.com
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+barraud@math.univ-lille1.fr
+
+This code defines several functions to make handling of transform
+attribute easier.
+'''
+import inkex, cubicsuperpath, bezmisc, simplestyle
+import copy, math, re
+
+def parseTransform(transf,mat=[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]):
+ if transf=="" or transf==None:
+ return(mat)
+ stransf = transf.strip()
+ result=re.match("(translate|scale|rotate|skewX|skewY|matrix)\s*\(([^)]*)\)\s*,?",stransf)
+#-- translate --
+ if result.group(1)=="translate":
+ args=result.group(2).replace(',',' ').split()
+ dx=float(args[0])
+ if len(args)==1:
+ dy=0.0
+ else:
+ dy=float(args[1])
+ matrix=[[1,0,dx],[0,1,dy]]
+#-- scale --
+ if result.group(1)=="scale":
+ args=result.group(2).replace(',',' ').split()
+ sx=float(args[0])
+ if len(args)==1:
+ sy=sx
+ else:
+ sy=float(args[1])
+ matrix=[[sx,0,0],[0,sy,0]]
+#-- rotate --
+ if result.group(1)=="rotate":
+ args=result.group(2).replace(',',' ').split()
+ a=float(args[0])*math.pi/180
+ if len(args)==1:
+ cx,cy=(0.0,0.0)
+ else:
+ cx,cy=list(map(float,args[1:]))
+ matrix=[[math.cos(a),-math.sin(a),cx],[math.sin(a),math.cos(a),cy]]
+ matrix=composeTransform(matrix,[[1,0,-cx],[0,1,-cy]])
+#-- skewX --
+ if result.group(1)=="skewX":
+ a=float(result.group(2))*math.pi/180
+ matrix=[[1,math.tan(a),0],[0,1,0]]
+#-- skewY --
+ if result.group(1)=="skewY":
+ a=float(result.group(2))*math.pi/180
+ matrix=[[1,0,0],[math.tan(a),1,0]]
+#-- matrix --
+ if result.group(1)=="matrix":
+ a11,a21,a12,a22,v1,v2=result.group(2).replace(',',' ').split()
+ matrix=[[float(a11),float(a12),float(v1)], [float(a21),float(a22),float(v2)]]
+
+ matrix=composeTransform(mat,matrix)
+ if result.end() < len(stransf):
+ return(parseTransform(stransf[result.end():], matrix))
+ else:
+ return matrix
+
+def formatTransform(mat):
+ return ("matrix(%f,%f,%f,%f,%f,%f)" % (mat[0][0], mat[1][0], mat[0][1], mat[1][1], mat[0][2], mat[1][2]))
+
+def invertTransform(mat):
+ det = mat[0][0]*mat[1][1] - mat[0][1]*mat[1][0]
+ if det !=0: # det is 0 only in case of 0 scaling
+ # invert the rotation/scaling part
+ a11 = mat[1][1]/det
+ a12 = -mat[0][1]/det
+ a21 = -mat[1][0]/det
+ a22 = mat[0][0]/det
+ # invert the translational part
+ a13 = -(a11*mat[0][2] + a12*mat[1][2])
+ a23 = -(a21*mat[0][2] + a22*mat[1][2])
+ return [[a11,a12,a13],[a21,a22,a23]]
+ else:
+ return[[0,0,-mat[0][2]],[0,0,-mat[1][2]]]
+
+def composeTransform(M1,M2):
+ a11 = M1[0][0]*M2[0][0] + M1[0][1]*M2[1][0]
+ a12 = M1[0][0]*M2[0][1] + M1[0][1]*M2[1][1]
+ a21 = M1[1][0]*M2[0][0] + M1[1][1]*M2[1][0]
+ a22 = M1[1][0]*M2[0][1] + M1[1][1]*M2[1][1]
+
+ v1 = M1[0][0]*M2[0][2] + M1[0][1]*M2[1][2] + M1[0][2]
+ v2 = M1[1][0]*M2[0][2] + M1[1][1]*M2[1][2] + M1[1][2]
+ return [[a11,a12,v1],[a21,a22,v2]]
+
+def composeParents(node, mat):
+ trans = node.get('transform')
+ if trans:
+ mat = composeTransform(parseTransform(trans), mat)
+ if node.getparent().tag == inkex.addNS('g','svg'):
+ mat = composeParents(node.getparent(), mat)
+ return mat
+
+def applyTransformToNode(mat,node):
+ m=parseTransform(node.get("transform"))
+ newtransf=formatTransform(composeTransform(mat,m))
+ node.set("transform", newtransf)
+
+def applyTransformToPoint(mat,pt):
+ x = mat[0][0]*pt[0] + mat[0][1]*pt[1] + mat[0][2]
+ y = mat[1][0]*pt[0] + mat[1][1]*pt[1] + mat[1][2]
+ pt[0]=x
+ pt[1]=y
+
+def applyTransformToPath(mat,path):
+ for comp in path:
+ for ctl in comp:
+ for pt in ctl:
+ applyTransformToPoint(mat,pt)
+
+def fuseTransform(node):
+ if node.get('d')==None:
+ #FIXME: how do you raise errors?
+ raise AssertionError('can not fuse "transform" of elements that have no "d" attribute')
+ t = node.get("transform")
+ if t == None:
+ return
+ m = parseTransform(t)
+ d = node.get('d')
+ p = cubicsuperpath.parsePath(d)
+ applyTransformToPath(m,p)
+ node.set('d', cubicsuperpath.formatPath(p))
+ del node.attrib["transform"]
+
+####################################################################
+##-- Some functions to compute a rough bbox of a given list of objects.
+##-- this should be shipped out in an separate file...
+
+def boxunion(b1,b2):
+ if b1 is None:
+ return b2
+ elif b2 is None:
+ return b1
+ else:
+ return((min(b1[0],b2[0]), max(b1[1],b2[1]), min(b1[2],b2[2]), max(b1[3],b2[3])))
+
+def roughBBox(path):
+ xmin,xMax,ymin,yMax = path[0][0][0][0],path[0][0][0][0],path[0][0][0][1],path[0][0][0][1]
+ for pathcomp in path:
+ for ctl in pathcomp:
+ for pt in ctl:
+ xmin = min(xmin,pt[0])
+ xMax = max(xMax,pt[0])
+ ymin = min(ymin,pt[1])
+ yMax = max(yMax,pt[1])
+ return xmin,xMax,ymin,yMax
+
+def refinedBBox(path):
+ xmin,xMax,ymin,yMax = path[0][0][1][0],path[0][0][1][0],path[0][0][1][1],path[0][0][1][1]
+ for pathcomp in path:
+ for i in range(1, len(pathcomp)):
+ cmin, cmax = cubicExtrema(pathcomp[i-1][1][0], pathcomp[i-1][2][0], pathcomp[i][0][0], pathcomp[i][1][0])
+ xmin = min(xmin, cmin)
+ xMax = max(xMax, cmax)
+ cmin, cmax = cubicExtrema(pathcomp[i-1][1][1], pathcomp[i-1][2][1], pathcomp[i][0][1], pathcomp[i][1][1])
+ ymin = min(ymin, cmin)
+ yMax = max(yMax, cmax)
+ return xmin,xMax,ymin,yMax
+
+def cubicExtrema(y0, y1, y2, y3):
+ cmin = min(y0, y3)
+ cmax = max(y0, y3)
+ d1 = y1 - y0
+ d2 = y2 - y1
+ d3 = y3 - y2
+ if (d1 - 2*d2 + d3):
+ if (d2*d2 > d1*d3):
+ t = (d1 - d2 + math.sqrt(d2*d2 - d1*d3))/(d1 - 2*d2 + d3)
+ if (t > 0) and (t < 1):
+ y = y0*(1-t)*(1-t)*(1-t) + 3*y1*t*(1-t)*(1-t) + 3*y2*t*t*(1-t) + y3*t*t*t
+ cmin = min(cmin, y)
+ cmax = max(cmax, y)
+ t = (d1 - d2 - math.sqrt(d2*d2 - d1*d3))/(d1 - 2*d2 + d3)
+ if (t > 0) and (t < 1):
+ y = y0*(1-t)*(1-t)*(1-t) + 3*y1*t*(1-t)*(1-t) + 3*y2*t*t*(1-t) + y3*t*t*t
+ cmin = min(cmin, y)
+ cmax = max(cmax, y)
+ elif (d3 - d1):
+ t = -d1/(d3 - d1)
+ if (t > 0) and (t < 1):
+ y = y0*(1-t)*(1-t)*(1-t) + 3*y1*t*(1-t)*(1-t) + 3*y2*t*t*(1-t) + y3*t*t*t
+ cmin = min(cmin, y)
+ cmax = max(cmax, y)
+ return cmin, cmax
+
+def computeBBox(aList,mat=[[1,0,0],[0,1,0]]):
+ bbox=None
+ for node in aList:
+ m = parseTransform(node.get('transform'))
+ m = composeTransform(mat,m)
+ #TODO: text not supported!
+ d = None
+ if node.get("d"):
+ d = node.get('d')
+ elif node.get('points'):
+ d = 'M' + node.get('points')
+ elif node.tag in [ inkex.addNS('rect','svg'), 'rect', inkex.addNS('image','svg'), 'image' ]:
+ d = 'M' + node.get('x', '0') + ',' + node.get('y', '0') + \
+ 'h' + node.get('width') + 'v' + node.get('height') + \
+ 'h-' + node.get('width')
+ elif node.tag in [ inkex.addNS('line','svg'), 'line' ]:
+ d = 'M' + node.get('x1') + ',' + node.get('y1') + \
+ ' ' + node.get('x2') + ',' + node.get('y2')
+ elif node.tag in [ inkex.addNS('circle','svg'), 'circle', \
+ inkex.addNS('ellipse','svg'), 'ellipse' ]:
+ rx = node.get('r')
+ if rx is not None:
+ ry = rx
+ else:
+ rx = node.get('rx')
+ ry = node.get('ry')
+ cx = float(node.get('cx', '0'))
+ cy = float(node.get('cy', '0'))
+ x1 = cx - float(rx)
+ x2 = cx + float(rx)
+ d = 'M %f %f ' % (x1, cy) + \
+ 'A' + rx + ',' + ry + ' 0 1 0 %f,%f' % (x2, cy) + \
+ 'A' + rx + ',' + ry + ' 0 1 0 %f,%f' % (x1, cy)
+
+ if d is not None:
+ p = cubicsuperpath.parsePath(d)
+ applyTransformToPath(m,p)
+ bbox=boxunion(refinedBBox(p),bbox)
+
+ elif node.tag == inkex.addNS('use','svg') or node.tag=='use':
+ refid=node.get(inkex.addNS('href','xlink'))
+ path = '//*[@id="%s"]' % refid[1:]
+ refnode = node.xpath(path)
+ bbox=boxunion(computeBBox(refnode,m),bbox)
+
+ bbox=boxunion(computeBBox(node,m),bbox)
+ return bbox
+
+
+def computePointInNode(pt, node, mat=[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]):
+ if node.getparent() is not None:
+ applyTransformToPoint(invertTransform(composeParents(node, mat)), pt)
+ return pt
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/laserdraw_export/meta.json b/extensions/fablabchemnitz/laserdraw_export/meta.json
new file mode 100644
index 0000000..a9d44d8
--- /dev/null
+++ b/extensions/fablabchemnitz/laserdraw_export/meta.json
@@ -0,0 +1,21 @@
+[
+ {
+ "name": "LaserDraw Export ()",
+ "id": "fablabchemnitz.de.laserdraw_export.",
+ "path": "laserdraw_export",
+ "dependent_extensions": null,
+ "original_name": "LaserDraw (LaserDRW) ",
+ "original_id": "com.scorchworks.output.",
+ "license": "GNU GPL v2",
+ "license_url": "https://www.scorchworks.com/LaserDRW_extension/LaserDRW_extension-0.06.zip",
+ "comment": "",
+ "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/laserdraw_export",
+ "fork_url": "https://www.scorchworks.com/LaserDRW_extension/LaserDRW_extension-0.06.zip",
+ "documentation_url": "https://stadtfabrikanten.org/pages/viewpage.action?pageId=55019345",
+ "inkscape_gallery_url": null,
+ "main_authors": [
+ "scorchworks.com",
+ "github.com/eridur-de"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/nextgenerator/meta.json b/extensions/fablabchemnitz/nextgenerator/meta.json
new file mode 100644
index 0000000..51ec780
--- /dev/null
+++ b/extensions/fablabchemnitz/nextgenerator/meta.json
@@ -0,0 +1,22 @@
+[
+ {
+ "name": "NextGenerator",
+ "id": "fablabchemnitz.de.nextgenerator",
+ "path": "nextgenerator",
+ "dependent_extensions": null,
+ "original_name": "NextGenerator",
+ "original_id": "de.vektorrascheln.extension.next_gen",
+ "license": "GNU GPL v3",
+ "license_url": "https://gitlab.com/Moini/nextgenerator/-/blob/master/LICENSE",
+ "comment": "",
+ "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/nextgenerator",
+ "fork_url": "https://gitlab.com/Moini/nextgenerator",
+ "documentation_url": "https://stadtfabrikanten.org/display/IFM/NextGenerator",
+ "inkscape_gallery_url": null,
+ "main_authors": [
+ "Aurélio A. Heckert",
+ "gitlab.com/Moini",
+ "github.com/eridur-de"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/nextgenerator/nextgenerator.inx b/extensions/fablabchemnitz/nextgenerator/nextgenerator.inx
new file mode 100644
index 0000000..93d2e11
--- /dev/null
+++ b/extensions/fablabchemnitz/nextgenerator/nextgenerator.inx
@@ -0,0 +1,69 @@
+
+
+ NextGenerator
+ fablabchemnitz.de.nextgenerator
+
+
+
+ /path/to/file.csv
+
+
+
+ 1
+
+
+
+
+
+
+
+
+
+
+
+ 300
+ %VAR_my_variable_name%
+ /tmp
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ all
+
+
+
+
+
+ Automatically replace values and export the result.
+
+
+
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/nextgenerator/nextgenerator.py b/extensions/fablabchemnitz/nextgenerator/nextgenerator.py
new file mode 100644
index 0000000..e3efa1e
--- /dev/null
+++ b/extensions/fablabchemnitz/nextgenerator/nextgenerator.py
@@ -0,0 +1,190 @@
+#!/usr/bin/env python3
+# coding=utf-8
+#
+# NextGenerator - an Inkscape extension to export images with automatically replaced values
+# Copyright (C) 2008 Aurélio A. Heckert (original Generator extension in Bash)
+# 2019-2021 Maren Hachmann (Python rewrite, update for Inkscape 1.0)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+"""
+An Inkscape extension to automatically replace values (text, attribute values)
+in an SVG file and to then export the result to various file formats.
+
+This is useful e.g. for generating images for name badges and other similar items.
+"""
+
+from __future__ import unicode_literals
+
+import os
+import csv
+import json
+import time #for debugging purposes
+import inkex
+import html
+from inkex.command import inkscape
+
+__version__ = '1.2'
+
+class NextGenerator(inkex.base.TempDirMixin, inkex.base.InkscapeExtension):
+ """Generate image files by replacing variables in the current file"""
+
+ def add_arguments(self, pars):
+ pars.add_argument("-c", "--csv_file", type=str, dest="csv_file", help="path to a CSV file")
+ pars.add_argument("-e", "--extra-vars", help="additional variables to replace and the corresponding columns, in JSON format")
+ pars.add_argument("-n", "--num_sets", type=int, default="1", help="number of sets in the template")
+ pars.add_argument("-f", "--format", help="file format to export to: png, pdf, svg, ps, eps")
+ pars.add_argument("-d", "--dpi", type=int, default="300", help="dpi value for exported raster images")
+ pars.add_argument("-o", "--output_folder", help="path to output folder")
+ pars.add_argument("-p", "--file_pattern", help="pattern for the output file")
+
+ pars.add_argument("-t", "--tab", default="", help="not needed at all")
+ pars.add_argument("-l", "--helptabs", default="", help="not needed at all")
+ pars.add_argument("-i", "--id", default="", help="not needed at all")
+
+ def effect(self):
+
+ # load the attributes that should be replaced in addition to textual values
+ if self.options.extra_vars == None:
+ self.options.extra_vars = '{}'
+
+ extra_vars = json.loads(self.options.extra_vars)
+
+
+ # load the CSV file
+ # spaces around commas will be stripped
+ csv.register_dialect('generator', 'excel', skipinitialspace=True)
+
+ with open(self.options.csv_file, newline='', encoding='utf-8-sig') as csvfile:
+
+ data = csv.DictReader(csvfile, dialect='generator')
+
+ if self.options.num_sets == 1:
+ for row in data:
+ export_base_name = self.options.file_pattern
+ self.new_doc = self.document
+ for i, (key, value) in enumerate(row.items()):
+ search_string = "%VAR_" + key + "%"
+ # replace any occurrances of %VAR_my_variable_name% in the SVG file source code
+ self.new_doc = self.new_doc.replace(search_string, html.escape(value))
+ # build the file name, still without file extension
+ export_base_name = export_base_name.replace(search_string, value)
+ for key, svg_cont in extra_vars.items():
+ if key in row.keys():
+ # replace any attributes and other SVG content by the values from the CSV file
+ self.new_doc = self.new_doc.replace(svg_cont, row[key])
+ else:
+ inkex.errormsg("The replacements in the generated images may be incomplete. Please check your entry '{key}' in the field for the non-text values.").format(key=key)
+ if self.export(export_base_name) != True:
+ return
+
+ elif self.options.num_sets > 1:
+ # we need a list to access specific rows and to be able to count it
+ data = list(data)
+
+ # check if user's indication of num_sets is compatible with file
+ for key in data[0].keys():
+ num_occurr = self.document.count("%VAR_" + key + "%")
+ # We ignore keys that don't appear in the document
+ if num_occurr != 0 and num_occurr != self.options.num_sets:
+ return inkex.errormsg("There are {0} occurrances of the variable '{1}' in the document, but the number of sets you indicated is {2}. Please make sure that each set contains all variables and that there are just as many sets in your document as you indicate.".format(num_occurr, key, self.options.num_sets))
+
+ # abusing negative floor division which rounds to the next lowest number to figure out how many pages we will get
+ num_exports = -((-len(data))//self.options.num_sets)
+ # now we hope that the document is properly prepared and the stacking order cycles through datasets - if not, the result will be nonsensical, but we can't know.
+
+ for export_file_num in range(num_exports):
+ # we only number the export files if there are sets
+ export_base_name = "".join([x if x.isalnum() else "_" for x in self.options.file_pattern]) + '_{}'.format(str(export_file_num))
+ self.new_doc = self.document
+
+ for set_num in range(self.options.num_sets):
+
+ # number of the data row in the CSV file
+ n = export_file_num * self.options.num_sets + set_num
+ if n < len(data):
+ dataset = data[n]
+ else:
+ # no more values available, stop trying to replace them
+ break
+
+ for i, (key, value) in enumerate(dataset.items()):
+ search_string = "%VAR_" + key + "%"
+ # replace the next occurrance of %VAR_my_variable_name% in the SVG file source code
+ self.new_doc = self.new_doc.replace(search_string, html.escape(value), 1)
+
+ for key, svg_cont in extra_vars.items():
+ if key in dataset.keys():
+ # replace any attributes and other SVG content by the values from the CSV file
+ self.new_doc = self.new_doc.replace(svg_cont, dataset[key], 1)
+ else:
+ inkex.errormsg(_("The replacements in the generated images may be incomplete. Please check your entry '{key}' in the field for the non-text values.").format(key=key))
+ self.export(export_base_name)
+
+ def export(self, export_base_name):
+
+ export_file_name = '{0}.{1}'.format(export_base_name, self.options.format)
+
+ if os.path.exists(self.options.output_folder):
+ export_file_path = os.path.join(self.options.output_folder, export_file_name)
+ else:
+ inkex.errormsg("The selected output folder does not exist.")
+ return False
+
+
+ if self.options.format == 'svg':
+ # would use this, but it cannot overwrite, nor handle strings for writing...:
+ # write_svg(self.new_doc, export_file_path)
+ with open(export_file_path, 'w') as f:
+ f.write(self.new_doc)
+ else:
+
+ actions = {
+ 'png' : 'export-dpi:{dpi};export-filename:{file_name};export-do;FileClose'.\
+ format(dpi=self.options.dpi, file_name=export_file_path),
+ 'pdf' : 'export-dpi:{dpi};export-pdf-version:1.5;export-text-to-path;export-filename:{file_name};export-do;FileClose'.\
+ format(dpi=self.options.dpi, file_name=export_file_path),
+ 'ps' : 'export-dpi:{dpi};export-text-to-path;export-filename:{file_name};export-do;FileClose'.\
+ format(dpi=self.options.dpi, file_name=export_file_path),
+ 'eps' : 'export-dpi:{dpi};export-text-to-path;export-filename:{file_name};export-do;FileClose'.\
+ format(dpi=self.options.dpi, file_name=export_file_path),
+ }
+
+ # create a temporary svg file from our string
+ temp_svg_name = '{0}.{1}'.format(export_base_name, 'svg')
+ temp_svg_path = os.path.join(self.tempdir, temp_svg_name)
+ #inkex.utils.debug("temp_svg_path=" + temp_svg_path)
+ with open(temp_svg_path, 'w') as f:
+ f.write(self.new_doc)
+ #inkex.utils.debug("self.new_doc=" + self.new_doc)
+ # let Inkscape do the exporting
+ # self.debug(actions[self.options.format])
+ cli_output = inkscape(temp_svg_path, actions=actions[self.options.format])
+
+ if len(cli_output) > 0:
+ self.debug(_("Inkscape returned the following output when trying to run the file export; the file export may still have worked:"))
+ self.debug(cli_output)
+ return False
+ return True
+
+ def load(self, stream):
+ return str(stream.read(), 'utf-8')
+
+ def save(self, stream):
+ # must be implemented, but isn't needed.
+ pass
+
+if __name__ == '__main__':
+ NextGenerator().run()
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/raster_perspective/meta.json b/extensions/fablabchemnitz/raster_perspective/meta.json
new file mode 100644
index 0000000..0167fba
--- /dev/null
+++ b/extensions/fablabchemnitz/raster_perspective/meta.json
@@ -0,0 +1,20 @@
+[
+ {
+ "name": "Raster Perspective",
+ "id": "fablabchemnitz.de.raster_perspective",
+ "path": "raster_perspective",
+ "dependent_extensions": null,
+ "original_name": "Perspective",
+ "original_id": "org.test.filter.imagePerspective",
+ "license": "GNU GPL v3",
+ "license_url": "https://github.com/s1291/InkRasterPerspective/blob/master/LICENSE",
+ "comment": "",
+ "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/raster_perspective",
+ "fork_url": "https://github.com/s1291/InkRasterPerspective",
+ "documentation_url": "https://stadtfabrikanten.org/display/IFM/Raster+Perspective",
+ "inkscape_gallery_url": null,
+ "main_authors": [
+ "github.com/s1291"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/raster_perspective/raster_perspective.inx b/extensions/fablabchemnitz/raster_perspective/raster_perspective.inx
new file mode 100644
index 0000000..9b934c5
--- /dev/null
+++ b/extensions/fablabchemnitz/raster_perspective/raster_perspective.inx
@@ -0,0 +1,16 @@
+
+
+ Raster Perspective
+ fablabchemnitz.de.raster_perspective
+
+ all
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/raster_perspective/raster_perspective.py b/extensions/fablabchemnitz/raster_perspective/raster_perspective.py
new file mode 100644
index 0000000..95c223d
--- /dev/null
+++ b/extensions/fablabchemnitz/raster_perspective/raster_perspective.py
@@ -0,0 +1,172 @@
+#!/usr/bin/env python3
+
+# Copyright (C) 2022 Samir OUCHENE, samirmath01@gmail.com
+
+import os
+import sys
+
+import io
+import inkex
+from inkex import Image
+from PIL import Image as PIL_Image
+from PIL.Image import Transform, Resampling
+import base64
+import numpy
+
+
+try:
+ from base64 import decodebytes
+except ImportError:
+ from base64 import decodestring as decodebytes
+
+
+class RasterPerspective(inkex.Effect):
+ def __init__(self):
+ inkex.Effect.__init__(self)
+
+ @staticmethod
+ def mime_to_ext(mime):
+ """Return an extension based on the mime type"""
+ # Most extensions are automatic (i.e. extension is same as minor part of mime type)
+ part = mime.split("/", 1)[1].split("+")[0]
+ return (
+ "."
+ + {
+ # These are the non-matching ones.
+ "svg+xml": ".svg",
+ "jpeg": ".jpg",
+ "icon": ".ico",
+ }.get(part, part)
+ )
+
+ def extract_image(self, node):
+ """Extract the node as if it were an image."""
+ xlink = node.get("xlink:href")
+ if not xlink.startswith("data:"):
+ return # Not embedded image data
+
+ try:
+ data = xlink[5:]
+ (mimetype, data) = data.split(";", 1)
+ (base, data) = data.split(",", 1)
+ except ValueError:
+ inkex.errormsg("Invalid image format found")
+ return
+
+ if base != "base64":
+ inkex.errormsg("Can't decode encoding: {}".format(base))
+ return
+
+ file_ext = self.mime_to_ext(mimetype)
+ return decodebytes(data.encode("utf-8"))
+
+ def find_coeffs(self, source_coords, target_coords):
+ matrix = []
+ for s, t in zip(source_coords, target_coords):
+ matrix.append([t[0], t[1], 1, 0, 0, 0, -s[0] * t[0], -s[0] * t[1]])
+ matrix.append([0, 0, 0, t[0], t[1], 1, -s[1] * t[0], -s[1] * t[1]])
+ A = numpy.array(matrix, dtype=float)
+ B = numpy.array(source_coords).reshape(8)
+ res = numpy.linalg.inv(A.T @ A) @ A.T @ B
+ return numpy.array(res).reshape(8)
+
+ def effect(self):
+ WARN = "Your selection must contain an image and a path with at least 4 points."
+ if len(self.options.ids) < 2:
+ inkex.errormsg(WARN)
+ exit()
+
+ the_image_node, envelope_node = self.svg.selection
+ if str(envelope_node) == "image" and str(the_image_node) == "path":
+ envelope_node, the_image_node = self.svg.selection #switch
+
+ if str(the_image_node) != "image" and str(envelope_node) != "path":
+ inkex.utils.debug(WARN)
+ return
+ img_width, img_height = the_image_node.width, the_image_node.height
+
+ try:
+ unit_to_vp = self.svg.unit_to_viewport
+ except AttributeError:
+ unit_to_vp = self.svg.uutounit
+
+ try:
+ vp_to_unit = self.svg.viewport_to_unit
+ except AttributeError:
+ vp_to_unit = self.svg.unittouu
+
+ img_width = unit_to_vp(img_width)
+ img_height = unit_to_vp(img_height)
+
+ nodes_pts = list(envelope_node.path.control_points)
+ node1 = (unit_to_vp(nodes_pts[0][0]), unit_to_vp(nodes_pts[0][1]))
+ node2 = (unit_to_vp(nodes_pts[1][0]), unit_to_vp(nodes_pts[1][1]))
+ node3 = (unit_to_vp(nodes_pts[2][0]), unit_to_vp(nodes_pts[2][1]))
+ node4 = (unit_to_vp(nodes_pts[3][0]), unit_to_vp(nodes_pts[3][1]))
+
+ nodes = [node1, node2, node3, node4]
+
+ xMax = max([node[0] for node in nodes])
+ xMin = min([node[0] for node in nodes])
+ yMax = max([node[1] for node in nodes])
+ yMin = min([node[1] for node in nodes])
+ # add some assertions (FIXME)
+
+ img_data = self.extract_image(the_image_node)
+ orig_image = PIL_Image.open(io.BytesIO(img_data))
+ pil_img_size = orig_image.size
+ scale = pil_img_size[0] / img_width
+
+ coeffs = self.find_coeffs(
+ [
+ (0, 0),
+ (img_width * scale, 0),
+ (img_width * scale, img_height * scale),
+ (0, img_height * scale),
+ ],
+ [
+ (node1[0] - xMin, node1[1] - yMin),
+ (node2[0] - xMin, node2[1] - yMin),
+ (node3[0] - xMin, node3[1] - yMin),
+ (node4[0] - xMin, node4[1] - yMin),
+ ],
+ )
+
+ W, H = xMax - xMin, yMax - yMin
+
+ final_w, final_h = int(W), int(H)
+
+ # Check if the image has transparency
+ hasTransparency = orig_image.mode in ("RGBA", "LA") or (
+ orig_image.mode == "P" and "transparency" in orig_image.info
+ )
+
+ transp_img = orig_image
+
+ # If the original image is not transparent, create a new image with alpha channel
+ if not hasTransparency:
+ transp_img = PIL_Image.new("RGBA", orig_image.size)
+ transp_img.format = "PNG"
+ transp_img.paste(orig_image)
+
+ image = transp_img.transform(
+ (final_w, final_h), Transform.PERSPECTIVE, coeffs, Resampling.BICUBIC
+ )
+
+ obj = inkex.Image()
+ obj.set("x", vp_to_unit(xMin))
+ obj.set("y", vp_to_unit(yMin))
+ obj.set("width", vp_to_unit(final_w))
+ obj.set("height", vp_to_unit(final_h))
+ # embed the transformed image
+ persp_img_data = io.BytesIO()
+ image.save(persp_img_data, transp_img.format)
+ mime = PIL_Image.MIME[transp_img.format]
+ b64 = base64.b64encode(persp_img_data.getvalue()).decode("utf-8")
+ uri = f"data:{mime};base64,{b64}"
+ obj.set("xlink:href", uri)
+ self.svg.add(obj)
+
+
+RasterPerspective = RasterPerspective()
+RasterPerspective.run()