From ce04e7bb1aaa5c5679c277e1f94b90fbb8babaf2 Mon Sep 17 00:00:00 2001 From: "DESKTOP-PUHFO60\\dsx2016" Date: Sat, 17 Oct 2026 15:10:51 +0800 Subject: [PATCH] Relocate DHT11 to rear snap bay and fix front/back closing lips. Regenerate STLs, update viewer closed pose, and document assembly. Co-authored-by: Cursor --- README.md | 1 + cad/README.md | 85 ++---- cad/cyd_climate_case.scad | 371 ++++++++------------------ cad/export/back.stl | Bin 70784 -> 76184 bytes cad/export/front.stl | Bin 29684 -> 28084 bytes cad/generate_stl.py | 312 ++++++++++++++-------- cad/viewer.html | 532 ++++++++++++++++---------------------- 7 files changed, 564 insertions(+), 737 deletions(-) diff --git a/README.md b/README.md index aad871c..bbe09f1 100644 --- a/README.md +++ b/README.md @@ -90,3 +90,4 @@ README.md # 本文档 | 2026-10-17 | 新增 CYD+DHT11 桌面斜立 3D 外壳(OpenSCAD / STL) | | 2026-10-17 | 增加浏览器 Three.js STL 预览页 `cad/viewer.html` | | 2026-10-17 | 预览增强:合盖/开盖、分解、装配动画、剖切/线框等 | +| 2026-10-17 | 外壳改定:DHT11 后盖卡位微突出;前后盖止口合盖 | diff --git a/cad/README.md b/cad/README.md index 1e9b35a..bfaddf5 100644 --- a/cad/README.md +++ b/cad/README.md @@ -1,86 +1,55 @@ # CYD 温湿度仪表盘 3D 外壳 -桌面斜立分体外壳,适配 **ESP32-2432S028R(CYD)** + **DHT11(CN1 / GPIO22)**。 +桌面斜立分体外壳,适配 **ESP32-2432S028R(CYD)** + **DHT11**。 + +## 设计要点(当前) + +- **DHT11 在后盖外侧**:卡位槽 + 左右卡舌 + 前端止挡,模块从背面按入 +- **传感器微突出**约 **1.8 mm**(通风口/探头露出后盖平面) +- **前后盖合盖**:前框公止口落入后盖母槽;可用 `4×M3×10` 辅助固定 +- USB 侧孔、BOOT/RST 开孔、桌面支脚 ## 浏览器预览 -不要直接双击 HTML(`file://` 无法加载 STL)。在项目根目录执行: - ```bash python -m http.server 8765 --directory cad ``` -然后打开:**http://localhost:8765/viewer.html** - -### 预览操作 +打开:**http://localhost:8765/viewer.html** | 操作 | 说明 | |------|------| -| 合盖 / 开盖 | 前框铰链开合动画 | -| 开合度滑条 | 0% 合拢 → 100% 打开 | -| 分解滑条 | 拉开零件看内部 | -| 播放装配 | 分解 → 合拢 → 合盖 全程 | -| 模拟板 / DHT | 显示 PCB 与传感器示意 | -| 线框 / 透视 / 剖切 | 看结构与开孔 | -| 等轴/正/侧/顶 | 快速视角 | -| 截图 | 导出 PNG | -| 快捷键 | `O` 开盖 · `C` 合盖 · `空格` 播放 · `W` 线框 · `X` 透视 | - -其它查看方式:切片软件打开 `export/*.stl`,或 [viewstl.com](https://www.viewstl.com/) / [3dviewer.net](https://3dviewer.net/)。 +| 合盖 / 开盖 | 前框相对后盖开合 | +| 开合度 / 分解 | 拖动查看止口与 DHT 仓 | +| 看后盖 | 快速转到背面看 DHT 突出 | +| 模拟屏板 / DHT11模块 | 示意件开关 | +| 快捷键 | `C` 合盖 · `O` 开盖 · `空格` 播放装配 | ## 零件 | 文件 | 说明 | |------|------| -| `viewer.html` | 浏览器 3D 预览 | -| `export/front.stl` | 前框(屏窗 + 压边) | -| `export/back.stl` | 后盖 + 斜立支脚 + DHT11 通风仓 | -| `cyd_climate_case.scad` | 参数化源文件(OpenSCAD,可改尺寸) | -| `generate_stl.py` | 无 OpenSCAD 时用 Python 导出 STL | +| `export/front.stl` | 前框(屏窗 + 公止口) | +| `export/back.stl` | 后盖(母止口 + DHT 卡位仓 + 支脚) | +| `generate_stl.py` | Python 生成 STL | +| `cyd_climate_case.scad` | OpenSCAD 参数源 | +| `viewer.html` | 交互预览 | -## 设计要点 - -- 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` 顶部参数后重新导出。 +PLA/PETG,0.2 mm,墙 2–3 圈,填充 15–20%,一般无需支撑。 +后盖建议 **外底朝下**(DHT 仓朝上或按切片预览,必要时对 DHT 卡舌加少量支撑)。 ## 装配 -1. DHT11 放入后盖通风仓,杜邦线经线槽接到 **CN1**(`3V3 / IO22 / GND`,按你现有接线) -2. CYD 屏朝前框放入,USB 对齐侧孔 -3. 合上后盖,四角 M3×10 拧紧(勿过紧防裂) -4. 桌面斜立放置;点屏可调背光 - -若孔位与你的板子有 ±1 mm 偏差,优先改 `generate_stl.py` 里 `USB_Y` / `MH_INSET` 后重导。 +1. 将 DHT11 模块从 **后盖背面** 按入卡位,探头朝外微突出;线经仓内小孔接到 CN1(GPIO22) +2. CYD 放入后盖腔体,USB 对齐 +3. 前框止口对准后盖槽 **合盖**,四角螺丝固定 +4. 桌面斜立使用 diff --git a/cad/cyd_climate_case.scad b/cad/cyd_climate_case.scad index e8a64a8..8c795e6 100644 --- a/cad/cyd_climate_case.scad +++ b/cad/cyd_climate_case.scad @@ -1,300 +1,131 @@ /* - * CYD ESP32-2432S028R + DHT11 桌面斜立外壳 - * Climate Dashboard Case — parametric OpenSCAD + * CYD + DHT11 外壳(与 generate_stl.py 一致) * - * 零件: - * part = "front" 前框(屏窗) - * part = "back" 后盖 + 斜立支架 + DHT 通风仓 - * part = "assembly" 装配预览(半透明板子) + * - DHT11:后盖外侧卡位仓,传感器微突出 + * - 前后盖:公/母止口合盖 * - * 导出 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 + * part = "front" | "back" | "assembly" */ -/* [Which part] */ -part = "assembly"; // ["front", "back", "assembly"] +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 孔 +pcb_l = 86; pcb_w = 50; +wall = 2.4; gap = 0.35; +bezel_t = 2.2; cavity = 7.5; +lip_h = 2.4; lip_t = 1.15; +disp_h = 4; skirt_h = disp_h + 1; +view_l = 59.5; view_w = 45; view_ix = 7; view_iy = 2.5; disp_x = 8; +mh_inset = 4; screw = 3.3; boss_od = 7; boss_id = 2.5; +usb_w = 15; usb_h = 9; usb_y = 30.5; +stand_foot = 16; +dht_l = 16; dht_w = 12.5; dht_h = 8; dht_proud = 1.8; dht_clip = 1.1; -/* [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; +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; +$fn = 40; -/* [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; // 支脚接触深度 +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]]; -/* [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 rr(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; - +module front() { + z1 = bezel_t + skirt_h; difference() { union() { - // 前表面 - rounded_rect(outer_l, outer_w, bezel_front_t, 3); - // 侧壁向下包住屏+PCB 前半 - translate([0, 0, bezel_front_t]) + rr(outer_l, outer_w, z1, 3); + // male lip + translate([wall-lip_t, wall-lip_t, z1-0.05]) 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); + rr(outer_l-2*(wall-lip_t), outer_w-2*(wall-lip_t), lip_h, 2.2); + translate([lip_t+0.15, lip_t+0.15, -0.1]) + rr(outer_l-2*wall-0.3, outer_w-2*wall-0.3, lip_h+0.5, 1.4); } } - - // 可视窗 - 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]); - } + translate([wall,wall,bezel_t]) rr(inner_l, inner_w, z1, 1.8); + // view + vx = wall+gap+disp_x+view_ix; vy = wall+gap+view_iy; + translate([vx-0.25, vy-0.25, -0.1]) cube([view_l+0.5, view_w+0.5, bezel_t+1]); + // screen pocket + translate([wall+gap+disp_x-0.25, wall+gap-0.15, bezel_t-0.05]) + cube([69.5, 50.3, disp_h+0.8]); + for (p=mh) + translate([wall+gap+p[0], wall+gap+p[1], -0.1]) + cylinder(d=screw+0.2, h=z1+lip_h+1); } } module dht_bay() { - // 局部:仓体贴在 outer 长边外侧 + cx = wall+gap+62; cy = wall+gap+pcb_w/2; + bay_l = dht_l+3.2; bay_w = dht_w+3.2; + bay_h = dht_h + wall*0.6 + dht_proud; 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]); + translate([cx-bay_l/2, cy-bay_w/2, -bay_h+wall*0.35]) + cube([bay_l, bay_w, bay_h+0.2]); + // cavity + translate([cx-dht_l/2-0.25, cy-dht_w/2-0.25, -bay_h+wall*0.35+1]) + cube([dht_l+0.5, dht_w+0.5, bay_h]); + // grill + translate([cx-dht_l/2+1.2, cy-dht_w/2+1.2, -bay_h-0.2]) + cube([dht_l-2.4, dht_w-2.4, wall+dht_proud+0.8]); + // wire hole into case + translate([cx-3.5, cy-4, wall-0.5]) cube([7, 8, 3]); } + // 卡位 left/right + z0 = -bay_h+wall*0.35+1.2; + translate([cx-dht_l/2-1.2, cy-3.5, z0]) cube([1.2+dht_clip, 7, 3.2]); + translate([cx+dht_l/2-dht_clip, cy-3.5, z0]) cube([1.2+dht_clip, 7, 3.2]); + // stop ledge + translate([cx-dht_l/2+1.5, cy-dht_w/2+0.8, -bay_h+wall*0.35+0.8]) + cube([dht_l-3, dht_w-1.6, 1]); } -// 带斜立支架的后盖(打印时支脚朝下) -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]) +module back() { + difference() { + union() { + rr(outer_l, outer_w, shell_h, 3); + // collar + female slot 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([0,0,shell_h-0.05]) rr(outer_l, outer_w, lip_h, 3); + translate([wall+0.2, wall+0.2, shell_h-0.2]) + rr(outer_l-2*wall-0.4, outer_w-2*wall-0.4, lip_h+0.5, 1.6); } - - // 后支撑肋 - translate([outer_l * 0.5 - 1.2, -stand_foot + 1, wall]) - cube([2.4, stand_foot - 1, shell_h - wall - 1]); + for (p=mh) + translate([wall+gap+p[0], wall+gap+p[1], wall]) + difference() { + cylinder(d=boss_od, h=cavity-0.4); + translate([0,0,1]) cylinder(d=boss_id, h=cavity); + } + // stand + translate([(outer_l-outer_l*0.7)/2, -stand_foot+0.4, 0]) + difference() { + cube([outer_l*0.7, stand_foot, shell_h*0.85]); + translate([2,2,wall]) cube([outer_l*0.7-4, stand_foot-2, shell_h]); + } + dht_bay(); + } + translate([wall,wall,wall]) rr(inner_l, inner_w, shell_h+lip_h, 1.8); + // female ring cut + translate([wall-lip_t-0.12, wall-lip_t-0.12, shell_h+0.15]) + difference() { + cube([outer_l-2*(wall-lip_t-0.12), outer_w-2*(wall-lip_t-0.12), lip_h-0.3]); + translate([lip_t+0.15, lip_t+0.15, -0.1]) + cube([outer_l-2*wall-0.1, outer_w-2*wall-0.1, lip_h]); + } + // USB + translate([-0.1, wall+gap+usb_y-usb_w/2, wall+1.2]) cube([wall+1.2, usb_w, usb_h]); } } -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(); +else if (part=="back") back(); +else { + color("teal", 0.85) back(); + translate([0,0,shell_h-2.2]) color("slategray", 0.75) + translate([0, outer_w, bezel_t+skirt_h+lip_h]) rotate([180,0,0]) front(); } - -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 index 77f0eb2a6e490b5962e4b81abbdc11afe3b13c86..3bb6f18a02bd5a264d716095f62137e0171e517b 100644 GIT binary patch literal 76184 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 diff --git a/cad/export/front.stl b/cad/export/front.stl index 1daf666ff03a3a6100bc14aa8b16f170746ac105..bfdb9861ff4dcbe90ef7e4d4d127a514ccc46987 100644 GIT binary patch literal 28084 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 literal 29684 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 go4pvR>APSTBefwA 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 O2)Bb#o&of|rT+u+PXyfn diff --git a/cad/generate_stl.py b/cad/generate_stl.py index 122f28c..c76a900 100644 --- a/cad/generate_stl.py +++ b/cad/generate_stl.py @@ -1,33 +1,43 @@ #!/usr/bin/env python3 -"""Generate printable STL for CYD climate case using trimesh + manifold boolean.""" +""" +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 numpy as np import trimesh -from trimesh.creation import box, cylinder +from trimesh.creation import box, cylinder # noqa: F401 — cylinder used for holes/bosses -# ---- mm, aligned with cyd_climate_case.scad ---- +# ---- mm ---- 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 +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.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 +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.2, 2.5 +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), @@ -37,141 +47,240 @@ MH = [ ] -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 + 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([x0 + dx / 2, y0 + dy / 2, z0 + dz / 2]) + m.apply_translation([xa + dx / 2, ya + dy / 2, za + dz / 2]) return m def u(*meshes: trimesh.Trimesh) -> trimesh.Trimesh: - result = meshes[0] + r = meshes[0] for m in meshes[1:]: - result = result.union(m, engine="manifold") - return result + r = r.union(m, engine="manifold") + return r def d(a: trimesh.Trimesh, *subs: trimesh.Trimesh) -> trimesh.Trimesh: - result = a + r = a for s in subs: - result = result.difference(s, engine="manifold") - return result + r = r.difference(s, engine="manifold") + return r def make_front() -> trimesh.Trimesh: - skirt_h = DISP_H + PCB_T_HALF - z1 = BEZEL_T + skirt_h + """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, z1) - cavity = aabb(WALL, WALL, BEZEL_T, OUTER_L - WALL, OUTER_W - WALL, z1 + 1) + 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.3, vy - 0.3, -0.5, vx + VIEW_L + 0.3, vy + VIEW_W + 0.3, BEZEL_T + 0.5) + view = aabb(vx - 0.25, vy - 0.25, -0.5, vx + VIEW_L + 0.25, vy + VIEW_W + 0.25, BEZEL_T + 0.6) - # 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) + 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, height=z1 + 2, sections=32) - hole.apply_translation([x, y, z1 / 2]) + 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 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) +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 - # 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) + # 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 - # SD slot on +Y - sd = aabb(WALL + GAP + 28, OUTER_W - WALL - 1, WALL + 2, WALL + GAP + 44, OUTER_W + 1, WALL + 5.2) + # 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, + ) - # 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]) + # 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, + ) - # vent slots on floor + # 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(5): - v = aabb(WALL + 12 + i * 12, WALL + 8, -0.5, WALL + 14 + i * 12, WALL + 18, WALL + 0.5) - vents.append(v) + 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, + )) - out = d(body, cavity, usb, sd, btn1, btn2, *vents) + # 卡位: 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.3, sections=32) - boss.apply_translation([x, y, WALL + (CAVITY - 0.3) / 2]) - hole = cylinder(radius=BOSS_ID / 2, height=CAVITY + 1, sections=24) + 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 - stand_len = OUTER_L * 0.72 + # stand foot (desk) + stand_len = OUTER_L * 0.7 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 = 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 * 0.5 - 1.2, -STAND_FOOT + 1, WALL, OUTER_L * 0.5 + 1.2, WALL, SHELL_H - 1) + 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) - # 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) + # DHT11 snap bay on outer back (protruding) + out = u(out, dht_pocket_on_back()) out.merge_vertices() return out @@ -181,19 +290,18 @@ def main() -> None: out_dir = Path(__file__).resolve().parent / "export" out_dir.mkdir(parents=True, exist_ok=True) - print("Building front…") + print("Building front (合盖公止口)…") front = make_front() - print("Building back…") + print("Building back (DHT后盖卡位 + 母止口)…") 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"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__": diff --git a/cad/viewer.html b/cad/viewer.html index 020ea3a..9437581 100644 --- a/cad/viewer.html +++ b/cad/viewer.html @@ -3,31 +3,26 @@ - CYD Climate Case — Interactive Preview + CYD Case — 合盖 / DHT后盖卡位

-

CYD Climate Case

-

左键旋转 · 滚轮缩放 · 右键平移 · 触屏双指缩放

+

CYD Case Preview

+

DHT11 在后盖外侧卡位,微微突出 · 前后盖止口合盖

-
合盖 / 开盖
+
合盖
- + @@ -65,14 +59,10 @@ 0%
- -
-
分解距离
-
- 分解 - - 0 -
+
+ 分解 + + 0
@@ -80,8 +70,8 @@
- - + +
@@ -92,13 +82,7 @@ - - -
-
@@ -107,14 +91,14 @@
+ -
-
外壳约 91.6 × 55.6 mm(外轮廓)
+

合盖=0%:前框止口落入后盖槽。DHT 从后盖背面卡入并突出约 1.8mm。

加载中…
@@ -140,70 +124,49 @@ renderer.setPixelRatio(Math.min(devicePixelRatio, 2)); renderer.setSize(innerWidth, innerHeight, false); renderer.outputColorSpace = THREE.SRGBColorSpace; - renderer.localClippingEnabled = true; const scene = new THREE.Scene(); scene.background = new THREE.Color(0x0b1220); - const cameraPersp = new THREE.PerspectiveCamera(45, innerWidth / innerHeight, 0.1, 3000); - const cameraOrtho = new THREE.OrthographicCamera(-1, 1, 1, -1, 0.1, 3000); - let camera = cameraPersp; - camera.position.set(130, 95, 150); + const camera = new THREE.PerspectiveCamera(42, innerWidth / innerHeight, 0.1, 4000); + camera.position.set(120, 80, 140); const controls = new OrbitControls(camera, canvas); controls.enableDamping = true; - controls.target.set(46, 8, 5); - scene.add(new THREE.AmbientLight(0xffffff, 0.5)); + scene.add(new THREE.AmbientLight(0xffffff, 0.55)); const key = new THREE.DirectionalLight(0xffffff, 1.1); - key.position.set(90, 130, 70); + key.position.set(80, 120, 60); scene.add(key); const fill = new THREE.DirectionalLight(0x88aacc, 0.4); - fill.position.set(-70, 50, -50); + fill.position.set(-60, 40, -80); scene.add(fill); - const grid = new THREE.GridHelper(220, 22, 0x3db8a8, 0x1e2c40); - grid.position.y = -22; + const grid = new THREE.GridHelper(240, 24, 0x3db8a8, 0x1e2c40); + grid.position.y = -40; scene.add(grid); const root = new THREE.Group(); scene.add(root); - const clipPlane = new THREE.Plane(new THREE.Vector3(-1, 0, 0), 46); - const matFront = new THREE.MeshStandardMaterial({ - color: 0x6a7b92, metalness: 0.12, roughness: 0.52, flatShading: true, - side: THREE.DoubleSide, clippingPlanes: [], clipShadows: true, + color: 0x6a7b92, metalness: 0.12, roughness: 0.5, flatShading: true, side: THREE.DoubleSide, }); const matBack = new THREE.MeshStandardMaterial({ - color: 0x2f9f92, metalness: 0.1, roughness: 0.48, flatShading: true, - side: THREE.DoubleSide, clippingPlanes: [], - }); - const matBoard = new THREE.MeshStandardMaterial({ - color: 0xc9a227, metalness: 0.35, roughness: 0.4, flatShading: true, + color: 0x2f9f92, metalness: 0.1, roughness: 0.48, flatShading: true, side: THREE.DoubleSide, }); + const matBoard = new THREE.MeshStandardMaterial({ color: 0xc9a227, metalness: 0.3, roughness: 0.4, flatShading: true }); const matScreen = new THREE.MeshStandardMaterial({ - color: 0x152238, metalness: 0.2, roughness: 0.35, emissive: 0x1a3048, emissiveIntensity: 0.35, - }); - const matDht = new THREE.MeshStandardMaterial({ - color: 0x3a3a3a, metalness: 0.1, roughness: 0.7, flatShading: true, + color: 0x101820, emissive: 0x1a3048, emissiveIntensity: 0.4, roughness: 0.35, }); + const matDht = new THREE.MeshStandardMaterial({ color: 0x333333, roughness: 0.65, flatShading: true }); + const matBlue = new THREE.MeshStandardMaterial({ color: 0x2a5cff, roughness: 0.4 }); - let meshFront = null; - let meshBack = null; - let boardGroup = null; - let dhtMesh = null; - let hinge = null; // pivot for lid - let lidT = 0; // 0 closed .. 1 open - let explodeT = 0; // 0..1 - let autoRotate = false; - let anim = null; // { from, to, t0, dur, kind } - - const CLOSED = { - lid: 0, - explode: 0, - frontLift: 10.5, - }; + let meshFront, meshBack, boardGroup, dhtGroup; + let frontPivot, backPivot; + let closedGap = 0; // measured stack height between parts when closed + let lidT = 0, explodeT = 0, autoRotate = false, anim = null; + let backSize = new THREE.Vector3(); function setStatus(msg, err = false) { statusEl.textContent = msg; @@ -215,137 +178,143 @@ return new Promise((resolve, reject) => { loader.load(url, (geo) => { geo.computeVertexNormals(); - geo.center(); resolve(geo); }, undefined, reject); }); } - function makeBoard() { + /** Put mesh so its bounding box min corner is at origin, Z up. */ + function normalizeMesh(mesh) { + mesh.geometry.computeBoundingBox(); + const bb = mesh.geometry.boundingBox.clone(); + const size = new THREE.Vector3(); + bb.getSize(size); + // STL from generator: XY = board plane, Z = thickness. Rotate to stand: screen faces +Z + mesh.rotation.set(0, 0, 0); + mesh.geometry.translate(-bb.min.x, -bb.min.y, -bb.min.z); + mesh.geometry.computeBoundingBox(); + return size; + } + + function makeBoard(w, h) { const g = new THREE.Group(); - const pcb = new THREE.Mesh(new THREE.BoxGeometry(86, 1.6, 50), matBoard); - const screen = new THREE.Mesh(new THREE.BoxGeometry(59.5, 0.6, 45), matScreen); - screen.position.set(4, 2.2, 0); - // bezel frame - const bezel = new THREE.Mesh(new THREE.BoxGeometry(69, 3.8, 50), new THREE.MeshStandardMaterial({ - color: 0x222830, roughness: 0.6, flatShading: true, + const pcb = new THREE.Mesh(new THREE.BoxGeometry(86, 50, 1.6), matBoard); + const bezel = new THREE.Mesh(new THREE.BoxGeometry(69, 50, 3.8), new THREE.MeshStandardMaterial({ + color: 0x222830, roughness: 0.55, flatShading: true, })); - bezel.position.set(4, 1.2, 0); - g.add(bezel); + bezel.position.set(4, 0, 2.7); + const screen = new THREE.Mesh(new THREE.BoxGeometry(59.5, 45, 0.5), matScreen); + screen.position.set(4, 0, 4.7); g.add(pcb); + g.add(bezel); g.add(screen); - // mount pins visual - for (const [x, z] of [[-39, -21], [39, -21], [39, 21], [-39, 21]]) { - const pin = new THREE.Mesh( - new THREE.CylinderGeometry(1.2, 1.2, 2, 12), - new THREE.MeshStandardMaterial({ color: 0x888888 }) - ); - pin.position.set(x, -1.2, z); - g.add(pin); - } + g.position.set(w / 2, h / 2, 0); return g; } function makeDht() { const g = new THREE.Group(); - const body = new THREE.Mesh(new THREE.BoxGeometry(16, 10, 12), matDht); - const grill = new THREE.Mesh( - new THREE.BoxGeometry(12, 0.4, 8), - new THREE.MeshStandardMaterial({ color: 0x666666 }) - ); - grill.position.y = 5.2; + const body = new THREE.Mesh(new THREE.BoxGeometry(16, 12.5, 8), matDht); + const sensor = new THREE.Mesh(new THREE.BoxGeometry(12, 9, 2.2), matBlue); + sensor.position.z = -5.1; // toward outside (negative Z of back exterior) g.add(body); - g.add(grill); + g.add(sensor); return g; } - function fitCamera(object) { - const box = new THREE.Box3().setFromObject(object); + function fit(obj) { + const box = new THREE.Box3().setFromObject(obj); const size = box.getSize(new THREE.Vector3()); const center = box.getCenter(new THREE.Vector3()); controls.target.copy(center); const maxDim = Math.max(size.x, size.y, size.z, 1); - const dist = maxDim * 1.85; - cameraPersp.position.set(center.x + dist * 0.75, center.y + dist * 0.42, center.z + dist * 0.75); - cameraPersp.near = maxDim / 200; - cameraPersp.far = maxDim * 80; - cameraPersp.updateProjectionMatrix(); - syncOrtho(); - if (camera === cameraOrtho) camera.position.copy(cameraPersp.position); + const dist = maxDim * 1.7; + camera.position.set(center.x + dist * 0.65, center.y - dist * 0.55, center.z + dist * 0.55); + camera.near = maxDim / 200; + camera.far = maxDim * 60; + camera.updateProjectionMatrix(); controls.update(); } - function syncOrtho() { - const aspect = innerWidth / innerHeight; - const dist = cameraPersp.position.distanceTo(controls.target); - const half = dist * 0.35; - cameraOrtho.left = -half * aspect; - cameraOrtho.right = half * aspect; - cameraOrtho.top = half; - cameraOrtho.bottom = -half; - cameraOrtho.near = cameraPersp.near; - cameraOrtho.far = cameraPersp.far; - cameraOrtho.position.copy(cameraPersp.position); - cameraOrtho.quaternion.copy(cameraPersp.quaternion); - cameraOrtho.updateProjectionMatrix(); - } - + /** + * Assembly frame (mm): + * - Back: exterior face at z=0 (DHT protrudes to z<0), rim toward +Z + * - Board: inside back cavity + * - Front: closes from +Z onto back rim when lidT=0 + */ function applyPose() { - if (!hinge || !meshBack) return; + if (!backPivot || !frontPivot) return; - // explode separates parts along Y / hinge axis - const ex = explodeT * 28; - meshBack.position.set(0, -ex * 0.35, 0); + const ex = explodeT * 35; + // open lid: lift + hinge rotate around Y at top edge + const open = lidT; + const lift = open * 28 + ex; + const hinge = open * THREE.MathUtils.degToRad(105); - // lid: 0 closed -> 1 fully open (~110 deg) - const angle = lidT * THREE.MathUtils.degToRad(110); - hinge.rotation.z = 0; - hinge.rotation.x = -angle; - hinge.position.set(0, CLOSED.frontLift + ex, 0); + backPivot.position.set(0, 0, -ex * 0.25); + backPivot.rotation.set(0, 0, 0); + + // closed: front sits on top of back rim + frontPivot.position.set(0, 0, closedGap + lift); + frontPivot.rotation.set(hinge, 0, 0); + // hinge pivot offset: rotate around top (+Y) edge + if (meshFront) { + const hy = backSize.y; + frontPivot.position.y = Math.sin(hinge) * 0; // keep + // rotate around top edge: move pivot + frontPivot.position.set(0, hy * 0.5 * (1 - Math.cos(hinge)), closedGap + lift + hy * 0.15 * Math.sin(hinge)); + frontPivot.rotation.set(hinge, 0, 0); + // compensate so mesh origin stays consistent — use sub-offset + meshFront.position.set(0, -hy * 0.0, 0); + } - // board sits between shells if (boardGroup) { - boardGroup.position.set(0, 4 + ex * 0.15, 0); + // sit inside back: above floor wall (~2.4) + boardGroup.position.set(backSize.x / 2, backSize.y / 2, 4.5 + ex * 0.1); boardGroup.rotation.set(0, 0, 0); } - if (dhtMesh) { - dhtMesh.position.set(28, 2 + ex * 0.1, 36); + + if (dhtGroup) { + // DHT bay is on back exterior around x~62+wall, y center — match generator + const cx = 2.4 + 0.35 + 62; + const cy = 2.4 + 0.35 + 25; + dhtGroup.position.set(cx, cy, -2.0 - 0.2 * ex); // slightly outside back face + dhtGroup.rotation.set(0, 0, 0); } } - function setLid(v, updateUi = true) { + function setLid(v, ui = true) { lidT = THREE.MathUtils.clamp(v, 0, 1); - if (updateUi) { + if (ui) { $("lid").value = String(Math.round(lidT * 100)); $("lid-val").textContent = `${Math.round(lidT * 100)}%`; } applyPose(); + $("btn-close").classList.toggle("active", lidT < 0.05); + $("btn-open").classList.toggle("active", lidT > 0.95); } - function setExplode(v, updateUi = true) { + function setExplode(v, ui = true) { explodeT = THREE.MathUtils.clamp(v, 0, 1); - if (updateUi) { + if (ui) { $("explode").value = String(Math.round(explodeT * 100)); $("explode-val").textContent = String(Math.round(explodeT * 100)); } applyPose(); } - function startAnim(kind) { - const now = performance.now(); - if (kind === "close") { - anim = { kind, fromLid: lidT, toLid: 0, fromEx: explodeT, toEx: 0, t0: now, dur: 900 }; - } else if (kind === "open") { - anim = { kind, fromLid: lidT, toLid: 1, fromEx: explodeT, toEx: 0, t0: now, dur: 900 }; - } else if (kind === "play") { - // assembly sequence: explode open -> close explode -> close lid - anim = { kind: "play", phase: 0, t0: now, dur: 700, fromLid: lidT, toLid: 1, fromEx: explodeT, toEx: 1 }; - } - setStatus(kind === "play" ? "播放装配动画…" : kind === "open" ? "开盖中…" : "合盖中…"); + function ease(t) { + return t < 0.5 ? 2 * t * t : 1 - (Math.pow(-2 * t + 2, 2) / 2); } - function ease(t) { - return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2; + function startAnim(kind) { + const now = performance.now(); + if (kind === "close") anim = { kind, t0: now, dur: 850, fromL: lidT, toL: 0, fromE: explodeT, toE: 0 }; + if (kind === "open") anim = { kind, t0: now, dur: 850, fromL: lidT, toL: 1, fromE: explodeT, toE: 0 }; + if (kind === "play") { + anim = { kind: "play", phase: 0, t0: now, dur: 700, fromL: lidT, toL: 1, fromE: explodeT, toE: 1 }; + } + setStatus(kind === "play" ? "装配播放中…" : kind === "open" ? "开盖…" : "合盖…"); } function tickAnim(now) { @@ -353,81 +322,26 @@ const u = ease(Math.min(1, (now - anim.t0) / anim.dur)); if (anim.kind === "play") { if (anim.phase === 0) { - setLid(THREE.MathUtils.lerp(anim.fromLid, 1, u), true); - setExplode(THREE.MathUtils.lerp(anim.fromEx, 1, u), true); - if (u >= 1) { - anim = { kind: "play", phase: 1, t0: now, dur: 800, fromLid: 1, toLid: 1, fromEx: 1, toEx: 0 }; - } + setLid(THREE.MathUtils.lerp(anim.fromL, 1, u)); + setExplode(THREE.MathUtils.lerp(anim.fromE, 1, u)); + if (u >= 1) anim = { kind: "play", phase: 1, t0: now, dur: 750, fromL: 1, toL: 1, fromE: 1, toE: 0 }; } else if (anim.phase === 1) { - setExplode(THREE.MathUtils.lerp(1, 0, u), true); - if (u >= 1) { - anim = { kind: "play", phase: 2, t0: now, dur: 900, fromLid: 1, toLid: 0, fromEx: 0, toEx: 0 }; - } + setExplode(THREE.MathUtils.lerp(1, 0, u)); + if (u >= 1) anim = { kind: "play", phase: 2, t0: now, dur: 900, fromL: 1, toL: 0, fromE: 0, toE: 0 }; } else { - setLid(THREE.MathUtils.lerp(1, 0, u), true); - if (u >= 1) { - anim = null; - setStatus("装配完成(合盖)"); - } + setLid(THREE.MathUtils.lerp(1, 0, u)); + if (u >= 1) { anim = null; setStatus("已合盖:前框止口落入后盖"); } } return; } - setLid(THREE.MathUtils.lerp(anim.fromLid, anim.toLid, u), true); - setExplode(THREE.MathUtils.lerp(anim.fromEx, anim.toEx, u), true); + setLid(THREE.MathUtils.lerp(anim.fromL, anim.toL, u)); + setExplode(THREE.MathUtils.lerp(anim.fromE, anim.toE, u)); if (u >= 1) { anim = null; setStatus(lidT < 0.05 ? "已合盖" : "已开盖"); } } - function setWire(on) { - for (const m of [matFront, matBack, matBoard, matScreen, matDht]) { - m.wireframe = on; - m.needsUpdate = true; - } - } - - function setXray(on) { - for (const m of [matFront, matBack]) { - m.transparent = on; - m.opacity = on ? 0.38 : 1; - m.depthWrite = !on; - m.needsUpdate = true; - } - } - - function setCut(on) { - $("cut-row").style.display = on ? "grid" : "none"; - const planes = on ? [clipPlane] : []; - matFront.clippingPlanes = planes; - matBack.clippingPlanes = planes; - matBoard.clippingPlanes = planes; - matScreen.clippingPlanes = planes; - matDht.clippingPlanes = planes; - updateCut(); - } - - function updateCut() { - const v = Number($("cut").value) / 100; - // slide plane across X - clipPlane.constant = 20 + v * 70; - $("cut-val").textContent = String(Math.round(v * 100)); - } - - function setView(name) { - const t = controls.target.clone(); - const d = camera.position.distanceTo(t) || 180; - const map = { - iso: new THREE.Vector3(t.x + d * 0.7, t.y + d * 0.45, t.z + d * 0.7), - front: new THREE.Vector3(t.x, t.y, t.z + d), - side: new THREE.Vector3(t.x + d, t.y, t.z), - top: new THREE.Vector3(t.x, t.y + d, t.z + 0.01), - }; - camera.position.copy(map[name] || map.iso); - if (camera === cameraOrtho) syncOrtho(); - controls.update(); - } - async function boot() { try { const [geoF, geoB] = await Promise.all([ @@ -437,130 +351,134 @@ meshBack = new THREE.Mesh(geoB, matBack); meshFront = new THREE.Mesh(geoF, matFront); + backSize = normalizeMesh(meshBack); + const frontSize = normalizeMesh(meshFront); - // Lay flat-ish for hinge animation: rotate STLs onto XZ plane - meshBack.rotation.x = -Math.PI / 2; - meshFront.rotation.x = -Math.PI / 2; + // Align both on XY; back Z=0 exterior, grows +Z; front will stack on rim + backPivot = new THREE.Group(); + backPivot.add(meshBack); - // Measure back size for hinge pivot at "top" edge (+Z after rotate) - meshBack.updateMatrixWorld(true); - const bb = new THREE.Box3().setFromObject(meshBack); - const size = bb.getSize(new THREE.Vector3()); - const center = bb.getCenter(new THREE.Vector3()); + frontPivot = new THREE.Group(); + frontPivot.add(meshFront); - hinge = new THREE.Group(); - // pivot near top edge of assembly - const pivotLocal = new THREE.Vector3(center.x, 0, bb.max.z - 1); - hinge.position.copy(pivotLocal); + // closedGap ≈ back shell height to rim (geometry max.z) + meshBack.geometry.computeBoundingBox(); + meshFront.geometry.computeBoundingBox(); + const backH = meshBack.geometry.boundingBox.max.z; + const frontH = meshFront.geometry.boundingBox.max.z; + // Front STL: bezel at z=0 (screen face), lip at +Z. For closing onto back, + // flip front so lip faces -Z toward back rim, screen faces +Z toward user. + meshFront.rotation.x = Math.PI; + meshFront.position.z = frontH; // after 180° about X, compensate + meshFront.position.y = frontSize.y; // fix Y flip from rot X - const frontHolder = new THREE.Group(); - frontHolder.add(meshFront); - // offset so when closed, front sits above back - meshFront.position.set(0, 0, 0); - frontHolder.position.set(-pivotLocal.x, 0, -pivotLocal.z); - hinge.add(frontHolder); + closedGap = backH - 2.2; // seat lip into groove (~LIP_H) - boardGroup = makeBoard(); - boardGroup.scale.set(1, 1, 1); - dhtMesh = makeDht(); + boardGroup = makeBoard(backSize.x, backSize.y); + // board local was centered; remake simpler placement + boardGroup.position.set(0, 0, 0); + boardGroup.children.forEach(() => {}); + // rebuild board at origin extents + root.remove(boardGroup); + boardGroup = new THREE.Group(); + { + const pcb = new THREE.Mesh(new THREE.BoxGeometry(86, 50, 1.6), matBoard); + pcb.position.set(2.4 + 0.35 + 43, 2.4 + 0.35 + 25, 5.5); + const bez = new THREE.Mesh(new THREE.BoxGeometry(69, 50, 3.8), new THREE.MeshStandardMaterial({ + color: 0x222830, roughness: 0.55, flatShading: true, + })); + bez.position.set(2.4 + 0.35 + 8 + 34.5, 2.4 + 0.35 + 25, 7.5); + const scr = new THREE.Mesh(new THREE.BoxGeometry(59.5, 45, 0.5), matScreen); + scr.position.set(2.4 + 0.35 + 8 + 7 + 29.75, 2.4 + 0.35 + 2.5 + 22.5, 9.6); + boardGroup.add(pcb, bez, scr); + } - root.add(meshBack); - root.add(hinge); + dhtGroup = makeDht(); + + root.add(backPivot); + root.add(frontPivot); root.add(boardGroup); - root.add(dhtMesh); - - // Center whole assembly at origin-ish - root.updateMatrixWorld(true); - const rootBox = new THREE.Box3().setFromObject(root); - const rootCenter = rootBox.getCenter(new THREE.Vector3()); - root.position.sub(rootCenter); + root.add(dhtGroup); setLid(0); setExplode(0); - fitCamera(root); - $("dims").textContent = - `外轮廓约 ${size.x.toFixed(1)} × ${size.z.toFixed(1)} mm(后盖包围盒) · 开合度可拖动`; - setStatus("就绪:拖「开合度」或点合盖/开盖/播放装配"); + fit(root); + setStatus(`就绪 · 后盖 ${backSize.x.toFixed(1)}×${backSize.y.toFixed(1)} mm · 点「合盖」或拖开合度`); } catch (e) { console.error(e); - setStatus("加载失败:请用 python -m http.server 8765 --directory cad 后打开 viewer.html", true); + setStatus("加载失败:请运行 python -m http.server 8765 --directory cad", true); } } - // UI bindings - $("lid").oninput = (e) => { - anim = null; - setLid(Number(e.target.value) / 100, false); - $("lid-val").textContent = `${e.target.value}%`; - }; - $("explode").oninput = (e) => { - anim = null; - setExplode(Number(e.target.value) / 100, false); - $("explode-val").textContent = e.target.value; - }; + $("lid").oninput = (e) => { anim = null; setLid(Number(e.target.value) / 100, false); $("lid-val").textContent = `${e.target.value}%`; }; + $("explode").oninput = (e) => { anim = null; setExplode(Number(e.target.value) / 100, false); $("explode-val").textContent = e.target.value; }; $("btn-close").onclick = () => startAnim("close"); $("btn-open").onclick = () => startAnim("open"); $("btn-play").onclick = () => startAnim("play"); - $("btn-stop").onclick = () => { anim = null; setStatus("已停止动画"); }; + $("btn-stop").onclick = () => { anim = null; setStatus("已停止"); }; - $("chk-front").onchange = (e) => { if (hinge) hinge.visible = e.target.checked; }; - $("chk-back").onchange = (e) => { if (meshBack) meshBack.visible = e.target.checked; }; + $("chk-front").onchange = (e) => { if (frontPivot) frontPivot.visible = e.target.checked; }; + $("chk-back").onchange = (e) => { if (backPivot) backPivot.visible = e.target.checked; }; $("chk-board").onchange = (e) => { if (boardGroup) boardGroup.visible = e.target.checked; }; - $("chk-dht").onchange = (e) => { if (dhtMesh) dhtMesh.visible = e.target.checked; }; - + $("chk-dht").onchange = (e) => { if (dhtGroup) dhtGroup.visible = e.target.checked; }; $("chk-grid").onchange = (e) => { grid.visible = e.target.checked; }; $("chk-auto").onchange = (e) => { autoRotate = e.target.checked; }; - $("chk-wire").onchange = (e) => setWire(e.target.checked); - $("chk-xray").onchange = (e) => setXray(e.target.checked); - $("chk-cut").onchange = (e) => setCut(e.target.checked); - $("cut").oninput = updateCut; - $("chk-ortho").onchange = (e) => { - if (e.target.checked) { - syncOrtho(); - camera = cameraOrtho; - } else { - cameraPersp.position.copy(camera.position); - camera = cameraPersp; + $("chk-wire").onchange = (e) => { + for (const m of [matFront, matBack, matBoard, matScreen, matDht, matBlue]) { + m.wireframe = e.target.checked; m.needsUpdate = true; } - controls.object = camera; - controls.update(); + }; + $("chk-xray").onchange = (e) => { + for (const m of [matFront, matBack]) { + m.transparent = e.target.checked; + m.opacity = e.target.checked ? 0.35 : 1; + m.depthWrite = !e.target.checked; + m.needsUpdate = true; + } + }; + $("chk-backview").onchange = (e) => { + if (e.target.checked) setView("back"); + else setView("iso"); }; - document.querySelectorAll("[data-view]").forEach((btn) => { - btn.onclick = () => setView(btn.dataset.view); - }); - $("btn-reset").onclick = () => { if (root.children.length) fitCamera(root); }; + function setView(name) { + const t = controls.target.clone(); + const d = camera.position.distanceTo(t) || 160; + const map = { + iso: new THREE.Vector3(t.x + d * 0.65, t.y - d * 0.5, t.z + d * 0.55), + front: new THREE.Vector3(t.x, t.y, t.z + d), + back: new THREE.Vector3(t.x, t.y, t.z - d), + side: new THREE.Vector3(t.x + d, t.y, t.z), + }; + camera.position.copy(map[name] || map.iso); + controls.update(); + } + document.querySelectorAll("[data-view]").forEach((b) => b.onclick = () => setView(b.dataset.view)); + $("btn-reset").onclick = () => { if (root.children.length) fit(root); }; $("btn-shot").onclick = () => { renderer.render(scene, camera); const a = document.createElement("a"); - a.href = renderer.domElement.toDataURL("image/png"); - a.download = "cyd-case-preview.png"; + a.download = "cyd-case.png"; + a.href = canvas.toDataURL("image/png"); a.click(); - setStatus("已截图下载"); }; addEventListener("resize", () => { - const aspect = innerWidth / innerHeight; - cameraPersp.aspect = aspect; - cameraPersp.updateProjectionMatrix(); - syncOrtho(); + camera.aspect = innerWidth / innerHeight; + camera.updateProjectionMatrix(); renderer.setSize(innerWidth, innerHeight, false); }); - - // keyboard shortcuts addEventListener("keydown", (e) => { if (e.target.matches("input,textarea")) return; - if (e.key === "o") startAnim("open"); if (e.key === "c") startAnim("close"); + if (e.key === "o") startAnim("open"); if (e.key === " ") { e.preventDefault(); startAnim("play"); } - if (e.key === "x") { $("chk-xray").checked = !$("chk-xray").checked; setXray($("chk-xray").checked); } - if (e.key === "w") { $("chk-wire").checked = !$("chk-wire").checked; setWire($("chk-wire").checked); } }); function tick(now) { requestAnimationFrame(tick); tickAnim(now); - if (autoRotate) root.rotation.y += 0.004; + if (autoRotate) root.rotation.z += 0.003; controls.update(); renderer.render(scene, camera); }