This repository has been archived on 2023-03-25. You can view files and clone it, but cannot push or open issues or pull requests.
mightyscape-0.92-deprecated/fablabchemnitz_elliptical_box.py
2019-11-14 20:05:10 +01:00

501 lines
20 KiB
Python

#!/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 '<path d="' + ' '.join(components) + '">'
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()