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

722 行
31 KiB
Python
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# -*- coding: utf-8 -*-
# v10:嵌入式底盖 —— 墙向下延长 3.6 包住底盖(墙底 −13.6),角柱仍止于 −10 当限位;其余同 v9
# v9:格栅 槽宽 6 / 竖筋 2.0 / 横筋 2.0 中心距 12(最长槽段 10),其余同 v7
# v7:无滤网版 —— 删西端滤网导轨,两端墙竖槽格栅,外形 x −18.4~115.9
# v6:两端墙都用竖槽格栅(西端 Ø60 圆孔取消),其余同 v5
# v5 外观:顶面 R1.5 圆角、圆头泄压槽、东风口竖槽格栅、Type-C 凹刻标识(其余同 v4)
# v4:去北墙两个窗口;南墙 Type-C 开口 = 母座口内侧轮廓 8.34×2.60 R0.7
# v3 短机版:风扇 6010(孔距 50.2、Ø5.2 过孔)+ 分风腔 10;内腔 x −16~113.5、外 −18.4~115.9、导轨唇边 −24.8
u"""**v2 = v1 + 西端插拔式滤网导轨 + 建模内倒角**(2026-09-13)。
导轨让包围盒西界变成唇边 x −43.8,add_fillets 的「包围盒角」判据会漏掉西侧两角 ⇒ 圆角在加导轨之前做。
(以下为 v1 说明)**一体顶盖** TopShell_v1(2026-09-13 用户改方案:顶盖一体打印 + 底盖一整块 + 螺丝固定)。
= 顶板 + 南/北侧墙 + 西/东端墙 + 4 根内角立柱,底部敞开,从上往下扣在底盖上。
外形与上一版六件式完全一致(175.0 × 110.8,顶外表面 z=74.4、墙底 z=−10),
内腔也一致(x −36~134 / y −1.655~104.345 / 顶内表面 z=72)⇒ 15 mm 分风腔、0.66 顶隙原样保留。
零件本地坐标:x、y = 装配坐标;**本地 z = 装配 z − 74.4**(顶外表面在本地 z=0,全部往 −z 拉)。
装配时纯平移 dz = +74.4,不旋转。
建模顺序是刻意安排的(只用本机验证过的「基准面草图 + 拉伸,孔画进同一张草图」):
[1] 4 根角柱 10×10 + 柱内 Ø4.2 孔,前视面草图,向 −z 拉 84.4(孔此时是通的)
[2] 四面墙:外框 + 内框的环形轮廓,向 −z 拉 84.4
[3] 顶板 + 11 条泄压缝,向 −z 拉 2.4 —— **顺带把柱孔上端封死**,
柱孔变成从底面进、深 82 的盲孔(热熔螺母压在底口 6 mm 内即可),顶面看不到孔
[4] 端墙外表面:Ø60 风口 + 4 × Ø3.4 风扇孔(面上草图切)
[5] 南墙外表面:2 个 13×9 Type-C 窗口
[6] 北墙外表面:J33 窗口 + 风扇出线窗口
面上草图的 uv 映射按候选逐个试,每刀之后回读「目标孔/窗口是否出现在设计位置」;
候选都经过检查:错误映射只会把轮廓画到墙外(切不到料、返回 None)。
若出现「切出了东西但不在目标位置」立即停手、不存盘。
"""
import io
import math
import os
import sys
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', write_through=True)
import pythoncom
import win32com.client as wc
DST = u'D:\\DCDC\\新建文件夹\\5blades\\TopShell_v10.SLDPRT'
EMPTY = wc.VARIANT(pythoncom.VT_DISPATCH, None)
ZOFF = 74.4 # 本地 z = 装配 z − ZOFF
X0, X1, Y0, Y1 = -18.4, 115.9, -4.055, 106.745 # 外形
IX0, IX1, IY0, IY1 = -16.0, 113.5, -1.655, 104.345 # 内腔
T = 2.4
H = 84.4 # 顶外 74.4 → 角柱底 −10(底盖顶面限位)
PLATE_T = 3.6 # 底盖厚
H_WALL = H + PLATE_T # v10:墙向下包住底盖 ⇒ 墙底 −13.6
SLOT_W, SLOT_L, SLOT_N, SLOT_SPAN = 1.5, 60.32, 11, 106.0 # C:圆头槽,长 60.32 使面积仍 = 90
SLOT_XC, SLOT_YC = (X0 + X1) / 2.0, 51.345
POST = 10.0
POST_RECTS = [(IX0, IX0 + POST, IY0, IY0 + POST), (IX0, IX0 + POST, IY1 - POST, IY1),
(IX1 - POST, IX1, IY0, IY0 + POST), (IX1 - POST, IX1, IY1 - POST, IY1)]
POST_HOLES = [((a + b) / 2.0, (c + d) / 2.0) for a, b, c, d in POST_RECTS]
R_INS = 2.1
FAN_CY, FAN_CZ = 51.345, 37.85
R_VENT, R_FAN = 30.0, 2.6 # 风扇孔 Ø5.2:机箱自攻螺丝要穿墙拧进风扇 Ø4.4 孔
END_CIRCLES = [(FAN_CY, FAN_CZ, R_VENT)] + [(FAN_CY + dy, FAN_CZ + dz, R_FAN)
for dy in (-25.1, 25.1) for dz in (-25.1, 25.1)]
SOUTH_WINDOWS = [(6.0, 19.0, -5.7, 3.3), (77.5, 90.5, -5.7, 3.3)] # x0,x1,z0,z1(装配)
NORTH_WINDOWS = [(72.14, 87.85, -3.0, 12.0), (0.53, 19.03, -3.0, 15.0)]
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)
def M(v):
return v / 1000.0
def L(z):
return z - ZOFF
app = wc.GetActiveObject('SldWorks.Application')
for d in list(app.GetDocuments or []):
try:
t = prop(d, 'GetTitle') or ''
if 'TOPSHELL_V10' in t.upper():
app.CloseDoc(t)
except Exception:
pass
tmpl = app.GetUserPreferenceStringValue(8)
app.NewDocument(tmpl, 0, 0.0, 0.0)
part = app.ActiveDoc
P(u'新建零件 %s' % prop(part, 'GetTitle'))
sm, fm = part.SketchManager, part.FeatureManager
def last_sketch():
nm, f = None, prop(part, 'FirstFeature')
while f:
if prop(f, 'GetTypeName2') == 'ProfileFeature':
nm = prop(f, 'Name')
f = prop(f, 'GetNextFeature')
return nm
def pick(name, typ):
part.ClearSelection2(True)
part.Extension.SelectByID2(name, typ, 0, 0, 0, False, 0, EMPTY, 0)
return part.SelectionManager.GetSelectedObjectCount2(-1)
def bodies():
return part.GetBodies2(0, True) or []
def faces():
b = bodies()
return prop(D(b[0]), 'GetFaces') if b else []
def bbox():
b = bodies()
if not b:
return None
xs = [[round(v * 1000, 3) for v in prop(D(x), 'GetBodyBox')] for x in b]
return [min(v[0] for v in xs), min(v[1] for v in xs), min(v[2] for v in xs),
max(v[3] for v in xs), max(v[4] for v in xs), max(v[5] for v in xs)]
def cyls():
out = []
for f in faces():
fc = D(f)
s = prop(fc, 'GetSurface')
if prop(s, 'IsCylinder'):
cp = list(prop(s, 'CylinderParams'))
fb = [v * 1000 for v in prop(fc, 'GetBox')]
ax = max(range(3), key=lambda i: abs(cp[3 + i]))
out.append((round(cp[6] * 1000, 2), ax, fb))
return out
def hist():
h = {}
for r, _, _ in cyls():
h[r] = h.get(r, 0) + 1
return dict(sorted(h.items()))
def planes():
u"""[(朝外法线轴, 符号, box)] —— 朝外 = PlaneParams × (sense ? −1 : +1)(NOTES §36)"""
out = []
for f in faces():
fc = D(f)
s = prop(fc, 'GetSurface')
if not prop(s, 'IsPlane'):
continue
n = list(prop(s, 'PlaneParams'))[:3]
if prop(fc, 'FaceInSurfaceSense'):
n = [-v for v in n]
ax = max(range(3), key=lambda i: abs(n[i]))
out.append((ax, 1 if n[ax] > 0 else -1, [v * 1000 for v in prop(fc, 'GetBox')],
prop(fc, 'GetArea') * 1e6, fc))
return out
def boss(depth, rev):
if pick(last_sketch(), 'SKETCH') != 1:
raise RuntimeError(u'草图没选中')
return fm.FeatureExtrusion3(True, False, rev, 0, 0, M(depth), 0.0,
False, False, False, False, 0.0, 0.0,
False, False, False, False, True, True, True, 0, 0.0, False)
def check(tag, want, tol=0.02):
got = bbox()
ok = got is not None and all(abs(got[i] - want[i]) <= tol for i in range(6))
P(u' %s box %s %s' % (tag, got, u'✅' if ok else u'⛔ 期望 %s' % want))
if not ok:
sys.exit(2)
# ── [1] 角柱 + 柱孔 ───────────────────────────────────────────────────────
P(u'\n[1] 4 根角柱 %.0f×%.0f + Ø%.1f 孔,向 −z 拉 %.1f' % (POST, POST, 2 * R_INS, H))
pick(u'前视基准面', 'PLANE')
sm.InsertSketch(True)
sm.AddToDB = True
for a, b, c, d in POST_RECTS:
sm.CreateCornerRectangle(M(a), M(c), 0.0, M(b), M(d), 0.0)
for hx, hy in POST_HOLES:
sm.CreateCircleByRadius(M(hx), M(hy), 0.0, M(R_INS))
sm.AddToDB = False
sm.InsertSketch(True)
boss(H, True)
check(u'角柱', [IX0, IY0, -H, IX1, IY1, 0.0])
# ── [2] 四面墙(环形轮廓)────────────────────────────────────────────────
P(u'\n[2] 四面墙 环形轮廓,向 −z 拉 %.1f(包住底盖)' % H_WALL)
pick(u'前视基准面', 'PLANE')
sm.InsertSketch(True)
sm.AddToDB = True
sm.CreateCornerRectangle(M(X0), M(Y0), 0.0, M(X1), M(Y1), 0.0)
sm.CreateCornerRectangle(M(IX0), M(IY0), 0.0, M(IX1), M(IY1), 0.0)
sm.AddToDB = False
sm.InsertSketch(True)
boss(H_WALL, True)
check(u'墙', [X0, Y0, -H_WALL, X1, Y1, 0.0])
def arc_dir(qc, q1, qm):
u"""草图坐标里:从 q1 出发、逆时针先经过弧中点 qm ⇒ 1,否则 −1(半圆也适用)"""
return 1 if (q1[0] - qc[0]) * (qm[1] - qc[1]) - (q1[1] - qc[1]) * (qm[0] - qc[0]) > 0 else -1
def stadium_front(xc, yc, w, L):
u"""前视基准面(草图坐标 = 模型 x, y)画长沿 y 的圆头槽"""
r, a = w / 2.0, L / 2.0 - w / 2.0
sm.CreateLine(M(xc + r), M(yc - a), 0.0, M(xc + r), M(yc + a), 0.0)
sm.CreateLine(M(xc - r), M(yc + a), 0.0, M(xc - r), M(yc - a), 0.0)
for cy, sg in ((yc + a, 1), (yc - a, -1)):
qc, q1, q2, qm = (xc, cy), (xc + r, cy), (xc - r, cy), (xc, cy + sg * r)
sm.CreateArc(M(qc[0]), M(qc[1]), 0.0, M(q1[0]), M(q1[1]), 0.0, M(q2[0]), M(q2[1]), 0.0,
arc_dir(qc, q1, qm))
# ── [3] 顶板 + 泄压缝(封住柱孔上端)──────────────────────────────────────
P(u'\n[3] 顶板 + %d 条泄压缝,向 −z 拉 %.1f' % (SLOT_N, T))
step = SLOT_SPAN / (SLOT_N - 1)
pick(u'前视基准面', 'PLANE')
sm.InsertSketch(True)
sm.AddToDB = True
sm.CreateCornerRectangle(M(X0), M(Y0), 0.0, M(X1), M(Y1), 0.0)
for i in range(SLOT_N):
xc = SLOT_XC - SLOT_SPAN / 2.0 + i * step
stadium_front(xc, SLOT_YC, SLOT_W, SLOT_L) # C:圆头槽,开孔面积与原 1.5×60 相同
sm.AddToDB = False
sm.InsertSketch(True)
boss(T, True)
check(u'顶板', [X0, Y0, -H_WALL, X1, Y1, 0.0])
ph = [c for c in cyls() if abs(c[0] - R_INS) < 0.01 and c[1] == 2]
P(u' 柱孔 R%.1f 沿 z:%d 个(期望 4)' % (R_INS, len(ph)))
for r, ax, fb in ph:
P(u' 中心 (%.2f, %.2f) z %.2f~%.2f(期望 %.1f~%.1f,上端被顶板封住)'
% ((fb[0] + fb[3]) / 2, (fb[1] + fb[4]) / 2, fb[2], fb[5], -H, -T))
if len(ph) != 4 or any(abs(fb[5] - (-T)) > 0.05 or abs(fb[2] + H) > 0.05 for _, _, fb in ph):
P(u'⛔ 柱孔不对,停手'); sys.exit(3)
slot_faces = sum(1 for ax, sg, fb, a, _ in planes()
if ax == 0 and abs(fb[2] + T) < 0.05 and abs(fb[5]) < 0.05
and abs(fb[4] - fb[1] - (SLOT_L - SLOT_W)) < 0.05)
P(u' 泄压缝侧面 %d 个(期望 %d = 每条 2)%s' % (slot_faces, 2 * SLOT_N,
u'✅' if slot_faces == 2 * SLOT_N else u'⛔'))
if slot_faces != 2 * SLOT_N:
sys.exit(3)
# ── [3b] 外轮廓 4 条竖边倒 R3(此时包围盒 = 盒体本身,导轨还没加)─────────
P(u'\n[3b] 外轮廓竖边倒 R3')
_bx = bbox()
_cand = []
for _e in prop(D(bodies()[0]), 'GetEdges'):
_E = D(_e)
try:
_p0 = [v * 1000 for v in prop(D(prop(_E, 'GetStartVertex')), 'GetPoint')]
_p1 = [v * 1000 for v in prop(D(prop(_E, 'GetEndVertex')), 'GetPoint')]
except Exception:
continue
if abs(_p0[0] - _p1[0]) > 1e-6 or abs(_p0[1] - _p1[1]) > 1e-6 or abs(_p0[2] - _p1[2]) < 1e-6:
continue
if all(min(abs(_p[i] - _bx[i]), abs(_p[i] - _bx[i + 3])) < 0.01 for _p in (_p0, _p1) for i in (0, 1)):
_cand.append(_E)
_done = 0
for _E in _cand:
for _r in (3.0, 2.0, 1.5):
part.ClearSelection2(True)
_E.Select4(False, EMPTY)
try:
_f = fm.FeatureFillet3(195, M(_r), 0, 0, 0, 0, 0, None, None, None, None, None, None, None)
except Exception:
_f = None
if _f:
_done += 1
break
P(u' 候选 %d 条,成功 %d 条;直方图 %s' % (len(_cand), _done, hist()))
if _done != 4 or hist().get(3.0, 0) != 4:
P(u'⛔ 圆角没做全,停手'); sys.exit(8)
# ── [3c] A:顶面外轮廓倒 R1.5(此时外轮廓 = 4 直边 + 4 段 R3 弧,导轨未加)──────────
TOP_R = 1.5
P(u'\n[3c] 顶面外轮廓倒 R%.1f' % TOP_R)
_bx = bbox()
_sel = []
def _onb(p):
return (min(abs(p[0] - _bx[0]), abs(p[0] - _bx[3])) < 0.01 or
min(abs(p[1] - _bx[1]), abs(p[1] - _bx[4])) < 0.01)
for _e in prop(D(bodies()[0]), 'GetEdges'):
_E = D(_e)
try:
_p0 = [v * 1000 for v in prop(D(prop(_E, 'GetStartVertex')), 'GetPoint')]
_p1 = [v * 1000 for v in prop(D(prop(_E, 'GetEndVertex')), 'GetPoint')]
except Exception:
continue
if abs(_p0[2]) > 0.01 or abs(_p1[2]) > 0.01:
continue
if _onb(_p0) and _onb(_p1):
_sel.append(_E)
P(u' 顶面外轮廓边 %d 条(期望 8 = 4 直边 + 4 段 R3 弧)' % len(_sel))
if len(_sel) != 8:
P(u'⛔ 外轮廓边数不对,停手'); sys.exit(8)
part.ClearSelection2(True)
for _i, _E in enumerate(_sel):
_E.Select4(_i > 0, EMPTY)
P(u' 选中 %d 条' % part.SelectionManager.GetSelectedObjectCount2(-1))
try:
_f = fm.FeatureFillet3(195, M(TOP_R), 0, 0, 0, 0, 0, None, None, None, None, None, None, None)
except Exception as _ex:
_f = None
P(u' 异常 %s' % str(_ex)[:80])
_h = hist()
P(u' FeatureFillet3 → %r;直方图 %s(R1.5 圆柱面应 4 = 4 条直边)' % (prop(_f, 'Name') if _f else _f, _h))
if _h.get(TOP_R, 0) != 4:
P(u'⛔ 顶面圆角没成,停手'); sys.exit(8)
# ── 面上草图切除(带候选映射)────────────────────────────────────────────
def pick_face(ax, sign, coord):
best, ba = None, -1.0
for a_, s_, fb, area, fc in planes():
if a_ != ax or s_ != sign:
continue
if abs(fb[ax] - coord) > 0.02 or abs(fb[ax + 3] - coord) > 0.02:
continue
if area > ba:
best, ba = fc, area
if best is None:
return False
part.ClearSelection2(True)
best.Select4(False, EMPTY)
return part.SelectionManager.GetSelectedObjectCount2(-1) == 1
def do_cut(depth):
if pick(last_sketch(), 'SKETCH') != 1:
return None
return fm.FeatureCut4(True, False, False, 0, 0, M(depth), 0.0,
False, False, False, False, 0.0, 0.0,
False, False, False, False, False,
True, True, False, False, False, 0, 0.0, False, False)
def sketch_xform():
sk = prop(sm, 'ActiveSketch')
if not sk:
return None
arr = list(prop(D(sk), 'ModelToSketchTransform').ArrayData)
Mx = [[arr[3 * c + r] for c in range(3)] for r in range(3)] # 列主序
return Mx, arr[9:12]
def to_sketch(X, p):
u"""模型点 p(mm)→ 草图坐标(米)"""
Mx, t = X
q = [v / 1000.0 for v in p]
return [sum(Mx[r][c] * q[c] for c in range(3)) + t[r] for r in range(3)]
def open_face_sketch(face, tag):
ax, sign, coord = face
for attempt in range(1, 4):
if not pick_face(*face):
P(u' ⛔ %s 选不到面' % tag); sys.exit(4)
sm.InsertSketch(True)
X = sketch_xform()
ok, nax, sz = False, -1, 99.0
if X:
nax = max(range(3), key=lambda i: abs(X[0][2][i]))
probe = [0.0, 0.0, 0.0]
probe[ax] = coord
sz = to_sketch(X, probe)[2] * 1000.0
ok = nax == ax and abs(sz) < 0.01
P(u' %s 进草图 第 %d 次:法线轴 %s(目标 %s)、墙面点草图 z = %.4f mm %s'
% (tag, attempt, 'xyz'[nax] if nax >= 0 else '?', 'xyz'[ax], sz, u'✅' if ok else u'✗ 平面不对,退出重进'))
if ok:
return X
sm.InsertSketch(True)
P(u' ⛔ %s 三次都没进到目标面,停手' % tag); sys.exit(4)
def cut_shapes(tag, face, circles, rects, verify):
nbefore = len(faces())
X = open_face_sketch(face, tag)
sm.AddToDB = True
for p, r in circles:
q = to_sketch(X, p)
sm.CreateCircleByRadius(q[0], q[1], 0.0, M(r))
for p0, p1 in rects:
q0, q1 = to_sketch(X, p0), to_sketch(X, p1)
sm.CreateCornerRectangle(q0[0], q0[1], 0.0, q1[0], q1[1], 0.0)
sm.AddToDB = False
sm.InsertSketch(True)
c = do_cut(T + 0.2)
ok = verify()
P(u' %s 切除 → %-12r 面数 %d→%d 目标 %s'
% (tag, prop(c, 'Name') if c else c, nbefore, len(faces()), u'✅' if ok else u'⛔'))
if not ok:
P(u' ⛔⛔ 目标没出现在设计位置 —— 停手、不存盘'); sys.exit(5)
def end_verify(xwall):
def v():
cs = cyls()
for cy, cz, r in END_CIRCLES:
hit = any(abs(rr - r) < 0.01 and ax_ == 0 and
abs((fb[1] + fb[4]) / 2 - cy) < 0.05 and abs((fb[2] + fb[5]) / 2 - L(cz)) < 0.05 and
min(abs(fb[0] - xwall), abs(fb[3] - xwall)) < 0.05
for rr, ax_, fb in cs)
if not hit:
return False
return True
return v
def rect_verify(wins, ywall0, ywall1):
def v():
ps = planes()
for x0, x1, z0, z1 in wins:
for xe in (x0, x1):
hit = any(ax_ == 0 and abs(fb[0] - xe) < 0.05 and abs(fb[3] - xe) < 0.05 and
abs(fb[2] - L(z0)) < 0.05 and abs(fb[5] - L(z1)) < 0.05 and
fb[1] >= ywall0 - 0.05 and fb[4] <= ywall1 + 0.05
for ax_, sg, fb, a_, _ in ps)
if not hit:
return False
return True
return v
P(u'\n[4] 端墙:Ø60 风口 + 4 × Ø%.1f 风扇孔(孔距 %.1f)' % (2 * R_FAN, 2 * abs(END_CIRCLES[1][0] - FAN_CY)))
# ── B:两端墙竖槽格栅 + 3 条横撑(v8:解决「长细筋打不出来」)─────────────────────
GR_R, GR_W, GR_RIB = 32.0, 6.0, 2.0 # v9:槽宽 6、竖筋 2.0(用户选 ③)
XB_W, XB_SEG = 2.0, 10.0 # 横筋宽 2.0,最长槽段 10 ⇒ 横筋中心距 12
XB_P = XB_SEG + XB_W
XB_Z = tuple(k * XB_P for k in range(-int(GR_R // XB_P) - 1, int(GR_R // XB_P) + 2))
def grille_slots():
p, out = GR_W + GR_RIB, []
n = int((GR_R - GR_W / 2) // p)
for k in range(-n, n + 1):
y = k * p
e = abs(y) + GR_W / 2
if e >= GR_R:
continue
L_ = 2 * math.sqrt(GR_R * GR_R - e * e)
if L_ <= GR_W + 1:
continue
out.append((FAN_CY + y, L_))
return out
def slot_segments(L_):
u"""一条槽被横筋切成的段:[(z0, z1, round0, round1)],z 相对风扇中心;round = 该端是真正的槽头(半圆)。
v9 通用规则:横筋可以切到槽头附近;短到放不下「端头 + 0.8 直边」的碎段做成实心(不画)。"""
r = GR_W / 2
h = L_ / 2
cuts = sorted((c - XB_W / 2, c + XB_W / 2) for c in XB_Z if c + XB_W / 2 > -h and c - XB_W / 2 < h)
ivs, z = [], -h
for lo, hi in cuts:
ivs.append((z, lo))
z = hi
ivs.append((z, h))
segs = []
for z0, z1 in ivs:
r0, r1 = abs(z0 + h) < 1e-9, abs(z1 - h) < 1e-9
need = (r if r0 else 0.0) + (r if r1 else 0.0) + 0.8
if z1 - z0 < need:
continue
segs.append((z0, z1, r0, r1))
return segs, 0
def seg_area(z0, z1, r0, r1):
r = GR_W / 2
a_ = (z1 - z0) - (r if r0 else 0) - (r if r1 else 0)
return GR_W * a_ + (math.pi * r * r / 2) * (int(r0) + int(r1))
GRS = grille_slots()
A_SLOTS = 0.0
N_FLAT = 0
LONGEST = 0.0
N_ROUND = 0
N_SEG = 0
for _ys, _L in GRS:
_segs, _ = slot_segments(_L)
A_SLOTS += sum(seg_area(*sg) for sg in _segs)
N_FLAT += sum(int(not sg[2]) + int(not sg[3]) for sg in _segs)
N_ROUND += sum(int(sg[2]) + int(sg[3]) for sg in _segs)
N_SEG += len(_segs)
LONGEST = max([LONGEST] + [sg[1] - sg[0] for sg in _segs])
A_HOLES = 4 * math.pi * R_FAN ** 2
P(u' 格栅:%d 条竖槽(宽 %.1f 竖筋 %.1f 外包 R%.0f)+ 横筋 %.1f(中心距 %.0f),共 %d 段;开孔 %.1f mm²(原 Ø60 的 %.1f%%),最长槽段 %.1f;半圆端 %d、平端 %d'
% (len(GRS), GR_W, GR_RIB, GR_R, XB_W, XB_P, N_SEG, A_SLOTS, A_SLOTS / (math.pi * 900) * 100, LONGEST, N_ROUND, N_FLAT))
def grille_cut(tag, xw, sg):
xi = xw - sg * 2.4 # 墙内表面
def face_area():
return sum(a_ for ax_, sgn, fb, a_, _ in planes()
if ax_ == 0 and sgn == sg and abs(fb[0] - xw) < 0.02 and abs(fb[3] - xw) < 0.02)
def flat_ends():
lo, hi = min(xw, xi), max(xw, xi)
return sum(1 for ax_, sgn, fb, a_, _ in planes()
if ax_ == 2 and abs(fb[0] - lo) < 0.05 and abs(fb[3] - hi) < 0.05 and abs((fb[4] - fb[1]) - GR_W) < 0.05)
caps0 = sum(1 for rr, ax_, fb in cyls() if abs(rr - GR_W / 2) < 0.01 and ax_ == 0)
flat0 = flat_ends()
a_before = face_area()
nbefore = len(faces())
X = open_face_sketch((0, sg, xw), tag)
sm.AddToDB = True
for cy, cz, r in END_CIRCLES[1:]:
q = to_sketch(X, (xw, cy, L(cz)))
sm.CreateCircleByRadius(q[0], q[1], 0.0, M(r))
zc = L(FAN_CZ)
r = GR_W / 2
def S(y, z):
return to_sketch(X, (xw, y, zc + z))
def line(y0, z0, y1, z1):
q0, q1 = S(y0, z0), S(y1, z1)
sm.CreateLine(q0[0], q0[1], 0.0, q1[0], q1[1], 0.0)
for ys, L_ in GRS:
segs, _ = slot_segments(L_)
for z0, z1, r0, r1 in segs:
b0 = z0 + (r if r0 else 0) # 直边起止
b1 = z1 - (r if r1 else 0)
line(ys + r, b0, ys + r, b1)
line(ys - r, b1, ys - r, b0)
for zz, rounded, s_ in ((b1, r1, 1), (b0, r0, -1)):
if rounded:
qc, q1, q2, qm = S(ys, zz), S(ys + r, zz), S(ys - r, zz), S(ys, zz + s_ * r)
sm.CreateArc(qc[0], qc[1], 0.0, q1[0], q1[1], 0.0, q2[0], q2[1], 0.0, arc_dir(qc, q1, qm))
else:
line(ys + r, zz, ys - r, zz)
sm.AddToDB = False
sm.InsertSketch(True)
c = do_cut(T + 0.2)
d_area = a_before - face_area()
ok_area = abs(d_area - (A_SLOTS + A_HOLES)) < 0.5
caps = sum(1 for rr, ax_, fb in cyls() if abs(rr - GR_W / 2) < 0.01 and ax_ == 0) - caps0
flats = flat_ends() - flat0
fan_ok = all(any(abs(rr - R_FAN) < 0.01 and ax_ == 0 and abs((fb[1] + fb[4]) / 2 - cy) < 0.05 and
abs((fb[2] + fb[5]) / 2 - L(cz)) < 0.05 and min(abs(fb[0] - xw), abs(fb[3] - xw)) < 0.05
for rr, ax_, fb in cyls()) for cy, cz, _ in END_CIRCLES[1:])
P(u' %s 切除 → %-12r 面数 %d→%d' % (tag, prop(c, 'Name') if c else c, nbefore, len(faces())))
P(u' 外表面面积减少 %.2f(理论 槽段 %.2f + 螺丝孔 %.2f = %.2f)%s'
% (d_area, A_SLOTS, A_HOLES, A_SLOTS + A_HOLES, u'✅' if ok_area else u'⛔'))
P(u' 槽头半圆柱 +%d(期望 %d)%s;横筋平端面 +%d(期望 %d)%s;风扇孔在位 %s'
% (caps, N_ROUND, u'✅' if caps == N_ROUND else u'⛔',
flats, N_FLAT, u'✅' if flats == N_FLAT else u'⛔', u'✅' if fan_ok else u'⛔'))
if not (ok_area and caps == N_ROUND and flats == N_FLAT and fan_ok):
P(u' ⛔⛔ %s 格栅不对,停手、不存盘' % tag); sys.exit(5)
grille_cut(u'西端墙', X0, -1)
grille_cut(u'东端墙', X1, +1)
# ── [5] 南墙 Type-C:完全等于母座口内侧轮廓(用户拍板 2026-09-13)──────────────
# 母座 USB-C_SMD-TYPE-C-31-M-12 实测(STEP,本地→装配 x=12.5−lx / z=−lz):
# 内侧平直边 x ±4.17、z 0.35~2.95(本地)⇒ 8.34 × 2.60;四角圆柱 R0.70,圆心 (±3.47, 1.05/2.25)
# ⇒ 装配口中心 J1 (12.50, −1.65)、J23 (84.00, −1.65);口面 y=0.20,墙外表面 y=−4.055(缩进 4.26)
# 北墙原有 J33 / 风扇出线两个窗口:用户要求去掉,不再切。
USB_W, USB_H, USB_R = 8.34, 2.60, 0.70
USB_CX = (12.50, 84.00)
USB_CZ = -1.65
P(u'\n[5] 南墙 Type-C ×2:%.2f × %.2f、R%.2f,中心 z=%.2f(= 母座口内侧轮廓)' % (USB_W, USB_H, USB_R, USB_CZ))
def usb_shapes(cx):
hw, hh, r, cz = USB_W / 2, USB_H / 2, USB_R, L(USB_CZ)
lines = [((cx + hw - r, cz + hh), (cx - hw + r, cz + hh)),
((cx - hw, cz + hh - r), (cx - hw, cz - hh + r)),
((cx - hw + r, cz - hh), (cx + hw - r, cz - hh)),
((cx + hw, cz - hh + r), (cx + hw, cz + hh - r))]
arcs = [((cx + hw - r, cz + hh - r), (cx + hw, cz + hh - r), (cx + hw - r, cz + hh)),
((cx - hw + r, cz + hh - r), (cx - hw + r, cz + hh), (cx - hw, cz + hh - r)),
((cx - hw + r, cz - hh + r), (cx - hw, cz - hh + r), (cx - hw + r, cz - hh)),
((cx + hw - r, cz - hh + r), (cx + hw - r, cz - hh), (cx + hw, cz - hh + r))]
return lines, arcs
def usb_verify():
hw, hh, r, cz = USB_W / 2, USB_H / 2, USB_R, L(USB_CZ)
cs = cyls()
ps = planes()
for cx in USB_CX:
for ccx, ccz in ((cx + hw - r, cz + hh - r), (cx - hw + r, cz + hh - r),
(cx - hw + r, cz - hh + r), (cx + hw - r, cz - hh + r)):
if not any(abs(rr - r) < 0.01 and ax_ == 1 and fb[1] >= Y0 - 0.05 and fb[4] <= IY0 + 0.05 and
(abs(fb[0] - (ccx - r)) < 0.05 or abs(fb[3] - (ccx + r)) < 0.05) and
(abs(fb[2] - (ccz - r)) < 0.05 or abs(fb[5] - (ccz + r)) < 0.05)
for rr, ax_, fb in cs):
return False
for xe in (cx - hw, cx + hw):
if not any(ax_ == 0 and abs(fb[0] - xe) < 0.05 and abs(fb[3] - xe) < 0.05 and
abs(fb[2] - (cz - hh + r)) < 0.05 and abs(fb[5] - (cz + hh - r)) < 0.05
for ax_, sg, fb, a_, _ in ps):
return False
for ze in (cz - hh, cz + hh):
if not any(ax_ == 2 and abs(fb[2] - ze) < 0.05 and abs(fb[5] - ze) < 0.05 and
abs(fb[0] - (cx - hw + r)) < 0.05 and abs(fb[3] - (cx + hw - r)) < 0.05
for ax_, sg, fb, a_, _ in ps):
return False
return True
nbefore = len(faces())
X = open_face_sketch((1, -1, Y0), u'南墙')
sm.AddToDB = True
nseg = 0
for cx in USB_CX:
lines, arcs = usb_shapes(cx)
for p0, p1 in lines:
q0, q1 = to_sketch(X, (p0[0], Y0, p0[1])), to_sketch(X, (p1[0], Y0, p1[1]))
nseg += bool(sm.CreateLine(q0[0], q0[1], 0.0, q1[0], q1[1], 0.0))
for c, p1, p2 in arcs:
qc = to_sketch(X, (c[0], Y0, c[1]))
q1 = to_sketch(X, (p1[0], Y0, p1[1]))
q2 = to_sketch(X, (p2[0], Y0, p2[1]))
cross = (q1[0] - qc[0]) * (q2[1] - qc[1]) - (q1[1] - qc[1]) * (q2[0] - qc[0])
dirn = 1 if cross > 0 else -1 # 草图坐标系里取 90° 短弧:叉积正 = 逆时针
nseg += bool(sm.CreateArc(qc[0], qc[1], 0.0, q1[0], q1[1], 0.0, q2[0], q2[1], 0.0, dirn))
sm.AddToDB = False
P(u' 草图段 %d(期望 16 = 2 × (4 直线 + 4 圆弧))' % nseg)
sm.InsertSketch(True)
if nseg != 16:
P(u' ⛔ 草图段数不对,停手'); sys.exit(5)
c = do_cut(T + 0.2)
ok = usb_verify()
P(u' 南墙 切除 → %-12r 面数 %d→%d 母座口轮廓 %s'
% (prop(c, 'Name') if c else c, nbefore, len(faces()), u'✅ 4 圆角 + 4 直边 ×2 全在位' if ok else u'⛔'))
if not ok:
P(u' ⛔⛔ 开口没落在母座口轮廓上 —— 停手、不存盘'); sys.exit(5)
# ── [5b] F:Type-C 右侧 9 mm 凹刻 0.6 —— J1「PWR 20V」闪电、J23「USB」箭头 + 圆点 ─────
SYM_DEPTH, SYM_DX = 0.6, 9.0
BOLT = [(0.8, 3.5), (-1.8, -0.3), (-0.1, -0.3), (-0.8, -3.5), (1.8, 0.5), (0.1, 0.5)]
ARROW = [(0.0, 3.5), (2.2, 0.9), (0.7, 0.9), (0.7, -1.2), (-0.7, -1.2), (-0.7, 0.9), (-2.2, 0.9)]
DOT_C, DOT_R = (0.0, -2.8), 0.8
def shoelace(pts):
return abs(sum(pts[i][0] * pts[(i + 1) % len(pts)][1] - pts[(i + 1) % len(pts)][0] * pts[i][1]
for i in range(len(pts)))) / 2.0
A_SYM = shoelace(BOLT) + shoelace(ARROW) + math.pi * DOT_R ** 2
P(u'\n[5b] Type-C 标识凹刻 %.1f:闪电 %.2f + 箭头 %.2f + 圆点 %.2f = %.2f mm²'
% (SYM_DEPTH, shoelace(BOLT), shoelace(ARROW), math.pi * DOT_R ** 2, A_SYM))
nbefore = len(faces())
X = open_face_sketch((1, -1, Y0), u'南墙标识')
sm.AddToDB = True
for (cx, poly) in ((USB_CX[0] + SYM_DX, BOLT), (USB_CX[1] + SYM_DX, ARROW)):
qs = [to_sketch(X, (cx + du, Y0, L(USB_CZ + dv))) for du, dv in poly]
for i in range(len(qs)):
a_, b_ = qs[i], qs[(i + 1) % len(qs)]
sm.CreateLine(a_[0], a_[1], 0.0, b_[0], b_[1], 0.0)
qd = to_sketch(X, (USB_CX[1] + SYM_DX + DOT_C[0], Y0, L(USB_CZ + DOT_C[1])))
sm.CreateCircleByRadius(qd[0], qd[1], 0.0, M(DOT_R))
sm.AddToDB = False
sm.InsertSketch(True)
if pick(last_sketch(), 'SKETCH') != 1:
P(u' ⛔ 标识草图没选中'); sys.exit(5)
c = fm.FeatureCut4(True, False, False, 0, 0, M(SYM_DEPTH), 0.0,
False, False, False, False, 0.0, 0.0,
False, False, False, False, False,
True, True, False, False, False, 0, 0.0, False, False)
floor = sum(a_ for ax_, sg, fb, a_, _ in planes()
if ax_ == 1 and sg == -1 and abs(fb[1] - (Y0 + SYM_DEPTH)) < 0.02 and abs(fb[4] - (Y0 + SYM_DEPTH)) < 0.02)
ok = abs(floor - A_SYM) < 0.05
P(u' 标识 切除 → %-12r 面数 %d→%d 凹槽底面积 %.3f(理论 %.3f)%s'
% (prop(c, 'Name') if c else c, nbefore, len(faces()), floor, A_SYM, u'✅' if ok else u'⛔'))
if not ok:
P(u' ⛔⛔ 标识没刻对,停手、不存盘'); sys.exit(5)
# ── 验收 ──────────────────────────────────────────────────────────────────
P(u'\n[7] 验收')
check(u'最终', [X0, Y0, -H_WALL, X1, Y1, 0.0])
h = hist()
WANT_H = {0.7: 8, 0.75: 2 * SLOT_N, 0.8: 1, 1.5: 4, 2.1: 4, 2.6: 8, 3.0: 4, 30.0: 0}
WANT_H[round(GR_W / 2, 2)] = WANT_H.get(round(GR_W / 2, 2), 0) + 2 * N_ROUND
P(u' 圆柱面直方图 %s(期望 %s)' % (h, dict(sorted(WANT_H.items()))))
if any(h.get(k, 0) != v for k, v in WANT_H.items()):
P(u'⛔ 孔数不对,不存盘'); sys.exit(7)
e1 = wc.VARIANT(pythoncom.VT_BYREF | pythoncom.VT_I4, 0)
w1 = wc.VARIANT(pythoncom.VT_BYREF | pythoncom.VT_I4, 0)
part.Extension.SaveAs3(DST, 0, 1, EMPTY, EMPTY, e1, w1)
P(u'\n另存 err=%s 存在=%s %.0f KB'
% (e1.value, os.path.exists(DST), os.path.getsize(DST) / 1024.0 if os.path.exists(DST) else 0))