Put DHT11 in a flush rear snap pocket with slight proud grill; nest front lip into back groove in the viewer. Co-authored-by: Cursor <cursoragent@cursor.com>
234 lines
7.6 KiB
Python
234 lines
7.6 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
CYD climate case — clean clamshell.
|
|
|
|
- Front: screen bezel + male lip
|
|
- Back: cavity + female groove + DHT11 snap pocket flush in back wall (tip proud ~1.6mm)
|
|
- No side “boxes”: removed bulky stand block and external DHT hang-on bay
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import trimesh
|
|
from trimesh.creation import box, cylinder
|
|
|
|
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 = 2.2
|
|
CAVITY = 8.0
|
|
LIP_H, LIP_CLEAR = 2.2, 0.2
|
|
USB_W, USB_H, USB_Y = 15.0, 9.0, 30.5
|
|
SCREW, BOSS_OD, BOSS_ID = 3.3, 7.0, 2.5
|
|
|
|
# DHT11 module pocket (from OUTSIDE of back face z=0)
|
|
DHT_L, DHT_W, DHT_H = 16.0, 12.5, 7.5
|
|
DHT_PROUD = 1.6
|
|
|
|
INNER_L = PCB_L + 2 * GAP
|
|
INNER_W = PCB_W + 2 * GAP
|
|
OUTER_L = INNER_L + 2 * WALL
|
|
OUTER_W = INNER_W + 2 * WALL
|
|
SKIRT = DISP_H + 1.0
|
|
# Back total height to mating plane (top of female groove floor)
|
|
BACK_BODY_H = WALL + CAVITY # rim starts here
|
|
FRONT_BODY_H = BEZEL_T + SKIRT
|
|
|
|
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 min(dx, dy, dz) < 1e-6:
|
|
raise ValueError(f"degenerate {[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(*ms: trimesh.Trimesh) -> trimesh.Trimesh:
|
|
r = ms[0]
|
|
for m in ms[1:]:
|
|
r = r.union(m, engine="manifold")
|
|
return r
|
|
|
|
|
|
def D(a: trimesh.Trimesh, *ss: trimesh.Trimesh) -> trimesh.Trimesh:
|
|
r = a
|
|
for s in ss:
|
|
r = r.difference(s, engine="manifold")
|
|
return r
|
|
|
|
|
|
def make_front() -> trimesh.Trimesh:
|
|
"""
|
|
Z=0: outer screen face
|
|
Z=FRONT_BODY_H: open rim with male lip extending to FRONT_BODY_H+LIP_H
|
|
"""
|
|
body = aabb(0, 0, 0, OUTER_L, OUTER_W, FRONT_BODY_H)
|
|
hollow = aabb(WALL, WALL, BEZEL_T, OUTER_L - WALL, OUTER_W - WALL, FRONT_BODY_H + 1)
|
|
|
|
vx = WALL + GAP + DISP_X + VIEW_IX
|
|
vy = WALL + GAP + VIEW_IY
|
|
view = aabb(vx - 0.2, vy - 0.2, -0.5, vx + VIEW_L + 0.2, vy + VIEW_W + 0.2, BEZEL_T + 0.5)
|
|
|
|
sx = WALL + GAP + DISP_X - 0.2
|
|
pocket = aabb(
|
|
sx, WALL + GAP - 0.1, BEZEL_T - 0.05,
|
|
sx + DISP_L + 0.4, WALL + GAP + DISP_W + 0.1, BEZEL_T + DISP_H + 0.6,
|
|
)
|
|
out = D(body, hollow, view, pocket)
|
|
|
|
# male lip: thin wall ring protruding past rim
|
|
lip_out = aabb(
|
|
WALL - 1.0, WALL - 1.0, FRONT_BODY_H - 0.05,
|
|
OUTER_L - (WALL - 1.0), OUTER_W - (WALL - 1.0), FRONT_BODY_H + LIP_H,
|
|
)
|
|
lip_in = aabb(
|
|
WALL + LIP_CLEAR, WALL + LIP_CLEAR, FRONT_BODY_H - 0.2,
|
|
OUTER_L - WALL - LIP_CLEAR, OUTER_W - WALL - LIP_CLEAR, FRONT_BODY_H + LIP_H + 0.5,
|
|
)
|
|
out = U(out, D(lip_out, lip_in))
|
|
|
|
for px, py in MH:
|
|
hole = cylinder(radius=SCREW / 2 + 0.1, height=FRONT_BODY_H + LIP_H + 4, sections=28)
|
|
hole.apply_translation([WALL + GAP + px, WALL + GAP + py, FRONT_BODY_H / 2])
|
|
out = D(out, hole)
|
|
|
|
out.merge_vertices()
|
|
return out
|
|
|
|
|
|
def make_back() -> trimesh.Trimesh:
|
|
"""
|
|
Z=0: exterior back face (DHT grill here, slightly proud)
|
|
Z=BACK_BODY_H: mating plane; female groove from BACK_BODY_H to BACK_BODY_H+LIP_H
|
|
"""
|
|
total_h = BACK_BODY_H + LIP_H
|
|
body = aabb(0, 0, 0, OUTER_L, OUTER_W, total_h)
|
|
cavity = aabb(WALL, WALL, WALL, OUTER_L - WALL, OUTER_W - WALL, total_h + 1)
|
|
out = D(body, cavity)
|
|
|
|
# female groove: cut a channel in the top rim for the male lip
|
|
# outer wall of groove
|
|
groove = aabb(
|
|
WALL - 1.0 - LIP_CLEAR, WALL - 1.0 - LIP_CLEAR, BACK_BODY_H + 0.1,
|
|
OUTER_L - (WALL - 1.0 - LIP_CLEAR), OUTER_W - (WALL - 1.0 - LIP_CLEAR), total_h + 0.2,
|
|
)
|
|
groove_keep = aabb(
|
|
WALL + 0.05, WALL + 0.05, BACK_BODY_H - 0.1,
|
|
OUTER_L - WALL - 0.05, OUTER_W - WALL - 0.05, total_h + 0.3,
|
|
)
|
|
out = D(out, D(groove, groove_keep))
|
|
|
|
# USB
|
|
uy0 = WALL + GAP + USB_Y - USB_W / 2
|
|
out = D(out, aabb(-0.5, uy0, WALL + 1.0, WALL + 1.0, uy0 + USB_W, WALL + 1.0 + USB_H))
|
|
|
|
# button holes
|
|
for y in (12.0, 22.0):
|
|
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)
|
|
|
|
# screw bosses
|
|
bosses = []
|
|
for px, py in MH:
|
|
x, y = WALL + GAP + px, WALL + GAP + py
|
|
boss = cylinder(radius=BOSS_OD / 2, height=CAVITY - 0.5, sections=28)
|
|
boss.apply_translation([x, y, WALL + (CAVITY - 0.5) / 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)
|
|
|
|
# ---- DHT11 flush pocket in back wall (NOT a side box) ----
|
|
# Center on back face
|
|
cx = OUTER_L * 0.5
|
|
cy = OUTER_W * 0.5
|
|
# Pocket carved from outside into wall+cavity enough for module
|
|
pocket = aabb(
|
|
cx - DHT_L / 2 - 0.3, cy - DHT_W / 2 - 0.3, -0.05,
|
|
cx + DHT_L / 2 + 0.3, cy + DHT_W / 2 + 0.3, WALL + DHT_H - 1.0,
|
|
)
|
|
out = D(out, pocket)
|
|
|
|
# Snap rails (卡位) left/right inside pocket — thin ledges
|
|
rail_z0, rail_z1 = 1.2, 1.2 + 2.2
|
|
left = aabb(cx - DHT_L / 2 - 0.3, cy - 3.2, rail_z0, cx - DHT_L / 2 + 1.0, cy + 3.2, rail_z1)
|
|
right = aabb(cx + DHT_L / 2 - 1.0, cy - 3.2, rail_z0, cx + DHT_L / 2 + 0.3, cy + 3.2, rail_z1)
|
|
# bottom stop (prevents falling inward)
|
|
stop = aabb(cx - 5, cy - DHT_W / 2 + 0.5, WALL + DHT_H - 2.2, cx + 5, cy + DHT_W / 2 - 0.5, WALL + DHT_H - 1.0)
|
|
out = U(out, left, right, stop)
|
|
|
|
# Proud grill ring on exterior (the only intentional protrusion)
|
|
grill_ring = aabb(
|
|
cx - DHT_L / 2 - 1.0, cy - DHT_W / 2 - 1.0, -DHT_PROUD,
|
|
cx + DHT_L / 2 + 1.0, cy + DHT_W / 2 + 1.0, 0.05,
|
|
)
|
|
grill_hole = aabb(
|
|
cx - DHT_L / 2 + 1.5, cy - DHT_W / 2 + 1.5, -DHT_PROUD - 0.2,
|
|
cx + DHT_L / 2 - 1.5, cy + DHT_W / 2 - 1.5, 0.2,
|
|
)
|
|
# slot vents in proud face
|
|
slots = []
|
|
for i in range(4):
|
|
slots.append(aabb(
|
|
cx - 5 + i * 2.8, cy - 4.5, -DHT_PROUD - 0.2,
|
|
cx - 5 + i * 2.8 + 1.2, cy + 4.5, 0.2,
|
|
))
|
|
out = U(out, D(grill_ring, grill_hole, *slots))
|
|
|
|
# tiny wire path already open via pocket into cavity
|
|
|
|
# Simple thin kickstand rib (not a hollow “格子”) — optional small triangle
|
|
# Skip bulky stand entirely per user feedback.
|
|
|
|
out.merge_vertices()
|
|
return out
|
|
|
|
|
|
def main() -> None:
|
|
out = Path(__file__).resolve().parent / "export"
|
|
out.mkdir(parents=True, exist_ok=True)
|
|
print("front…")
|
|
front = make_front()
|
|
print("back…")
|
|
back = make_back()
|
|
front.export(out / "front.stl")
|
|
back.export(out / "back.stl")
|
|
|
|
# write mate info for viewer
|
|
meta = out / "mate.json"
|
|
meta.write_text(
|
|
"{\n"
|
|
f' "outer_l": {OUTER_L},\n'
|
|
f' "outer_w": {OUTER_W},\n'
|
|
f' "front_body_h": {FRONT_BODY_H},\n'
|
|
f' "back_body_h": {BACK_BODY_H},\n'
|
|
f' "lip_h": {LIP_H},\n'
|
|
f' "dht_proud": {DHT_PROUD}\n'
|
|
"}\n",
|
|
encoding="utf-8",
|
|
)
|
|
print(f"front {front.extents} watertight={front.is_watertight}")
|
|
print(f"back {back.extents} watertight={back.is_watertight}")
|
|
print("closed stack height ≈", BACK_BODY_H + FRONT_BODY_H, "mm (lips nest, not add full)")
|
|
print("Wrote STLs + mate.json")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|