2 Commits

Author SHA1 Message Date
892e9d04c6 Optimize case v1.1 with rounded corners and FDM cantilever snaps.
Adds catch windows, locating pegs, and a snap coupon for print tuning; keep lip alignment and rear DHT bay.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-17 15:20:09 +08:00
c7d4b5a0c5 Document stable tag v1.0-case-closed for easy rollback.
Keep a named branch pointing at the closed-case snapshot.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-17 15:16:12 +08:00
8 changed files with 351 additions and 138 deletions

View File

@@ -41,11 +41,11 @@ pio device monitor -b 115200
## 3D 外壳 ## 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/viewer.html`](cad/viewer.html)(需本地 HTTP,见下)
- 参数源 / 说明:[`cad/README.md`](cad/README.md) - 参数与打印建议:[`cad/README.md`](cad/README.md)
```bash ```bash
# 浏览器查看 # 浏览器查看
@@ -53,11 +53,11 @@ python -m http.server 8765 --directory cad
# 打开 http://localhost:8765/viewer.html # 打开 http://localhost:8765/viewer.html
# 重新导出 STL # 重新导出 STL
pip install trimesh manifold3d numpy pip install trimesh manifold3d numpy shapely
python cad/generate_stl.py python cad/generate_stl.py
``` ```
固定用 `4 × M3×10`;DHT11 装后盖通风仓,线接 CN1。 合盖以卡扣为主、止口对齐;四角 `M3×10` 可选。建议 PETG;DHT11 装后盖背面卡位,线接 CN1。
## 项目结构 ## 项目结构
@@ -80,6 +80,25 @@ README.md # 本文档
详见 `.cursor/rules/git-and-docs.mdc`。 详见 `.cursor/rules/git-and-docs.mdc`。
## 稳定版快照(可随时切回)
当前保留的外壳/预览稳定版标签:**`v1.0-case-closed`**
(提交 `646cf45`:后盖平面 DHT 卡位微突出 + 前后盖止口合盖,无旁侧多余格子)
```bash
# 查看标签
git tag -l
# 临时查看该版(只读)
git switch --detach v1.0-case-closed
# 基于该版开新分支继续改
git switch -c restore/v1.0-case-closed v1.0-case-closed
# 整库硬切回该版(会丢弃之后未提交改动,慎用)
git reset --hard v1.0-case-closed
```
## 变更记录 ## 变更记录
| 日期 | 说明 | | 日期 | 说明 |
@@ -92,3 +111,5 @@ README.md # 本文档
| 2026-10-17 | 预览增强:合盖/开盖、分解、装配动画、剖切/线框等 | | 2026-10-17 | 预览增强:合盖/开盖、分解、装配动画、剖切/线框等 |
| 2026-10-17 | 外壳改定:DHT11 后盖卡位微突出;前后盖止口合盖 | | 2026-10-17 | 外壳改定:DHT11 后盖卡位微突出;前后盖止口合盖 |
| 2026-10-17 | 去掉旁侧多余凸块;修复合盖对齐;DHT 改为后盖平面卡位 | | 2026-10-17 | 去掉旁侧多余凸块;修复合盖对齐;DHT 改为后盖平面卡位 |
| 2026-10-17 | 打标签 `v1.0-case-closed` 作为可回滚稳定版 |
| 2026-10-17 | 外壳 v1.1:外圆角 R3.5、四侧悬臂卡扣、定位柱、卡扣试打样件 |

View File

@@ -1,21 +1,39 @@
# CYD 温湿度仪表盘 3D 外壳 # CYD 温湿度仪表盘 3D 外壳(v1.1)
## 结构说明(当前版) ## 结构说明
只有两件:**前框** + **后盖**。 只有两件:**前框** + **后盖**。外轮廓圆角 R=3.5mm,主要靠 **4 处悬臂卡扣** 合盖(止口仍保留对齐;四角 M3 可选)。
| 部位 | 作用 | | 部位 | 作用 |
|------|------| |------|------|
| 前框 | 屏窗;公止口,合盖时插入后盖 | | 前框 | 屏窗;公止口;长边各 2 根悬臂卡扣(钩朝外) |
| 后盖 | 装板子;母止口;**背面中心 DHT11 卡位通风口**(探头微突出约 1.6mm) | | 后盖 | 装板子;母止口;卡扣窗口 + 卡台;**背面中心 DHT11 卡位通风口**(探头微突出约 1.6mm) |
| DHT 示意 | 预览里蓝色/灰块,表示传感器装在后盖背面卡位里 | | 定位柱 | 四角短 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%)。 前框止口落入后盖槽,卡扣钩入后盖窗口卡台;预览默认合盖(开合度 0%)。
对齐数据在 `export/mate.json`。 对齐数据在 `export/mate.json`(`version: 1.1-snaps`)。
稳定旧版(无卡扣、仅止口)标签:仓库根 README 的 `v1.0-case-closed`。
## 浏览器预览 ## 浏览器预览
@@ -31,11 +49,13 @@ python -m http.server 8765 --directory cad
## 导出 STL ## 导出 STL
```bash ```bash
pip install trimesh manifold3d numpy pip install trimesh manifold3d numpy shapely
python cad/generate_stl.py python cad/generate_stl.py
``` ```
输出:`export/front.stl`、`back.stl`、`snap_coupon.stl`、`mate.json`。
## 装配 ## 装配
1. DHT11 从**后盖背面**按入中心卡位,探头朝外略突出,线进腔内接 CN1/GPIO22 1. DHT11 从**后盖背面**按入中心卡位,探头朝外略突出,线进腔内接 CN1/GPIO22
2. CYD 放入后盖,前框合盖,四角 M3×10 可选固定 2. CYD 放入后盖,前框对准止口/定位柱合盖,听四侧卡扣入位;四角 M3×10 可选

Binary file not shown.

Binary file not shown.

View File

@@ -1,8 +1,14 @@
{ {
"outer_l": 91.5, "outer_l": 91.6,
"outer_w": 55.5, "outer_w": 55.599999999999994,
"front_body_h": 7.2, "front_body_h": 7.2,
"back_body_h": 10.4, "back_body_h": 10.4,
"lip_h": 2.2, "lip_h": 2.0,
"dht_proud": 1.6 "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"
} }

BIN
cad/export/snap_coupon.stl Normal file

Binary file not shown.

View File

@@ -1,31 +1,59 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
CYD climate case — clean clamshell. CYD climate case v1.1 — rounded clamshell + FDM snap clips.
- Front: screen bezel + male lip Design refs (FDM snap-fit best practice):
- Back: cavity + female groove + DHT11 snap pocket flush in back wall (tip proud ~1.6mm) - Cantilever L≈12–16mm, root t≈1.6–2.0mm, width≥5mm
- No side “boxes”: removed bulky stand block and external DHT hang-on bay - 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 from __future__ import annotations
import json
from pathlib import Path from pathlib import Path
import numpy as np
import trimesh 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 PCB_L, PCB_W = 86.0, 50.0
MH_INSET = 4.0 MH_INSET = 4.0
DISP_X, DISP_L, DISP_W, DISP_H = 8.0, 69.0, 50.0, 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 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 BEZEL_T = 2.2
CAVITY = 8.0 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 USB_W, USB_H, USB_Y = 15.0, 9.0, 30.5
SCREW, BOSS_OD, BOSS_ID = 3.3, 7.0, 2.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_L, DHT_W, DHT_H = 16.0, 12.5, 7.5
DHT_PROUD = 1.6 DHT_PROUD = 1.6
@@ -34,8 +62,7 @@ INNER_W = PCB_W + 2 * GAP
OUTER_L = INNER_L + 2 * WALL OUTER_L = INNER_L + 2 * WALL
OUTER_W = INNER_W + 2 * WALL OUTER_W = INNER_W + 2 * WALL
SKIRT = DISP_H + 1.0 SKIRT = DISP_H + 1.0
# Back total height to mating plane (top of female groove floor) BACK_BODY_H = WALL + CAVITY
BACK_BODY_H = WALL + CAVITY # rim starts here
FRONT_BODY_H = BEZEL_T + SKIRT FRONT_BODY_H = BEZEL_T + SKIRT
MH = [ MH = [
@@ -45,6 +72,17 @@ MH = [
(MH_INSET, PCB_W - MH_INSET), (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: def aabb(x0, y0, z0, x1, y1, z1) -> trimesh.Trimesh:
xa, xb = sorted((float(x0), float(x1))) 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))) za, zb = sorted((float(z0), float(z1)))
dx, dy, dz = xb - xa, yb - ya, zb - za dx, dy, dz = xb - xa, yb - ya, zb - za
if min(dx, dy, dz) < 1e-6: if min(dx, dy, dz) < 1e-6:
raise ValueError(f"degenerate {[xa, ya, za, xb, yb, zb]}") raise ValueError(f"degenerate box {xa,ya,za,xb,yb,zb}")
m = box(extents=[dx, dy, dz]) m = trimesh.creation.box(extents=[dx, dy, dz])
m.apply_translation([xa + dx / 2, ya + dy / 2, za + dz / 2]) m.apply_translation([xa + dx / 2, ya + dy / 2, za + dz / 2])
return m return m
@@ -65,168 +103,295 @@ def U(*ms: trimesh.Trimesh) -> trimesh.Trimesh:
return r return r
def D(a: trimesh.Trimesh, *ss: trimesh.Trimesh) -> trimesh.Trimesh: def Diff(a: trimesh.Trimesh, *ss: trimesh.Trimesh) -> trimesh.Trimesh:
r = a r = a
for s in ss: for s in ss:
r = r.difference(s, engine="manifold") r = r.difference(s, engine="manifold")
return r 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 Snap arm on front: root near FRONT_BODY_H, extends +Z (into back when closed).
Z=FRONT_BODY_H: open rim with male lip extending to FRONT_BODY_H+LIP_H 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) # Build in local: arm along +Z, thickness in Y, width in X
hollow = aabb(WALL, WALL, BEZEL_T, OUTER_L - WALL, OUTER_W - WALL, FRONT_BODY_H + 1) # 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 vx = WALL + GAP + DISP_X + VIEW_IX
vy = WALL + GAP + VIEW_IY 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 sx = WALL + GAP + DISP_X - 0.2
pocket = aabb( out = Diff(
sx, WALL + GAP - 0.1, BEZEL_T - 0.05, out,
sx + DISP_L + 0.4, WALL + GAP + DISP_W + 0.1, BEZEL_T + DISP_H + 0.6, 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 # male lip (rounded ring approx via outer-inner)
lip_out = aabb( lip_out = rounded_prism(OUTER_L - 1.2, OUTER_W - 1.2, LIP_H, CORNER_R - 0.6, FRONT_BODY_H - 0.05)
WALL - 1.0, WALL - 1.0, FRONT_BODY_H - 0.05, lip_out.apply_translation([0.6, 0.6, 0])
OUTER_L - (WALL - 1.0), OUTER_W - (WALL - 1.0), FRONT_BODY_H + LIP_H, 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])
lip_in = aabb( out = U(out, Diff(lip_out, lip_in))
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))
# 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: 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]) hole.apply_translation([WALL + GAP + px, WALL + GAP + py, FRONT_BODY_H / 2])
out = D(out, hole) out = Diff(out, hole)
out.merge_vertices() out.merge_vertices()
return out return out
def make_back() -> trimesh.Trimesh: 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 total_h = BACK_BODY_H + LIP_H
body = aabb(0, 0, 0, OUTER_L, OUTER_W, total_h) body = rounded_prism(OUTER_L, OUTER_W, total_h, CORNER_R, 0)
cavity = aabb(WALL, WALL, WALL, OUTER_L - WALL, OUTER_W - WALL, total_h + 1) cavity = rounded_prism(INNER_L, INNER_W, total_h + 1, INNER_R, WALL)
out = D(body, cavity) cavity.apply_translation([WALL, WALL, 0])
out = Diff(body, cavity)
# female groove: cut a channel in the top rim for the male lip # female groove for lip
# outer wall of groove 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 = aabb( groove.apply_translation([0.4, 0.4, 0])
WALL - 1.0 - LIP_CLEAR, WALL - 1.0 - LIP_CLEAR, BACK_BODY_H + 0.1, keep = rounded_prism(INNER_L + 0.2, INNER_W + 0.2, LIP_H + 0.6, INNER_R, BACK_BODY_H - 0.1)
OUTER_L - (WALL - 1.0 - LIP_CLEAR), OUTER_W - (WALL - 1.0 - LIP_CLEAR), total_h + 0.2, keep.apply_translation([WALL - 0.1, WALL - 0.1, 0])
) out = Diff(out, Diff(groove, keep))
groove_keep = aabb(
WALL + 0.05, WALL + 0.05, BACK_BODY_H - 0.1, # locating sockets
OUTER_L - WALL - 0.05, OUTER_W - WALL - 0.05, total_h + 0.3, 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)
out = D(out, D(groove, groove_keep)) 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 # USB
uy0 = WALL + GAP + USB_Y - USB_W / 2 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): 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]) 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 = [] bosses = []
for px, py in MH: for px, py in MH:
x, y = WALL + GAP + px, WALL + GAP + py 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]) 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]) hole.apply_translation([x, y, WALL + CAVITY / 2])
bosses.append(D(boss, hole)) bosses.append(Diff(boss, hole))
out = U(out, *bosses) out = U(out, *bosses)
# ---- DHT11 flush pocket in back wall (NOT a side box) ---- # DHT pocket + proud rounded grill
# Center on back face cx, cy = OUTER_L / 2, OUTER_W / 2
cx = OUTER_L * 0.5 out = Diff(
cy = OUTER_W * 0.5 out,
# Pocket carved from outside into wall+cavity enough for module 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),
pocket = aabb(
cx - DHT_L / 2 - 0.3, cy - DHT_W / 2 - 0.3, -0.05,
cx + DHT_L / 2 + 0.3, cy + DHT_W / 2 + 0.3, WALL + DHT_H - 1.0,
) )
out = D(out, pocket) out = U(
out,
# Snap rails (卡位) left/right inside pocket — thin ledges aabb(cx - DHT_L / 2 - 0.3, cy - 3.2, 1.2, cx - DHT_L / 2 + 1.0, cy + 3.2, 3.4),
rail_z0, rail_z1 = 1.2, 1.2 + 2.2 aabb(cx + DHT_L / 2 - 1.0, cy - 3.2, 1.2, cx + DHT_L / 2 + 0.3, cy + 3.2, 3.4),
left = aabb(cx - DHT_L / 2 - 0.3, cy - 3.2, rail_z0, cx - DHT_L / 2 + 1.0, cy + 3.2, rail_z1) 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),
right = aabb(cx + DHT_L / 2 - 1.0, cy - 3.2, rail_z0, cx + DHT_L / 2 + 0.3, cy + 3.2, rail_z1)
# bottom stop (prevents falling inward)
stop = aabb(cx - 5, cy - DHT_W / 2 + 0.5, WALL + DHT_H - 2.2, cx + 5, cy + DHT_W / 2 - 0.5, WALL + DHT_H - 1.0)
out = U(out, left, right, stop)
# Proud grill ring on exterior (the only intentional protrusion)
grill_ring = aabb(
cx - DHT_L / 2 - 1.0, cy - DHT_W / 2 - 1.0, -DHT_PROUD,
cx + DHT_L / 2 + 1.0, cy + DHT_W / 2 + 1.0, 0.05,
) )
grill_hole = aabb( # rounded proud grill via buffered square
cx - DHT_L / 2 + 1.5, cy - DHT_W / 2 + 1.5, -DHT_PROUD - 0.2, 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(
cx + DHT_L / 2 - 1.5, cy + DHT_W / 2 - 1.5, 0.2, 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 = [] slots = []
for i in range(4): for i in range(4):
slots.append(aabb( slots.append(
cx - 5 + i * 2.8, cy - 4.5, -DHT_PROUD - 0.2, 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)
cx - 5 + i * 2.8 + 1.2, cy + 4.5, 0.2, )
)) out = U(out, Diff(grill, ghole, *slots))
out = U(out, D(grill_ring, grill_hole, *slots))
# tiny wire path already open via pocket into cavity
# Simple thin kickstand rib (not a hollow “格子”) — optional small triangle
# Skip bulky stand entirely per user feedback.
out.merge_vertices() out.merge_vertices()
return out 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: def main() -> None:
out = Path(__file__).resolve().parent / "export" out = Path(__file__).resolve().parent / "export"
out.mkdir(parents=True, exist_ok=True) out.mkdir(parents=True, exist_ok=True)
print("front…")
print("front (rounded + snaps)…")
front = make_front() front = make_front()
print("back…") print("back (rounded + catches)…")
back = make_back() back = make_back()
print("snap coupon…")
coupon = make_snap_coupon()
front.export(out / "front.stl") front.export(out / "front.stl")
back.export(out / "back.stl") back.export(out / "back.stl")
coupon.export(out / "snap_coupon.stl")
# write mate info for viewer meta = {
meta = out / "mate.json" "outer_l": OUTER_L,
meta.write_text( "outer_w": OUTER_W,
"{\n" "front_body_h": FRONT_BODY_H,
f' "outer_l": {OUTER_L},\n' "back_body_h": BACK_BODY_H,
f' "outer_w": {OUTER_W},\n' "lip_h": LIP_H,
f' "front_body_h": {FRONT_BODY_H},\n' "dht_proud": DHT_PROUD,
f' "back_body_h": {BACK_BODY_H},\n' "corner_r": CORNER_R,
f' "lip_h": {LIP_H},\n' "snap_w": SNAP_W,
f' "dht_proud": {DHT_PROUD}\n' "snap_l": SNAP_L,
"}\n", "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", encoding="utf-8",
) )
print(f"front {front.extents} watertight={front.is_watertight}")
print(f"back {back.extents} watertight={back.is_watertight}") print(f"front {front.extents} wt={front.is_watertight}")
print("closed stack height ≈", BACK_BODY_H + FRONT_BODY_H, "mm (lips nest, not add full)") print(f"back {back.extents} wt={back.is_watertight}")
print("Wrote STLs + mate.json") print(f"coupon {coupon.extents} wt={coupon.is_watertight}")
print("Wrote front/back/snap_coupon + mate.json")
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -62,8 +62,8 @@
</div> </div>
<p class="faq"> <p class="faq">
后盖中心微凸的是 <b>DHT11 卡位通风口</b>(传感器按入后盖背面,探头略突出)。<br/> v1.1:外轮廓圆角 + 长边四侧悬臂卡扣合盖;后盖中心微凸是 <b>DHT11 卡位通风口</b>。<br/>
已去掉旁边多余的大“格子”(旧版支脚块 / 外挂仓)。 已去掉旁边多余的大“格子”。旧无卡扣版见标签 <code>v1.0-case-closed</code>。
</p> </p>
<div id="status">加载中…</div> <div id="status">加载中…</div>
</div> </div>
@@ -231,7 +231,8 @@
root.add(backG); root.add(frontG); root.add(dhtG); root.add(backG); root.add(frontG); root.add(dhtG);
setLid(0); setExp(0); fit(); setLid(0); setExp(0); fit();
msg(`合盖对齐已按 mate.json · 外壳 ${mate.outer_l}×${mate.outer_w} mm · DHT 突出 ${mate.dht_proud} mm`); const ver = mate.version ? ` · ${mate.version}` : "";
msg(`合盖对齐已按 mate.json${ver} · 外壳 ${mate.outer_l}×${mate.outer_w} mm · R${mate.corner_r ?? "?"} · DHT 突出 ${mate.dht_proud} mm`);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
msg("加载失败:请用 python -m http.server 8765 --directory cad", true); msg("加载失败:请用 python -m http.server 8765 --directory cad", true);