desk-andon

Programmable Desktop Tower Light
git clone https://git.sinitax.com/sinitax/desk-andon
Log | Files | Refs | Submodules | sfeed.txt

main.py (17555B)


      1from types import SimpleNamespace
      2from cadquery import Workplane
      3from math import sin, cos, pi
      4
      5if "show_object" not in globals():
      6    show_object = lambda v: ()
      7
      8nothing = 0.00001
      9origin = (0, 0, 0)
     10XY = Workplane("XY")
     11center_xy = (True, True, False)
     12m3hole = 2.8 / 2
     13
     14class V(SimpleNamespace):
     15    def __add__(self, rhs):
     16        assert(set(self.__dict__.keys()) <= set(rhs.__dict__.keys()))
     17        items = [(k, v + rhs.__dict__[k]) for k,v in self.__dict__.items()]
     18        return V(**dict(items))
     19
     20    def __sub__(self, rhs):
     21        assert(set(self.__dict__.keys()) <= set(rhs.__dict__.keys()))
     22        items = [(k, v - rhs.__dict__[k]) for k,v in self.__dict__.items()]
     23        return V(**dict(items))
     24
     25    def __mul__(self, rhs):
     26        assert(isinstance(rhs, int))
     27        items = [(k, v *rhs) for k,v in self.__dict__.items()]
     28        return V(**dict(items))
     29
     30    def xyz(self):
     31        return (self.x, self.y, self.z)
     32
     33def Ext(part, bounds):
     34    setattr(part, "bounds", bounds)
     35    return part
     36
     37def V3(x, y, z):
     38    return V(x=x, y=y, z=z)
     39
     40def V21(v):
     41    return V3(2 * v, 2 * v, v)
     42
     43def box(*args):
     44    return XY.box(*args, centered=center_xy)
     45
     46def cylinder(r, h):
     47    return XY.circle(r).extrude(h)
     48
     49def build_usbc(width, height, depth, tol):
     50    def shape(width, height, depth):
     51        radius = height / 2 - nothing
     52        offset = width / 2 - radius
     53        part = box(width - radius * 2, height, depth)
     54        part = part.union(cylinder(radius, depth).translate((-offset, 0, 0)))
     55        part = part.union(cylinder(radius, depth).translate((offset, 0, 0)))
     56        return part
     57    def gen(tol):
     58        dims = (width + 2 * tol, height + 2 * tol, depth + 2 * tol)
     59        part = shape(*dims).translate((0, 0, -tol))
     60        if tol != 0:
     61            part = part.union(shape(*dims[:2], 20).translate((0, 0, -20)))
     62        return part
     63    part = gen(0).translate((0, -height / 2, 0)).rotate(origin, (1, 0, 0), -90)
     64    bounds = gen(tol).translate((0, -height / 2, 0)).rotate(origin, (1, 0, 0), -90)
     65    return Ext(part, bounds)
     66
     67def build_usbc_board(board, usbc, tol, usbc_overhang, hole_radius, hole_offset,
     68                     bolt_radius, bolt_head_radius, bolt_depth, usbc_height):
     69    def gen(tol):
     70        part = box(*(board + V3(tol, tol, tol) * 2).xyz()).translate((0, 0, -tol))
     71        part_usbc = build_usbc(**usbc.__dict__)
     72        if tol != 0:
     73            part_usbc = part_usbc.bounds
     74        part = part.union(part_usbc.rotate(origin, (0, 0, 1), 180)\
     75                          .translate((0, board.y / 2 + usbc_overhang, usbc_height)))
     76        for x in (-1, 1):
     77            if tol != 0:
     78                part = part.union(cylinder(bolt_head_radius, 10)
     79                        .translate((x * (board.x / 2 - hole_offset), board.y / 2 - hole_offset, board.z + tol)))
     80                part = part.union(cylinder(m3hole, board.z + 2 * tol + bolt_depth)
     81                        .translate((x * (board.x / 2 - hole_offset), board.y / 2 - hole_offset, -tol-bolt_depth)))
     82            else:
     83                part = part.cut(cylinder(hole_radius, board.z)
     84                        .translate((x * (board.x / 2 - hole_offset), board.y / 2 - hole_offset, 0)))
     85        return part.translate((0, -board.y/2, 0))
     86    return Ext(gen(0), gen(tol))
     87
     88def build_bolt(bolt, shaft, tol):
     89    height = shaft + bolt.head_height
     90
     91    def gen(tol):
     92        part = cylinder(bolt.head_radius + tol, bolt.head_height + 2 * tol)\
     93            .translate((0, 0, height + 2 * tol - bolt.head_height - 2 * tol))
     94        if tol != 0:
     95            part = part.union(cylinder(bolt.cut_radius + tol, height + 2 * tol))
     96            part = part.translate((0, 0, -tol))
     97        else:
     98            part = part.union(cylinder(bolt.radius, height))
     99        return part
    100
    101    return Ext(gen(0), gen(tol))
    102
    103def build_rod(rod, tol):
    104    part = XY.circle(rod.outer).circle(rod.inner).extrude(rod.len)
    105
    106    bounds = cylinder(rod.outer + tol, rod.len + 2 * tol).translate((0, 0, -tol))
    107
    108    return Ext(part, bounds)
    109
    110def build_oled(outer, screen, tol, hole, pins, pinlen, ribbon):
    111    def gen(tol):
    112        slab = outer.z - screen.z
    113        lower = V3(outer.x + 2 * tol, outer.y + 2 * tol, slab + 2 * tol)
    114        upper = screen + V3(tol, tol, (10 if tol != 0 else 0)) * 2
    115        part2 = box(*upper.xyz()).translate((0, 0, lower.z))
    116        part = box(*lower.xyz()).union(part2).translate((0, 0, -tol))
    117        for y in range(-1, 2, 2):
    118            for x in range(-1, 2, 2):
    119                part = part.cut(cylinder(hole - tol, outer.z - tol)
    120                    .translate((x * 21 / 2, y * 22 / 2, 0)))
    121        part = part.union(box(*pins.xyz()).translate((0, outer.y / 2 - pins.y / 2, slab)))
    122        part = part.union(box(pins.x, pins.y, pinlen)\
    123                    .translate((0, outer.y / 2 - pins.y / 2, slab - pinlen)))
    124        if tol != 0:
    125            part = part.union(box(*(ribbon + V21(tol)).xyz()).translate((0, -outer.y / 2 + ribbon.y / 2, slab)))
    126        return part
    127    return Ext(gen(0), gen(tol))
    128
    129def build_antenna(radius, height, top_offset, top_space, top_radius, tol, hex_height, hex_radius):
    130    def gen(tol):
    131        part = cylinder(radius + tol, height + 2 * tol).translate((0, 0, -tol))
    132        if tol != 0:
    133            part = part.union(cylinder(top_radius + 2 * tol, top_space + tol).translate((0, 0, top_offset - tol)))
    134            part = part.union(cylinder(top_radius + tol + 4, height + tol - top_offset - top_space) \
    135                              .translate((0, 0, top_offset + top_space)))
    136            part = part.union(XY.polygon(6, hex_radius + tol * 2) \
    137                              .extrude(10 + hex_height + tol).translate((0, 0, -10)))
    138        else:
    139            part = part.union(cylinder(top_radius, height - top_offset).translate((0, 0, top_offset)))
    140            part = part.union(XY.polygon(6, hex_radius).extrude(hex_height))
    141        return part
    142    return Ext(gen(0), gen(tol))
    143
    144def build_base(base, rod, rod_shaft, usbc_board, bolt, wall, tol, lid, board_space, debug=False):
    145    outer = base
    146    inner = outer - V3(2 * wall.x, 2 * wall.y, wall.z) + V21(tol) 
    147
    148    tower_radius = bolt.radius + wall.x + 1
    149    tower_height = inner.z
    150
    151    floor = wall.z - tol
    152    part = box(*base.xyz())
    153    part = part.cut(box(*inner.xyz()).translate((0, 0, floor)))
    154
    155    antenna_wall_radius = lid.antenna.radius + 4
    156    antenna_rod = cylinder(antenna_wall_radius+wall.x, tower_height) \
    157        .cut(cylinder(antenna_wall_radius, tower_height))
    158    part = part.union(antenna_rod.translate((lid.antenna_offset + V3(0, -outer.y / 2, floor)).xyz()))
    159    part = part.union(antenna_rod.translate((lid.antenna_offset + V3(0, -outer.y / 2, floor) - V3(2 * lid.antenna_offset.x, 0, 0)).xyz()))
    160    part = part.union(box(abs(lid.antenna_offset.x)*2-2*nothing, (antenna_wall_radius+wall.x)*2, tower_height)\
    161                      .translate((0, lid.antenna_offset.y - outer.y / 2, floor)))
    162
    163    board_tol = 2 * tol
    164    main_wall_dist = board_space.x + 2 * board_tol + wall.x
    165    part = part.union(box(wall.x, inner.y, tower_height).translate((-main_wall_dist/2, 0, floor)))
    166    part = part.union(box(wall.x, inner.y, tower_height).translate((main_wall_dist/2, 0, floor)))
    167
    168    part = part.cut(box(main_wall_dist - wall.x, inner.y, tower_height).translate((0, 0, floor)))
    169    part = part.cut(box(abs(lid.antenna_offset.x)*2, antenna_wall_radius*2, tower_height)\
    170                      .translate((0, lid.antenna_offset.y - outer.y / 2, floor)))
    171
    172    for y in range(-1, 2, 2):
    173        for x in range(-1, 2, 2):
    174            pos = V3(x * lid.hole_spacing.x / 2, y * lid.hole_spacing.y / 2, floor)
    175            offset_x = (inner.x - lid.hole_spacing.x) / 2
    176            offset_y = (inner.y - lid.hole_spacing.y) / 2
    177            part = part.union(box(offset_x, offset_y + tower_radius, tower_height) \
    178                              .translate((pos + V3(offset_x*x/2, y*(offset_y-(offset_y+tower_radius)/2), 0)).xyz()))
    179            part = part.union(box(offset_x + tower_radius, offset_y, tower_height) \
    180                                  .translate((pos + V3(x*(offset_x-(offset_x+tower_radius)/2), offset_y*y/2, 0)).xyz()))
    181            part = part.union(cylinder(tower_radius, tower_height).translate(pos.xyz()))
    182
    183    # rod cut-out
    184    rod_pos = rod_shaft.pos + V3(0, 0, floor)
    185    base_rod_outer = rod.outer + 6
    186    part = part.union(cylinder(base_rod_outer, tower_height).translate(rod_pos.xyz()))
    187
    188    fill_y = inner.y / 2 - rod_shaft.pos.y
    189    part = part.union(box(main_wall_dist, 2 * base_rod_outer - 4, tower_height).translate(rod_pos.xyz()).translate((0, 1, 0)))
    190    part_rod = cylinder(rod.outer + tol, tower_height).translate(rod_pos.xyz())
    191    part = part.cut(part_rod)
    192    part = part.union(box(main_wall_dist, fill_y, 3).translate(rod_pos.xyz())\
    193                      .translate((0, -rod_shaft.pos.y+inner.y/2-fill_y/2, 0)) \
    194                      .intersect(part_rod))
    195    #part = part.cut(cylinder(rod_hole.radius, 50).rotate(origin, (1, 0, 0), 90)
    196    #                .translate((rod_pos + V3(0, 0, rod_hole.height)).xyz()))
    197    part = part.cut(box(20, 25, 8).translate(rod_pos.xyz()).translate((0, -rod.outer - wall.x/2)))
    198    part = part.cut(box(8, 40, 5).translate(rod_pos.xyz()).translate((0, 10)))
    199
    200    part = part.cut(build_lid(**lid.__dict__).bounds.translate((0, 0, base.z - lid.base.z)))
    201
    202    board_hole = V(outer = 3, inner = m3hole, height = 4)
    203    print("Board Space", board_space)
    204    board_space_inner = board_space - V3(2 * board_hole.outer, 2 * board_hole.outer, 0)
    205    board_center = V3(0, -inner.y / 2 + board_space.y / 2 + board_tol, floor)
    206    for y in (-1, 1):
    207        for x in (-1, 1):
    208            pos = board_center + V3(x * board_space_inner.x / 2, y * board_space_inner.y / 2, 0)
    209            part = part.union(cylinder(board_hole.outer, board_hole.height).translate(pos.xyz()))
    210            dist = board_hole.outer + board_tol
    211            if y == 1:
    212                part = part.union(box(dist, board_hole.outer * 2, board_hole.height-nothing)
    213                            .translate((pos.x + x * dist / 2, pos.y, pos.z)))
    214            else:
    215                dist2 = dist + board_hole.outer
    216                part = part.union(box(dist, dist2, board_hole.height)
    217                            .translate((pos.x + x * dist / 2,
    218                                        pos.y - dist + dist2 / 2, pos.z)))
    219                part = part.union(box(dist2, dist, board_hole.height)
    220                            .translate((pos.x + x * (dist - dist2 / 2),
    221                                        pos.y - dist / 2, pos.z)))
    222            part = part.cut(cylinder(board_hole.inner, board_hole.height).translate(pos.xyz()))
    223
    224    part_usbc = build_usbc_board(**usbc_board.__dict__)
    225    usbc_board_pos = V3(0, outer.y / 2 - usbc_board.usbc_overhang, floor + 4)
    226    part = part.union(box(usbc_board.board.x, usbc_board.board.y, 4)\
    227                      .translate((usbc_board_pos - V3(0, usbc_board.board.y / 2, 4)).xyz()))
    228    part = part.cut(part_usbc.bounds.translate(usbc_board_pos.xyz()))
    229    if debug:
    230        part = part.union(part_usbc.translate(usbc_board_pos.xyz()))
    231
    232    return part
    233
    234def build_lid(base, rod, rod_shaft, wall, bolt, bolt_depth, tol, hole_spacing,
    235              oled, oled_offset, antenna, antenna_offset, rod_support, debug=False):
    236    outer = base
    237    inner = base - V21(wall + tol * 3)
    238    rod_wall = V(outer = rod.outer + tol * 2 + wall, inner = rod.outer + tol * 2, z = rod_shaft.height)
    239
    240    part_bolt = build_bolt(bolt, bolt_depth, tol)
    241
    242    def gen(tol):
    243        outer_padded = outer + V3(tol, tol, tol) * 4
    244        inner_padded = inner - V3(tol, tol, tol) * 4
    245        part = box(*outer_padded.xyz()).translate((0, 0, -tol*2))
    246        part = part.cut(box(*inner_padded.xyz()).translate((0, 0, -tol*2)))
    247        for y in range(-1, 2, 2):
    248            for x in range(-1, 2, 2):
    249                pos = V3(x * hole_spacing.x / 2, y * hole_spacing.y / 2, outer.z)
    250                head_radius = bolt.head_radius + wall + tol * 2
    251                head_height = bolt.head_height + wall + tol * 2
    252                offset_x = (outer.x - 2 * wall - hole_spacing.x) / 2 + tol * 2
    253                offset_y = (outer.y - 2 * wall - hole_spacing.y) / 2 + tol * 2
    254                part = part.union(cylinder(head_radius, head_height) \
    255                                  .translate((pos + V3(0, 0, -head_height)).xyz()))
    256                part = part.union(box(offset_x, offset_y + head_radius, head_height) \
    257                                  .translate((pos + V3(offset_x*x/2, y*(offset_y-(offset_y+head_radius)/2), -head_height)).xyz()))
    258                part = part.union(box(offset_x + head_radius, offset_y, head_height) \
    259                                  .translate((pos + V3(x*(offset_x-(offset_x+head_radius)/2), offset_y*y/2, -head_height)).xyz()))
    260                bolt_pos = (pos + V3(0, 0, -bolt_depth - bolt.head_height)).xyz()
    261                if tol != 0:
    262                    part = part.union(part_bolt.bounds.translate(bolt_pos))
    263                else:
    264                    part = part.cut(part_bolt.bounds.translate(bolt_pos))
    265                    part = part.cut(cylinder(bolt.radius + tol, bolt_depth + bolt.head_height).translate(bolt_pos))
    266        for r in range(0, 360, 90):
    267            part = part.cut(box(outer.x, 8, 2).rotate(origin, (1, 0, 0), -45)
    268                            .translate((0, outer.x / 2, outer.z - 2)).rotate(origin, (0, 0, 1), r))
    269
    270        oled_pos = (oled_offset + V3(0, -outer_padded.y / 2, outer_padded.z - oled.outer.z)).xyz()
    271        part_oled = build_oled(**oled.__dict__)
    272        part = part.union(box(*oled.outer.xyz()).translate(oled_pos))
    273        part = part.cut(part_oled.bounds.translate(oled_pos))
    274        if debug:
    275            part = part.union(part_oled.translate(oled_pos))
    276
    277        antenna_pos = (antenna_offset + V3(0, -outer_padded.y / 2, outer_padded.z - antenna.top_offset - antenna.top_space)).xyz()
    278        part_antenna = build_antenna(**antenna.__dict__)
    279        part = part.union(cylinder(antenna.top_radius + 2, antenna.height).translate(antenna_pos))
    280        part = part.cut(part_antenna.bounds.translate(antenna_pos))
    281        if debug:
    282            part = part.union(part_antenna.translate(antenna_pos))
    283
    284        part = part.union(cylinder(rod.outer + tol * 2 + rod_support.thickness, rod_support.height + 2 * tol)
    285                        .translate((rod_shaft.pos + V3(0, 0, inner.z - rod_support.height - 2 * tol)).xyz()))
    286
    287        return part
    288
    289    part = gen(0)
    290    part_rod_wall = cylinder(rod_wall.outer + 5 + tol * 2, 10)
    291    part = part.cut(part_rod_wall.translate((rod_shaft.pos + V3(0, 0, outer.z - 1)).xyz()))
    292    part = part.cut(build_rod(rod, tol).bounds.translate((rod_shaft.pos + V3(0, 0, inner.z - rod_support.height - 2 * tol)).xyz()))
    293
    294    part_bounds = gen(tol)
    295
    296    return Ext(part, part_bounds)
    297
    298def build_sleeve(rod, rod_shaft, tol, wall):
    299    rod_wall = V(outer = rod.outer + tol + wall, inner = rod.outer + tol, z = rod_shaft.height)
    300    part = cylinder(rod_wall.outer + 5, 2)
    301    part = part.union(cylinder(rod_wall.outer, rod_wall.z))
    302    part = part.cut(build_rod(rod, tol).bounds)
    303    return part
    304
    305def main():
    306    # TEST PRINT
    307    base = V3(80, 80, 15)
    308    rod_shaft = V(pos = V3(0, 0, 0), height = 20)
    309
    310    base = V3(120, 120, 30)
    311    rod_shaft = V(pos = V3(0, 23, 0), height = 35)
    312
    313    tol = 0.05
    314    rod = V(outer = 25.2 / 2, inner = 21 / 2, len = 500)
    315    thin_wall = 2.4
    316    thick_wall = 4
    317    bolt = V(radius = 3 / 2, head_radius = 5.4 / 2, head_height = 3, cut_radius = m3hole)
    318
    319    oled = V(outer = V3(25.5, 26, 4), screen = V3(24.9, 16.6, 1.6),
    320             tol = tol * 2, hole = 1, pins = V3(4 * 2.54, 2.54, 1),
    321             pinlen = 8, ribbon = V3(9, 5, 1.1))
    322
    323    antenna = V(radius = 6.2/2, height = 13, top_offset = 7.8,
    324            top_radius = 7.6/2, top_space = 2.8, tol = tol * 2,
    325            hex_height = 2, hex_radius = 9)
    326
    327    lid = V3(base.x, base.y, 10)
    328    lid_hole_spacing = V3(lid.x - 15, lid.y - 15, 0)
    329    insert = V(radius=5 / 2, height=5)
    330    base_wall = V3(thick_wall, thick_wall, thin_wall)
    331
    332    bolt_depth = base.z - bolt.head_height # let bolt go through
    333
    334    usbc = V(width = 9, height = 3, depth = 7.3, tol = tol * 2)
    335    usbc_board = V(usbc = usbc, board = V3(22, 13, 2), usbc_overhang = 1,
    336                   hole_radius = 3.2/2, hole_offset = 2.8, tol = tol,
    337                   bolt_radius = 3/2, bolt_head_radius = 3, bolt_depth = 4, usbc_height = 1.70)
    338
    339    board_space = V3(36, 48, 10)
    340    rod_support = V(thickness = thin_wall, height = 9)
    341    #rod_hole = V(height = 12, radius = m3hole)
    342
    343    #show_object(build_rod(rod).translate((0, 0, 10)))
    344
    345    sleeve_ctx = V(rod=rod, rod_shaft=rod_shaft, tol=tol, wall=thin_wall)
    346    #show_object(build_sleeve(**sheath_ctx.__dict__).translate((-100, 0, 0)))
    347
    348    lid_ctx = V(base=lid, rod=rod, rod_shaft=rod_shaft,
    349                wall=thin_wall, bolt=bolt, bolt_depth=bolt_depth, tol=tol,
    350                hole_spacing=lid_hole_spacing, oled=oled, oled_offset=V3(0, 20, 0),
    351                antenna=antenna, antenna_offset=V3(-30, 20, 0), rod_support=rod_support)
    352    show_object(build_lid(**lid_ctx.__dict__).translate((0, 0, 50)))
    353
    354    base_ctx = V(base=base, rod=rod, rod_shaft=rod_shaft, bolt=bolt,
    355                 wall=base_wall, tol=tol, usbc_board=usbc_board,
    356                 board_space=board_space)
    357    #show_object(build_base(**base_ctx.__dict__, lid=lid_ctx))
    358
    359    #show_object(build_oled(**oled.__dict__).bounds)
    360
    361    #show_object(build_antenna(**antenna.__dict__).bounds)
    362
    363    #show_object(build_usbc_board(**usbc_board.__dict__).bounds)
    364
    365main()