cylindertransport.py

Wed, 05 Apr 2017 00:59:45 +0200

author
mdd
date
Wed, 05 Apr 2017 00:59:45 +0200
changeset 14
ba3d8c56e6f5
parent 11
098335a1d510
permissions
-rw-r--r--

code cleanup

"""
Calculation of spacer pipes for Scuba cylinder transportation
2017 by NeoSoft, mdd
Input: see commandline help
Output: 2D schematic & 3D OpenSCAD script
"""
from math import sqrt
from PIL import Image, ImageDraw, ImageFont
import argparse, sys
from data import CYLINDER, PIPES
from stl import OPENSCAD_MODULES, precompile_all_stl
from config import FONTBASE, SCALE3D

def offset(r_1, r_2):
    """
    Calculate horizontal center offset of two circles
    so they tangent each other
    """
    return 2 * sqrt(r_1 * r_2)

class CylinderSpacerCalculator(object):
    """
    Class to calculate transport spacer pipes between
    Scuba cylinders
    """
    def __init__(self, cylinders, space_min=10):
        self.cylinders = cylinders
        self.space_min = space_min
        self.font = FONTBASE + "arial.ttf"
        self.width = 0 # will be calculated
        self.scad = {
            "tmpl": """// Color support only in compile mode (F5)
// cylindertransport.py OpenSCAD script
$fn = 10;
""" + OPENSCAD_MODULES,
            "spacer": "",
            "cylinder": ""
            }
        self.circles = []
        self.spacings = []
        self.margin = 20

    def calc_min(self, r_1, r_2):
        """
        stupider annaehreungsversuch, bis sich die beiden
        Tauchflaschen r_1 und r_2 nicht mehr beruehren
        Rueckgabe: 3 Zylinderradien und das label der verwendeten Roehre
        """
        for pipe in PIPES:
            i = pipe[1] / 2
            x_1 = offset(r_1, i)
            x_2 = offset(r_2, i)
            posx = (x_1 + x_2) - (r_1 + r_2)
            if posx >= self.space_min:
                #print "// %s Pipe (%.1fmm), Cylinder spacing: %imm" % (
                #    pipe[0], pipe[1], posx)
                return [r_1, i, r_2, pipe]
        print "// Abort: no suitable pipe found"
        sys.exit(1)

    def _circle(self, posx, radius, data, cylinder=""):
        """
        Push the circle definition for later rendering
        """
        if cylinder == "":
            size = 0.5
            txt = data[0]
        else:
            size = 1.0
            txt = "Tank " + cylinder
        self.circles.append([
            posx, radius, txt, size, data, cylinder
            ])

    def calculate(self):
        """
        Calculate all cylinder and spacer circles
        """
        # first bottle spacer
        r_1 = CYLINDER[self.cylinders[0]][0] / 2
        r_2 = CYLINDER[self.cylinders[1]][0] / 2
        r_1, r_2, r_3, data = self.calc_min(r_1, r_2)
        posx = self.margin + r_2 # start offset x
        self._circle(posx, r_2, data)
        self.scad["spacer"] += "spacer(%f, %f, %f, %f);\n" % (
            posx * SCALE3D, r_2 * SCALE3D, r_3 * SCALE3D,
            CYLINDER[self.cylinders[0]][1] * SCALE3D)
        posx += offset(r_2, r_3)

        for i in range(0, len(self.cylinders) - 1):
            r_1 = CYLINDER[self.cylinders[i]][0] / 2
            r_2 = CYLINDER[self.cylinders[i+1]][0] / 2
            r_1, r_2, r_3, data = self.calc_min(r_1, r_2)
            # draw cylinder
            self._circle(posx, r_1, \
                CYLINDER[self.cylinders[i]], self.cylinders[i])
            self.scad["cylinder"] += "tank(%f, %f, %f);\n" % (
                posx * SCALE3D, r_1 * SCALE3D,
                CYLINDER[self.cylinders[i]][1] * SCALE3D)
            sx1 = posx + r_1
            posx += offset(r_1, r_2)
            # draw right spacer
            self._circle(posx, r_2, data)
            self.scad["spacer"] += "spacer(%f, %f, %f, %f);\n" % (
                posx * SCALE3D, r_2 * SCALE3D, r_1 * SCALE3D,
                CYLINDER[self.cylinders[i]][1] * SCALE3D)
            posx += offset(r_2, r_3)
            sx2 = posx - r_3
            if i == (len(self.cylinders) - 2):
                # draw last bottle
                self._circle(posx, r_3, \
                    CYLINDER[self.cylinders[i + 1]], self.cylinders[i + 1])
                self.scad["cylinder"] += "tank(%f, %f, %f);\n" % (
                    posx * SCALE3D, r_3 * SCALE3D,
                    CYLINDER[self.cylinders[i + 1]][1] * SCALE3D)
                posx += offset(r_2, r_3)

            self.spacings.append([sx1, sx2])

        # last bottle spacer pipe
        self._circle(posx, r_2, data)
        self.scad["spacer"] += "spacer(%f, %f, %f, %f);\n" % (
            posx * SCALE3D, r_2 * SCALE3D, r_3 * SCALE3D,
            CYLINDER[self.cylinders[-1]][1] * SCALE3D)
        return int(posx + r_2 + self.margin)

    def centertext(self, draw, posx, posy, txt, size):
        """
        Centers text at position horizontally and vertically
        """
        font = ImageFont.truetype(self.font, int(24 * size))
        tox, toy = draw.textsize(txt, font=font)
        draw.text((posx - tox / 2, posy - toy / 2), \
            txt, font=font, fill='#ffffff')

    def render_image(self):
        """
        Start the calculation and return rendered PIL image object
        """
        self.width = self.calculate()
        image = Image.new('1', (self.width, 250)) # create new image
        draw = ImageDraw.Draw(image)
        # draw calculated circles
        for posx, radius, txt, size, data, cylinder in self.circles:
            draw.arc([posx - radius, self.margin, \
                posx + radius, 2 * radius + self.margin], \
                0, 360, 'white')
            if txt != "":
                self.centertext(draw, posx, radius + self.margin, txt, size)

        # draw the spacing between cylinders
        spacer_y1 = 200
        spacer_y2 = 220
        for sx1, sx2 in self.spacings:
            draw.line((sx1, spacer_y1, sx1, spacer_y2), fill='#ffffff')
            draw.line((sx2, spacer_y1, sx2, spacer_y2), fill='#ffffff')
            self.centertext(draw, sx1 + (sx2 - sx1) / 2, \
                spacer_y2 + 10, "%imm" % (sx2 - sx1), 0.5)

        return image

def run():
    """
    Command line program invocation
    """
    parser = argparse.ArgumentParser(description=\
        "Calculate spacer pipes for pressure cylinder transport\n" +\
        "Known cylinder types:\n" + ", ".join(sorted(CYLINDER.keys())))
    parser.add_argument('cylinders', metavar='cylinder', \
        type=str, nargs='+', help='cylinder types')
    parser.add_argument('--space', dest='space_min', \
        type=int, default=10, \
        help='minimum space between cylinders (mm)')
    parser.add_argument('--scad', dest='scad', \
        type=str, default="", metavar='filename', \
        help='Write OpenSCAD script file')
    parser.add_argument('--precompile', dest='precompile', \
        default=False, action='store_true', \
        help='Precompile STL objects for 3D View')

    options = parser.parse_args()

    if options.precompile:
        precompile_all_stl()

    for test in options.cylinders:
        if not test in CYLINDER.keys():
            print "Cylinder type '%s' is unknown" % test
            sys.exit(1)

    worker = CylinderSpacerCalculator(
        options.cylinders, options.space_min)

    img = worker.render_image()
    img.show()

    if options.scad != "":
        with open(options.scad, "w") as fd:
            fd.write(worker.scad["tmpl"])
            # center the object
            fd.write("translate([%f,0,0]) {\n" % (
                ((worker.width - 2 * worker.margin) / -2) * SCALE3D))
            fd.write(worker.scad["cylinder"])
            fd.write(worker.scad["spacer"])
            fd.write("}\n")

if __name__ == "__main__":
    run()

mercurial