Thu, 06 Apr 2017 19:00:07 +0200
ht is not PVC....
4 | 1 | """ |
2 | Calculation of spacer pipes for Scuba cylinder transportation | |
3 | 2017 by NeoSoft, mdd | |
4 | Input: see commandline help | |
5 | Output: 2D schematic & 3D OpenSCAD script | |
6 | """ | |
0 | 7 | from math import sqrt |
8 | from PIL import Image, ImageDraw, ImageFont | |
9 | import argparse, sys | |
2 | 10 | from data import CYLINDER, PIPES |
9 | 11 | from stl import OPENSCAD_MODULES, precompile_all_stl |
12 | from config import FONTBASE, SCALE3D | |
1 | 13 | |
4 | 14 | def offset(r_1, r_2): |
15 | """ | |
16 | Calculate horizontal center offset of two circles | |
17 | so they tangent each other | |
18 | """ | |
19 | return 2 * sqrt(r_1 * r_2) | |
0 | 20 | |
5 | 21 | class CylinderSpacerCalculator(object): |
4 | 22 | """ |
23 | Class to calculate transport spacer pipes between | |
24 | Scuba cylinders | |
25 | """ | |
26 | def __init__(self, cylinders, space_min=10): | |
27 | self.cylinders = cylinders | |
28 | self.space_min = space_min | |
29 | self.font = FONTBASE + "arial.ttf" | |
8 | 30 | self.width = 0 # will be calculated |
31 | self.scad = { | |
32 | "tmpl": """// Color support only in compile mode (F5) | |
33 | // cylindertransport.py OpenSCAD script | |
34 | $fn = 10; | |
9 | 35 | """ + OPENSCAD_MODULES, |
8 | 36 | "spacer": "", |
37 | "cylinder": "" | |
38 | } | |
4 | 39 | self.circles = [] |
40 | self.spacings = [] | |
41 | self.margin = 20 | |
0 | 42 | |
4 | 43 | def calc_min(self, r_1, r_2): |
44 | """ | |
45 | stupider annaehreungsversuch, bis sich die beiden | |
46 | Tauchflaschen r_1 und r_2 nicht mehr beruehren | |
47 | Rueckgabe: 3 Zylinderradien und das label der verwendeten Roehre | |
48 | """ | |
5 | 49 | for pipe in PIPES: |
50 | i = pipe[1] / 2 | |
4 | 51 | x_1 = offset(r_1, i) |
52 | x_2 = offset(r_2, i) | |
5 | 53 | posx = (x_1 + x_2) - (r_1 + r_2) |
54 | if posx >= self.space_min: | |
11 | 55 | #print "// %s Pipe (%.1fmm), Cylinder spacing: %imm" % ( |
56 | # pipe[0], pipe[1], posx) | |
57 | return [r_1, i, r_2, pipe] | |
5 | 58 | print "// Abort: no suitable pipe found" |
4 | 59 | sys.exit(1) |
0 | 60 | |
14 | 61 | def _circle(self, posx, radius, data, cylinder=""): |
4 | 62 | """ |
63 | Push the circle definition for later rendering | |
64 | """ | |
11 | 65 | if cylinder == "": |
66 | size = 0.5 | |
67 | txt = data[0] | |
68 | else: | |
69 | size = 1.0 | |
70 | txt = "Tank " + cylinder | |
4 | 71 | self.circles.append([ |
11 | 72 | posx, radius, txt, size, data, cylinder |
4 | 73 | ]) |
3 | 74 | |
11 | 75 | def calculate(self): |
4 | 76 | """ |
77 | Calculate all cylinder and spacer circles | |
78 | """ | |
79 | # first bottle spacer | |
80 | r_1 = CYLINDER[self.cylinders[0]][0] / 2 | |
81 | r_2 = CYLINDER[self.cylinders[1]][0] / 2 | |
11 | 82 | r_1, r_2, r_3, data = self.calc_min(r_1, r_2) |
5 | 83 | posx = self.margin + r_2 # start offset x |
11 | 84 | self._circle(posx, r_2, data) |
8 | 85 | self.scad["spacer"] += "spacer(%f, %f, %f, %f);\n" % ( |
9 | 86 | posx * SCALE3D, r_2 * SCALE3D, r_3 * SCALE3D, |
87 | CYLINDER[self.cylinders[0]][1] * SCALE3D) | |
5 | 88 | posx += offset(r_2, r_3) |
0 | 89 | |
5 | 90 | for i in range(0, len(self.cylinders) - 1): |
4 | 91 | r_1 = CYLINDER[self.cylinders[i]][0] / 2 |
92 | r_2 = CYLINDER[self.cylinders[i+1]][0] / 2 | |
11 | 93 | r_1, r_2, r_3, data = self.calc_min(r_1, r_2) |
4 | 94 | # draw cylinder |
14 | 95 | self._circle(posx, r_1, \ |
11 | 96 | CYLINDER[self.cylinders[i]], self.cylinders[i]) |
8 | 97 | self.scad["cylinder"] += "tank(%f, %f, %f);\n" % ( |
9 | 98 | posx * SCALE3D, r_1 * SCALE3D, |
99 | CYLINDER[self.cylinders[i]][1] * SCALE3D) | |
5 | 100 | sx1 = posx + r_1 |
101 | posx += offset(r_1, r_2) | |
4 | 102 | # draw right spacer |
11 | 103 | self._circle(posx, r_2, data) |
8 | 104 | self.scad["spacer"] += "spacer(%f, %f, %f, %f);\n" % ( |
9 | 105 | posx * SCALE3D, r_2 * SCALE3D, r_1 * SCALE3D, |
106 | CYLINDER[self.cylinders[i]][1] * SCALE3D) | |
5 | 107 | posx += offset(r_2, r_3) |
108 | sx2 = posx - r_3 | |
4 | 109 | if i == (len(self.cylinders) - 2): |
110 | # draw last bottle | |
14 | 111 | self._circle(posx, r_3, \ |
11 | 112 | CYLINDER[self.cylinders[i + 1]], self.cylinders[i + 1]) |
8 | 113 | self.scad["cylinder"] += "tank(%f, %f, %f);\n" % ( |
9 | 114 | posx * SCALE3D, r_3 * SCALE3D, |
115 | CYLINDER[self.cylinders[i + 1]][1] * SCALE3D) | |
5 | 116 | posx += offset(r_2, r_3) |
3 | 117 | |
4 | 118 | self.spacings.append([sx1, sx2]) |
119 | ||
120 | # last bottle spacer pipe | |
11 | 121 | self._circle(posx, r_2, data) |
8 | 122 | self.scad["spacer"] += "spacer(%f, %f, %f, %f);\n" % ( |
9 | 123 | posx * SCALE3D, r_2 * SCALE3D, r_3 * SCALE3D, |
124 | CYLINDER[self.cylinders[-1]][1] * SCALE3D) | |
5 | 125 | return int(posx + r_2 + self.margin) |
3 | 126 | |
5 | 127 | def centertext(self, draw, posx, posy, txt, size): |
128 | """ | |
129 | Centers text at position horizontally and vertically | |
130 | """ | |
4 | 131 | font = ImageFont.truetype(self.font, int(24 * size)) |
132 | tox, toy = draw.textsize(txt, font=font) | |
5 | 133 | draw.text((posx - tox / 2, posy - toy / 2), \ |
4 | 134 | txt, font=font, fill='#ffffff') |
0 | 135 | |
4 | 136 | def render_image(self): |
137 | """ | |
138 | Start the calculation and return rendered PIL image object | |
139 | """ | |
11 | 140 | self.width = self.calculate() |
8 | 141 | image = Image.new('1', (self.width, 250)) # create new image |
4 | 142 | draw = ImageDraw.Draw(image) |
143 | # draw calculated circles | |
11 | 144 | for posx, radius, txt, size, data, cylinder in self.circles: |
5 | 145 | draw.arc([posx - radius, self.margin, \ |
146 | posx + radius, 2 * radius + self.margin], \ | |
147 | 0, 360, 'white') | |
4 | 148 | if txt != "": |
5 | 149 | self.centertext(draw, posx, radius + self.margin, txt, size) |
0 | 150 | |
4 | 151 | # draw the spacing between cylinders |
152 | spacer_y1 = 200 | |
153 | spacer_y2 = 220 | |
154 | for sx1, sx2 in self.spacings: | |
155 | draw.line((sx1, spacer_y1, sx1, spacer_y2), fill='#ffffff') | |
156 | draw.line((sx2, spacer_y1, sx2, spacer_y2), fill='#ffffff') | |
157 | self.centertext(draw, sx1 + (sx2 - sx1) / 2, \ | |
158 | spacer_y2 + 10, "%imm" % (sx2 - sx1), 0.5) | |
3 | 159 | |
4 | 160 | return image |
0 | 161 | |
5 | 162 | def run(): |
163 | """ | |
164 | Command line program invocation | |
165 | """ | |
4 | 166 | parser = argparse.ArgumentParser(description=\ |
167 | "Calculate spacer pipes for pressure cylinder transport\n" +\ | |
168 | "Known cylinder types:\n" + ", ".join(sorted(CYLINDER.keys()))) | |
169 | parser.add_argument('cylinders', metavar='cylinder', \ | |
170 | type=str, nargs='+', help='cylinder types') | |
171 | parser.add_argument('--space', dest='space_min', \ | |
172 | type=int, default=10, \ | |
3 | 173 | help='minimum space between cylinders (mm)') |
6 | 174 | parser.add_argument('--scad', dest='scad', \ |
175 | type=str, default="", metavar='filename', \ | |
176 | help='Write OpenSCAD script file') | |
9 | 177 | parser.add_argument('--precompile', dest='precompile', \ |
178 | default=False, action='store_true', \ | |
179 | help='Precompile STL objects for 3D View') | |
0 | 180 | |
4 | 181 | options = parser.parse_args() |
0 | 182 | |
9 | 183 | if options.precompile: |
184 | precompile_all_stl() | |
185 | ||
4 | 186 | for test in options.cylinders: |
187 | if not test in CYLINDER.keys(): | |
188 | print "Cylinder type '%s' is unknown" % test | |
189 | sys.exit(1) | |
0 | 190 | |
4 | 191 | worker = CylinderSpacerCalculator( |
192 | options.cylinders, options.space_min) | |
3 | 193 | |
5 | 194 | img = worker.render_image() |
195 | img.show() | |
1 | 196 | |
14 | 197 | if options.scad != "": |
6 | 198 | with open(options.scad, "w") as fd: |
8 | 199 | fd.write(worker.scad["tmpl"]) |
14 | 200 | # center the object |
11 | 201 | fd.write("translate([%f,0,0]) {\n" % ( |
14 | 202 | ((worker.width - 2 * worker.margin) / -2) * SCALE3D)) |
8 | 203 | fd.write(worker.scad["cylinder"]) |
204 | fd.write(worker.scad["spacer"]) | |
205 | fd.write("}\n") | |
5 | 206 | |
207 | if __name__ == "__main__": | |
208 | run() |