Compare commits
2 Commits
v1.0-case-
...
v1.1-case-
| Author | SHA1 | Date | |
|---|---|---|---|
| 892e9d04c6 | |||
| c7d4b5a0c5 |
31
README.md
31
README.md
@@ -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、四侧悬臂卡扣、定位柱、卡扣试打样件 |
|
||||||
|
|||||||
@@ -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.
@@ -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
BIN
cad/export/snap_coupon.stl
Normal file
Binary file not shown.
@@ -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__":
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user