# -*- coding: utf-8 -*- u"""导出 TopShell_v10 / BottomPlate_v6 为打印用 STL(2026-09-13)。 1. 从 swconst.tlb 读 swLengthUnit_e 里毫米的值(不靠记忆) 2. 备份当前 STL 用户设置 → 设为:二进制、毫米、自定义精度(偏差 0.01 mm、角度 5°)、不自动平移、 关预览与保存后信息框(否则会弹模态框卡住 COM,NOTES §34) 3. SaveAs3 导出(零件坐标)→ 恢复原设置 4. numpy-stl 读回核对:包围盒 = 模型包围盒(顺带验单位)、每条边恰被 2 个三角形共用(封闭)、 体积与 SolidWorks 质量属性体积一致 5. 顶盖翻转 180°(绕 x)使**顶面贴床**;两件都平移到 x/y/z ≥ 0;报告贴床面积 """ import io import os import sys sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', write_through=True) import numpy as np import pythoncom import win32com.client as wc from stl import mesh as stlmesh PARTS = [(u'TopShell_v10', True), (u'BottomPlate_v6', False)] # (零件名, 是否倒扣打印) SRC_DIR = u'D:\\DCDC\\新建文件夹\\5blades\\' OUT_DIR = SRC_DIR + u'print\\' EMPTY = wc.VARIANT(pythoncom.VT_DISPATCH, None) T_BIN, T_INFO, T_NOTRANS, T_PREVIEW = 69, 70, 71, 191 I_QUAL, I_UNITS = 78, 211 D_DEV, D_ANG = 2, 3 Q_CUSTOM = 3 def prop(o, n): v = getattr(o, n) return v() if type(v).__name__ in ('method', 'function', 'builtin_function_or_method') else v def D(x): return x if hasattr(x, '_oleobj_') else wc.Dispatch(x) def P(*a): print(*a) tlb = pythoncom.LoadTypeLib(r'D:\Program Files\SOLIDWORKS Corp\SOLIDWORKS\swconst.tlb') MM = None for i in range(tlb.GetTypeInfoCount()): if tlb.GetDocumentation(i)[0] != 'swLengthUnit_e': continue ti = tlb.GetTypeInfo(i) for v in range(ti.GetTypeAttr().cVars): vd = ti.GetVarDesc(v) if ti.GetNames(vd.memid)[0] == 'swMM': MM = vd.value P(u'swLengthUnit_e.swMM = %s' % MM) if MM is None: P(u'⛔ 类型库里找不到毫米枚举,停手'); sys.exit(2) app = wc.GetActiveObject('SldWorks.Application') backup = { 'T': {k: app.GetUserPreferenceToggle(k) for k in (T_BIN, T_INFO, T_NOTRANS, T_PREVIEW)}, 'I': {k: app.GetUserPreferenceIntegerValue(k) for k in (I_QUAL, I_UNITS)}, 'D': {k: app.GetUserPreferenceDoubleValue(k) for k in (D_DEV, D_ANG)}, } P(u'原 STL 设置 %s' % backup) app.SetUserPreferenceToggle(T_BIN, True) app.SetUserPreferenceToggle(T_INFO, False) app.SetUserPreferenceToggle(T_NOTRANS, True) app.SetUserPreferenceToggle(T_PREVIEW, False) app.SetUserPreferenceIntegerValue(I_QUAL, Q_CUSTOM) app.SetUserPreferenceIntegerValue(I_UNITS, MM) app.SetUserPreferenceDoubleValue(D_DEV, 0.01 / 1000.0) app.SetUserPreferenceDoubleValue(D_ANG, np.radians(5.0)) P(u'已设:二进制 / 毫米 / 自定义 偏差 0.01 mm 角度 5° / 不平移 / 关预览与信息框') os.makedirs(OUT_DIR, exist_ok=True) results = [] try: for name, flip in PARTS: path = SRC_DIR + name + u'.SLDPRT' part = None for d in app.GetDocuments or []: if (prop(d, 'GetPathName') or '').lower() == path.lower(): part = d if part is None: e = wc.VARIANT(pythoncom.VT_BYREF | pythoncom.VT_I4, 0) w = wc.VARIANT(pythoncom.VT_BYREF | pythoncom.VT_I4, 0) part = app.OpenDoc6(path, 1, 1, '', e, w) e0 = wc.VARIANT(pythoncom.VT_BYREF | pythoncom.VT_I4, 0) app.ActivateDoc3(prop(part, 'GetTitle'), False, 0, e0) part = app.ActiveDoc bodies = part.GetBodies2(0, True) or [] bb = [v * 1000 for v in prop(D(bodies[0]), 'GetBodyBox')] vol_sw = None try: mp = part.Extension.CreateMassProperty vol_sw = prop(D(mp), 'Volume') * 1e9 except Exception as ex: P(u' 质量属性读取失败 %s' % str(ex)[:60]) # ⛔ 零件里若有选中的面/边,SaveAs STL 只导出选中部分(首次导出顶盖只得到标识凹槽 252 个三角形、 # 底盖只得到一个倒角 36 个三角形)⇒ 导出前必须清空选择 part.ClearSelection2(True) P(u' 导出前选中数 %d(应 0)' % part.SelectionManager.GetSelectedObjectCount2(-1)) raw = OUT_DIR + u'_raw_' + name + u'.stl' e1 = wc.VARIANT(pythoncom.VT_BYREF | pythoncom.VT_I4, 0) w1 = wc.VARIANT(pythoncom.VT_BYREF | pythoncom.VT_I4, 0) part.Extension.SaveAs3(raw, 0, 1, EMPTY, EMPTY, e1, w1) ok_file = os.path.exists(raw) and os.path.getsize(raw) > 1000 P(u'\n== %s == 导出 err=%s 存在=%s %.0f KB' % (name, e1.value, ok_file, os.path.getsize(raw) / 1024 if ok_file else 0)) if not ok_file: results.append((name, False)); continue m = stlmesh.Mesh.from_file(raw) pts = m.vectors.reshape(-1, 3) mn, mx = pts.min(axis=0), pts.max(axis=0) box_ok = np.allclose(mn, bb[0:3], atol=0.05) and np.allclose(mx, bb[3:6], atol=0.05) P(u' 三角形 %d;STL 包围盒 %s ~ %s' % (len(m.vectors), np.round(mn, 3), np.round(mx, 3))) P(u' 模型包围盒 %s ~ %s %s(单位毫米)' % (np.round(bb[0:3], 3), np.round(bb[3:6], 3), u'✅' if box_ok else u'⛔')) q = np.round(pts / 1e-4).astype(np.int64).reshape(-1, 3, 3) edges = {} for tri in q: a, b, c = (tuple(v) for v in tri) for e_ in ((a, b), (b, c), (c, a)): k = (min(e_), max(e_)) edges[k] = edges.get(k, 0) + 1 bad = sum(1 for v in edges.values() if v != 2) P(u' 边 %d 条,非「恰 2 个三角形共用」的边 %d 条 %s' % (len(edges), bad, u'✅ 封闭' if bad == 0 else u'⛔ 有破洞/非流形')) vol, cog, inertia = m.get_mass_properties() vol = abs(vol) if vol_sw: dv = abs(vol - vol_sw) / vol_sw * 100 P(u' 体积 STL %.1f mm³ / SolidWorks %.1f mm³,差 %.3f%% %s' % (vol, vol_sw, dv, u'✅' if dv < 0.5 else u'⛔')) else: dv = 0.0 P(u' 体积 STL %.1f mm³(SolidWorks 体积没读到,未比对)' % vol) if flip: m.rotate([1.0, 0.0, 0.0], np.radians(180.0)) # 绕 x 翻 180°:顶面朝下贴床 pts = m.vectors.reshape(-1, 3) m.vectors -= pts.min(axis=0) pts = m.vectors.reshape(-1, 3) size = pts.max(axis=0) - pts.min(axis=0) z0 = m.vectors[:, :, 2] on_bed = np.all(np.abs(z0) < 1e-3, axis=1) bed_area = float(m.areas[on_bed].sum()) out = OUT_DIR + u'PhoneRail_' + name + u'.stl' m.save(out) P(u' 打印朝向:%s;外形 %.1f × %.1f × %.1f;贴床面积 %.0f mm²' % (u'倒扣(顶面贴床)' if flip else u'平放(底面贴床)', size[0], size[1], size[2], bed_area)) P(u' → %s %.0f KB' % (out, os.path.getsize(out) / 1024)) os.remove(raw) results.append((name, box_ok and bad == 0 and dv < 0.5)) finally: for k, v in backup['T'].items(): app.SetUserPreferenceToggle(k, v) for k, v in backup['I'].items(): app.SetUserPreferenceIntegerValue(k, v) for k, v in backup['D'].items(): app.SetUserPreferenceDoubleValue(k, v) P(u'\n已恢复原 STL 设置') P(u'总判:%s' % (u'✅ 全过' if all(ok for _, ok in results) and len(results) == len(PARTS) else u'⛔ %s' % results))