文件
phonerail/.claude/r2/shell/export_stl_v10.py
T

166 行
7.4 KiB
Python
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# -*- 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))