#!/usr/bin/env python # We will use the inkex module with the predefined Effect base class. import inkex # The simplestyle module provides functions for style parsing. import simplestyle from math import * from collections import namedtuple #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 (we do not use them all so a cleanup may be in order) objStyle = simplestyle.formatStyle( {'stroke': '#000000', 'stroke-width': 0.28, 'fill': 'none' }) greenStyle = simplestyle.formatStyle( {'stroke': '#00ff00', 'stroke-width': 0.28, 'fill': 'none' }) def draw_SVG_square((w,h), (x,y), parent): attribs = { 'style': objStyle, 'height': str(h), 'width': str(w), 'x': str(x), 'y': str(y) } inkex.etree.SubElement(parent, inkex.addNS('rect', 'svg'), attribs) def draw_SVG_ellipse((rx, ry), center, parent, start_end=(0, 2*pi), transform=''): ell_attribs = {'style': objStyle, inkex.addNS('cx', 'sodipodi'): str(center.x), inkex.addNS('cy', 'sodipodi'): str(center.y), inkex.addNS('rx', 'sodipodi'): str(rx), inkex.addNS('ry', 'sodipodi'): str(ry), inkex.addNS('start', 'sodipodi'): str(start_end[0]), inkex.addNS('end', 'sodipodi'): str(start_end[1]), inkex.addNS('open', 'sodipodi'): 'true', #all ellipse sectors we will draw are open inkex.addNS('type', 'sodipodi'): 'arc', 'transform': transform } inkex.etree.SubElement(parent, inkex.addNS('path', 'svg'), ell_attribs) def draw_SVG_arc((rx, ry), x_axis_rot): arc_attribs = {'style': objStyle, 'rx': str(rx), 'ry': str(ry), 'x-axis-rotation': str(x_axis_rot), 'large-arc': '', 'sweep': '', 'x': '', 'y': '' } #name='part' style = {'stroke': '#000000', 'fill': 'none'} drw = {'style':simplestyle.formatStyle(style),inkex.addNS('label','inkscape'):name,'d':XYstring} inkex.etree.SubElement(parent, inkex.addNS('path', 'svg'), drw) inkex.addNS('', 'svg') def draw_SVG_text(coordinate, txt, parent): text = inkex.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', simplestyle.formatStyle(style)) parent.append(text) def SVG_move_to(x, y): return "M %d %d" % (x, y) def SVG_line_to(x, y): return "L %d %d" % (x, y) def SVG_arc_to(rx, ry, x, y): la = sw = 0 return "A %d %d 0 %d %d" % (rx, ry, la, sw, x, y) def SVG_path(components): return '' def SVG_curve(parent, segments, style, closed=True): #pathStr = 'M '+ segments[0] pathStr = ' '.join(segments) if closed: pathStr += ' z' attributes = { 'style': style, 'd': pathStr} inkex.etree.SubElement(parent, inkex.addNS('path', 'svg'), attributes) #draw an SVG line segment between the given (raw) points def draw_SVG_line(start, end, parent, style = objStyle): line_attribs = {'style': style, 'd': 'M '+str(start.x)+','+str(start.y)+' L '+str(end.x)+','+str(end.y)} inkex.etree.SubElement(parent, inkex.addNS('path', 'svg'), line_attribs) def _makeCurvedSurface(topLeft, w, h, cutSpacing, hCutCount, thickness, parent, invertNotches = False, centralRib = False): group = inkex.etree.SubElement(parent, 'g') 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 = [0] topHCuts = [] bottomHCuts = [] 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 draw_SVG_line(aColStart, aColStart + cut / 2, group) for j in range(hCutCount - 1): pos = aColStart + cut / 2 + ySpacing + (cut + ySpacing) * j draw_SVG_line(pos, pos + cut, group) draw_SVG_line(aColStart + height - cut / 2, aColStart + height, group) # 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 draw_SVG_line(holeTopLeft + plateThickness + xSpacing, holeTopLeft + plateThickness, group) draw_SVG_line(holeTopLeft, holeTopLeft + xSpacing, group) elif j == hCutCount // 2: end += plateThickness / 2 if j == 0: # first row end += inset elif j == hCutCount - 1: # last row start -= inset draw_SVG_line(start, end, group) #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): draw_SVG_line(c[1], c[0], group) draw_SVG_line(topLeft + height, topLeft, group) for c in topHCuts: draw_SVG_line(c[0], c[1], group) draw_SVG_line(topLeft + width, topLeft + width + height, group) notchEdges.append(w) return notchEdges def _makeNotchedEllipse(center, ellipse, startAngle, thickness, notches, parent, invertNotches): startAngle += pi # rotate 180 degrees to put the lid on the topside c2 = ellipse.notchCoordinate(ellipse.rAngle(startAngle), thickness) a1 = atan2((ellipse.w/2 + thickness) * c2.y, (ellipse.h/2 + thickness) * c2.x) for n in range(1, len(notches) - 1): startA = ellipse.angleFromDist(startAngle, notches[n]) endA = ellipse.angleFromDist(startAngle, notches[n + 1]) c1 = center + ellipse.coordinateFromAngle(endA) c2 = ellipse.notchCoordinate(endA, thickness) a2 = atan2((ellipse.w/2 + thickness) * c2.y, (ellipse.h/2 + thickness) * c2.x) c2 += center if (n % 2 == 1) != invertNotches: draw_SVG_ellipse((ellipse.w / 2, ellipse.h / 2), center, parent, (startA, endA)) draw_SVG_line(c1, c2, parent) else: draw_SVG_ellipse((ellipse.w / 2 + thickness, ellipse.h / 2 + thickness), center, parent, (a1, a2)) draw_SVG_line(c2, c1, parent) a1 = a2 class Ellipse(): nrPoints = 1000 #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, w, h): self.h = h self.w = w EllipsePoint = namedtuple('EllipsePoint', 'angle coord cDist') self.ellData = [EllipsePoint(0, Coordinate(w/2, 0), 0)] # (angle, x, y, cumulative distance from angle = 0) angle = 0 self.angleStep = 2 * pi / self.nrPoints #note: the render angle (ra) corresponds to the angle from the ellipse center (ca) according to: # ca = atan(w/h * tan(ra)) for i in range(self.nrPoints): angle += self.angleStep prev = self.ellData[-1] x, y = w / 2 * cos(angle), h / 2 * sin(angle) self.ellData.append(EllipsePoint(angle, Coordinate(x, y), prev.cDist + hypot(prev.coord.x - x, prev.coord.y - y))) self.circumference = self.ellData[-1].cDist #inkex.debug("circ: %d" % self.circumference) def rAngle(self, a): """Convert an angle measured from ellipse center to the angle used to generate ellData (used for lookups)""" cf = 0 if a > pi / 2: cf = pi if a > 3 * pi / 2: cf = 2 * pi return atan(self.w / self.h * tan(a)) + cf def coordinateFromAngle(self, angle): """Coordinate of the point at angle.""" return Coordinate(self.w / 2 * cos(angle), self.h / 2 * sin(angle)) def notchCoordinate(self, angle, notchHeight): """Coordinate for a notch at the given angle. The notch is perpendicular to the ellipse.""" angle %= (2 * pi) #some special cases to avoid divide by zero: if angle == 0: return (0, Coordinate(self.w / 2 + notchHeight, 0)) elif angle == pi: return (pi, Coordinate(-self.w / 2 - notchHeight, 0)) elif angle == pi / 2: return(pi / 2, Coordinate(0, self.h / 2 + notchHeight)) elif angle == 3 * pi / 2: return(3 * pi / 2, Coordinate(0, -self.h / 2 - notchHeight)) x = self.w / 2 * cos(angle) derivative = self.h / self.w * -x / sqrt((self.w / 2) ** 2 - x ** 2) if angle > pi: derivative = -derivative normal = -1 / derivative nAngle = atan(normal) if angle > pi / 2 and angle < 3 * pi / 2: nAngle += pi nCoordinate = self.coordinateFromAngle(angle) + Coordinate(cos(nAngle), sin(nAngle)) * notchHeight return nCoordinate def distFromAngles(self, a1, a2): """Distance accross the surface from point at angle a2 to point at angle a2. Measured in CCW sense.""" i1 = int(self.rAngle(a1) / self.angleStep) p1 = self.rAngle(a1) % self.angleStep l1 = self.ellData[i1 + 1].cDist - self.ellData[i1].cDist i2 = int(self.rAngle(a2) / self.angleStep) p2 = self.rAngle(a2) % self.angleStep l2 = self.ellData[i2 + 1].cDist - self.ellData[i2].cDist if a1 <= a2: len = self.ellData[i2].cDist - self.ellData[i1].cDist + l2 * p2 - l1 * p1 else: len = self.circumference + self.ellData[i2].cDist - self.ellData[i1].cDist return len def angleFromDist(self, startAngle, relDist): """Returns the angle that you get when starting at startAngle and moving a distance (dist) in CCW direction""" si = int(self.rAngle(startAngle) / self.angleStep) p = self.rAngle(startAngle) % self.angleStep l = self.ellData[si + 1].cDist - self.ellData[si].cDist startDist = self.ellData[si].cDist + p * l absDist = relDist + startDist if absDist > self.ellData[-1].cDist: # wrap around zero angle absDist -= self.ellData[-1].cDist iMin = 0 iMax = self.nrPoints count = 0 while iMax - iMin > 1: # binary search count += 1 iHalf = iMin + (iMax - iMin) // 2 if self.ellData[iHalf].cDist < absDist: iMin = iHalf else: iMax = iHalf stepDist = self.ellData[iMax].cDist - self.ellData[iMin].cDist return self.ellData[iMin].angle + self.angleStep * (absDist - self.ellData[iMin].cDist)/stepDist class Coordinate: def __init__(self, x, y): self.x = x self.y = 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 __div__(self, quotient): return Coordinate(self.x / quotient, self.y / quotient) class EllipticalBox(inkex.Effect): """ Creates a new layer with the drawings for a parametrically generaded box. """ def __init__(self): inkex.Effect.__init__(self) self.knownUnits = ['in', 'pt', 'px', 'mm', 'cm', 'm', 'km', 'pc', 'yd', 'ft'] self.OptionParser.add_option('-u', '--unit', action = 'store', type = 'string', dest = 'unit', default = 'mm', help = 'Unit, should be one of ') self.OptionParser.add_option('-t', '--thickness', action = 'store', type = 'float', dest = 'thickness', default = '3.0', help = 'Material thickness') self.OptionParser.add_option('-x', '--width', action = 'store', type = 'float', dest = 'width', default = '3.0', help = 'Box width') self.OptionParser.add_option('-z', '--height', action = 'store', type = 'float', dest = 'height', default = '10.0', help = 'Box height') self.OptionParser.add_option('-y', '--depth', action = 'store', type = 'float', dest = 'depth', default = '3.0', help = 'Box depth') self.OptionParser.add_option('-d', '--cut_dist', action = 'store', type = 'float', dest = 'cut_dist', default = '1.5', help = 'Distance between cuts on the wrap around. Note that this value will change slightly to evenly fill up the available space.') self.OptionParser.add_option('--auto_cut_dist', action = 'store', type = 'inkbool', dest = 'auto_cut_dist', default = 'false', help = 'Automatically set the cut distance based on the curvature.') self.OptionParser.add_option('-c', '--cut_nr', action = 'store', type = 'int', dest = 'cut_nr', default = '3', help = 'Number of cuts across the depth of the box.') self.OptionParser.add_option('-a', '--lid_angle', action = 'store', type = 'float', dest = 'lid_angle', default = '120', help = 'Angle that forms the lid (in degrees, measured from centerpoint of the ellipse)') self.OptionParser.add_option('-b', '--body_ribcount', action = 'store', type = 'int', dest = 'body_ribcount', default = '0', help = 'Number of ribs in the body') self.OptionParser.add_option('-l', '--lid_ribcount', action = 'store', type = 'int', dest = 'lid_ribcount', default = '0', help = 'Number of ribs in the lid') self.OptionParser.add_option('-n', '--invert_lid_notches', action = 'store', type = 'inkbool', dest = 'invert_lid_notches', default = 'false', help = 'Invert the notch pattern on the lid (to prevent sideways motion)') self.OptionParser.add_option('-r', '--central_rib_lid', action = 'store', type = 'inkbool', dest = 'centralRibLid', default = 'false', help = 'Create a central rib in the lid') self.OptionParser.add_option('-R', '--central_rib_body', action = 'store', type = 'inkbool', dest = 'centralRibBody', default = 'false', help = 'Create a central rib in the body') 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): """ 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: inkex.errormsg('Error: Dimensions must be non zero') error = True if self.options.cut_nr < 1: inkex.errormsg('Error: Number of cuts should be at least 1') error = True if (self.options.centralRibLid or self.options.centralRibBody) and self.options.cut_nr % 2 == 1: inkex.errormsg('Error: Central rib is only valid with an even number of cuts') error = True if self.options.unit not in self.knownUnits: inkex.errormsg('Error: unknown unit. '+ self.options.unit) error = True if error: exit() # convert units unit = self.options.unit H = self.unittouu(str(self.options.height) + unit) W = self.unittouu(str(self.options.width) + unit) D = self.unittouu(str(self.options.depth) + unit) thickness = self.unittouu(str(self.options.thickness) + unit) cutSpacing = self.unittouu(str(self.options.cut_dist) + unit) cutNr = self.options.cut_nr svg = self.document.getroot() docWidth = self.unittouu(svg.get('width')) docHeigh = self.unittouu(svg.attrib['height']) layer = inkex.etree.SubElement(svg, 'g') layer.set(inkex.addNS('label', 'inkscape'), 'Elliptical Box') layer.set(inkex.addNS('groupmode', 'inkscape'), 'layer') ell = Ellipse(W, H) #body and lid lidAngleRad = self.options.lid_angle * 2 * pi / 360 lidStartAngle = pi / 2 - lidAngleRad / 2 lidEndAngle = pi / 2 + lidAngleRad / 2 lidLength = ell.distFromAngles(lidStartAngle, lidEndAngle) bodyLength = ell.distFromAngles(lidEndAngle, lidStartAngle) # do not put elements right at the edge of the page xMargin = 3 yMargin = 3 bodyNotches = _makeCurvedSurface(Coordinate(xMargin, yMargin), bodyLength, D, cutSpacing, cutNr, thickness, layer, False, self.options.centralRibBody) lidNotches = _makeCurvedSurface(Coordinate(xMargin, D + 2 * yMargin), lidLength, D, cutSpacing, cutNr, thickness, layer, not self.options.invert_lid_notches, self.options.centralRibLid) a1 = lidEndAngle # create elliptical sides sidesGrp = inkex.etree.SubElement(layer, 'g') elCenter = Coordinate(xMargin + thickness + W / 2, 2 * D + H / 2 + thickness + 3 * yMargin) # indicate the division between body and lid if self.options.invert_lid_notches: draw_SVG_line(elCenter, elCenter + ell.coordinateFromAngle(ell.rAngle(lidStartAngle + pi)), sidesGrp, greenStyle) draw_SVG_line(elCenter, elCenter + ell.coordinateFromAngle(ell.rAngle(lidEndAngle + pi)), sidesGrp, greenStyle) else: angleA = ell.angleFromDist(lidStartAngle, lidNotches[2]) angleB = ell.angleFromDist(lidStartAngle, lidNotches[-2]) draw_SVG_line(elCenter, elCenter + ell.coordinateFromAngle(angleA + pi), sidesGrp, greenStyle) draw_SVG_line(elCenter, elCenter + ell.coordinateFromAngle(angleB + pi), sidesGrp, greenStyle) _makeNotchedEllipse(elCenter, ell, lidEndAngle, thickness, bodyNotches, sidesGrp, False) _makeNotchedEllipse(elCenter, ell, lidStartAngle, thickness, lidNotches, sidesGrp, not self.options.invert_lid_notches) # ribs spacer = Coordinate(0, 10) innerRibCenter = Coordinate(xMargin + thickness + W / 2, 2 * D + 1.5 * (H + 2 *thickness) + 4 * yMargin) innerRibGrp = inkex.etree.SubElement(layer, 'g') outerRibCenter = Coordinate(2 * xMargin + 1.5 * (W + 2 * thickness) , 2 * D + 1.5 * (H + 2 * thickness) + 4 * yMargin) outerRibGrp = inkex.etree.SubElement(layer, 'g') if self.options.centralRibLid: _makeNotchedEllipse(innerRibCenter, ell, lidStartAngle, thickness, lidNotches, innerRibGrp, False) _makeNotchedEllipse(outerRibCenter, ell, lidStartAngle, thickness, lidNotches, outerRibGrp, True) if self.options.centralRibBody: _makeNotchedEllipse(innerRibCenter + spacer, ell, lidEndAngle, thickness, bodyNotches, innerRibGrp, False) _makeNotchedEllipse(outerRibCenter + spacer, ell, lidEndAngle, thickness, bodyNotches, outerRibGrp, True) if self.options.centralRibLid or self.options.centralRibBody: draw_SVG_text(elCenter, 'side (duplicate this)', sidesGrp) draw_SVG_text(innerRibCenter, 'inside rib', innerRibGrp) draw_SVG_text(outerRibCenter, 'outside rib', outerRibGrp) # Create effect instance and apply it. effect = EllipticalBox() effect.affect()