#!/usr/bin/env python3 """ CYD climate case STL generator. Design intent: - DHT11 on the BACK cover, held by snap/clip posts (卡位), tip slightly proud of back face - Front + back close with a interlocking lip (合盖) """ from __future__ import annotations from pathlib import Path import trimesh from trimesh.creation import box, cylinder # noqa: F401 — cylinder used for holes/bosses # ---- mm ---- PCB_L, PCB_W = 86.0, 50.0 MH_INSET = 4.0 DISP_X, DISP_L, DISP_W, DISP_H = 8.0, 69.0, 50.0, 4.0 VIEW_L, VIEW_W, VIEW_IX, VIEW_IY = 59.5, 45.0, 7.0, 2.5 WALL, GAP = 2.4, 0.35 BEZEL_T, CAVITY = 2.2, 7.5 LIP_H, LIP_T = 2.4, 1.15 # front male / back female for 合盖 USB_W, USB_H, USB_Y = 15.0, 9.0, 30.5 STAND_FOOT = 16.0 SCREW = 3.3 BOSS_OD, BOSS_ID = 7.0, 2.5 # DHT11 module (common PCB module ~15.5 x 12 x 7.5 + pins) DHT_MOD_L, DHT_MOD_W, DHT_MOD_H = 16.0, 12.5, 8.0 DHT_PROUD = 1.8 # how much grill/tip sticks out past back outer face DHT_CLIP = 1.1 # undercut lip thickness for 卡位 INNER_L = PCB_L + 2 * GAP INNER_W = PCB_W + 2 * GAP OUTER_L = INNER_L + 2 * WALL OUTER_W = INNER_W + 2 * WALL SHELL_H = CAVITY + WALL SKIRT_H = DISP_H + 1.0 MH = [ (MH_INSET, MH_INSET), (PCB_L - MH_INSET, MH_INSET), (PCB_L - MH_INSET, PCB_W - MH_INSET), (MH_INSET, PCB_W - MH_INSET), ] def aabb(x0, y0, z0, x1, y1, z1) -> trimesh.Trimesh: xa, xb = sorted((float(x0), float(x1))) ya, yb = sorted((float(y0), float(y1))) za, zb = sorted((float(z0), float(z1))) dx, dy, dz = xb - xa, yb - ya, zb - za if dx < 1e-6 or dy < 1e-6 or dz < 1e-6: raise ValueError(f"degenerate box {[xa,ya,za,xb,yb,zb]}") m = box(extents=[dx, dy, dz]) m.apply_translation([xa + dx / 2, ya + dy / 2, za + dz / 2]) return m def u(*meshes: trimesh.Trimesh) -> trimesh.Trimesh: r = meshes[0] for m in meshes[1:]: r = r.union(m, engine="manifold") return r def d(a: trimesh.Trimesh, *subs: trimesh.Trimesh) -> trimesh.Trimesh: r = a for s in subs: r = r.difference(s, engine="manifold") return r def make_front() -> trimesh.Trimesh: """Front bezel: screen window + skirt + male lip for closing onto back.""" z1 = BEZEL_T + SKIRT_H + LIP_H body = aabb(0, 0, 0, OUTER_L, OUTER_W, BEZEL_T + SKIRT_H) # hollow skirt cavity = aabb(WALL, WALL, BEZEL_T, OUTER_L - WALL, OUTER_W - WALL, BEZEL_T + SKIRT_H + 1) vx = WALL + GAP + DISP_X + VIEW_IX vy = WALL + GAP + VIEW_IY view = aabb(vx - 0.25, vy - 0.25, -0.5, vx + VIEW_L + 0.25, vy + VIEW_W + 0.25, BEZEL_T + 0.6) sx = WALL + GAP + DISP_X - 0.25 pocket = aabb( sx, WALL + GAP - 0.15, BEZEL_T - 0.05, sx + DISP_L + 0.5, WALL + GAP + DISP_W + 0.15, BEZEL_T + DISP_H + 0.8, ) out = d(body, cavity, view, pocket) # male lip (inserts into back female groove) lip_outer = aabb( WALL - LIP_T, WALL - LIP_T, BEZEL_T + SKIRT_H - 0.05, OUTER_L - (WALL - LIP_T), OUTER_W - (WALL - LIP_T), BEZEL_T + SKIRT_H + LIP_H, ) lip_inner = aabb( WALL + 0.15, WALL + 0.15, BEZEL_T + SKIRT_H - 0.1, OUTER_L - WALL - 0.15, OUTER_W - WALL - 0.15, BEZEL_T + SKIRT_H + LIP_H + 0.5, ) lip = d(lip_outer, lip_inner) out = u(out, lip) # screw clearance through bezel for px, py in MH: x = WALL + GAP + px y = WALL + GAP + py hole = cylinder(radius=SCREW / 2 + 0.1, height=z1 + 4, sections=28) hole.apply_translation([x, y, (BEZEL_T + SKIRT_H) / 2]) out = d(out, hole) out.merge_vertices() return out def dht_pocket_on_back() -> trimesh.Trimesh: """ Pocket on outer back face (z=0 side is exterior bottom of shell). Module inserts from outside; clips hold it; grill protrudes DHT_PROUD. Coordinate: back shell sits z=0 exterior, z=SHELL_H toward PCB. Pocket carved into floor from outside, clips on the outer face. """ # center DHT on back wall area away from USB (right-ish of board back) cx = WALL + GAP + 62 cy = WALL + GAP + PCB_W / 2 # outer boss that contains the module (sits mostly outside z<=0) bay_l = DHT_MOD_L + 3.2 bay_w = DHT_MOD_W + 3.2 bay_h = DHT_MOD_H + WALL * 0.6 + DHT_PROUD # bay extends below z=0 (outside) and a bit into floor bay = aabb( cx - bay_l / 2, cy - bay_w / 2, -bay_h + WALL * 0.35, cx + bay_l / 2, cy + bay_w / 2, WALL + 0.2, ) # cavity for module (from outside) cav = aabb( cx - DHT_MOD_L / 2 - 0.25, cy - DHT_MOD_W / 2 - 0.25, -bay_h + WALL * 0.35 + 1.0, cx + DHT_MOD_L / 2 + 0.25, cy + DHT_MOD_W / 2 + 0.25, WALL - 0.3, ) # window through back wall into interior for wires (small) wire = aabb( cx - 3.5, cy - 4.0, WALL - 0.5, cx + 3.5, cy + 4.0, WALL + 2.5, ) # front face of bay: grill opening, leaving rim so tip is proud grill = aabb( cx - DHT_MOD_L / 2 + 1.2, cy - DHT_MOD_W / 2 + 1.2, -bay_h - 0.2, cx + DHT_MOD_L / 2 - 1.2, cy + DHT_MOD_W / 2 - 1.2, -bay_h + WALL + DHT_PROUD + 0.5, ) # side vent slots vents = [] for i in range(3): vents.append(aabb( cx - DHT_MOD_L / 2 - 2, cy - 4 + i * 3.2, -bay_h + 3, cx - DHT_MOD_L / 2 + 0.2, cy - 4 + i * 3.2 + 1.4, -bay_h + 8, )) vents.append(aabb( cx + DHT_MOD_L / 2 - 0.2, cy - 4 + i * 3.2, -bay_h + 3, cx + DHT_MOD_L / 2 + 2, cy - 4 + i * 3.2 + 1.4, -bay_h + 8, )) # 卡位: L/R snap ledges + end stops (press-fit module from outside) z_base = -bay_h + WALL * 0.35 + 1.2 left_clip = aabb( cx - DHT_MOD_L / 2 - 1.2, cy - 3.5, z_base, cx - DHT_MOD_L / 2 + DHT_CLIP, cy + 3.5, z_base + 3.2, ) right_clip = aabb( cx + DHT_MOD_L / 2 - DHT_CLIP, cy - 3.5, z_base, cx + DHT_MOD_L / 2 + 1.2, cy + 3.5, z_base + 3.2, ) # end stop ledge near grill (prevents module falling out front) stop = aabb( cx - DHT_MOD_L / 2 + 1.5, cy - DHT_MOD_W / 2 + 0.8, -bay_h + WALL * 0.35 + 0.8, cx + DHT_MOD_L / 2 - 1.5, cy + DHT_MOD_W / 2 - 0.8, -bay_h + WALL * 0.35 + 0.8 + 1.0, ) # rear retention bar (卡入后沿) retain = aabb( cx - 5.0, cy - DHT_MOD_W / 2 - 0.2, WALL - 2.2, cx + 5.0, cy + DHT_MOD_W / 2 + 0.2, WALL - 0.4, ) bay = d(bay, cav, grill, wire, *vents) bay = u(bay, left_clip, right_clip, stop, retain) return bay def make_back() -> trimesh.Trimesh: """Back shell: cavity, female lip, screw bosses, stand, DHT snap bay on outer back.""" body = aabb(0, 0, 0, OUTER_L, OUTER_W, SHELL_H + LIP_H) cavity = aabb(WALL, WALL, WALL, OUTER_L - WALL, OUTER_W - WALL, SHELL_H + LIP_H + 1) # female groove for front lip (near top rim) groove = aabb( WALL - LIP_T - 0.15, WALL - LIP_T - 0.15, SHELL_H, OUTER_L - (WALL - LIP_T - 0.15), OUTER_W - (WALL - LIP_T - 0.15), SHELL_H + LIP_H + 0.2, ) # keep inner wall so groove is a channel, not open to cavity fully # actually: cut a U-channel from the top rim groove_cut = aabb( WALL - LIP_T - 0.1, WALL - LIP_T - 0.1, SHELL_H - 0.05, OUTER_L - (WALL - LIP_T - 0.1), OUTER_W - (WALL - LIP_T - 0.1), SHELL_H + LIP_H + 0.3, ) keep_inner = aabb( WALL + 0.05, WALL + 0.05, SHELL_H - 0.2, OUTER_L - WALL - 0.05, OUTER_W - WALL - 0.05, SHELL_H + LIP_H + 0.5, ) # rim solid then groove: start from body, remove cavity, then remove groove channel between lip walls out = d(body, cavity) # Add rim collar then cut female slot collar = aabb(0, 0, SHELL_H - 0.05, OUTER_L, OUTER_W, SHELL_H + LIP_H) collar_in = aabb(WALL + 0.2, WALL + 0.2, SHELL_H - 0.2, OUTER_L - WALL - 0.2, OUTER_W - WALL - 0.2, SHELL_H + LIP_H + 0.5) collar = d(collar, collar_in) out = u(out, collar) # female slot (accepts front male lip) slot = aabb( WALL - LIP_T - 0.12, WALL - LIP_T - 0.12, SHELL_H + 0.15, OUTER_L - (WALL - LIP_T - 0.12), OUTER_W - (WALL - LIP_T - 0.12), SHELL_H + LIP_H - 0.15, ) slot_keep = aabb( WALL + 0.05, WALL + 0.05, SHELL_H, OUTER_L - WALL - 0.05, OUTER_W - WALL - 0.05, SHELL_H + LIP_H + 0.2, ) # slot is the ring between slot outer and slot_keep — cut only the ring slot_ring = d(slot, slot_keep) out = d(out, slot_ring) # USB usb_y0 = WALL + GAP + USB_Y - USB_W / 2 usb = aabb(-1, usb_y0, WALL + 1.2, WALL + 1.2, usb_y0 + USB_W, WALL + 1.2 + USB_H) out = d(out, usb) # SD optional slit sd = aabb(WALL + GAP + 28, OUTER_W - WALL - 1, WALL + 2, WALL + GAP + 44, OUTER_W + 1, WALL + 5) out = d(out, sd) # BOOT/RST access for y in (12, 22): btn = cylinder(radius=2.2, height=WALL + 2, sections=20) btn.apply_translation([WALL + GAP + 10, WALL + GAP + y, WALL / 2]) out = d(out, btn) # floor vents (not under DHT) vents = [] for i in range(4): vents.append(aabb(WALL + 10 + i * 12, WALL + 8, -0.5, WALL + 12 + i * 12, WALL + 16, WALL + 0.5)) out = d(out, *vents) # screw bosses bosses = [] for px, py in MH: x = WALL + GAP + px y = WALL + GAP + py boss = cylinder(radius=BOSS_OD / 2, height=CAVITY - 0.4, sections=28) boss.apply_translation([x, y, WALL + (CAVITY - 0.4) / 2]) hole = cylinder(radius=BOSS_ID / 2, height=CAVITY + 1, sections=20) hole.apply_translation([x, y, WALL + CAVITY / 2]) bosses.append(d(boss, hole)) out = u(out, *bosses) # stand foot (desk) stand_len = OUTER_L * 0.7 sx = (OUTER_L - stand_len) / 2 foot = aabb(sx, -STAND_FOOT + 0.4, 0, sx + stand_len, WALL, SHELL_H * 0.85) foot_in = aabb(sx + 2, -STAND_FOOT + 2.2, WALL, sx + stand_len - 2, WALL - 0.15, SHELL_H) foot = d(foot, foot_in) rib = aabb(OUTER_L / 2 - 1.2, -STAND_FOOT + 1, WALL, OUTER_L / 2 + 1.2, WALL, SHELL_H * 0.8) out = u(out, foot, rib) # DHT11 snap bay on outer back (protruding) out = u(out, dht_pocket_on_back()) out.merge_vertices() return out def main() -> None: out_dir = Path(__file__).resolve().parent / "export" out_dir.mkdir(parents=True, exist_ok=True) print("Building front (合盖公止口)…") front = make_front() print("Building back (DHT后盖卡位 + 母止口)…") back = make_back() front.export(out_dir / "front.stl") back.export(out_dir / "back.stl") print(f"front watertight={front.is_watertight} faces={len(front.faces)}") print(f"back watertight={back.is_watertight} faces={len(back.faces)}") print(f"Wrote {out_dir / 'front.stl'}") print(f"Wrote {out_dir / 'back.stl'}") print(f"Outer ~ {OUTER_L:.1f} x {OUTER_W:.1f} mm; DHT proud ~{DHT_PROUD} mm") if __name__ == "__main__": main()