Back to all skills
cnc-part-designSKILL.md ↗

从零把 CAD 件建出来并送 CNC:用 cadquery+OCC 出真 STEP、用 fontTools+skia 把汉字变成光滑解析曲面、 用 Blender 无头渲染预览,以及嘉立创免费打样的设计期量化红线(槽宽>1.1 / 壁厚>1.0 / 吸附面≥35% / 禁雕刻)。 **只管"建"**——修复已有的 STEP(窄槽、薄壁、尖薄末端)请用 `cnc-dfm-repair`。 Triggers: "CNC", "STEP", "STL", "cad", "CAD", "三维模型", "建模", "3D 打印", "3D打印", "嘉立创", "JLC", "免费打样", "机械加工", "铣", "雕刻", "纪念币", "纪念章", "铭牌", "吊牌", "改字体", "字体轮廓", "文字浮雕", "CAD 导出", "solidworks", "freecad", "cadquery".


从零建 CAD 件并送 CNC

先分清你要做的是哪件事

任务用什么
从零建模(改字形、回转体、浮雕、导出 STEP)这份
修一个已有 STEP 让它过 DFM(窄槽/薄壁/尖尾)cnc-dfm-repair
量一个已有 STEP 的槽宽/壁厚、验证改了没改坏cnc-dfm-repair(面-面精确距离 + 编辑掩膜差分)

别重复造轮子。 本项目里我为了"找窄槽在哪"自己写了三套几何度量, 三套都被证伪(非单调、报出负槽宽、同一字在两个字号下自相矛盾),最后管用的是 cnc-dfm-repair 那套——对真实 BRep 直接量面-面精确距离(BRepExtrema_DistShapeShape), 配合 DFM 回报反推:原始槽宽 g₀ = DFM 报的槽宽 − 2×削细量。

本机工具链

工具用途
cadquery 2.8(含 OCC)建体、文字、导出 STEP。要真 STEP 必须用它按解析曲面重建
OCPcadquery 没暴露的底层:布尔、建面建环、Bezier、精确距离、ShapeFix
fontTools从 .otf 取汉字的精确贝塞尔轮廓(RecordingPen)
skia-python路径布尔/偏移(skia.Op、getFillPath)
gmshSTEP → STL(自带 OCCT),喂渲染和测量
Blender 4.5.3无头渲染。没有 STEP 导入器,只吃 STL/OBJ

Python 3.12。跑脚本一律 python -I(隔离模式,避免临时目录里的同名文件被 import)。

关键判断:网格不能转 STEP

STL 是三角面片、STEP 是数学曲面。硬把 7 万三角面转成 STEP → 7 万个微小平面, Ø80 的圆变 200 边形,CAM 读进去要么报错要么生成几万条碎刀路。 用户说"要 STEP"=要能送加工的 CAD 文件,不是要转格式 → 用 cadquery 重建。

命令行出 STEP 的骨架见 references/cadquery-patterns.md。

汉字 → 光滑解析曲面(这条链是本技能的核心)

为什么要走这条路:cq.Workplane.text() 用的是 OCC 自带的字体轮廓, 出来的是一堆碎面(「黎」119 个曲面),而且会带退化微边—— 拉伸后就是 0.001mm² 的微壁,DFM 判「碎面」。 把轮廓换成字体原生的贝塞尔之后,「黎」的侧壁从几百个 PLANE 变成 86 个光滑 EXTRUSION。

pip install skia-python fonttools
fontTools 取轮廓 → skia 做路径运算 → OCC Geom_BezierCurve 建边 → 拉伸

完整可用实现:references/thin-glyph.py。要点:

pen = RecordingPen()
font.getGlyphSet()[glyphName].draw(pen)        # moveTo/lineTo/curveTo/qCurveTo/closePath
heights = [p[1] for op, args in pen.value for p in args if p is not None]
scale = target_height / (max(heights) - min(heights))   # 见下面"基线"那条

槽宽和壁厚:第一选择是平移笔画

两条硬线(槽宽 >1.1mm、壁厚 >1.0mm)不要靠削细或补料去凑——那是拿形状换合规。 不要用圆柱(或胶囊体)去改笔画,这条是硬规则:圆柱与笔画的交线是圆弧,正面看就是个圆疙瘩。 正确做法是只平移,不改形。

关键实测:保持 thin=0(字体原始字重)时,"细于 1.0mm 的残留"在六个测试字上全部是 0.000mm²。 薄壁问题根本不存在,它完全是「削细」自己造出来的;补料又是在补自己造的伤。 平移只改位置——槽宽按平移量线性变宽,笔画粗细一点没变,一条杠杆同时满足两条线。

字形是多个独立部件时(glyph_solid 返回 Compound),按部件索引平移:

NUDGE="莺:0:0:-1.05;高:0:0:-0.20|2:0:0.30"    # 字:块:dx:dy,"|" 分隔同字的多个块

做法:先用 BRepExtrema_DistShapeShape 量每对部件的天然间距,把小于 1.2 的补到 1.2 左右 (留 0.1 余量)。平移会改变外包尺寸,必须重新核对字到凹壁、字到字、字到挂孔, 以及变高后是否顶到挂孔;必要时整体缩小 halfdiag 换空间。

其余手段只在平移解决不了时才用,代价明确:

  • thin=t:每边削 t,g=g₀+2t、w=w₀−2t 对冲,而且制造薄壁(见上)。 同字号下 g₀+w₀ 是常数,等比放大不腾余量。本项目曾削细到 0.65 硬凑槽宽,整批件都挂在薄壁上。
  • fix_slot=1.1:closing 填窄缝,改形并把相邻部件焊成一坨。
  • fix_wall=w:opening 残差再胀回去,把轮廓打成毛刺(一个字 9 个环 → 65–123 个环)。
  • thicken=[(x,y)]:沿笔画自身轮廓局部膨胀,视觉上顺,但仍是加料;且薄点若夹在窄槽两侧, 补厚必然把槽挤死(实测 1.3mm → 0.70mm)。坐标为字形局部坐标。

一定会踩的 API 坑

  • 枚举是 skia.Path.Verb,不是 skia.Verb

  • qCurveTo 是 TrueType 式的连续二次曲线打包(n 个离曲线点 + 1 个在曲线点), 中间隐含的中点要自己补;全是离曲线点的闭合串要单独处理

  • Geom_BezierCurve 遇到控制点重合的退化段会抛 ConstructionError → 重合点先合并, 剩两点就降级成直线

  • skia 的路径布尔在近相切处会整个失败(RuntimeError: Failed to apply op), 而且同一形状换个尺寸就失败(浮点简并)。修法:按百万分之一微扰缩放重试几次, 全失败就退回未处理的轮廓(宁可字粗一点,别让整个构建崩掉)

  • ⚠️ 绝对不要按"环面积的正负"分外墙和洞。 skia 不保证整条路径一个绕向: 「黎」回来是 CCW,「君」整条回来是 CW。按符号分类会让「君」的每个环都成"洞", 只剩最大的那个当外墙 → 墨迹掉 72%、包围盒从 26mm 塌到 9.6mm。 正解:按包含关系嵌套分类——面积从大到小遍历,落进已有外环内的就是那个外环的洞

  • 洞只属于一个外环:写 for i in outers: for j in holes: face.Add(hole) 会把同一个洞 塞进每个外环,面全部非法。射线法判断归属,分给最小的包含它的外环

  • 零面积碎片环要丢:skia 差集留下的 sliver 拉伸后是垃圾实体,还会吞掉加给它的洞。 下限用 max(1e-4×最大环, 0.01mm²)

  • 微段要在轮廓层并掉:_edge() 里合并重合控制点的阈值用 1e-3(不是 1e-9), 否则微米级重合点会拉伸成微壁。但别把这个阈值再放大(试过 1e-3 之外的全局调法, 整个实体散成 12 块)

  • 改了参数、输出逐字节没变 = 参数根本没接上。 add_material_for_rules 曾把 wall 的位置传成 slot, 于是「补壁」长期是空操作,而界面、日志、实体有效性全都正常。判别法极简:改一个参数后算字的面积或轮廓数, 一样就是没生效。这类「标准机制静默无效」比抛异常难查得多

  • 不要用自制几何度量判断"薄"和"细"。 腐蚀到空、opening 残差面积这两个指标在本项目里都被证伪过 (同一个字在不同半径下结果非单调、还会报出物理上不可能的数值)。可信的只有平台判定和 「凹面底面轮廓间距」——后者见 cnc-dfm-repair/scripts/dish_slots.py

文字排版:基线

整套字符共用同一个缩放系数,按基线定位。 逐字按"字高"缩放会把句点炸成数字大小(句点的墨迹高度只有数字的 1/3)。 要实现"共用系数 + 不居中",_to_skia 需要 scale= 和 center=False 两个口子。

OCC 布尔:SetArguments / SetTools 不能混

这是本项目最贵的一个坑——症状是"字形并不进币体、静默失败", 当时误判成"OCC 吃不下去光滑曲面",差点去换内核。

# ✗ 全塞进 SetArguments → IsDone()=False,Shape() 是 Null TopoDS_Shape
op.SetArguments([body, p1, p2, ...])

# ✓ 被融体放 SetArguments,工具放 SetTools
op.SetArguments([body])
op.SetTools([p1, p2, ...])

校准法(references/fuse-calibration.py,跑一次就知道是"调用错"还是"几何错"): 拿一个体积能手算的最小算例,把"结果必须等于多少"先写死—— 币体 25324 + 方块 920 − 重叠 800 = 25444,改调用方式后分毫不差。

教训:报 IsDone=False 时先怀疑自己的调用,再怀疑内核。

Shape.fuse() 在复杂输入上会静默返回错误结果(面数变少、体积变负), 必须每步打印 len(result.Solids()) 和 Volume() 盯着。

渲染:Blender 无头

见 references/blender-preview.md。要点:--background --factory-startup --python, bpy.ops.wm.stl_import / stl_export(4.5 改名了),EEVEE Next。 四视角 + 局部特写 + 拼图(compose.py)。 closeup.py 的 SCALE 是正交视野的毫米数——传 2.0 等于看 2mm 的区域,会拍到一片空白。

数字和渲染,两边都得看

渲染图会骗人:布尔静默失败后埋在实心内部的字、面片化的轮廓,渲染都画得像正常的。

但反过来同样成立——有些错数值一个都抓不到。 那个"绕向分类"bug 让两个字只剩轮廓线, 而实体合法、体积为正、无开边、无碎面全绿。是用户看渲染图一眼指出来的。

数值全绿不等于做对了。

嘉立创免费打样:设计期就要对的量化红线

检测项口径设计时怎么满足
窄槽槽宽 >1.1mm字缝、字与字之间、浮雕与壁之间全是槽,都要过线
薄壁壁厚 >1.0mm笔画本身、补料、细桥
底部装夹面可吸附面积 ≥包围盒 35%真空吸盘,见下
内圆角直径 ≥1.0mm圆体字天然满足;花瓣/波浪的 V 形凹口必然收窄,要么加圆角要么整体收细
Logo不支持雕刻文字一律做凸起

尺寸 2×2×2 ~ 200×200×15mm;材料仅 6061 铝;上下两面都能加工(限的是侧面特征)。

吸附面:唯一的杠杆是加宽圆环

判据是"和最低面齐平的接触面积":

  • 凹面底比外圈环低(哪怕只低 0.5mm)→ 碰不到吸盘,不算
  • 凸起浮雕高出环面 → 变成接触点把平面顶起来 → 该面失效
  • 字顶受"与外圈齐平"约束也不能再高
  • 包围盒是下料尺寸(Ø80 的件按 90×90 = 8100mm² 算),不是零件尺寸

能贴住吸盘的只有「外圈环 + 与外圈齐平的字顶」,所以唯一的杠杆就是环面有多宽 → 缩小凹面半径(DISH_R)。 实测:DISH_R 34.3→28.0 把环面从 1100 提到 2350mm²,正面接触 24.6%→37.7%、反面 22.5%→35.5%。 代价是凹面窄了、字必须缩小塞得进去。

别想别的招:把凹面做浅来"蒙混"(被明确否掉);反面留空虽然最省事(整块平面 62%), 但反面一旦要有字,那块完整平面就没了。

日期 / 装饰不要放在圆环上

圆环上任何凸起都会把接触面顶掉——实测日期凸 0.9mm 时正面最高层只剩 45mm²(环面 1930mm² 全悬空)。 装饰排在凹面里,字顶与环面齐平,既不碰吸盘又计入接触面积。

排布:字与字之间也算"槽"

不只字内部的笔画缝是槽——字与字之间的空隙、浮雕与外壁之间的空隙,全是槽,都要过线。

别用外接框间距当真实间距。 fit_r(外接圆半径)是按墨迹包围盒算出来的, 框就是墨迹的边界,一点余量都没有。本项目翻过车:按框算「黎」底边 −3.6、 「君婷」顶边 −4.1,间距 0.5mm,还把这数字写进了注释,却拿它跟"看着有缝"比、 没跟线比 → 被判窄槽。

改完必须实测,而且量真实几何(BRepExtrema_DistShapeShape),别量重建出来的轮廓。

视觉

  • 圆体字的笔画末端是圆的 → 内圆角天然过;方黑体不一定
  • 花瓣/波浪等造型的 V 形凹口底部必然收窄,几乎一定踩窄槽
  • 把造型做成一条光滑闭合曲线(周期插值 B 样条)而不是"若干小圆拼花瓣"—— 后者圆与圆之间会留 <1.1mm 的窄缝,直接是窄槽;前者每朵花只有 3 个面 (1 个光滑侧壁 + 顶底),零碎面

工作流

  1. 先分清"建"还是"修"——修的话去 cnc-dfm-repair
  2. 找参考把尺寸量清楚(STEP 的 CARTESIAN_POINT 直接正则扫即可,不用 CAD 库)
  3. 用 cadquery 按解析曲面重建,别指望格式转换
  4. 汉字走 fontTools → skia → OCC 那条链,别用 Workplane.text()
  5. 每步量体积/包围盒/开边
  6. 无头 Blender 渲染给用户看
  7. 送 CNC 前把上面的红线对着改;上传后以 DFM 橙点为准,不要推理

参考

  • references/thin-glyph.py — 汉字 → 光滑 BREP 的完整实现(fontTools → skia → OCC), 含 thin 削细、fix_slot / fix_wall 补料、thicken_at 定点补厚
  • references/fuse-calibration.py — 布尔失败时的最小算例校准,跑一次定性
  • references/suction-area.py — 量吸附面。自动认厚度轴,按高度层汇总共面平面
  • references/face-audit.py — 面类型分布 + 碎面清单(带中心坐标)
  • references/cadquery-patterns.md — 旋转体、文字、圆环阵列、导出
  • references/blender-preview.md — 无头渲染与多视图拼图

修复 / 检测 / 验证已有 STEP → 用 cnc-dfm-repair 技能,第一道是 scripts/dish_slots.py(凹面底面轮廓间距 + 退化边检查,实测与平台判定一致), 再往下是 scripts/step_audit.py(面-面精确间距 + 编辑掩膜布尔差分)和该项目的实测记录。