#!/usr/bin/python import cairo import math from functools import wraps def dist(dx, dy): return (dx*dx+dy*dy)**0.5 def restore(func): @wraps(func) def f(self, *args, **kw): self.ctx.save() pt = self.ctx.get_current_point() func(self, *args, **kw) self.ctx.restore() self.ctx.move_to(*pt) return f class BoltPolicy: """Abstract class Distributes (bed) bolts on a number of segments (fingers of a finger joint) """ def drawbolt(self, pos): """Add a bolt to this segment?""" return False def numFingers(self, numfingers): """returns next smaller, possible number of fingers""" return numFingers def _even(self, numFingers): return (numFingers//2) * 2 def _odd(self, numFingers): if numFingers % 2: return numFingers else: return numFingers - 1 class Bolts(BoltPolicy): """Distribute a fixed number of bolts evenly""" def __init__(self, bolts=1): self.bolts = bolts def numFingers(self, numFingers): if self.bolts % 2: self.fingers = self._even(numFingers) else: self.fingers = numFingers return self.fingers def drawBolt(self, pos): if pos > self.fingers//2: pos = self.fingers - pos if pos==0: return False if pos == self.fingers//2 and not (self.bolts % 2): return False result = (math.floor((float(pos)*(self.bolts+1)/self.fingers)-0.01) != math.floor((float(pos+1)*(self.bolts+1)/self.fingers)-0.01)) #print pos, result, ((float(pos)*(self.bolts+1)/self.fingers)-0.01), ((float(pos+1)*(self.bolts+1)/self.fingers)-0.01) return result ############################################################################# ### Settings ############################################################################# class Settings: absolute_params = { } relative_params = { } def __init__(self, thickness, relative=True, **kw): self.values = self.absolute_params.copy() factor = 1.0 if relative: factor = thickness for name, value in self.relative_params.iteritems(): self.values[name] = value * factor self.setValues(thickness, relative, **kw) def setValues(self, thickness, relative=True, **kw): factor = 1.0 if relative: factor = thickness for name, value in kw.iteritems(): if name in self.absolute_params: self.values[name] = value elif name in self.relative_params: self.values[name] = value * factor else: raise ValueError, "Unknown parameter for %s: %s" % ( self.__class__.__name__, name) def __getattr__(self, name): return self.values[name] ############################################################################# ### Edges ############################################################################# class Edge: char = 'e' def __init__(self, boxes, settings): self.boxes = boxes self.ctx = boxes.ctx self.settings = settings def __getattr__(self, name): """Hack for using unalter code form Boxes class""" return getattr(self.boxes, name) def __call__(self, length, **kw): self.ctx.move_to(0,0) self.ctx.line_to(length, 0) self.ctx.translate(*self.ctx.get_current_point()) def width(self): return 0.0 def margin(self): return self.boxes.spacing def spacing(self): return self.width() + self.margin() def startAngle(self): return 0.0 def endAngle(self): return 0.0 class OutSetEdge(Edge): char = 'E' def width(self): return self.boxes.thickness class FingerJointSettings(Settings): relative_params = { "space" : 1.0, "finger" : 1.0, "height" : 1.0, "width" : 1.0, } class FingerJointEdge(Edge): char = 'f' positive = True def __call__(self, length, bedBolts=None, bedBoltSettings=None, **kw): positive = self.positive space, finger = self.settings.space, self.settings.finger fingers = int((length-space) // (space+finger)) if bedBolts: fingers = bedBolts.numFingers(fingers) leftover = length - fingers*(space+finger) - space s, f, thickness = space, finger, self.thickness d, d_nut, h_nut, l, l1 = bedBoltSettings or self.bedBoltSettings p = 1 if positive else -1 self.edge(leftover/2.0) for i in xrange(fingers): if not positive and bedBolts and bedBolts.drawBolt(i): self.hole(0.5*space, 0.5*self.thickness, 0.5*d) if positive and bedBolts and bedBolts.drawBolt(i): self.bedBoltHole(s, bedBoltSettings) else: self.edge(s) self.corner(-90*p) self.edge(self.settings.height) self.corner(90*p) self.edge(f) self.corner(90*p) self.edge(self.settings.height) self.corner(-90*p) self.edge(s+leftover/2.0) def margin(self): return self.boxes.spacing + self.boxes.thickness class FingerJointEdgeCounterPart(FingerJointEdge): char = 'F' positive = False def width(self): return self.boxes.thickness def margin(self): return self.boxes.spacing class FingerHoleEdge(Edge): char = 'h' def __call__(self, length, dist=None, bedBolts=None, bedBoltSettings=None, **kw): if dist is None: dist = self.fingerHoleEdgeWidth * self.thickness self.ctx.save() self.moveTo(0, dist+self.thickness/2) self.fingerHoles(length, bedBolts, bedBoltSettings) self.ctx.restore() # XXX continue path self.ctx.move_to(0, 0) self.ctx.line_to(length, 0) self.ctx.translate(*self.ctx.get_current_point()) def width(self): return (self.fingerHoleEdgeWidth+1) * self.thickness class DoveTailSettings(Settings): absolute_params = { "angle" : 50, } relative_params = { "size" : 3, "depth" : 1.5, "radius" : 0.2, } class DoveTailJoint(Edge): char = 'd' positive = True def __call__(self, length, **kw): s = self.settings radius = max(s.radius, self.boxes.burn) # no smaller than burn positive = self.positive a = s.angle + 90 alpha = 0.5*math.pi - math.pi*s.angle/180.0 l1 = radius/math.tan(alpha/2.0) diffx = 0.5*s.depth/math.tan(alpha) l2 = 0.5*s.depth / math.sin(alpha) sections = int((length) // (s.size*2)) leftover = length - sections*s.size*2 p = 1 if positive else -1 self.edge((s.size+leftover)/2.0+diffx-l1) for i in xrange(sections): self.corner(-1*p*a, radius) self.edge(2*(l2-l1)) self.corner(p*a, radius) self.edge(2*(diffx-l1)+s.size) self.corner(p*a, radius) self.edge(2*(l2-l1)) self.corner(-1*p*a, radius) if iidx: return param[idx] else: return None else: return param ############################################################ ### Turtle graphics commands ############################################################ def corner(self, degrees, radius=0): rad = degrees*math.pi/180 if degrees > 0: self.ctx.arc(0, radius+self.burn, radius+self.burn, -0.5*math.pi, rad - 0.5*math.pi) elif radius > self.burn: self.ctx.arc_negative(0, -(radius-self.burn), radius-self.burn, 0.5*math.pi, rad + 0.5*math.pi) else: # not rounded inner corner self.ctx.arc_negative(0, self.burn-radius, self.burn-radius, -0.5*math.pi, -0.5*math.pi+rad) self.continueDirection(rad) def edge(self, length): self.ctx.move_to(0,0) self.ctx.line_to(length, 0) self.ctx.translate(*self.ctx.get_current_point()) def curveTo(self, x1, y1, x2, y2, x3, y3): """control point 1, control point 2, end point""" self.ctx.curve_to(x1, y1, x2, y2, x3, y3) dx = x3-x2 dy = y3-y2 rad = math.atan2(dy, dx) self.continueDirection(rad) def bedBoltHole(self, length, bedBoltSettings=None): d, d_nut, h_nut, l, l1 = bedBoltSettings or self.bedBoltSettings self.edge((length-d)/2.0) self.corner(90) self.edge(l1) self.corner(90) self.edge((d_nut-d)/2.0) self.corner(-90) self.edge(h_nut) self.corner(-90) self.edge((d_nut-d)/2.0) self.corner(90) self.edge(l-l1-h_nut) self.corner(-90) self.edge(d) self.corner(-90) self.edge(l-l1-h_nut) self.corner(90) self.edge((d_nut-d)/2.0) self.corner(-90) self.edge(h_nut) self.corner(-90) self.edge((d_nut-d)/2.0) self.corner(90) self.edge(l1) self.corner(90) self.edge((length-d)/2.0) def grip(self, length, depth): """corrugated edge useful as an gipping area""" grooves = int(length // (depth*2.0)) + 1 depth = length / grooves / 4.0 for groove in xrange(grooves): self.corner(90, depth) self.corner(-180, depth) self.corner(90, depth) def _latchHole(self, length): self.edge(1.1*self.thickness) self.corner(-90) self.edge(length/2.0+0.2*self.thickness) self.corner(-90) self.edge(1.1*self.thickness) def _latchGrip(self, length): self.corner(90, self.thickness/4.0) self.grip(length/2.0-self.thickness/2.0-0.2*self.thickness, self.thickness/2.0) self.corner(90, self.thickness/4.0) def latch(self, length, positive=True, reverse=False): """Fix a flex box door at the box positive: False: Door side; True: Box side reverse: True when running away from the latch """ if positive: if reverse: self.edge(length/2.0-self.burn) self.corner(-90) self.edge(self.thickness) self.corner(90) self.edge(length/2.0) self.corner(90) self.edge(self.thickness) self.corner(-90) if not reverse: self.edge(length/2.0-self.burn) else: if reverse: self._latchGrip(length) else: self.corner(90) self._latchHole(length) if not reverse: self._latchGrip(length) else: self.corner(90) def handle(self, x, h, hl, r=30): """Creates and Edge with a handle""" d = (x-hl-2*r)/2.0 if d < 0: print "Handle too wide" self.ctx.save() # Hole self.moveTo(d+2*r, 0) self.edge(hl-2*r) self.corner(-90, r) self.edge(h-3*r) self.corner(-90, r) self.edge(hl-2*r) self.corner(-90, r) self.edge(h-3*r) self.corner(-90, r) self.ctx.restore() self.moveTo(0,0) self.curveTo(d, 0, d, 0, d, -h+r) self.curveTo(r, 0, r, 0, r, r) self.edge(hl) self.curveTo(r, 0, r, 0, r, r) self.curveTo(h-r, 0, h-r, 0, h-r, -d) ### Navigation def moveTo(self, x, y=0.0, degrees=0): self.ctx.move_to(0, 0) self.ctx.translate(x, y) self.ctx.rotate(degrees*math.pi/180.0) self.ctx.move_to(0, 0) def continueDirection(self, angle=0): self.ctx.translate(*self.ctx.get_current_point()) self.ctx.rotate(angle) def move(self, x, y, where, before=False): """Intended to be used by parts where can be combinations of "up", "down", "left", "right" and "only" when "only" is included the move is only done when before is True The function returns whether actual drawing of the part should be omited. """ if not where: return False terms = where.split() dontdraw = before and "only" in terms moves = { "up": (0, y, False), "down" : (0, -y, True), "left" : (-x, 0, True), "right" : (x, 0, False), "only" : (0, 0, None), } for term in terms: if not term in moves: raise ValueError, "Unknown direction: '%s'" % term x, y, movebeforeprint = moves[term] if movebeforeprint and before: self.moveTo(x, y) elif (not movebeforeprint and not before) or dontdraw: self.moveTo(x, y) return dontdraw # Building blocks def fingerHolesAt(self, x, y, length, angle=90, bedBolts=None, bedBoltSettings=None): self.ctx.save() self.moveTo(x, y, angle) self.fingerHoles(length, bedBolts, bedBoltSettings) self.ctx.restore() @restore def hole(self, x, y, r): self.moveTo(x+r, y) self.ctx.arc(-r, 0, r, 0, 2*math.pi) @restore def rectangularHole(self, x, y, dx, dy, r=0): self.moveTo(x+r-dx/2.0, y-dy/2.0, 180) for d in (dy, dx, dy, dx): self.corner(-90, r) self.edge(d) # hexHoles def hexHolesRectangle(self, x, y, settings=None, skip=None): """ Fills a rectangle with holes. r : radius of holes b : space between holes style : what types of holes (not yet implemented) skip : function to check if hole should be present gets x, y, r, b, posx, posy """ if settings is None: settings = self.hexHolesSettings r, b, style = settings w = r+b/2.0 dist = w * math.cos(math.pi/6.0) # how many half circles do fit cx = int((x-2*r) // (w)) + 2 cy = int((y-2*r) // (dist)) + 2 # what's left on the sides lx = (x - (2*r+(cx-2)*w))/2.0 ly = (y - (2*r+((cy//2)*2)*dist-2*dist))/2.0 for i in xrange(cy//2): for j in xrange((cx-(i%2))//2): px = 2*j*w + r + lx py = i*2*dist + r + ly if i % 2: px += w if skip and skip(x, y, r, b, px, py): continue self.hole(px, py, r) def __skipcircle(self, x, y, r, b, posx, posy): cx, cy = x/2.0, y/2.0 return (dist(posx-cx, posy-cy) > (cx-r)) def hexHolesCircle(self, d, settings=None): d2 = d/2.0 self.hexHolesRectangle(d, d, settings=settings, skip=self.__skipcircle) def hexHolesPlate(self, x, y, rc, settings=None): def skip(x, y, r, b, posx, posy): posx = abs(posx-(x/2.0)) posy = abs(posy-(y/2.0)) wx = 0.5*x-rc-r wy = 0.5*y-rc-r if (posx <= wx) or (posy <= wx): return 0 return dist(posx-wx, posy-wy) > rc self.hexHolesRectangle(x, y, settings, skip=skip) def hexHolesHex(self, h, settings=None, grow=None): if settings is None: settings = self.hexHolesSettings r, b, style = settings self.ctx.rectangle(0, 0, h, h) w = r+b/2.0 dist = w * math.cos(math.pi/6.0) cy = 2 * int((h-4*dist)// (4*w)) + 1 leftover = h-2*r-(cy-1)*2*r if grow=='space ': b += leftover / (cy-1) / 2 # recalulate with adjusted values w = r+b/2.0 dist = w * math.cos(math.pi/6.0) self.moveTo(h/2.0-(cy//2)*2*w, h/2.0) for j in xrange(cy): self.hole(2*j*w, 0, r) for i in xrange(1, cy/2+1): for j in xrange(cy-i): self.hole(j*2*w+i*w, i*2*dist, r) self.hole(j*2*w+i*w, -i*2*dist, r) ################################################## ### parts ################################################## def roundedPlate(self, x, y, r, callback=None, holesMargin=None, holesSettings=None, bedBolts=None, bedBoltSettings=None, move=None): """fits surroundingWall first edge is split to have a joint in the middle of the side callback is called at the beginning of the straight edges 0, 1 for the two part of the first edge, 2, 3, 4 for the others set holesMargin to get hex holes. """ overallwidth = x+2*self.fingerJointEdge.spacing() overallheight = y+2*self.fingerJointEdge.spacing() if self.move(overallwidth, overallheight, move, before=True): return self.ctx.save() self.moveTo(r+self.fingerJointEdge.spacing(), self.fingerJointEdge.spacing()) self.cc(callback, 0) self.fingerJointEdge(x/2.0-r, bedBolts=self.getEntry(bedBolts, 0), bedBoltSettings=self.getEntry(bedBoltSettings, 0)) self.cc(callback, 1) self.fingerJointEdge(x/2.0-r, bedBolts=self.getEntry(bedBolts, 1), bedBoltSettings=self.getEntry(bedBoltSettings, 1)) for i, l in zip(range(3), (y, x, y)): self.corner(90, r) self.cc(callback, i+2) self.fingerJointEdge(l-2*r, bedBolts=self.getEntry(bedBolts, i+2), bedBoltSettings=self.getEntry(bedBoltSettings, i+2)) self.corner(90, r) self.ctx.restore() self.ctx.save() if holesMargin is not None: self.moveTo(holesMargin, holesMargin) if r > holesMargin: r -= holesMargin else: r = 0 self.hexHolesPlate(x-2*holesMargin, y-2*holesMargin, r, settings=holesSettings) self.ctx.restore() self.move(overallwidth, overallheight, move) def surroundingWall(self, x, y, r, h, bottom='e', top='e', callback=None, move=None): """ h : inner height, not counting the joints callback is called a beginn of the flat sides with 0 for right half of first x side; 1 and 3 for y sides; 2 for second x side 4 for second half of the first x side """ c4 = (r+self.burn)*math.pi*0.5 # circumference of quarter circle c4 = 0.9 * c4 # stretch flex 10% top = self.edges.get(top, top) bottom = self.edges.get(bottom, bottom) topwidth = top.width() bottomwidth = bottom.width() overallwidth = 2*x + 2*y - 8*r + 4*c4 + \ self.edges["d"].spacing() + self.edges["D"].spacing() overallheight = h + top.spacing() + bottom.spacing() if self.move(overallwidth, overallheight, move, before=True): return self.ctx.save() self.moveTo(self.edges["D"].margin(), bottom.margin()) self.cc(callback, 0, y=bottomwidth+self.burn) bottom(x/2.0-r) for i, l in zip(range(4), (y, x, y, 0)): self.flexEdge(c4, h+topwidth+bottomwidth) self.cc(callback, i+1, y=bottomwidth+self.burn) if i < 3: bottom(l-2*r) bottom(x/2.0-r) self.corner(90) self.edge(bottomwidth) self.doveTailJoint(h) self.edge(topwidth) self.corner(90) top(x/2.0-r) for i, l in zip(range(4), (y, x, y, 0)): self.edge(c4) if i < 3: top(l - 2*r) top(x/2.0-r) self.corner(90) self.edge(topwidth) self.doveTailJointCounterPart(h) self.edge(bottomwidth) self.corner(90) self.ctx.restore() self.move(overallwidth, overallheight, move) def rectangularWall(self, x, y, edges="eeee", holesMargin=None, holesSettings=None, callbacks=None, bedBolts=None, bedBoltSettings=None, move=None): if len(edges) != 4: raise ValueError, "four edges required" edges = [self.edges.get(e, e,) for e in edges] edges += edges # append for wrapping around overallwidth = x + edges[-1].spacing() + edges[1].spacing() overallheight = y + edges[0].spacing() + edges[2].spacing() if self.move(overallwidth, overallheight, move, before=True): return self.ctx.save() self.moveTo(edges[-1].margin(), edges[0].margin()) for i, l in enumerate((x, y, x, y)): self.cc(callbacks, i) self.edge(edges[i-1].width()) edges[i](l, bedBolts=self.getEntry(bedBolts, i), bedBoltSettings=self.getEntry(bedBoltSettings, i)) self.edge(edges[i+1].width()) self.corner(90-edges[i].endAngle()-edges[i+1].startAngle()) if holesMargin is not None: self.moveTo(holesMargin+edges[-1].width(), holesMargin+edges[0].width()) self.hexHolesRectangle(x-2*holesMargin, y-2*holesMargin) self.ctx.restore() self.move(overallwidth, overallheight, move) ################################################## ### main ################################################## def render(self, x, y, h): self.ctx.save() self.moveTo(10, 10) self.roundedPlate(x, y, 0) self.moveTo(x+40, 0) self.rectangularWall(x, y, "FFFF") self.ctx.restore() self.moveTo(10, y+20) for i in range(2): for l in (x, y): self.rectangularWall(l, h, "hffF") self.moveTo(l+20, 0) self.moveTo(-x-y-40, h+20) self.ctx.stroke() self.surface.flush() self.surface.finish() if __name__ == '__main__': b = Boxes(900, 700) b.render(100, 161.8, 120)