Provide OpenSCAD source, watertight front/back STLs, and print/assembly docs. Co-authored-by: Cursor <cursoragent@cursor.com>
201 lines
6.0 KiB
Python
201 lines
6.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate printable STL for CYD climate case using trimesh + manifold boolean."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
import trimesh
|
|
from trimesh.creation import box, cylinder
|
|
|
|
# ---- mm, aligned with cyd_climate_case.scad ----
|
|
PCB_L, PCB_W = 86.0, 50.0
|
|
MH_INSET = 4.0
|
|
DISP_L, DISP_W, DISP_H, DISP_X = 69.0, 50.0, 4.0, 8.0
|
|
VIEW_L, VIEW_W, VIEW_IX, VIEW_IY = 59.5, 45.0, 7.0, 2.5
|
|
WALL, GAP = 2.4, 0.4
|
|
BEZEL_T, CAVITY = 2.0, 7.0
|
|
PCB_T_HALF = 0.8
|
|
USB_W, USB_H, USB_Y = 14.0, 8.5, 30.5
|
|
STAND_FOOT = 18.0
|
|
DHT_L, DHT_W, DHT_H = 22.0, 18.0, 14.0
|
|
SCREW = 3.3
|
|
BOSS_OD, BOSS_ID = 7.2, 2.5
|
|
|
|
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
|
|
|
|
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 place(mesh: trimesh.Trimesh, center: tuple[float, float, float]) -> trimesh.Trimesh:
|
|
m = mesh.copy()
|
|
m.apply_translation(np.array(center) - m.bounding_box.centroid)
|
|
return m
|
|
|
|
|
|
def aabb(x0, y0, z0, x1, y1, z1) -> trimesh.Trimesh:
|
|
dx, dy, dz = x1 - x0, y1 - y0, z1 - z0
|
|
m = box(extents=[dx, dy, dz])
|
|
m.apply_translation([x0 + dx / 2, y0 + dy / 2, z0 + dz / 2])
|
|
return m
|
|
|
|
|
|
def u(*meshes: trimesh.Trimesh) -> trimesh.Trimesh:
|
|
result = meshes[0]
|
|
for m in meshes[1:]:
|
|
result = result.union(m, engine="manifold")
|
|
return result
|
|
|
|
|
|
def d(a: trimesh.Trimesh, *subs: trimesh.Trimesh) -> trimesh.Trimesh:
|
|
result = a
|
|
for s in subs:
|
|
result = result.difference(s, engine="manifold")
|
|
return result
|
|
|
|
|
|
def make_front() -> trimesh.Trimesh:
|
|
skirt_h = DISP_H + PCB_T_HALF
|
|
z1 = BEZEL_T + skirt_h
|
|
|
|
body = aabb(0, 0, 0, OUTER_L, OUTER_W, z1)
|
|
cavity = aabb(WALL, WALL, BEZEL_T, OUTER_L - WALL, OUTER_W - WALL, z1 + 1)
|
|
|
|
vx = WALL + GAP + DISP_X + VIEW_IX
|
|
vy = WALL + GAP + VIEW_IY
|
|
view = aabb(vx - 0.3, vy - 0.3, -0.5, vx + VIEW_L + 0.3, vy + VIEW_W + 0.3, BEZEL_T + 0.5)
|
|
|
|
# screen pocket
|
|
sx = WALL + GAP + DISP_X - 0.3
|
|
pocket = aabb(sx, WALL + GAP - 0.2, BEZEL_T - 0.05, sx + DISP_L + 0.6, WALL + GAP + DISP_W + 0.2, z1 + 0.5)
|
|
|
|
out = d(body, cavity, view, pocket)
|
|
|
|
for px, py in MH:
|
|
x = WALL + GAP + px
|
|
y = WALL + GAP + py
|
|
hole = cylinder(radius=SCREW / 2, height=z1 + 2, sections=32)
|
|
hole.apply_translation([x, y, z1 / 2])
|
|
out = d(out, hole)
|
|
|
|
out.merge_vertices()
|
|
return out
|
|
|
|
|
|
def make_back() -> trimesh.Trimesh:
|
|
body = aabb(0, 0, 0, OUTER_L, OUTER_W, SHELL_H)
|
|
cavity = aabb(WALL, WALL, WALL, OUTER_L - WALL, OUTER_W - WALL, SHELL_H + 1)
|
|
|
|
# USB on -X
|
|
usb_y0 = WALL + GAP + USB_Y - USB_W / 2
|
|
usb = aabb(-1, usb_y0, WALL + 1.5, WALL + 1, usb_y0 + USB_W, WALL + 1.5 + USB_H)
|
|
|
|
# SD slot on +Y
|
|
sd = aabb(WALL + GAP + 28, OUTER_W - WALL - 1, WALL + 2, WALL + GAP + 44, OUTER_W + 1, WALL + 5.2)
|
|
|
|
# button holes on floor
|
|
btn1 = cylinder(radius=2.3, height=WALL + 2, sections=24)
|
|
btn1.apply_translation([WALL + GAP + 10, WALL + GAP + 12, WALL / 2])
|
|
btn2 = cylinder(radius=2.3, height=WALL + 2, sections=24)
|
|
btn2.apply_translation([WALL + GAP + 10, WALL + GAP + 22, WALL / 2])
|
|
|
|
# vent slots on floor
|
|
vents = []
|
|
for i in range(5):
|
|
v = aabb(WALL + 12 + i * 12, WALL + 8, -0.5, WALL + 14 + i * 12, WALL + 18, WALL + 0.5)
|
|
vents.append(v)
|
|
|
|
out = d(body, cavity, usb, sd, btn1, btn2, *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.3, sections=32)
|
|
boss.apply_translation([x, y, WALL + (CAVITY - 0.3) / 2])
|
|
hole = cylinder(radius=BOSS_ID / 2, height=CAVITY + 1, sections=24)
|
|
hole.apply_translation([x, y, WALL + CAVITY / 2])
|
|
bosses.append(d(boss, hole))
|
|
out = u(out, *bosses)
|
|
|
|
# stand foot
|
|
stand_len = OUTER_L * 0.72
|
|
sx = (OUTER_L - stand_len) / 2
|
|
foot = aabb(sx, -STAND_FOOT + 0.5, 0, sx + stand_len, WALL, SHELL_H)
|
|
# hollow foot
|
|
foot_in = aabb(sx + 2, -STAND_FOOT + 2.5, WALL, sx + stand_len - 2, WALL - 0.2, SHELL_H + 1)
|
|
foot = d(foot, foot_in)
|
|
rib = aabb(OUTER_L * 0.5 - 1.2, -STAND_FOOT + 1, WALL, OUTER_L * 0.5 + 1.2, WALL, SHELL_H - 1)
|
|
out = u(out, foot, rib)
|
|
|
|
# DHT bay
|
|
dx = WALL + GAP + 48
|
|
dy = OUTER_W - 0.2
|
|
bay = aabb(dx, dy, WALL, dx + DHT_L, dy + DHT_W + WALL, WALL + DHT_H)
|
|
bay_in = aabb(dx + 1.2, dy + 1.2, WALL + 1.0, dx + DHT_L - 1.2, dy + DHT_W, WALL + DHT_H + 1)
|
|
# top vents
|
|
top_vents = []
|
|
for i in range(5):
|
|
tv = aabb(
|
|
dx + 2.5 + i * 3.6,
|
|
dy + 3,
|
|
WALL + DHT_H - WALL - 0.2,
|
|
dx + 2.5 + i * 3.6 + 1.5,
|
|
dy + DHT_W - 2,
|
|
WALL + DHT_H + 0.5,
|
|
)
|
|
top_vents.append(tv)
|
|
# side vents
|
|
for i in range(3):
|
|
sv = aabb(
|
|
dx + 4 + i * 5.5,
|
|
dy + DHT_W + WALL - 0.5,
|
|
WALL + 3,
|
|
dx + 4 + i * 5.5 + 1.5,
|
|
dy + DHT_W + WALL + 1,
|
|
WALL + 9,
|
|
)
|
|
top_vents.append(sv)
|
|
bay = d(bay, bay_in, *top_vents)
|
|
|
|
# cable channel from bay to CN1
|
|
channel = aabb(dx + 2, OUTER_W - WALL - 2, WALL + 2, dx + 10, OUTER_W + DHT_W, WALL + 6)
|
|
out = d(u(out, bay), channel)
|
|
|
|
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…")
|
|
back = make_back()
|
|
|
|
front.export(out_dir / "front.stl")
|
|
back.export(out_dir / "back.stl")
|
|
|
|
print(f"front: {front.faces.shape[0]} faces, watertight={front.is_watertight}")
|
|
print(f"back: {back.faces.shape[0]} faces, watertight={back.is_watertight}")
|
|
print(f"Outer: {OUTER_L:.1f} x {OUTER_W:.1f} x front/back shells")
|
|
print(f"Wrote {out_dir / 'front.stl'}")
|
|
print(f"Wrote {out_dir / 'back.stl'}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|