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 + + + + + + + .lyz + image/lyz + Laser Draw LYZ (*.lyz) + LaserDraw LYZ Output + true + + + \ 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 + + + + + + + + .zip + application/x-zip + Laser Draw LYZ (ZIP)(*.zip) + LaserDraw LYZ Output Zipped + true + + + \ 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()