gen_tophat.py (12741B)
1 #!/usr/bin/env python3 2 r""" 3 gen_tophat.py — drop-on TOPHAT accessory for the badge case (novelty, sibling of gen_cat_ears). 4 5 A hollow stovepipe tophat that sits astride the centred lanyard tab. The tab pokes up 6 into the crown; the lanyard clip lives INSIDE the hollow and the cord threads out a slot 7 in the crown top — so the badge looks like it's wearing a hat that's being lifted off by 8 the lanyard. Loads from ABOVE (straight down over the tab), friction-held. 9 10 FRONT (looking at the badge face-on) SECTION (Z = case depth runs L→R) 11 12 ╔═══════════════╗ crown, hollow ╔═══════════════╗ 13 ║ ┌───┐ slot ║ ║ ║ <- flat top 14 ║ │ │ ║ crown top with ║ ┌───────┐ ║ w/ cord slot 15 ║ │tab│ clip ║ the cord slot ║ │ hollow │ ║ 16 ═════╬═══╧═══╧════════╬═ hatband (2-col) ║ │ ┌──┐ │ ║ <- fins grip the 17 ▓▓▓ ║ ║ ═════╬═══╪══▓▓══╪═══╬═ tab's 9mm faces 18 ══╪═══╩═══════════════╩══╪══ brim on the ▓▓▓║ │tab│ │ ║▓▓▓ 19 │ badge │ top edge ══╧══╧══╧══╧══╧══╧══ brim ↔ top edge 20 │ │ 0 22 (Z, tray-wall-top depth) 21 22 RETENTION: two fins inside the crown hug the tab's two 9mm-apart faces at a slide-fit 23 (TOL/side, a PETG push fit like the cat-ears frame). They're tied into the crown wall by 24 low webs so they aren't loose islands, and they grip over the tab's ~6mm straight run 25 (below the tab's rounded top). The brim bottoms out on the badge top edge — that flat 26 seat plus the tab's 22mm depth is what stops the hat pitching fore/aft. Anti-rotation is 27 free: the tab is a rectangular key. 28 29 LANYARD ROUTING (v0.1): the cord loops OVER the tab's rounded top inside the hollow and 30 exits the crown-top slot; the clip stays captured below the slot. This bypasses the tab's 31 side-to-side bore (which the fins cover), so the hang point rises a few mm above the 32 bore's CoG-tuned line — fine for a novelty, may add a touch of pitch. A future variant can 33 split the fins with a bore relief to keep the tuned interface. 34 35 Badge dimensions are IMPORTED from gen_stl (plate_outline / cavity_depth / P / ear_*), so 36 the hat can never drift out of sync with the tab it wraps. 37 38 BUILD: the round bodies of revolution (brim, crown tube, roof, hatband) are built in a 39 local Z-up frame with annulus_slab and rotated onto the badge (local +Z → world +Y, a 40 det-+1 rotation so the watertight normals survive). The rectangular fins/webs are plain 41 world-space boxes. Everything composes by OVERLAP (same trick as gen_stl's bosses / the 42 lanyard tab): each piece is its own closed solid, the slicer unions them, so the reported 43 mesh has expected non-manifold seams at the overlaps — that's not a defect here. 44 45 Print BRIM-DOWN, crown up: the opening is on the bed (no bridge there), fins and brim grow 46 straight up. The only overhang is the flat crown top — a ~29mm bridge broken by the slot; 47 add a few support lines or accept minor interior sag (the top SURFACE prints clean). 48 49 Out: ./stl/accessories/tophat_<ver>.stl 50 """ 51 52 import math 53 import os 54 import sys 55 56 sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) 57 58 from gen_stl import ( # noqa: E402 59 P, plate_outline, cavity_depth, 60 ensure_ccw, rounded_rect, circle_loop, perimeter_sample, annulus_slab, box, 61 write_stl, bbox, edge_manifold_issues, mesh_volume_cc, signed_area, 62 _DENSITY_G_CC, git_rev, 63 ) 64 65 # ---------------------------------------------------------------------------- 66 # PARAMS — the hat's own numbers. Everything about the BADGE/tab is imported. 67 # ---------------------------------------------------------------------------- 68 VERSION = "v0.1" 69 LAYOUT = "B" # the production layout 70 71 # --- the hat silhouette -------------------------------------------------------- 72 CROWN_OD = 34.0 # crown outer diameter 73 WALL = 2.6 # crown/brim/roof wall thickness 74 CROWN_H = 36.0 # total hat height above the badge top edge 75 CROWN_TOP_T = 2.6 # thickness of the flat crown top (the roof) 76 BRIM_OD = 48.0 # brim outer diameter 77 BRIM_T = 3.0 # brim thickness 78 79 # --- the cord/clip exit slot in the crown top --------------------------------- 80 CLIP_W = 22.35 # measured lanyard clip-mechanism width (the long side must clear it) 81 SLOT_CLEAR = 1.0 # clearance added around the clip on the long side (X) 82 SLOT_HX = (CLIP_W + SLOT_CLEAR) / 2.0 # long half-width across the badge (X) -> 23.35mm slot 83 SLOT_HY = 3.5 # short half-depth (badge Z) -> 7mm slot (+1mm wiggle over the old 6mm) 84 SLOT_R = 2.5 # slot corner rounding 85 86 # --- the hatband (a proud ring, a 2nd-colour swap target like the ear recess) -- 87 BAND = True 88 BAND_PROUD = 1.2 # how far the band stands off the crown 89 BAND_Z0, BAND_Z1 = BRIM_T - 0.4, BRIM_T + 3.6 # sit the band ON the brim (0.4 overlap) so its 90 # proud underside is backed by the brim top — no floating ledge, so 91 # no support material in a brim↔band gap (printed brim-down) 92 93 # --- the tab socket (retention) ------------------------------------------------ 94 TOL = 0.30 # per-side slide clearance, fin ↔ tab face. Push fit in PETG. 95 FIN_T = 2.2 # fin thickness (outboard of the grip face) 96 FIN_H = 7.0 # fin height above the top edge (grips the tab's ~6mm straight run) 97 FIN_Z_INSET = 2.0 # fin ends held this far off the tab's front/back faces 98 WEB_H = 5.0 # tie-web height (fin outer face -> crown inner wall) 99 WEB_Z0, WEB_Z1 = 6.0, 16.0 # tie-web depth span (kept where a straight box stays inside 100 # the round wall — see the derivation below) 101 WEB_EMBED = 1.6 # how far the web reaches past the crown inner wall (into material) 102 103 SEG = 96 # circle resolution for the revolved bodies 104 105 MATERIAL = P["material"] 106 INFILL = P["infill"] 107 108 # ---------------------------------------------------------------------------- 109 # DERIVED — badge/tab geometry, straight from gen_stl. No magic numbers. 110 # ---------------------------------------------------------------------------- 111 W, H = plate_outline(LAYOUT) # badge face, mm (top edge at y=H) 112 TAB_DEPTH = P["plate_t"] + cavity_depth(LAYOUT) # tab spans z 0..TAB_DEPTH (tray wall top) 113 CX = W / 2.0 # hat axis, x (tab is centred) 114 CZ = TAB_DEPTH / 2.0 # hat axis, z (tab depth centre) 115 116 EAR_W = P["ear_w"] # tab width (x) 117 EAR_H = P["ear_h"] # tab protrusion above the top edge (y) 118 EAR_R = P["ear_corner_r"] # tab top-corner rounding 119 TAB_X0 = (W - EAR_W) / 2.0 # tab faces in x 120 TAB_X1 = TAB_X0 + EAR_W 121 122 CROWN_ID = CROWN_OD - 2 * WALL # crown inner diameter 123 ROOF_OR = CROWN_ID / 2.0 + 0.5 # roof plug outer radius (0.5 into the wall) 124 125 # fin x-bands: the grip faces sit TOL outboard of the tab faces 126 FIN_L_X1 = TAB_X0 - TOL # left fin inner (grip) face 127 FIN_L_X0 = FIN_L_X1 - FIN_T 128 FIN_R_X0 = TAB_X1 + TOL # right fin inner (grip) face 129 FIN_R_X1 = FIN_R_X0 + FIN_T 130 FIN_Z0 = CZ - TAB_DEPTH / 2.0 + FIN_Z_INSET # = FIN_Z_INSET 131 FIN_Z1 = CZ + TAB_DEPTH / 2.0 - FIN_Z_INSET 132 133 OUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "stl", "accessories") 134 135 136 # ---------------------------------------------------------------------------- 137 # local Z-up build -> world (local +Z is the hat axis -> world +Y, up out of the 138 # top edge). Rotation about world X by -90deg (det +1) so annulus_slab's 139 # by-construction normals survive; then translate the axis to (CX, H, CZ). 140 # ---------------------------------------------------------------------------- 141 142 def _to_world(tris): 143 def m(p): 144 lx, ly, lz = p 145 return (lx + CX, lz + H, -ly + CZ) 146 return [(m(a), m(b), m(c)) for a, b, c in tris] 147 148 149 def _slot_loop(k): 150 """The cord-exit slot as a CCW loop of k points (a rounded rect centred on the axis).""" 151 rr = ensure_ccw(rounded_rect(-SLOT_HX, -SLOT_HY, SLOT_HX, SLOT_HY, SLOT_R, 16)) 152 return perimeter_sample(rr, k) 153 154 155 # ---------------------------------------------------------------------------- 156 # build 157 # ---------------------------------------------------------------------------- 158 159 def make_hat(): 160 local = [] # tris built in the local Z-up frame (revolved bodies) 161 162 # --- brim: a flat ring; inner = crown bore so the tab/fins pass through ------- 163 brim_out = circle_loop(0, 0, BRIM_OD / 2.0, SEG) 164 brim_in = circle_loop(0, 0, CROWN_ID / 2.0, SEG) 165 local += annulus_slab(ensure_ccw(brim_out), ensure_ccw(brim_in), 0.0, BRIM_T) 166 167 # --- crown tube: the stovepipe wall (overlaps the brim by 0.4) ---------------- 168 crown_out = circle_loop(0, 0, CROWN_OD / 2.0, SEG) 169 crown_in = circle_loop(0, 0, CROWN_ID / 2.0, SEG) 170 local += annulus_slab(ensure_ccw(crown_out), ensure_ccw(crown_in), BRIM_T - 0.4, CROWN_H) 171 172 # --- roof: the flat top, plugging the bore from inside, with the cord slot ----- 173 roof_out = circle_loop(0, 0, ROOF_OR, SEG) # 0.5 into the wall -> no coincident face 174 roof_in = _slot_loop(SEG) 175 local += annulus_slab(ensure_ccw(roof_out), ensure_ccw(roof_in), 176 CROWN_H - CROWN_TOP_T, CROWN_H) 177 178 # --- hatband: a proud ring (2nd-colour swap) ---------------------------------- 179 if BAND: 180 band_out = circle_loop(0, 0, CROWN_OD / 2.0 + BAND_PROUD, SEG) 181 band_in = circle_loop(0, 0, CROWN_OD / 2.0 - 0.4, SEG) # 0.4 into the wall 182 local += annulus_slab(ensure_ccw(band_out), ensure_ccw(band_in), BAND_Z0, BAND_Z1) 183 184 tris = _to_world(local) 185 186 # --- retention: two grip fins + two tie-webs, in WORLD space ------------------ 187 y0 = H 188 y_fin1 = H + FIN_H 189 y_web1 = H + WEB_H 190 # fins hug the tab's x-faces over its depth 191 tris += box(FIN_L_X0, y0, FIN_Z0, FIN_L_X1, y_fin1, FIN_Z1) 192 tris += box(FIN_R_X0, y0, FIN_Z0, FIN_R_X1, y_fin1, FIN_Z1) 193 # webs tie each fin's outer face into the crown inner wall (embed WEB_EMBED past it) 194 wall_l = CX - CROWN_ID / 2.0 # crown inner wall x at the centre plane 195 wall_r = CX + CROWN_ID / 2.0 196 tris += box(wall_l - WEB_EMBED, y0, WEB_Z0, FIN_L_X1, y_web1, WEB_Z1) 197 tris += box(FIN_R_X0, y0, WEB_Z0, wall_r + WEB_EMBED, y_web1, WEB_Z1) 198 199 return tris 200 201 202 def main(): 203 os.makedirs(OUT_DIR, exist_ok=True) 204 tris = make_hat() 205 206 path = os.path.join(OUT_DIR, "tophat_%s.stl" % VERSION) 207 write_stl(path, tris, "badge tophat %s / case %s" % (VERSION, P["case_version"])) 208 209 bw, bh, bd = bbox(tris) 210 vol = mesh_volume_cc(tris) # approx: overlaps double-count a little 211 grams = vol * _DENSITY_G_CC[MATERIAL] * INFILL 212 bad = edge_manifold_issues(tris) 213 214 print("tophat %s (badge case %s, layout %s, git %s)" % ( 215 VERSION, P["case_version"], LAYOUT, git_rev() or "?")) 216 print(" sits on tab %.1f(x) x %.1f(up) x %.1f(z-depth) mm at badge top-centre" % ( 217 EAR_W, EAR_H, TAB_DEPTH)) 218 print(" hat envelope %.1f x %.1f x %.1f mm (%d tris)" % (bw, bh, bd, len(tris))) 219 print(" crown OD %.1f, ID %.1f, height %.1f, wall %.1f" % ( 220 CROWN_OD, CROWN_ID, CROWN_H, WALL)) 221 print(" brim OD %.1f, thick %.1f" % (BRIM_OD, BRIM_T)) 222 print(" cord slot %.1f x %.1f mm (r%.1f) in the crown top" % ( 223 2 * SLOT_HX, 2 * SLOT_HY, SLOT_R)) 224 print(" tab socket fins %.1f thick, %.2f/side slide, grip %.1f mm tall%s" % ( 225 FIN_T, TOL, FIN_H, ", hatband band" if BAND else "")) 226 print(" material %s @ %.0f%% -> ~%.1f cc, ~%.0f g (approx, overlap-composed)" % ( 227 MATERIAL, INFILL * 100, vol, grams)) 228 print(" manifold %s" % ("OK (closed)" if bad == 0 else 229 "%d open edges (overlap-composed: brim/crown/roof/fins union)" % bad)) 230 print(" -> %s" % path) 231 232 233 if __name__ == "__main__": 234 main()