166 行
7.4 KiB
Python
166 行
7.4 KiB
Python
# -*- 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))
|