From c35ed7a7a9503d5221579a1969a3d2786c35b274 Mon Sep 17 00:00:00 2001 From: "DESKTOP-PUHFO60\\dsx2016" Date: Sat, 17 Oct 2026 14:58:34 +0800 Subject: [PATCH] Add parametric CYD desk-stand case with DHT11 vent bay. Provide OpenSCAD source, watertight front/back STLs, and print/assembly docs. Co-authored-by: Cursor --- README.md | 16 ++ cad/README.md | 59 ++++++++ cad/cyd_climate_case.scad | 300 ++++++++++++++++++++++++++++++++++++++ cad/export/back.stl | Bin 0 -> 70784 bytes cad/export/front.stl | Bin 0 -> 29684 bytes cad/generate_stl.py | 200 +++++++++++++++++++++++++ 6 files changed, 575 insertions(+) create mode 100644 cad/README.md create mode 100644 cad/cyd_climate_case.scad create mode 100644 cad/export/back.stl create mode 100644 cad/export/front.stl create mode 100644 cad/generate_stl.py diff --git a/README.md b/README.md index 86e1dc3..2af6f13 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,20 @@ pio device monitor -b 115200 | `cyd_28r_inv` | 颜色反相 / 发白 | | `cyd_28_st7789` | USB-C 双口 / ST7789 变体 | +## 3D 外壳 + +桌面斜立分体壳(前框 + 后盖支架 + DHT11 通风仓),可直接打印: + +- STL:[`cad/export/front.stl`](cad/export/front.stl)、[`cad/export/back.stl`](cad/export/back.stl) +- 参数源 / 说明:[`cad/README.md`](cad/README.md) + +```bash +pip install trimesh manifold3d numpy +python cad/generate_stl.py +``` + +固定用 `4 × M3×10`;DHT11 装后盖通风仓,线接 CN1。 + ## 项目结构 ``` @@ -46,6 +60,7 @@ platformio.ini # 板级与库依赖 include/lv_conf.h # LVGL 配置 src/main.cpp # 仪表盘主程序 scripts/exclude_lvgl_asm.py # 排除 ESP32 不兼容的 LVGL ASM +cad/ # 3D 外壳(OpenSCAD + STL) .cursor/rules/ # Cursor 协作规则 README.md # 本文档 ``` @@ -66,3 +81,4 @@ README.md # 本文档 | 2026-10-17 | 初版:LVGL 仪表盘 + DHT11(GPIO22),平滑/舒适度/趋势/背光 | | 2026-10-17 | 曾加入 ECO 熄屏休眠,后改回常亮正常版 | | 2026-10-17 | 初始化 Git,补充 README 与提交约定 | +| 2026-10-17 | 新增 CYD+DHT11 桌面斜立 3D 外壳(OpenSCAD / STL) | diff --git a/cad/README.md b/cad/README.md new file mode 100644 index 0000000..8aaf8af --- /dev/null +++ b/cad/README.md @@ -0,0 +1,59 @@ +# CYD 温湿度仪表盘 3D 外壳 + +桌面斜立分体外壳,适配 **ESP32-2432S028R(CYD)** + **DHT11(CN1 / GPIO22)**。 + +## 零件 + +| 文件 | 说明 | +|------|------| +| `export/front.stl` | 前框(屏窗 + 压边) | +| `export/back.stl` | 后盖 + 斜立支脚 + DHT11 通风仓 | +| `cyd_climate_case.scad` | 参数化源文件(OpenSCAD,可改尺寸) | +| `generate_stl.py` | 无 OpenSCAD 时用 Python 导出 STL | + +## 设计要点 + +- PCB 基准:`86 × 50 × 1.6` mm,四角安装孔 +- 前框可视窗约 `59.5 × 45` mm +- 后盖背面净空约 7 mm;Micro-USB / USB-C 合并开孔 +- BOOT / RST 背面开孔;底部散热缝 +- **DHT11 独立通风仓**(后盖长边外侧),减少板温干扰;线槽通向 CN1 +- 固定:`4 × M3×10` 自攻进后盖塑料柱(孔径约 2.5 mm) + +## 打印建议 + +| 参数 | 建议 | +|------|------| +| 材料 | PLA 或 PETG | +| 层高 | 0.2 mm | +| 喷嘴 | 0.4 mm | +| 墙厚 | 2–3 圈 | +| 填充 | 15–20% | +| 支撑 | **不需要**(按 STL 默认朝向) | +| 前框 | 屏窗朝上打印 | +| 后盖 | 底板朝下,支脚伸出床外方向按切片预览摆放 | + +## 重新导出 STL + +```bash +pip install trimesh manifold3d numpy +python cad/generate_stl.py +``` + +或用 OpenSCAD: + +```bash +openscad -o cad/export/front.stl -D part=\"front\" cad/cyd_climate_case.scad +openscad -o cad/export/back.stl -D part=\"back\" cad/cyd_climate_case.scad +``` + +改尺寸:编辑 `cyd_climate_case.scad` / `generate_stl.py` 顶部参数后重新导出。 + +## 装配 + +1. DHT11 放入后盖通风仓,杜邦线经线槽接到 **CN1**(`3V3 / IO22 / GND`,按你现有接线) +2. CYD 屏朝前框放入,USB 对齐侧孔 +3. 合上后盖,四角 M3×10 拧紧(勿过紧防裂) +4. 桌面斜立放置;点屏可调背光 + +若孔位与你的板子有 ±1 mm 偏差,优先改 `generate_stl.py` 里 `USB_Y` / `MH_INSET` 后重导。 diff --git a/cad/cyd_climate_case.scad b/cad/cyd_climate_case.scad new file mode 100644 index 0000000..e8a64a8 --- /dev/null +++ b/cad/cyd_climate_case.scad @@ -0,0 +1,300 @@ +/* + * CYD ESP32-2432S028R + DHT11 桌面斜立外壳 + * Climate Dashboard Case — parametric OpenSCAD + * + * 零件: + * part = "front" 前框(屏窗) + * part = "back" 后盖 + 斜立支架 + DHT 通风仓 + * part = "assembly" 装配预览(半透明板子) + * + * 导出 STL(本机安装 OpenSCAD 后): + * openscad -o export/front.stl -D part=\"front\" cyd_climate_case.scad + * openscad -o export/back.stl -D part=\"back\" cyd_climate_case.scad + */ + +/* [Which part] */ +part = "assembly"; // ["front", "back", "assembly"] + +/* [PCB / CYD] */ +pcb_l = 86.0; +pcb_w = 50.0; +pcb_t = 1.6; +pcb_corner_r = 4.0; +mh_inset = 4.0; // 安装孔距角边中心 +mh_dia = 3.2; // PCB 孔 + +/* [Display] */ +disp_l = 69.0; +disp_w = 50.0; +disp_h = 4.0; +disp_x = 8.0; // 屏相对 PCB 左边偏移 +view_l = 59.5; +view_w = 45.0; +view_inset_x = 7.0; // 相对屏模组 +view_inset_y = 2.5; + +/* [Case shell] */ +wall = 2.4; +gap = 0.4; // 板与壳间隙 +front_lip = 1.2; // 屏边压边宽度 +bezel_front_t = 2.0; // 前框面板厚度 +cavity_back = 7.0; // 背面元件净空 +stand_angle = 60; // 桌面倾角(度,相对水平) +stand_foot = 18; // 支脚接触深度 + +/* [USB dual cutout] */ +usb_dual = true; +usb_w = 14.0; // 双口合并开孔宽 +usb_h = 8.5; +usb_y = 30.5; // 相对 PCB 原点(短边为 X=0 侧) + +/* [Buttons] */ +btn_enable = true; +btn_dia = 4.5; + +/* [DHT11 bay] */ +dht_enable = true; +dht_bay_l = 22; +dht_bay_w = 18; +dht_bay_h = 14; +dht_vent_slot = 1.6; +dht_vent_gap = 2.8; + +/* [Screws] */ +screw_m3 = 3.2; +boss_od = 7.2; +boss_id = 2.6; // 自攻进塑料 +$fn = 48; + +// ---- derived ---- +inner_l = pcb_l + 2 * gap; +inner_w = pcb_w + 2 * gap; +outer_l = inner_l + 2 * wall; +outer_w = inner_w + 2 * wall; +case_h = bezel_front_t + disp_h + pcb_t + cavity_back + wall; + +mh_pts = [ + [mh_inset, mh_inset], + [pcb_l - mh_inset, mh_inset], + [pcb_l - mh_inset, pcb_w - mh_inset], + [mh_inset, pcb_w - mh_inset] +]; + +module rounded_rect(l, w, h, r) { + hull() { + for (x = [r, l - r], y = [r, w - r]) + translate([x, y, 0]) cylinder(h = h, r = r); + } +} + +module cyd_mock() { + color("gold") + difference() { + rounded_rect(pcb_l, pcb_w, pcb_t, pcb_corner_r); + for (p = mh_pts) + translate([p[0], p[1], -0.1]) + cylinder(d = mh_dia, h = pcb_t + 0.2); + } + color("black") + translate([disp_x, 0, pcb_t]) + cube([disp_l, disp_w, disp_h]); + color("navy") + translate([disp_x + view_inset_x, view_inset_y, pcb_t + disp_h]) + cube([view_l, view_w, 0.2]); +} + +// 前框:从屏面方向看,Z=0 为最前表面 +module front_bezel() { + view_x = wall + gap + disp_x + view_inset_x; + view_y = wall + gap + view_inset_y; + + difference() { + union() { + // 前表面 + rounded_rect(outer_l, outer_w, bezel_front_t, 3); + // 侧壁向下包住屏+PCB 前半 + translate([0, 0, bezel_front_t]) + difference() { + rounded_rect(outer_l, outer_w, disp_h + pcb_t * 0.5, 3); + translate([wall, wall, -0.1]) + rounded_rect(inner_l, inner_w, disp_h + pcb_t + 1, 2); + } + // 止口(与后盖搭接) + translate([wall * 0.45, wall * 0.45, bezel_front_t + disp_h + pcb_t * 0.5 - 0.01]) + difference() { + rounded_rect(outer_l - wall * 0.9, outer_w - wall * 0.9, 2.2, 2.4); + translate([wall * 0.55, wall * 0.55, -0.1]) + rounded_rect(outer_l - wall * 2.0, outer_w - wall * 2.0, 3, 1.6); + } + } + + // 可视窗 + translate([view_x - 0.3, view_y - 0.3, -0.1]) + cube([view_l + 0.6, view_w + 0.6, bezel_front_t + 1]); + + // 屏模组沉台(背面) + translate([wall + gap + disp_x - 0.3, wall + gap - 0.2, bezel_front_t - 0.05]) + cube([disp_l + 0.6, disp_w + 0.4, disp_h + 0.5]); + + // 螺丝通孔 + for (p = mh_pts) + translate([wall + gap + p[0], wall + gap + p[1], -0.1]) + cylinder(d = screw_m3 + 0.2, h = case_h); + } + + // 压屏边框内侧(窄边) + difference() { + translate([wall + gap + disp_x, wall + gap, bezel_front_t]) + cube([disp_l, disp_w, 0.8]); + translate([view_x - 0.2, view_y - 0.2, bezel_front_t - 0.1]) + cube([view_l + 0.4, view_w + 0.4, 2]); + } +} + +// 后盖主体(不含支架倾转);本地坐标:PCB 底面朝上放置时的壳 +module back_shell_body() { + shell_h = cavity_back + wall; + pcb_z = wall + cavity_back; // PCB 底面高度(元件朝下进腔) + + difference() { + union() { + rounded_rect(outer_l, outer_w, shell_h, 3); + + // 止口凹槽配合前框(外壳顶部一圈台阶) + translate([0, 0, shell_h - 0.01]) + difference() { + rounded_rect(outer_l, outer_w, 2.0, 3); + translate([wall * 0.5, wall * 0.5, -0.05]) + rounded_rect(outer_l - wall, outer_w - wall, 2.5, 2.4); + } + + // 螺丝柱 + for (p = mh_pts) { + translate([wall + gap + p[0], wall + gap + p[1], wall]) + difference() { + cylinder(d = boss_od, h = cavity_back - 0.2); + translate([0, 0, 1]) + cylinder(d = boss_id, h = cavity_back); + } + } + + // DHT 通风仓(PCB 长边外侧,靠近 CN1:x≈51) + if (dht_enable) { + dht_x = wall + gap + 48; + dht_y = outer_w - wall * 0.2; + translate([dht_x, dht_y, wall]) + dht_bay(); + } + } + + // 内腔 + translate([wall, wall, wall]) + rounded_rect(inner_l, inner_w, shell_h, 1.8); + + // USB 开孔(短边 x=0 侧) + usb_cut_y = wall + gap + usb_y - usb_w / 2; + translate([-0.1, usb_cut_y, wall + 1.5]) + cube([wall + 1.2, usb_w, usb_h]); + + // SD 卡槽(可选宽缝) + translate([wall + gap + 28, outer_w - wall - 0.1, wall + 2]) + cube([16, wall + 1, 3.2]); + + // BOOT / RST 软按开孔(背面) + if (btn_enable) { + translate([wall + gap + 10, wall + gap + 12, -0.1]) + cylinder(d = btn_dia, h = wall + 0.3); + translate([wall + gap + 10, wall + gap + 22, -0.1]) + cylinder(d = btn_dia, h = wall + 0.3); + } + + // 散热缝 + for (i = [0:4]) + translate([wall + 12 + i * 12, wall + 8, -0.1]) + hull() { + cylinder(d = 2.2, h = wall + 0.3); + translate([0, 10, 0]) cylinder(d = 2.2, h = wall + 0.3); + } + + // DHT 线槽通向 CN1 + if (dht_enable) { + translate([wall + gap + 48, outer_w - wall - 2, wall + 2]) + cube([8, wall + 4, 4]); + } + } +} + +module dht_bay() { + // 局部:仓体贴在 outer 长边外侧 + difference() { + translate([0, 0, 0]) + rounded_rect(dht_bay_l, dht_bay_w + wall, dht_bay_h, 2); + translate([wall * 0.6, wall * 0.6, wall * 0.6]) + cube([dht_bay_l - wall * 1.2, dht_bay_w, dht_bay_h]); + + // 顶面百叶通风 + for (i = [0:4]) + translate([3 + i * dht_vent_gap, dht_bay_w * 0.3, dht_bay_h - wall - 0.1]) + cube([dht_vent_slot, dht_bay_w * 0.55, wall + 0.3]); + + // 侧面通风 + for (i = [0:2]) + translate([4 + i * 5.5, dht_bay_w + wall - 0.2, 3 + i * 0.2]) + cube([dht_vent_slot, wall + 0.4, 6]); + } +} + +// 带斜立支架的后盖(打印时支脚朝下) +module back_with_stand() { + shell_h = cavity_back + wall; + + // 后盖:PCB 元件朝下;支架使屏幕斜立 + // 组装时:屏幕朝向 -Z 转 stand_angle + union() { + back_shell_body(); + + // 三角形支脚(沿 PCB 长边下方) + stand_len = outer_l * 0.72; + stand_x = (outer_l - stand_len) / 2; + // 支脚从后盖底面向后延伸 + translate([stand_x, -stand_foot + 0.5, 0]) + difference() { + cube([stand_len, stand_foot, shell_h]); + // 挖空减料 + translate([2, 2, wall]) + cube([stand_len - 4, stand_foot - 2, shell_h]); + // 斜切贴合桌面视觉(打印可平放) + translate([-0.1, 0, shell_h]) + rotate([-stand_angle + 90, 0, 0]) + cube([stand_len + 0.2, 40, 40]); + } + + // 后支撑肋 + translate([outer_l * 0.5 - 1.2, -stand_foot + 1, wall]) + cube([2.4, stand_foot - 1, shell_h - wall - 1]); + } +} + +module assembly() { + // 后盖在下 + color("SteelBlue", 0.85) back_with_stand(); + + // PCB:元件朝下坐在腔顶 + translate([wall + gap, wall + gap, wall + cavity_back]) + rotate([180, 0, 0]) + translate([0, -pcb_w, -pcb_t]) + %cyd_mock(); + + // 前框盖上 + translate([0, 0, wall + cavity_back + pcb_t + disp_h + bezel_front_t]) + rotate([180, 0, 0]) + translate([0, -outer_w, 0]) + color("SlateGray", 0.7) front_bezel(); +} + +if (part == "front") + front_bezel(); +else if (part == "back") + back_with_stand(); +else + assembly(); diff --git a/cad/export/back.stl b/cad/export/back.stl new file mode 100644 index 0000000000000000000000000000000000000000..77f0eb2a6e490b5962e4b81abbdc11afe3b13c86 GIT binary patch literal 70784 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 literal 0 HcmV?d00001 diff --git a/cad/export/front.stl b/cad/export/front.stl new file mode 100644 index 0000000000000000000000000000000000000000..1daf666ff03a3a6100bc14aa8b16f170746ac105 GIT binary patch literal 29684 zcmb`QO~|FmRmY1+j0jl-H%Z(D5kZ837A27Mt?tZdlmT6YnD~hx9gsv9W+Q}9%t8i< zx-cL!kvI$O8Iu+@=!i1?UKbN=&5R#N6m_zY3<1FyX7Gc~LOlOdb)NcF-M8+27C!L4 zUGJ^)uTypE{7;>yp1Mz`^#6PP@Eh1(Ir6WkAOD;DOMm-2S)V=q)Ia5yU;1q?>+iTG z1f-t&%9l5ENbBDb@yCDnTz>s$KA%KUa~9(7QPmL_UM$o`rKNze*w1n1z^Uw?H+uPF77 zul+Y{^{XFx$KlPdK6Wr|g*HLiXRllLH$VT=hkyLh*KP_yi*nXb&MB#V>6vv9jj|!W z`RgA&yz`k)x(>88*ax;U#7AEI_~EDSKX|uvpeE6}C>x?*<9v+v`NXZ1o*ATtD@RIw z_-p^Wd16W{1f{#obx`U@{_q=b1*6soNy(oyDxu{mDxV;V zePlo7s9tIG_P1TV?<2a_>i39{74%c~t zxssCnR{kp;r<|h4RL^SL8X$IVD3}HAJFzlr#D0y}D|>N~}RS)8A4uHXR#l z6Kha*D@S~E4jvl^6Fm{-On*zsdUR|&Ppm;X)8A71hOdA0_@T;L_oeVxNafKhfBwaf zA3ybivo^5?<$UADn{GvXOi75}f8oJ>n~%gAlwC*o^bma#q8isFXK%BW%^)HirMND6 z1Z@m91mYE?7_U4^H;Who@rqK6R~d245b|_+e6kCX%+YPMzLqyz83K$N`Oz}~GRhES z6#SKFGRhEOR00pUB9l>uz=}gD&J9-_Lx6(`(SZ6qcaXhON)@qj5G*1e$w5QdI5rM~ zMdTwnXb5r;EFvGtK|>g0Hx7bD1Y78u#|-m6+DbhEUAa zyj#x(DRD(UpwCgxDJkY^-lrPpxqQGsx16+<4WXF3A$pB7Ds*U8TiFoJ6MY8tY0Npw zK8Be8jUV1C*(2r|Ww&g%ZsUjRfRuZCv@1Kcvh}L*+;yn^W_M?8E4xRArQNz$VO@-H z@Dg~8>1dJRLQ!bPy#F9N z0Dd1e^hzEQ*$dHYgxIC^&m^YowPDKKD=}CYT(4uU2?W=!QO8^ph7o=o<_v;T@R&0QO2K2!ASeZoIfI}SJmwTpl3!XsHouzk9a_3d*BMl_?0Hm|g7@Uw zctw>1Jdt-0Ce_hZ)_xbw2csILkkuHvT}6nornft0mPBX_k%dvmNk(Sv>naB##&)~D zD(b%tL0gUOP6$^Z9qNfz%j)g*4x64oFiO2TR$cMC-;>|C@hM~0GdXJXZ!ot@k*R?g zbQGbOC*HX6%4VR|`^4HKf>Me~-~0AU2u0K< zo;-NF)VJ=HvVl+E%Z?y=jZnt%>;HDk5k#*M?7L#+TmI}_7e-F0YT=6rpNA@?$ah%> zr4&UkTbELbF_&kMQi?<||7%-ON|7&SbB&;s;$zI_8bK*V@R-dtLN$?>aQ@c_wySmJ z#*Oz}nDbGJSxsYXsoEH2C{@dhaFw;ePDA@$W1BU<4{7s}Ys>b#NEZ?C-2uHufOmd( z)MhZR5#XKQfwdVtg8)DLp0@e!IYj4&??yF#oI!ve7U2}-oLy%S;D={5mvtb1_!Jn_MwKIRCb*9fup)h~UYBZyuj6dAtyrSEqH(QAY_LuAM)L>^cq1a zu`4XSMu_KOD@CHP!Wto-$9xdIMzAMDuMvu(VO>Shu-h8Jz7y>T{VKD!E6WT^@6*Ax zmUH7etbJW|9gLU(7I~y@vze=?|DF(Fk*{iP&d(sgB40V%oS#8}MZTW5**t>)i+nwA z9&-i(7WrD+{OSw>Eb_Is&Hot$SmbMM^OA~4^%~~`^XzNwpyOHue970rVH_JG;<679mJLdH2qWXPPSDjQLO(Lj3(5gFq1VXZYi<65gdfLId) z5$!AT=o)*)o@hTHe4)xl`ynOmqtui$l#1)~QTyAo`#{GyBv~)!?aX(gEY5^%&6eUX z)yz_|0ufzLw@=@{2)hjk&oH80)8%^$+{=h^^rQAF5Nb=i5tN#HCFGdP#eR}%PIh#P zW=}Pm+t{ZHM4g*y|EhGv>3)(mV;rnIQ!yWEyN^R-vA>61l|blo`*G=*kDe!n6ni!3 z5YV2!_Wpj?zudj8XsssS_2{#=73)>zH6p(CSMJ_cw3Yi4!Y+w42anLSMDd+f~X2Y$Ss*{0oTQIxI6pSJd2xl_+}QG&?SyGTe-q~! zWkWn+rzSu4d_5PAnnW)_*$~g#`Q3*P{lssg-BHf3zVaE2!$yA9MxI!Ma(?~iJ`2CH z89cTbOsqjU|IdGXcEKpx3SM#=eayzX^jxib9L8}5fms{oWAt`4UVqAwy`mIm&_w%H zj_e6+b*5L8g00RVC`F61hrLgg=Y3b6h&O%U-WKiF2ui*6?!7JAtr3*^&VRVKMY}bE zQg^;$2(GgYrg+uIK`Eh?n^wVBzWmtnFY9+R5=V=2=9p6YXFK2Z+6O(BB-WsuIi~F- z;7w^{4a%8ga@HOjYZGfw&Ky%k{Mz&NbTn&Fb{&1>h}1*eg go4pvR>APSTBefwA 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 O2)Bb#o&of|rT+u+PXyfn literal 0 HcmV?d00001 diff --git a/cad/generate_stl.py b/cad/generate_stl.py new file mode 100644 index 0000000..122f28c --- /dev/null +++ b/cad/generate_stl.py @@ -0,0 +1,200 @@ +#!/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()