diff --git a/README.md b/README.md index a59b3ce..6e22b0a 100644 --- a/README.md +++ b/README.md @@ -41,11 +41,11 @@ pio device monitor -b 115200 ## 3D 外壳 -桌面斜立分体壳(前框 + 后盖支架 + DHT11 通风仓),可直接打印: +v1.1 圆角分体壳(前框 + 后盖 + 四侧悬臂卡扣 + 后盖 DHT 卡位),可直接打印: -- STL:[`cad/export/front.stl`](cad/export/front.stl)、[`cad/export/back.stl`](cad/export/back.stl) +- STL:[`cad/export/front.stl`](cad/export/front.stl)、[`cad/export/back.stl`](cad/export/back.stl)、[`cad/export/snap_coupon.stl`](cad/export/snap_coupon.stl)(试打卡扣) - 浏览器预览:[`cad/viewer.html`](cad/viewer.html)(需本地 HTTP,见下) -- 参数源 / 说明:[`cad/README.md`](cad/README.md) +- 参数与打印建议:[`cad/README.md`](cad/README.md) ```bash # 浏览器查看 @@ -53,11 +53,11 @@ python -m http.server 8765 --directory cad # 打开 http://localhost:8765/viewer.html # 重新导出 STL -pip install trimesh manifold3d numpy +pip install trimesh manifold3d numpy shapely python cad/generate_stl.py ``` -固定用 `4 × M3×10`;DHT11 装后盖通风仓,线接 CN1。 +合盖以卡扣为主、止口对齐;四角 `M3×10` 可选。建议 PETG;DHT11 装后盖背面卡位,线接 CN1。 ## 项目结构 @@ -112,3 +112,4 @@ git reset --hard v1.0-case-closed | 2026-10-17 | 外壳改定:DHT11 后盖卡位微突出;前后盖止口合盖 | | 2026-10-17 | 去掉旁侧多余凸块;修复合盖对齐;DHT 改为后盖平面卡位 | | 2026-10-17 | 打标签 `v1.0-case-closed` 作为可回滚稳定版 | +| 2026-10-17 | 外壳 v1.1:外圆角 R3.5、四侧悬臂卡扣、定位柱、卡扣试打样件 | diff --git a/cad/README.md b/cad/README.md index e95b16b..813330a 100644 --- a/cad/README.md +++ b/cad/README.md @@ -1,21 +1,39 @@ -# CYD 温湿度仪表盘 3D 外壳 +# CYD 温湿度仪表盘 3D 外壳(v1.1) -## 结构说明(当前版) +## 结构说明 -只有两件:**前框** + **后盖**。 +只有两件:**前框** + **后盖**。外轮廓圆角 R=3.5mm,主要靠 **4 处悬臂卡扣** 合盖(止口仍保留对齐;四角 M3 可选)。 | 部位 | 作用 | |------|------| -| 前框 | 屏窗;公止口,合盖时插入后盖 | -| 后盖 | 装板子;母止口;**背面中心 DHT11 卡位通风口**(探头微突出约 1.6mm) | -| DHT 示意 | 预览里蓝色/灰块,表示传感器装在后盖背面卡位里 | +| 前框 | 屏窗;公止口;长边各 2 根悬臂卡扣(钩朝外) | +| 后盖 | 装板子;母止口;卡扣窗口 + 卡台;**背面中心 DHT11 卡位通风口**(探头微突出约 1.6mm) | +| 定位柱 | 四角短 peg / 孔,合盖防错位 | +| `snap_coupon.stl` | 单独试打卡扣样件,调间隙后再打整壳 | -**没有**外壳旁边的大“格子”。旧版里那两块是:侧向支脚块、外挂 DHT 仓——已删除,避免误解。 +**没有**外壳旁边的大“格子”(旧支脚块 / 外挂仓已删)。 + +## 卡扣参数(按 FDM 常见经验) + +参考悬臂卡扣实践(应变控制、根部过渡、配合间隙、PETG 反复开合更稳): + +| 参数 | 本壳取值 | 说明 | +|------|----------|------| +| 自由臂长 L | 11 mm | 偏短可再加长降低应变 | +| 根厚 / 尖厚 | 1.7 → 0.95 mm | 锥形臂,应力分散 | +| 钩深 undercut | 0.75 mm | 含 lead-in 斜面 | +| 侧向间隙 | 0.45 mm | FDM 合盖/卡扣常用约 0.4–0.5 | +| 止口间隙 | 0.45 mm | 同左 | +| 材料建议 | **PETG** | PLA 可试装,反复开合易脆 | + +打印:卡扣臂尽量落在 **XY 弯曲平面**(不要让弯曲撕开层层)。先打 `snap_coupon.stl`,太紧就略加大间隙或减钩深。 ## 合盖 -前框止口落入后盖槽;预览默认合盖(开合度 0%)。 -对齐数据在 `export/mate.json`。 +前框止口落入后盖槽,卡扣钩入后盖窗口卡台;预览默认合盖(开合度 0%)。 +对齐数据在 `export/mate.json`(`version: 1.1-snaps`)。 + +稳定旧版(无卡扣、仅止口)标签:仓库根 README 的 `v1.0-case-closed`。 ## 浏览器预览 @@ -31,11 +49,13 @@ python -m http.server 8765 --directory cad ## 导出 STL ```bash -pip install trimesh manifold3d numpy +pip install trimesh manifold3d numpy shapely python cad/generate_stl.py ``` +输出:`export/front.stl`、`back.stl`、`snap_coupon.stl`、`mate.json`。 + ## 装配 1. DHT11 从**后盖背面**按入中心卡位,探头朝外略突出,线进腔内接 CN1/GPIO22 -2. CYD 放入后盖,前框合盖,四角 M3×10 可选固定 +2. CYD 放入后盖,前框对准止口/定位柱合盖,听四侧卡扣入位;四角 M3×10 可选 diff --git a/cad/export/back.stl b/cad/export/back.stl index 6d61b39..cf91c08 100644 Binary files a/cad/export/back.stl and b/cad/export/back.stl differ diff --git a/cad/export/front.stl b/cad/export/front.stl index 654522b..85bbf0b 100644 Binary files a/cad/export/front.stl and b/cad/export/front.stl differ diff --git a/cad/export/mate.json b/cad/export/mate.json index fe8bfe0..41f1285 100644 --- a/cad/export/mate.json +++ b/cad/export/mate.json @@ -1,8 +1,14 @@ { - "outer_l": 91.5, - "outer_w": 55.5, + "outer_l": 91.6, + "outer_w": 55.599999999999994, "front_body_h": 7.2, "back_body_h": 10.4, - "lip_h": 2.2, - "dht_proud": 1.6 + "lip_h": 2.0, + "dht_proud": 1.6, + "corner_r": 3.5, + "snap_w": 6.5, + "snap_l": 11.0, + "snap_hook": 0.75, + "lip_clear": 0.45, + "version": "1.1-snaps" } diff --git a/cad/export/snap_coupon.stl b/cad/export/snap_coupon.stl new file mode 100644 index 0000000..6f59d8f Binary files /dev/null and b/cad/export/snap_coupon.stl differ diff --git a/cad/generate_stl.py b/cad/generate_stl.py index 8ef61ce..3ab41ff 100644 --- a/cad/generate_stl.py +++ b/cad/generate_stl.py @@ -1,31 +1,59 @@ #!/usr/bin/env python3 """ -CYD climate case — clean clamshell. +CYD climate case v1.1 — rounded clamshell + FDM snap clips. -- 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 +Design refs (FDM snap-fit best practice): + - Cantilever L≈12–16mm, root t≈1.6–2.0mm, width≥5mm + - Undercut ≈0.7mm, lead-in ≈35°, root fillet ≥0.5·t + - Mating clearance ≈0.4–0.5mm (FDM) + - Prefer PETG for repeated open/close; print snap arms in XY bend plane + +Features: + - Outer R=3.5mm / inner R≈2.0mm rounded shell + - 4 side cantilever snaps (front) + catch windows (back) + - Corner locating pegs / sockets for 合盖对齐 + - DHT11 flush rear pocket with slight proud grill + - Optional M3 bosses kept as backup """ from __future__ import annotations +import json from pathlib import Path +import numpy as np import trimesh -from trimesh.creation import box, cylinder +from shapely.geometry import Point, box +from trimesh.creation import cylinder, extrude_polygon +# ---- board ---- 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 + +# ---- shell ---- +WALL, GAP = 2.4, 0.40 +CORNER_R = 3.5 +INNER_R = 2.0 BEZEL_T = 2.2 CAVITY = 8.0 -LIP_H, LIP_CLEAR = 2.2, 0.2 +LIP_H = 2.0 +LIP_CLEAR = 0.45 # FDM clearance around lip 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) +# ---- snap (cantilever) ---- +SNAP_W = 6.5 +SNAP_L = 11.0 # free length +SNAP_T_ROOT = 1.7 +SNAP_T_TIP = 0.95 +SNAP_HOOK = 0.75 # undercut +SNAP_LEAD = 0.9 # lead-in ramp height along arm +SNAP_CLEAR = 0.45 # side gap around moving arm +SNAP_Z_ROOT = 1.2 # root starts below front rim into skirt + +# ---- DHT ---- DHT_L, DHT_W, DHT_H = 16.0, 12.5, 7.5 DHT_PROUD = 1.6 @@ -34,8 +62,7 @@ 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 +BACK_BODY_H = WALL + CAVITY FRONT_BODY_H = BEZEL_T + SKIRT MH = [ @@ -45,6 +72,17 @@ MH = [ (MH_INSET, PCB_W - MH_INSET), ] +# snap centers along long sides (x), inset from ends +SNAP_XS = [OUTER_L * 0.28, OUTER_L * 0.72] + + +def rounded_prism(l: float, w: float, h: float, r: float, z0: float = 0.0) -> trimesh.Trimesh: + r = min(r, l / 2 - 0.05, w / 2 - 0.05) + poly = box(r, r, l - r, w - r).buffer(r, resolution=8) + mesh = extrude_polygon(poly, height=h) + mesh.apply_translation([0, 0, z0]) + return mesh + def aabb(x0, y0, z0, x1, y1, z1) -> trimesh.Trimesh: xa, xb = sorted((float(x0), float(x1))) @@ -52,8 +90,8 @@ def aabb(x0, y0, z0, x1, y1, z1) -> trimesh.Trimesh: 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]) + raise ValueError(f"degenerate box {xa,ya,za,xb,yb,zb}") + m = trimesh.creation.box(extents=[dx, dy, dz]) m.apply_translation([xa + dx / 2, ya + dy / 2, za + dz / 2]) return m @@ -65,168 +103,295 @@ def U(*ms: trimesh.Trimesh) -> trimesh.Trimesh: return r -def D(a: trimesh.Trimesh, *ss: trimesh.Trimesh) -> trimesh.Trimesh: +def Diff(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: +def cantilever_snap(x_center: float, y_wall: float, outward_sign: int) -> trimesh.Trimesh: """ - Z=0: outer screen face - Z=FRONT_BODY_H: open rim with male lip extending to FRONT_BODY_H+LIP_H + Snap arm on front: root near FRONT_BODY_H, extends +Z (into back when closed). + Hook faces outward (+/- Y). + outward_sign: +1 for +Y wall (hook +Y), -1 for -Y wall. """ - 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) + # Build in local: arm along +Z, thickness in Y, width in X + # Root fillet approximated by stepped thickening + pieces = [] + n = 6 + for i in range(n): + t = i / (n - 1) + # taper thickness root -> tip + th = SNAP_T_ROOT * (1 - t) + SNAP_T_TIP * t + z0 = FRONT_BODY_H - SNAP_Z_ROOT + t * (SNAP_L + SNAP_Z_ROOT) + z1 = FRONT_BODY_H - SNAP_Z_ROOT + (t + 1 / n) * (SNAP_L + SNAP_Z_ROOT) + 0.05 + # arm sits inside wall face, hook later + if outward_sign > 0: + y0 = y_wall - WALL + SNAP_CLEAR + y1 = y0 + th + else: + y1 = y_wall + WALL - SNAP_CLEAR + y0 = y1 - th + pieces.append(aabb(x_center - SNAP_W / 2, y0, z0, x_center + SNAP_W / 2, y1, z1)) + arm = U(*pieces) + + # hook wedge at tip (lead-in ramp) — min thickness keeps manifold volumes valid + tip_z = FRONT_BODY_H - SNAP_Z_ROOT + SNAP_L + SNAP_Z_ROOT + hook_steps = [] + for i in range(5): + t = (i + 1) / 5 # start above 0 so h>0 + h = max(0.25, SNAP_HOOK * min(1.0, t / 0.55)) + z0 = tip_z - SNAP_LEAD + (i / 5) * SNAP_LEAD + z1 = tip_z - SNAP_LEAD + ((i + 1) / 5) * SNAP_LEAD + 0.08 + if outward_sign > 0: + y0 = y_wall - WALL + SNAP_CLEAR + SNAP_T_TIP + y1 = y0 + h + else: + y1 = y_wall + WALL - SNAP_CLEAR - SNAP_T_TIP + y0 = y1 - h + hook_steps.append(aabb(x_center - SNAP_W / 2 + 0.4, y0, z0, x_center + SNAP_W / 2 - 0.4, y1, z1)) + return U(arm, *hook_steps) + + +def catch_window(x_center: float, y_wall: float, outward_sign: int) -> trimesh.Trimesh: + """Opening / ledge cut in back wall for snap hook.""" + # Window through wall where hook passes, then a ledge below for catch + win_h = SNAP_L + 2.5 + z0 = BACK_BODY_H - 1.0 + z1 = z0 + win_h + if outward_sign > 0: + return aabb( + x_center - SNAP_W / 2 - SNAP_CLEAR, + y_wall - WALL - 0.2, + z0, + x_center + SNAP_W / 2 + SNAP_CLEAR, + y_wall + 0.6, + z1, + ) + return aabb( + x_center - SNAP_W / 2 - SNAP_CLEAR, + y_wall - 0.6, + z0, + x_center + SNAP_W / 2 + SNAP_CLEAR, + y_wall + WALL + 0.2, + z1, + ) + + +def catch_ledge(x_center: float, y_wall: float, outward_sign: int) -> trimesh.Trimesh: + """Internal ledge the hook seats under (retention).""" + z0 = BACK_BODY_H + 0.3 + z1 = z0 + 1.4 + if outward_sign > 0: + return aabb( + x_center - SNAP_W / 2 + 0.3, + OUTER_W - WALL - 0.2, + z0, + x_center + SNAP_W / 2 - 0.3, + OUTER_W - WALL + SNAP_HOOK + 0.3, + z1, + ) + return aabb( + x_center - SNAP_W / 2 + 0.3, + WALL - SNAP_HOOK - 0.3, + z0, + x_center + SNAP_W / 2 - 0.3, + WALL + 0.2, + z1, + ) + + +def make_front() -> trimesh.Trimesh: + body = rounded_prism(OUTER_L, OUTER_W, FRONT_BODY_H, CORNER_R, 0) + hollow = rounded_prism(INNER_L, INNER_W, FRONT_BODY_H + 1, INNER_R, BEZEL_T) + hollow.apply_translation([WALL, WALL, 0]) + out = Diff(body, hollow) + + # view + pocket 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) - + out = Diff(out, 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 = Diff( + out, + 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)) + # male lip (rounded ring approx via outer-inner) + lip_out = rounded_prism(OUTER_L - 1.2, OUTER_W - 1.2, LIP_H, CORNER_R - 0.6, FRONT_BODY_H - 0.05) + lip_out.apply_translation([0.6, 0.6, 0]) + lip_in = rounded_prism(INNER_L + LIP_CLEAR * 2, INNER_W + LIP_CLEAR * 2, LIP_H + 0.6, INNER_R, FRONT_BODY_H - 0.2) + lip_in.apply_translation([WALL - LIP_CLEAR, WALL - LIP_CLEAR, 0]) + out = U(out, Diff(lip_out, lip_in)) + # locating pegs (导柱) near corners — stick into back sockets + pegs = [] + for px, py in [(8.0, 8.0), (OUTER_L - 8.0, 8.0), (OUTER_L - 8.0, OUTER_W - 8.0), (8.0, OUTER_W - 8.0)]: + peg = cylinder(radius=1.4, height=LIP_H + 2.2, sections=20) + peg.apply_translation([px, py, FRONT_BODY_H + (LIP_H + 2.2) / 2 - 0.3]) + pegs.append(peg) + out = U(out, *pegs) + + # 4 snaps on long sides + snaps = [] + for x in SNAP_XS: + snaps.append(cantilever_snap(x, OUTER_W, +1)) + snaps.append(cantilever_snap(x, 0.0, -1)) + out = U(out, *snaps) + + # optional screw holes (backup) for px, py in MH: - hole = cylinder(radius=SCREW / 2 + 0.1, height=FRONT_BODY_H + LIP_H + 4, sections=28) + hole = cylinder(radius=SCREW / 2 + 0.1, height=FRONT_BODY_H + LIP_H + 6, sections=24) hole.apply_translation([WALL + GAP + px, WALL + GAP + py, FRONT_BODY_H / 2]) - out = D(out, hole) + out = Diff(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) + body = rounded_prism(OUTER_L, OUTER_W, total_h, CORNER_R, 0) + cavity = rounded_prism(INNER_L, INNER_W, total_h + 1, INNER_R, WALL) + cavity.apply_translation([WALL, WALL, 0]) + out = Diff(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)) + # female groove for lip + groove = rounded_prism(OUTER_L - 0.8, OUTER_W - 0.8, LIP_H + 0.3, CORNER_R - 0.4, BACK_BODY_H + 0.1) + groove.apply_translation([0.4, 0.4, 0]) + keep = rounded_prism(INNER_L + 0.2, INNER_W + 0.2, LIP_H + 0.6, INNER_R, BACK_BODY_H - 0.1) + keep.apply_translation([WALL - 0.1, WALL - 0.1, 0]) + out = Diff(out, Diff(groove, keep)) + + # locating sockets + for px, py in [(8.0, 8.0), (OUTER_L - 8.0, 8.0), (OUTER_L - 8.0, OUTER_W - 8.0), (8.0, OUTER_W - 8.0)]: + sock = cylinder(radius=1.55, height=3.2, sections=20) + sock.apply_translation([px, py, BACK_BODY_H + 1.4]) + out = Diff(out, sock) + + # snap catch windows + ledges + for x in SNAP_XS: + out = Diff(out, catch_window(x, OUTER_W, +1)) + out = Diff(out, catch_window(x, 0.0, -1)) + out = U(out, catch_ledge(x, OUTER_W, +1)) + out = U(out, catch_ledge(x, 0.0, -1)) # 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)) + out = Diff(out, aabb(-0.5, uy0, WALL + 1.0, WALL + 1.0, uy0 + USB_W, WALL + 1.0 + USB_H)) - # button holes + # buttons for y in (12.0, 22.0): - btn = cylinder(radius=2.2, height=WALL + 2, sections=20) + btn = cylinder(radius=2.2, height=WALL + 2, sections=18) btn.apply_translation([WALL + GAP + 10, WALL + GAP + y, WALL / 2]) - out = D(out, btn) + out = Diff(out, btn) - # screw bosses + # M3 bosses backup 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 = cylinder(radius=BOSS_OD / 2, height=CAVITY - 0.5, sections=24) boss.apply_translation([x, y, WALL + (CAVITY - 0.5) / 2]) - hole = cylinder(radius=BOSS_ID / 2, height=CAVITY + 1, sections=20) + hole = cylinder(radius=BOSS_ID / 2, height=CAVITY + 1, sections=18) hole.apply_translation([x, y, WALL + CAVITY / 2]) - bosses.append(D(boss, hole)) + bosses.append(Diff(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, + # DHT pocket + proud rounded grill + cx, cy = OUTER_L / 2, OUTER_W / 2 + out = Diff( + out, + 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, + out = U( + out, + aabb(cx - DHT_L / 2 - 0.3, cy - 3.2, 1.2, cx - DHT_L / 2 + 1.0, cy + 3.2, 3.4), + aabb(cx + DHT_L / 2 - 1.0, cy - 3.2, 1.2, cx + DHT_L / 2 + 0.3, cy + 3.2, 3.4), + 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), ) - 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, + # rounded proud grill via buffered square + grill_poly = box(cx - DHT_L / 2 - 0.8, cy - DHT_W / 2 - 0.8, cx + DHT_L / 2 + 0.8, cy + DHT_W / 2 + 0.8).buffer( + 1.2, resolution=6 ) - # slot vents in proud face + grill = extrude_polygon(grill_poly, height=DHT_PROUD + 0.1) + grill.apply_translation([0, 0, -DHT_PROUD]) + hole_poly = box(cx - DHT_L / 2 + 1.6, cy - DHT_W / 2 + 1.6, cx + DHT_L / 2 - 1.6, cy + DHT_W / 2 - 1.6).buffer( + 0.8, resolution=5 + ) + ghole = extrude_polygon(hole_poly, height=DHT_PROUD + 0.4) + ghole.apply_translation([0, 0, -DHT_PROUD - 0.2]) 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. + slots.append( + aabb(cx - 5 + i * 2.8, cy - 4.2, -DHT_PROUD - 0.2, cx - 5 + i * 2.8 + 1.2, cy + 4.2, 0.15) + ) + out = U(out, Diff(grill, ghole, *slots)) out.merge_vertices() return out +def make_snap_coupon() -> trimesh.Trimesh: + """Small test piece: one snap + one catch for tuning before full print.""" + base = aabb(0, 0, 0, 40, 28, 3) + # fake front strip with one snap + strip = aabb(5, 20, 3, 35, 28, 3 + FRONT_BODY_H * 0.35) + snap = cantilever_snap(20, 28, +1) + # shift snap down a bit for coupon + front = U(strip, snap) + # fake back catch wall + wall = aabb(5, 0, 0, 35, 3.2, 18) + win = catch_window(20, 3.2, +1) + # remap catch to coupon coords roughly + catch = aabb(20 - SNAP_W / 2 - 0.5, -0.2, 8, 20 + SNAP_W / 2 + 0.5, 3.5, 18) + ledge = aabb(20 - SNAP_W / 2 + 0.3, 2.5, 10, 20 + SNAP_W / 2 - 0.3, 2.5 + SNAP_HOOK + 0.4, 11.4) + back = U(Diff(wall, catch), ledge) + # place back beside front + back.apply_translation([0, -8, 0]) + label = aabb(2, 12, 3, 3.2, 18, 4.5) # marker nub + return U(base, front, back, label) + + def main() -> None: out = Path(__file__).resolve().parent / "export" out.mkdir(parents=True, exist_ok=True) - print("front…") + + print("front (rounded + snaps)…") front = make_front() - print("back…") + print("back (rounded + catches)…") back = make_back() + print("snap coupon…") + coupon = make_snap_coupon() + front.export(out / "front.stl") back.export(out / "back.stl") + coupon.export(out / "snap_coupon.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", + meta = { + "outer_l": OUTER_L, + "outer_w": OUTER_W, + "front_body_h": FRONT_BODY_H, + "back_body_h": BACK_BODY_H, + "lip_h": LIP_H, + "dht_proud": DHT_PROUD, + "corner_r": CORNER_R, + "snap_w": SNAP_W, + "snap_l": SNAP_L, + "snap_hook": SNAP_HOOK, + "lip_clear": LIP_CLEAR, + "version": "1.1-snaps", + } + (out / "mate.json").write_text( + json.dumps(meta, indent=2) + "\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") + + print(f"front {front.extents} wt={front.is_watertight}") + print(f"back {back.extents} wt={back.is_watertight}") + print(f"coupon {coupon.extents} wt={coupon.is_watertight}") + print("Wrote front/back/snap_coupon + mate.json") if __name__ == "__main__": diff --git a/cad/viewer.html b/cad/viewer.html index 0e912ae..671942c 100644 --- a/cad/viewer.html +++ b/cad/viewer.html @@ -62,8 +62,8 @@
- 后盖中心微凸的是 DHT11 卡位通风口(传感器按入后盖背面,探头略突出)。
- 已去掉旁边多余的大“格子”(旧版支脚块 / 外挂仓)。
+ v1.1:外轮廓圆角 + 长边四侧悬臂卡扣合盖;后盖中心微凸是 DHT11 卡位通风口。
+ 已去掉旁边多余的大“格子”。旧无卡扣版见标签 v1.0-case-closed。