boxespy/boxes/__init__.py

1272 lines
41 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
2014-03-16 18:26:12 +01:00
# Copyright (C) 2013-2014 Florian Festi
#
# 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 <http://www.gnu.org/licenses/>.
2013-04-01 09:42:29 +02:00
try:
import cairocffi
2016-08-17 15:07:41 +02:00
cairocffi.install_as_pycairo()
except ImportError:
pass
2013-04-01 09:42:29 +02:00
import cairo
import math
import argparse
from argparse import ArgumentParser
import re
from functools import wraps
from boxes import edges
2016-06-27 09:10:00 +02:00
from boxes import formats
2016-07-11 14:59:11 +02:00
from boxes import gears
from boxes import pulley
from boxes import parts
2013-04-01 09:42:29 +02:00
2016-08-17 15:07:41 +02:00
2016-04-10 22:51:25 +02:00
### Helpers
2013-04-17 13:25:30 +02:00
def dist(dx, dy):
2016-03-28 16:55:41 +02:00
"""
Return distance
:param dx: delta x
:param dy: delat y
"""
2016-08-17 15:07:41 +02:00
return (dx * dx + dy * dy) ** 0.5
2013-04-01 09:42:29 +02:00
def restore(func):
2016-03-28 16:55:41 +02:00
"""
Wrapper: Restore coordiantes after function
:param func: function to wrap
"""
2016-08-17 15:07:41 +02:00
@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)
2016-08-17 15:07:41 +02:00
return f
2016-08-17 15:07:41 +02:00
2016-06-21 19:06:33 +02:00
def holeCol(func):
"""
Wrapper: color holes differently
:param func: function to wrap
"""
2016-08-17 15:07:41 +02:00
2016-06-21 19:06:33 +02:00
@wraps(func)
def f(self, *args, **kw):
self.ctx.stroke()
self.ctx.set_source_rgb(0.0, 0.0, 1.0)
func(self, *args, **kw)
self.ctx.stroke()
self.ctx.set_source_rgb(0.0, 0.0, 0.0)
2016-08-17 15:07:41 +02:00
return f
2016-06-21 19:06:33 +02:00
#############################################################################
### Building blocks
#############################################################################
2013-07-25 14:22:55 +02:00
class NutHole:
2016-03-28 16:55:41 +02:00
"""Draw a hex nut"""
2013-07-25 14:22:55 +02:00
sizes = {
2016-08-17 15:07:41 +02:00
"M1.6": (3.2, 1.3),
"M2": (4, 1.6),
"M2.5": (5, 2.0),
"M3": (5.5, 2.4),
"M4": (7, 3.2),
"M5": (8, 4.7),
"M6": (10, 5.2),
"M8": (13.7, 6.8),
2016-08-17 15:07:41 +02:00
"M10": (16, 8.4),
"M12": (18, 10.8),
"M14": (21, 12.8),
"M16": (24, 14.8),
"M20": (30, 18.0),
"M24": (36, 21.5),
"M30": (46, 25.6),
"M36": (55, 31),
"M42": (65, 34),
"M48": (75, 38),
"M56": (85, 45),
"M64": (95, 51),
}
2013-07-25 14:22:55 +02:00
def __init__(self, boxes, settings):
self.boxes = boxes
self.ctx = boxes.ctx
self.settings = settings
@restore
2016-06-21 19:06:33 +02:00
@holeCol
2013-07-25 14:22:55 +02:00
def __call__(self, size, x=0, y=0, angle=0):
2016-03-23 09:12:14 +01:00
size = self.sizes.get(size, (size,))[0]
2016-08-17 15:07:41 +02:00
side = size / 3 ** 0.5
2013-07-25 14:22:55 +02:00
self.boxes.moveTo(x, y, angle)
2016-08-17 15:07:41 +02:00
self.boxes.moveTo(-0.5 * side, 0.5 * size, angle)
2013-07-25 14:22:55 +02:00
for i in range(6):
self.boxes.edge(side)
self.boxes.corner(-60)
2016-08-17 15:07:41 +02:00
2016-04-10 22:51:25 +02:00
##############################################################################
### Argument types
##############################################################################
def argparseSections(s):
2016-03-28 16:55:41 +02:00
"""
Parse sections parameter
:param s: string to parse
"""
m = re.match(r"(\d+(\.\d+)?)/(\d+)", s)
if m:
n = int(m.group(3))
2016-08-17 15:07:41 +02:00
print([float(m.group(1))] * n)
return [float(m.group(1)) / n] * n
m = re.match(r"(\d+(\.\d+)?)\*(\d+)", s)
if m:
n = int(m.group(3))
2016-08-17 15:07:41 +02:00
return [float(m.group(1))] * n
try:
return [float(part) for part in s.split(":")]
except ValueError:
raise argparse.ArgumentTypeError("Don't understand sections string")
2016-08-17 15:07:41 +02:00
class ArgparseEdgeType:
names = edges.getDescriptions()
edges = []
def __init__(self, edges=None):
if edges:
self.edges = list(edges)
def __call__(self, pattern):
if len(pattern) != 1:
raise ValueError("Edge type can only have one letter.")
if pattern not in self.edges:
raise ValueError("Use one of the following values: " +
", ".join(edges))
return pattern
def html(self, name, default):
options = "\n".join(
("""<option value="%s"%s>%s %s</option>""" %
(e, ' selected="selected"' if e == default else "",
e, self.names.get(e, "")) for e in self.edges))
return """<select name="%s" size="1">\n%s</select>\n""" % (name, options)
2016-08-17 15:07:41 +02:00
2016-04-10 22:51:25 +02:00
##############################################################################
### Main class
##############################################################################
2013-04-01 09:42:29 +02:00
class Boxes:
2016-03-28 16:55:41 +02:00
"""Main class -- Generator should sub class this """
2013-04-01 09:42:29 +02:00
webinterface = True
def __init__(self):
2016-06-27 09:10:00 +02:00
self.formats = formats.Formats()
2016-03-08 09:33:45 +01:00
self.argparser = ArgumentParser(description=self.__doc__)
self.argparser.add_argument(
2016-08-17 15:07:41 +02:00
"--fingerjointfinger", action="store", type=float, default=1.0,
help="width of the fingers in multiples of thickness")
self.argparser.add_argument(
2016-08-17 15:07:41 +02:00
"--fingerjointspace", action="store", type=float, default=1.0,
help="width of the space between fingers in multiples of thickness")
self.argparser.add_argument(
2016-08-17 15:07:41 +02:00
"--fingerjointsurrounding", action="store", type=float, default=1.0,
help="amount of space needed at the end in multiples of normal spaces")
self.argparser.add_argument(
2016-08-17 15:07:41 +02:00
"--thickness", action="store", type=float, default=4.0,
help="thickness of the material")
self.argparser.add_argument(
2016-08-17 15:07:41 +02:00
"--output", action="store", type=str, default="box.svg",
help="name of resulting file")
2016-06-27 09:10:00 +02:00
self.argparser.add_argument(
2016-08-17 15:07:41 +02:00
"--format", action="store", type=str, default="svg",
2016-06-27 09:10:00 +02:00
choices=self.formats.getFormats(),
help="format of resulting file")
self.argparser.add_argument(
2016-08-17 15:07:41 +02:00
"--debug", action="store", type=bool, default=False,
help="print surrounding boxes for some structures")
self.argparser.add_argument(
2016-08-17 15:07:41 +02:00
"--reference", action="store", type=float, default=100,
help="print reference rectangle with given length")
self.argparser.add_argument(
2016-08-17 15:07:41 +02:00
"--burn", action="store", type=float, default=0.05,
help="burn correction in mm")
def open(self):
2016-03-28 16:55:41 +02:00
"""
Prepare for rendering
Call this function from your .render() method
"""
2016-08-17 15:07:41 +02:00
self.spacing = 2 * self.burn + 0.5 * self.thickness
2016-08-17 15:07:41 +02:00
self.bedBoltSettings = (3, 5.5, 2, 20, 15) # d, d_nut, h_nut, l, l1
self.hexHolesSettings = (5, 3, 'circle') # r, dist, style
2016-06-27 09:10:00 +02:00
self.surface, self.ctx = self.formats.getSurface(self.format, self.output)
2016-08-17 15:07:41 +02:00
self.ctx.set_line_width(2 * self.burn)
self._buildObjects()
if self.reference:
self.move(10, 10, "up", before=True)
self.ctx.rectangle(0, 0, self.reference, 10)
if self.reference < 40:
2016-08-17 15:07:41 +02:00
self.text("%.fmm" % self.reference, self.reference + 5, 5,
align="middle left")
else:
2016-08-17 15:07:41 +02:00
self.text("%.fmm" % self.reference, self.reference / 2.0, 5,
align="middle center")
self.move(10, 10, "up")
def buildArgParser(self, *l):
2016-03-28 16:55:41 +02:00
"""
Add commonly used commandf line parameters
:param \*l: parameter names
"""
for arg in l:
if arg == "x":
self.argparser.add_argument(
2016-08-17 15:07:41 +02:00
"--x", action="store", type=float, default=100.0,
help="inner width in mm")
elif arg == "y":
self.argparser.add_argument(
2016-08-17 15:07:41 +02:00
"--y", action="store", type=float, default=100.0,
help="inner depth in mm")
elif arg == "sx":
self.argparser.add_argument(
2016-08-17 15:07:41 +02:00
"--sx", action="store", type=argparseSections,
default="50*3",
2016-04-02 21:15:05 +02:00
help="""sections left to right in mm. Possible formats: overallwidth/numberof sections e.g. "250/5"; sectionwidth*numberofsections e.g. "50*5"; section widths separated by ":" e.g. "30:25.5:70"
""")
elif arg == "sy":
self.argparser.add_argument(
2016-08-17 15:07:41 +02:00
"--sy", action="store", type=argparseSections,
default="50*3",
help="""sections back to front in mm. See --sx for format""")
elif arg == "h":
self.argparser.add_argument(
2016-08-17 15:07:41 +02:00
"--h", action="store", type=float, default=100.0,
help="inner height in mm")
elif arg == "hi":
self.argparser.add_argument(
2016-08-17 15:07:41 +02:00
"--hi", action="store", type=float, default=0.0,
2016-03-08 22:27:55 +01:00
help="inner height of inner walls in mm (leave to zero for same as outer walls)")
elif arg == "bottom_edge":
self.argparser.add_argument(
2016-08-17 15:07:41 +02:00
"--bottom_edge", action="store",
2016-04-19 21:02:05 +02:00
type=ArgparseEdgeType("Fhs"), choices=list("Fhs"),
default="h",
help="edge type for bottom edge")
elif arg == "top_edge":
self.argparser.add_argument(
2016-08-17 15:07:41 +02:00
"--top_edge", action="store",
type=ArgparseEdgeType("ecESik"), choices=list("ecESik"),
2016-04-19 21:02:05 +02:00
default="e", help="edge type for top edge")
2016-08-17 15:07:41 +02:00
elif arg == "outside":
self.argparser.add_argument(
2016-08-17 15:07:41 +02:00
"--outside", action="store", type=bool, default=False,
2016-07-09 23:15:10 +02:00
help="treat sizes as outside measurements that include the walls")
else:
raise ValueError("No default for argument", arg)
def parseArgs(self, args=None):
2016-03-28 16:55:41 +02:00
"""
Parse command line parameters
:param args: (Default value = None) parameters, None for using sys.argv
"""
2016-08-17 15:07:41 +02:00
for key, value in vars(self.argparser.parse_args(args=args)).items():
setattr(self, key, value)
2016-06-27 09:10:00 +02:00
# Change file ending to format if not given explicitly
if getattr(self, 'output', None) == 'box.svg':
self.output = 'box.' + getattr(self, "format", "svg")
def addPart(self, part, name=None):
2016-03-28 16:55:41 +02:00
"""
Add Edge or other part instance to this one and add it as attribute
:param part: Callable
:param name: (Default value = None) attribute name (__name__ as default)
"""
if name is None:
name = part.__class__.__name__
name = name[0].lower() + name[1:]
2016-08-17 15:07:41 +02:00
# if not hasattr(self, name):
if isinstance(part, edges.BaseEdge):
self.edges[part.char] = part
else:
setattr(self, name, part)
def _buildObjects(self):
2016-03-28 16:55:41 +02:00
"""Add default edges and parts """
self.edges = {}
self.addPart(edges.Edge(self, None))
self.addPart(edges.OutSetEdge(self, None))
s = edges.GripSettings(self.thickness)
self.addPart(edges.GrippingEdge(self, s))
# Finger joints
# Share settings object
s = edges.FingerJointSettings(self.thickness)
s.setValues(self.thickness,
finger=getattr(self, "fingerjointfinger", 1.0),
space=getattr(self, "fingerjointspace", 1.0),
surroundingspaces=getattr(self, "fingerjointsurrounding", 1.0))
self.addPart(edges.FingerJointEdge(self, s))
self.addPart(edges.FingerJointEdgeCounterPart(self, s))
self.addPart(edges.FingerHoles(self, s), name="fingerHolesAt")
self.addPart(edges.FingerHoleEdge(self, None))
# Stackable
2016-04-09 14:27:51 +02:00
ss = edges.StackableSettings(self.thickness)
self.addPart(edges.StackableEdge(self, ss, s))
self.addPart(edges.StackableEdgeTop(self, ss, s))
# Dove tail joints
s = edges.DoveTailSettings(self.thickness)
self.addPart(edges.DoveTailJoint(self, s))
self.addPart(edges.DoveTailJointCounterPart(self, s))
# Flex
s = edges.FlexSettings(self.thickness)
self.addPart(edges.FlexEdge(self, s))
# Clickable
s = edges.ClickSettings(self.thickness)
self.addPart(edges.ClickConnector(self, s))
self.addPart(edges.ClickEdge(self, s))
2016-05-25 19:59:49 +02:00
# Hinges
s = edges.HingeSettings(self.thickness)
2016-08-17 15:07:41 +02:00
2016-05-25 19:59:49 +02:00
for i in range(1, 4):
self.addPart(edges.Hinge(self, s, i))
self.addPart(edges.HingePin(self, s, i))
2016-08-17 15:07:41 +02:00
# Nuts
2013-07-25 14:22:55 +02:00
self.addPart(NutHole(self, None))
2016-07-11 14:59:11 +02:00
# Gears
self.addPart(gears.Gears(self))
self.addPart(pulley.Pulley(self))
self.addPart(parts.Parts(self))
2013-07-25 14:22:55 +02:00
def adjustSize(self, l, e1=True, e2=True):
try:
total = sum(l)
2016-08-17 15:07:41 +02:00
walls = (len(l) - 1) * self.thickness
except TypeError:
total = l
walls = 0
if isinstance(e1, edges.BaseEdge):
walls += e1.startwidth() + e1.margin()
elif e1:
walls += self.thickness
2016-08-17 15:07:41 +02:00
if isinstance(e2, edges.BaseEdge):
walls += e2.startwidth + e2.margin()
elif e2:
walls += self.thickness
try:
2016-08-17 15:07:41 +02:00
factor = (total - walls) / total
return [s * factor for s in l]
except TypeError:
return l - walls
2016-03-29 19:15:32 +02:00
def render(self):
"""Implement this method in your sub class.
You will typically need to call .parseArgs() before calling this one"""
self.open()
2016-03-29 19:15:32 +02:00
# Change settings and creat new Edges and part classes here
raise NotImplemented
self.close()
2013-04-01 09:42:29 +02:00
def cc(self, callback, number, x=0.0, y=None):
2016-03-28 16:55:41 +02:00
"""Call callback from edge of a part
:param callback: callback (callable or list of callables)
:param number: number of the callback
:param x: (Default value = 0.0) x position to be call on
:param y: (Default value = None) y position to be called on (default does burn correction)
"""
if y is None:
y = self.burn
self.ctx.save()
self.moveTo(x, y)
if callable(callback):
if number is None:
callback()
else:
callback(number)
2016-08-17 15:07:41 +02:00
elif hasattr(callback, '__getitem__'):
try:
callback = callback[number]
if callable(callback):
callback()
2013-04-16 05:21:38 +02:00
except (KeyError, IndexError):
pass
except:
self.ctx.restore()
raise
2016-08-17 15:07:41 +02:00
self.ctx.restore()
def getEntry(self, param, idx):
2016-03-28 16:55:41 +02:00
"""
Get entry from list or items itself
:param param: list or item
:param idx: index in list
"""
if isinstance(param, list):
2016-08-17 15:07:41 +02:00
if len(param) > idx:
return param[idx]
else:
return None
else:
return param
2014-03-21 21:08:54 +01:00
def close(self):
2016-03-28 16:55:41 +02:00
"""Finish rendering
Call at the end of your .render() method"""
2014-03-21 21:08:54 +01:00
self.ctx.stroke()
self.surface.flush()
self.surface.finish()
2016-06-27 09:10:00 +02:00
self.formats.convert(self.output, self.format)
2013-04-01 09:42:29 +02:00
############################################################
### Turtle graphics commands
############################################################
def corner(self, degrees, radius=0):
2016-03-28 16:55:41 +02:00
"""
Draw a corner
This is what does the burn corrections
:param degrees: angle
:param radius: (Default value = 0)
"""
2016-08-17 15:07:41 +02:00
rad = degrees * math.pi / 180
2013-04-01 09:42:29 +02:00
if degrees > 0:
2016-08-17 15:07:41 +02:00
self.ctx.arc(0, radius + self.burn, radius + self.burn,
-0.5 * math.pi, rad - 0.5 * math.pi)
elif radius > self.burn:
2016-08-17 15:07:41 +02:00
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)
2013-04-01 18:32:07 +02:00
self.continueDirection(rad)
2013-04-01 09:42:29 +02:00
def edge(self, length):
2016-03-28 16:55:41 +02:00
"""
Simple line
:param length: length in mm
"""
2016-08-17 15:07:41 +02:00
self.ctx.move_to(0, 0)
2013-04-01 09:42:29 +02:00
self.ctx.line_to(length, 0)
self.ctx.translate(*self.ctx.get_current_point())
def curveTo(self, x1, y1, x2, y2, x3, y3):
2016-03-28 16:55:41 +02:00
"""control point 1, control point 2, end point
:param x1:
:param y1:
:param x2:
:param y2:
:param x3:
:param y3:
"""
self.ctx.curve_to(x1, y1, x2, y2, x3, y3)
2016-08-17 15:07:41 +02:00
dx = x3 - x2
dy = y3 - y2
rad = math.atan2(dy, dx)
self.continueDirection(rad)
2014-04-20 21:54:33 +02:00
def polyline(self, *args):
2016-03-28 16:55:41 +02:00
"""
Draw multiple connected lines
:param \*args: Alternating length in mm and angle. angle may be tuple
(angle, radius)
2016-03-28 16:55:41 +02:00
"""
2014-04-20 21:54:33 +02:00
for i, arg in enumerate(args):
if i % 2:
if isinstance(arg, tuple):
self.corner(*arg)
else:
self.corner(arg)
2014-04-20 21:54:33 +02:00
else:
self.edge(arg)
def bedBoltHole(self, length, bedBoltSettings=None):
2016-03-28 16:55:41 +02:00
"""
Draw an edge with slot for a bed bolt
:param length: length of the edge in mm
:param bedBoltSettings: (Default value = None) Dimmensions of the slot
"""
d, d_nut, h_nut, l, l1 = bedBoltSettings or self.bedBoltSettings
2016-08-17 15:07:41 +02:00
self.edge((length - d) / 2.0)
self.corner(90)
self.edge(l1)
self.corner(90)
2016-08-17 15:07:41 +02:00
self.edge((d_nut - d) / 2.0)
self.corner(-90)
self.edge(h_nut)
self.corner(-90)
2016-08-17 15:07:41 +02:00
self.edge((d_nut - d) / 2.0)
self.corner(90)
2016-08-17 15:07:41 +02:00
self.edge(l - l1 - h_nut)
self.corner(-90)
self.edge(d)
self.corner(-90)
2016-08-17 15:07:41 +02:00
self.edge(l - l1 - h_nut)
self.corner(90)
2016-08-17 15:07:41 +02:00
self.edge((d_nut - d) / 2.0)
self.corner(-90)
self.edge(h_nut)
self.corner(-90)
2016-08-17 15:07:41 +02:00
self.edge((d_nut - d) / 2.0)
self.corner(90)
self.edge(l1)
self.corner(90)
2016-08-17 15:07:41 +02:00
self.edge((length - d) / 2.0)
def edgeCorner(self, edge1, edge2, angle=90):
2016-06-30 11:40:42 +02:00
"""Make a corner between two Edges. Take width of edges into account"""
2016-08-17 15:07:41 +02:00
self.edge(edge2.startwidth() / math.sin(math.radians(180 - angle)))
self.corner(angle)
2016-08-17 15:07:41 +02:00
self.edge(edge1.endwidth() / math.sin(math.radians(180 - angle)))
2013-04-01 09:42:29 +02:00
def grip(self, length, depth):
2016-03-28 16:55:41 +02:00
"""Corrugated edge useful as an gipping area
:param length: length
:param depth: depth of the grooves
"""
2016-08-17 15:07:41 +02:00
grooves = int(length // (depth * 2.0)) + 1
depth = length / grooves / 4.0
2014-03-03 21:45:01 +01:00
for groove in range(grooves):
self.corner(90, depth)
self.corner(-180, depth)
self.corner(90, depth)
def _latchHole(self, length):
2016-03-28 16:55:41 +02:00
"""
:param length:
"""
2016-08-17 15:07:41 +02:00
self.edge(1.1 * self.thickness)
self.corner(-90)
2016-08-17 15:07:41 +02:00
self.edge(length / 2.0 + 0.2 * self.thickness)
self.corner(-90)
2016-08-17 15:07:41 +02:00
self.edge(1.1 * self.thickness)
def _latchGrip(self, length):
2016-03-28 16:55:41 +02:00
"""
:param length:
"""
2016-08-17 15:07:41 +02:00
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):
2016-03-28 16:55:41 +02:00
"""Latch to fix a flex box door to the box
:param length: length in mm
:param positive: (Default value = True) False: Door side; True: Box side
:param reverse: (Default value = False) True when running away from the latch
"""
if positive:
if reverse:
self.edge(length / 2.0)
self.corner(-90)
self.edge(self.thickness)
self.corner(90)
2016-08-17 15:07:41 +02:00
self.edge(length / 2.0)
self.corner(90)
self.edge(self.thickness)
self.corner(-90)
if not reverse:
self.edge(length / 2.0)
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):
2016-03-28 16:55:41 +02:00
"""Creates an Edge with a handle
:param x: width in mm
:param h: height in mm
:param hl: height if th grip hole
:param r: (Default value = 30) radius of the corners
"""
2016-08-17 15:07:41 +02:00
d = (x - hl - 2 * r) / 2.0
2013-04-16 05:07:26 +02:00
if d < 0:
2014-03-03 21:45:01 +01:00
print("Handle too wide")
2013-04-16 05:07:26 +02:00
self.ctx.save()
# Hole
2016-08-17 15:07:41 +02:00
self.moveTo(d + 2 * r, 0)
self.edge(hl - 2 * r)
2013-04-16 05:07:26 +02:00
self.corner(-90, r)
2016-08-17 15:07:41 +02:00
self.edge(h - 3 * r)
2013-04-16 05:07:26 +02:00
self.corner(-90, r)
2016-08-17 15:07:41 +02:00
self.edge(hl - 2 * r)
2013-04-16 05:07:26 +02:00
self.corner(-90, r)
2016-08-17 15:07:41 +02:00
self.edge(h - 3 * r)
2013-04-16 05:07:26 +02:00
self.corner(-90, r)
self.ctx.restore()
2016-08-17 15:07:41 +02:00
self.moveTo(0, 0)
2013-04-16 05:07:26 +02:00
2016-08-17 15:07:41 +02:00
self.curveTo(d, 0, d, 0, d, -h + r)
2013-04-16 05:07:26 +02:00
self.curveTo(r, 0, r, 0, r, r)
self.edge(hl)
self.curveTo(r, 0, r, 0, r, r)
2016-08-17 15:07:41 +02:00
self.curveTo(h - r, 0, h - r, 0, h - r, -d)
2013-04-16 05:07:26 +02:00
2013-04-01 09:42:29 +02:00
### Navigation
def moveTo(self, x, y=0.0, degrees=0):
2016-03-28 16:55:41 +02:00
"""
Move coordinate system to given point
:param x:
:param y: (Default value = 0.0)
:param degrees: (Default value = 0)
"""
self.ctx.move_to(0, 0)
2013-04-01 09:42:29 +02:00
self.ctx.translate(x, y)
2016-08-17 15:07:41 +02:00
self.ctx.rotate(degrees * math.pi / 180.0)
2013-04-01 09:42:29 +02:00
self.ctx.move_to(0, 0)
2013-04-01 18:32:07 +02:00
def continueDirection(self, angle=0):
2016-03-28 16:55:41 +02:00
"""
Set coordinate system to current position (end point)
:param angle: (Default value = 0) heading
"""
2013-04-01 09:42:29 +02:00
self.ctx.translate(*self.ctx.get_current_point())
2013-04-01 18:32:07 +02:00
self.ctx.rotate(angle)
2013-04-01 09:42:29 +02:00
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.
2016-03-28 16:55:41 +02:00
:param x: width of part
:param y: height of part
:param where: which direction to move
:param before: (Default value = False) called before or after part being drawn
"""
if not where:
where = ""
terms = where.split()
dontdraw = before and "only" in terms
x += self.spacing
y += self.spacing
moves = {
"up": (0, y, False),
2016-08-17 15:07:41 +02:00
"down": (0, -y, True),
"left": (-x, 0, True),
"right": (x, 0, False),
"only": (0, 0, None),
}
if not before:
# restore position
self.ctx.restore()
for term in terms:
if not term in moves:
2014-03-03 21:45:01 +01:00
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)
if not dontdraw:
if before:
# save position
self.ctx.save()
2016-08-17 15:07:41 +02:00
self.moveTo(self.spacing / 2.0, self.spacing / 2.0)
return dontdraw
@restore
2016-06-21 19:06:33 +02:00
@holeCol
def hole(self, x, y, r):
2016-03-28 16:55:41 +02:00
"""
Draw a round hole
:param x: position
:param y: postion
:param r: radius
"""
2016-03-30 20:52:21 +02:00
r -= self.burn
if r < 0:
r = 1E-9
2016-08-17 15:07:41 +02:00
self.moveTo(x + r, y)
self.ctx.arc(-r, 0, r, 0, 2 * math.pi)
2013-07-20 17:51:54 +02:00
@restore
2016-06-21 19:06:33 +02:00
@holeCol
2013-07-20 17:51:54 +02:00
def rectangularHole(self, x, y, dx, dy, r=0):
2016-03-28 16:55:41 +02:00
"""
Draw an rectangulat hole
:param x: position
:param y: position
:param dx: width
:param dy: height
:param r: (Default value = 0) radius of the corners
"""
2016-08-17 15:07:41 +02:00
self.moveTo(x + r - dx / 2.0, y - dy / 2.0, 180)
2013-07-20 17:51:54 +02:00
for d in (dy, dx, dy, dx):
self.corner(-90, r)
2016-08-17 15:07:41 +02:00
self.edge(d - 2 * r)
2013-07-20 17:51:54 +02:00
@restore
def text(self, text, x=0, y=0, angle=0, align=""):
2016-03-28 16:55:41 +02:00
"""
Draw text
:param text: text to render
:param x: (Default value = 0)
:param y: (Default value = 0)
:param angle: (Default value = 0)
:param align: (Default value = "") string with combinations of (top|middle|bottom) and (left|center|right) separated by a space
"""
self.moveTo(x, y, angle)
(tx, ty, width, height, dx, dy) = self.ctx.text_extents(text)
align = align.split()
moves = {
2016-08-17 15:07:41 +02:00
"top": (0, -height),
"middle": (0, -0.5 * height),
"bottom": (0, 0),
"left": (0, 0),
"center": (-0.5 * width, 0),
"right": (-width, 0),
}
for a in align:
if a in moves:
self.moveTo(*moves[a])
else:
raise ValueError("Unknown alignment: %s" % align)
self.ctx.scale(1, -1)
self.ctx.show_text(text)
2013-07-20 17:52:28 +02:00
@restore
def NEMA(self, size, x=0, y=0, angle=0):
2016-03-28 16:55:41 +02:00
"""Draw holes for mounting a NEMA stepper motor
:param size: Nominal size in tenths of inches
:param x: (Default value = 0)
:param y: (Default value = 0)
:param angle: (Default value = 0)
"""
2013-07-20 17:52:28 +02:00
nema = {
# motor,flange, holes, screws
2016-08-17 15:07:41 +02:00
8: (20.3, 16, 15.4, 3),
11: (28.2, 22, 23, 4),
14: (35.2, 22, 26, 4),
16: (39.2, 22, 31, 4),
17: (42.2, 22, 31, 4),
23: (56.4, 38.1, 47.1, 5.2),
24: (60, 36, 49.8, 5.1),
34: (86.3, 73, 69.8, 6.6),
42: (110, 55.5, 89, 8.5),
}
2013-07-20 17:52:28 +02:00
width, flange, holedistance, diameter = nema[size]
self.moveTo(x, y, angle)
if self.debug:
self.rectangularHole(0, 0, width, width)
2016-08-17 15:07:41 +02:00
self.hole(0, 0, 0.5 * flange)
2013-07-20 17:52:28 +02:00
for x in (-1, 1):
for y in (-1, 1):
2016-08-17 15:07:41 +02:00
self.hole(x * 0.5 * holedistance,
y * 0.5 * holedistance,
0.5 * diameter)
2013-07-20 17:52:28 +02:00
# hexHoles
def hexHolesRectangle(self, x, y, settings=None, skip=None):
2016-03-28 16:55:41 +02:00
"""Fills a rectangle with holes in a hex pattern.
Settings have:
r : radius of holes
b : space between holes
style : what types of holes (not yet implemented)
2016-03-28 16:55:41 +02:00
:param x: width
2016-04-09 20:54:05 +02:00
:param y: height
2016-03-28 16:55:41 +02:00
:param settings: (Default value = None)
:param skip: (Default value = None) function to check if hole should be present
gets x, y, r, b, posx, posy
2016-03-28 16:55:41 +02:00
"""
if settings is None:
settings = self.hexHolesSettings
r, b, style = settings
2016-08-17 15:07:41 +02:00
w = r + b / 2.0
dist = w * math.cos(math.pi / 6.0)
# how many half circles do fit
2016-08-17 15:07:41 +02:00
cx = int((x - 2 * r) // (w)) + 2
cy = int((y - 2 * r) // (dist)) + 2
# what's left on the sides
2016-08-17 15:07:41 +02:00
lx = (x - (2 * r + (cx - 2) * w)) / 2.0
ly = (y - (2 * r + ((cy // 2) * 2) * dist - 2 * dist)) / 2.0
2016-08-17 15:07:41 +02:00
for i in range(cy // 2):
for j in range((cx - (i % 2)) // 2):
px = 2 * j * w + r + lx
py = i * 2 * dist + r + ly
if i % 2:
2013-04-17 13:23:23 +02:00
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):
2016-08-17 15:07:41 +02:00
cx, cy = x / 2.0, y / 2.0
return (dist(posx - cx, posy - cy) > (cx - r))
def hexHolesCircle(self, d, settings=None):
2016-03-28 16:55:41 +02:00
"""
Fill circle with holes in a hex pattern
:param d: diameter of the circle
:param settings: (Default value = None)
"""
2016-08-17 15:07:41 +02:00
d2 = d / 2.0
self.hexHolesRectangle(d, d, settings=settings, skip=self.__skipcircle)
def hexHolesPlate(self, x, y, rc, settings=None):
2016-03-28 16:55:41 +02:00
"""
Fill a plate with holes in a hex pattern
:param x: width
:param y: height
:param rc: radius of the corners
:param settings: (Default value = None)
"""
2016-08-17 15:07:41 +02:00
2013-04-17 13:25:30 +02:00
def skip(x, y, r, b, posx, posy):
2016-03-28 16:55:41 +02:00
"""
:param x:
:param y:
:param r:
:param b:
:param posx:
:param posy:
"""
2016-08-17 15:07:41 +02:00
posx = abs(posx - (x / 2.0))
posy = abs(posy - (y / 2.0))
2013-04-17 13:25:30 +02:00
2016-08-17 15:07:41 +02:00
wx = 0.5 * x - rc - r
wy = 0.5 * y - rc - r
2013-04-17 13:25:30 +02:00
if (posx <= wx) or (posy <= wx):
return 0
2016-08-17 15:07:41 +02:00
return dist(posx - wx, posy - wy) > rc
self.hexHolesRectangle(x, y, settings, skip=skip)
2013-04-17 13:25:30 +02:00
def hexHolesHex(self, h, settings=None, grow=None):
2016-03-28 16:55:41 +02:00
"""
Fill a hexagon with holes in a hex pattern
:param h: height
:param settings: (Default value = None)
:param grow: (Default value = None)
"""
if settings is None:
settings = self.hexHolesSettings
r, b, style = settings
self.ctx.rectangle(0, 0, h, h)
2016-08-17 15:07:41 +02:00
w = r + b / 2.0
dist = w * math.cos(math.pi / 6.0)
cy = 2 * int((h - 4 * dist) // (4 * w)) + 1
2016-08-17 15:07:41 +02:00
leftover = h - 2 * r - (cy - 1) * 2 * r
if grow == 'space ':
b += leftover / (cy - 1) / 2
# recalulate with adjusted values
2016-08-17 15:07:41 +02:00
w = r + b / 2.0
dist = w * math.cos(math.pi / 6.0)
2016-08-17 15:07:41 +02:00
self.moveTo(h / 2.0 - (cy // 2) * 2 * w, h / 2.0)
2014-03-03 21:45:01 +01:00
for j in range(cy):
2016-08-17 15:07:41 +02:00
self.hole(2 * j * w, 0, r)
for i in range(1, cy / 2 + 1):
for j in range(cy - i):
self.hole(j * 2 * w + i * w, i * 2 * dist, r)
self.hole(j * 2 * w + i * w, -i * 2 * dist, r)
2016-07-12 16:12:28 +02:00
def flex2D(self, x, y, width=1):
width *= self.thickness
2016-08-17 15:07:41 +02:00
cx = int(x // (5 * width))
2016-07-12 16:12:28 +02:00
wx = x / 5. / cx
2016-08-17 15:07:41 +02:00
cy = int(y // (5 * width))
2016-07-12 16:12:28 +02:00
wy = y / 5. / cy
2016-08-17 15:07:41 +02:00
armx = (4 * wx, 90, 4 * wy, 90, 2 * wx, 90, 2 * wy)
army = (4 * wy, 90, 4 * wx, 90, 2 * wy, 90, 2 * wx)
2016-07-12 16:12:28 +02:00
for i in range(cx):
for j in range(cy):
2016-08-17 15:07:41 +02:00
if (i + j) % 2:
2016-07-12 16:12:28 +02:00
self.ctx.save()
2016-08-17 15:07:41 +02:00
self.moveTo((5 * i) * wx, (5 * j) * wy)
2016-07-12 16:12:28 +02:00
self.polyline(*armx)
self.ctx.restore()
self.ctx.save()
2016-08-17 15:07:41 +02:00
self.moveTo((5 * i + 5) * wx, (5 * j + 5) * wy, -180)
2016-07-12 16:12:28 +02:00
self.polyline(*armx)
self.ctx.restore()
else:
self.ctx.save()
2016-08-17 15:07:41 +02:00
self.moveTo((5 * i + 5) * wx, (5 * j) * wy, 90)
2016-07-12 16:12:28 +02:00
self.polyline(*army)
self.ctx.restore()
self.ctx.save()
2016-08-17 15:07:41 +02:00
self.moveTo((5 * i) * wx, (5 * j + 5) * wy, -90)
2016-07-12 16:12:28 +02:00
self.polyline(*army)
self.ctx.restore()
self.ctx.stroke()
##################################################
### parts
##################################################
def roundedPlate(self, x, y, r, callback=None,
holesMargin=None, holesSettings=None,
bedBolts=None, bedBoltSettings=None,
move=None):
2016-03-28 16:55:41 +02:00
"""Plate with rounded corner fitting to .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
2016-03-28 16:55:41 +02:00
:param x: width
:param y: hight
:param r: radius of the corners
:param callback: (Default value = None)
:param holesMargin: (Default value = None) set to get hex holes
:param holesSettings: (Default value = None)
:param bedBolts: (Default value = None)
:param bedBoltSettings: (Default value = None)
:param move: (Default value = None)
"""
2016-08-17 15:07:41 +02:00
overallwidth = x + 2 * self.edges["f"].spacing()
overallheight = y + 2 * self.edges["f"].spacing()
if self.move(overallwidth, overallheight, move, before=True):
return
self.moveTo(self.edges["f"].margin(),
self.edges["f"].margin())
self.moveTo(r, 0)
self.cc(callback, 0)
2016-08-17 15:07:41 +02:00
self.edges["f"](x / 2.0 - r, bedBolts=self.getEntry(bedBolts, 0),
bedBoltSettings=self.getEntry(bedBoltSettings, 0))
self.cc(callback, 1)
2016-08-17 15:07:41 +02:00
self.edges["f"](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)
2016-08-17 15:07:41 +02:00
self.cc(callback, i + 2)
self.edges["f"](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()
self.moveTo(self.edges["f"].margin(),
self.edges["f"].margin())
if holesMargin is not None:
self.moveTo(holesMargin, holesMargin)
if r > holesMargin:
r -= holesMargin
else:
r = 0
2016-08-17 15:07:41 +02:00
self.hexHolesPlate(x - 2 * holesMargin, y - 2 * holesMargin, r,
settings=holesSettings)
2013-12-15 20:41:18 +01:00
self.ctx.stroke()
self.move(overallwidth, overallheight, move)
def surroundingWall(self, x, y, r, h,
bottom='e', top='e',
left="D", right="d",
callback=None,
move=None):
2016-03-28 16:55:41 +02:00
"""h : inner height, not counting the joints
callback is called a beginn of the flat sides with
2016-03-28 16:55:41 +02:00
* 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
:param x: width of matching roundedPlate
:param y: height of matching roundedPlate
:param r: corner radius of matching roundedPlate
:param h: height of the wall
:param bottom: (Default value = 'e') Edge type
:param top: (Default value = 'e') Edge type
:param callback: (Default value = None)
:param move: (Default value = None)
"""
2016-08-17 15:07:41 +02:00
c4 = (r + self.burn) * math.pi * 0.5 # circumference of quarter circle
c4 = c4 / self.edges["X"].settings.stretch
top = self.edges.get(top, top)
bottom = self.edges.get(bottom, bottom)
left = self.edges.get(left, left)
right = self.edges.get(right, right)
# XXX assumes startwidth == endwidth
topwidth = top.startwidth()
bottomwidth = bottom.startwidth()
2016-08-17 15:07:41 +02:00
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.moveTo(left.spacing(), bottom.margin())
2016-08-17 15:07:41 +02:00
self.cc(callback, 0, y=bottomwidth + self.burn)
bottom(x / 2.0 - r)
if (y - 2 * r) < 1E-3:
self.edges["X"](2 * c4, h + topwidth + bottomwidth)
self.cc(callback, 2, y=bottomwidth + self.burn)
bottom(x - 2 * r)
self.edges["X"](2 * c4, h + topwidth + bottomwidth)
self.cc(callback, 4, y=bottomwidth + self.burn)
else:
for i, l in zip(range(4), (y, x, y, 0)):
2016-08-17 15:07:41 +02:00
self.edges["X"](c4, h + topwidth + bottomwidth)
self.cc(callback, i + 1, y=bottomwidth + self.burn)
if i < 3:
2016-08-17 15:07:41 +02:00
bottom(l - 2 * r)
bottom(x / 2.0 - r)
self.edgeCorner(bottom, right, 90)
right(h)
self.edgeCorner(right, top, 90)
2016-08-17 15:07:41 +02:00
top(x / 2.0 - r)
for i, l in zip(range(4), (y, x, y, 0)):
self.edge(c4)
if i < 3:
2016-08-17 15:07:41 +02:00
top(l - 2 * r)
top(x / 2.0 - r)
self.edgeCorner(top, left, 90)
left(h)
self.edgeCorner(left, bottom, 90)
2013-12-15 20:41:18 +01:00
self.ctx.stroke()
self.move(overallwidth, overallheight, move)
def rectangularWall(self, x, y, edges="eeee",
holesMargin=None, holesSettings=None,
bedBolts=None, bedBoltSettings=None,
2013-07-20 17:54:07 +02:00
callback=None,
move=None):
2016-03-28 16:55:41 +02:00
"""
Rectangular wall for all kind of box like objects
:param x: width
:param y: height
:param edges: (Default value = "eeee") bottom, right, top, left
:param holesMargin: (Default value = None)
:param holesSettings: (Default value = None)
:param bedBolts: (Default value = None)
:param bedBoltSettings: (Default value = None)
:param callback: (Default value = None)
:param move: (Default value = None)
"""
if len(edges) != 4:
2014-03-03 21:45:01 +01:00
raise ValueError("four edges required")
2013-11-26 22:53:06 +01:00
edges = [self.edges.get(e, e) for e in edges]
2016-08-17 15:07:41 +02:00
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.moveTo(edges[-1].spacing(), edges[0].margin())
for i, l in enumerate((x, y, x, y)):
2016-08-17 15:07:41 +02:00
self.cc(callback, i, y=edges[i].startwidth() + self.burn)
edges[i](l,
bedBolts=self.getEntry(bedBolts, i),
bedBoltSettings=self.getEntry(bedBoltSettings, i))
2016-08-17 15:07:41 +02:00
self.edgeCorner(edges[i], edges[i + 1], 90)
if holesMargin is not None:
2016-08-17 15:07:41 +02:00
self.moveTo(holesMargin + edges[-1].endwidth(),
holesMargin + edges[0].startwidth())
self.hexHolesRectangle(x - 2 * holesMargin, y - 2 * holesMargin)
2013-12-15 20:41:18 +01:00
self.ctx.stroke()
self.move(overallwidth, overallheight, move)
2016-09-18 17:20:24 +02:00
def rectangularTriangle(self, x, y, edges="eee", num=1,
2016-09-18 17:20:24 +02:00
bedBolts=None, bedBoltSettings=None,
callback=None,
move=None):
"""
Rectangular triangular wall
:param x: width
:param y: height
:param edges: (Default value = "eee") bottom, right[, diagonal]
:param num: (Default value = 1) number of triangles
2016-09-18 17:20:24 +02:00
:param bedBolts: (Default value = None)
:param bedBoltSettings: (Default value = None)
:param callback: (Default value = None)
:param move: (Default value = None)
"""
edges = [self.edges.get(e, e) for e in edges]
if len(edges) == 2:
edges.append(self.edges["e"])
if len(edges) != 3:
raise ValueError("two or three edges required")
width = x + edges[-1].spacing() + edges[1].spacing()
height = y + edges[0].spacing() + edges[2].spacing()
if num > 1:
width += edges[-1].spacing() + edges[1].spacing() + 2*self.spacing
height += edges[0].spacing() + edges[2].spacing() + self.spacing
overallwidth = width * (num // 2 + num % 2)
overallheight = height
2016-09-18 17:20:24 +02:00
alpha = math.degrees(math.atan(y/float(x)))
if self.move(overallwidth, overallheight, move, before=True):
return
if num > 1:
self.moveTo(self.spacing + edges[-1].spacing())
for n in range(num):
self.moveTo(edges[-1].spacing()+self.spacing, edges[0].margin())
if n % 2 == 1:
self.moveTo(2*edges[1].spacing()+self.spacing, 0)
if num > 1:
self.moveTo(edges[1].spacing(), 0)
for i, l in enumerate((x, y)):
self.cc(callback, i, y=edges[i].startwidth() + self.burn)
edges[i](l,
bedBolts=self.getEntry(bedBolts, i),
bedBoltSettings=self.getEntry(bedBoltSettings, i))
self.edgeCorner(edges[i], edges[i + 1], 90)
self.corner(alpha)
self.cc(callback, 2)
edges[2]((x**2+y**2)**0.5)
self.corner(180-alpha)
self.ctx.stroke()
if n % 2:
self.moveTo(-edges[1].spacing()-2*self.spacing-edges[-1].spacing(), height-edges[1].spacing(), 180)
else:
self.moveTo(width+1*edges[1].spacing()-self.spacing-2*edges[-1].spacing(), height-edges[1].spacing(), 180)
2016-09-18 17:20:24 +02:00
self.move(overallwidth, overallheight, move)