"""v23 通用机型版 3 个打印件的实体模型 → STL（manifold3d 布尔运算）。
建模坐标沿用拆解图：x 宽（±50）、y 高（0 在桌面）、z 长（+z 是前口）。导出时转成打印朝向、Z 朝上。"""
import math, json, sys, os
import numpy as np
import manifold3d as m3
from manifold3d import Manifold, CrossSection, JoinType
import trimesh

m3.set_circular_segments(64)
OUT = sys.argv[1] if len(sys.argv) > 1 else '.'
os.makedirs(OUT, exist_ok=True)

# ---------- 尺寸（和拆解图一致） ----------
W, L, H, SPLIT, WT, FL = 100.0, 190.0, 138.2, 120.25, 2.4, 3.0
XI, ZI = W/2 - WT, L/2 - WT          # 47.6 / 92.6 内壁
PIV_Y, PIV_Z = SPLIT, -97.0          # 合页轴
SLOT_W, SLOT_Y, RIB = 45.0, 48.0, 7.0
PZ = 3.5

# 孔径（含配合间隙）
D_HINGE = 3.4        # M3 合页销
D_MAG, MAG_DEPTH = 6.15, 3.2   # Ø6×3 磁铁压入
D_INSERT, INSERT_DEPTH = 3.2, 4.5  # M2 热熔铜螺母
D_M2_CLEAR = 2.4
D_FAN_SCREW = 2.6

def box(x0, x1, y0, y1, z0, z1):
    a, b = sorted((x0, x1)); c, d = sorted((y0, y1)); e, f = sorted((z0, z1))
    return Manifold.cube([b - a, d - c, f - e]).translate([a, c, e])
def cyl_y(r, h, x, y0, z):          # 轴沿 y，从 y0 往上 h
    return Manifold.cylinder(h, r).rotate([-90, 0, 0]).translate([x, y0, z])
def cyl_x(r, h, x0, y, z):          # 轴沿 x，从 x0 往 +x
    return Manifold.cylinder(h, r).rotate([0, 90, 0]).translate([x0, y, z])
def cyl_z(r, h, x, y, z0):
    return Manifold.cylinder(h, r).translate([x, y, z0])
def ccw(pts):
    a = 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)))
    return pts if a > 0 else pts[::-1]
def prism_xy(pts, z0, z1):          # (x,y) 多边形沿 z 拉伸
    return CrossSection([ccw(pts)]).extrude(z1 - z0).translate([0, 0, z0])
def prism_yz(pts, x0, x1):          # (z,y) 多边形沿 x 拉伸：2D u=z, v=y
    cs = CrossSection([ccw(pts)]).extrude(x1 - x0)          # 局部 x=u, y=v, z 为拉伸
    # 局部 (u,v,w) → 世界 (w, v, -u)… 用旋转 [0,90,0]：(x,y,z)->(z,y,-x)，所以世界 z = -u。把 u 取反即可
    return cs.rotate([0, 90, 0]).translate([x0, 0, 0])
def teardrop_x(d, x0, x1, apex_up=True):
    """沿 x 的泪滴孔（免支撑），2D 平面 (u=−z, v=y)。"""
    r = d / 2; pts = []
    n = 40
    # 圆弧从 45° 走到 135°（经过底部）
    a0, a1 = (math.radians(45), math.radians(-225)) if apex_up else (math.radians(-45), math.radians(225))
    for i in range(n + 1):
        a = a0 + (a1 - a0) * i / n
        pts.append([r * math.cos(a), r * math.sin(a)])
    pts.append([0, (r * math.sqrt(2)) * (1 if apex_up else -1)])
    return prism_yz(pts, x0, x1)
def rrect_xz(x0, x1, z0, z1, r):     # 圆角矩形板（俯视 x,z），返回 CrossSection 在 (x, z)
    cs = CrossSection.square([x1 - x0 - 2*r, z1 - z0 - 2*r]).translate([x0 + r, z0 + r])
    return cs.offset(r, JoinType.Round)
def plate_from_xz(cs, y0, t):        # CrossSection(x,z) → 厚 t 的板，从 y0 往上
    # 2D (u=x, v=z) 拉伸沿局部 z；旋转 [-90,0,0]：(x,y,z)->(x,z,-y)… 需要 v→z、拉伸→y：用 [90,0,0]：(x,y,z)->(x,-z,y)
    return cs.extrude(t).rotate([-90, 0, 0]).mirror([0, 0, 1]).translate([0, y0, 0])
def union(parts):
    out = parts[0]
    for p in parts[1:]: out = out + p
    return out
def gusset_corner(x_wall, z_wall, x_in, z_in, y_top, drop):
    """磁铁座下面的 45° 斜撑：顶面矩形 (x_in..x_wall)×(z_in..z_wall) 在 y_top，收到墙角点 (x_wall, y_top-drop, z_wall)。"""
    slab = box(x_in, x_wall, y_top - 0.01, y_top, z_in, z_wall)
    tip = Manifold.cube([0.02, 0.02, 0.02]).translate([x_wall - 0.01 if x_wall > 0 else x_wall - 0.01, y_top - drop, z_wall - 0.02 if z_wall > 0 else z_wall])
    return (slab + tip).hull()

# ================= P1 底仓 =================
def build_body():
    parts = []
    # 四壁 + 底，整块实心再掏空
    outer = box(-W/2, W/2, 0, SPLIT, -L/2, L/2)
    inner = box(-XI, XI, FL, SPLIT + 1, -ZI, ZI)
    shell = outer - inner
    # 外侧竖边 R2 圆角（俯视 4 角）
    corner_cut = outer - plate_from_xz(rrect_xz(-W/2, W/2, -L/2, L/2, 2.0), -1, SPLIT + 2)
    shell = shell - corner_cut
    # 前口 90×45，上角 8 mm 倒角减小桥接
    slot = prism_xy([[-SLOT_W, FL], [SLOT_W, FL], [SLOT_W, SLOT_Y - 8], [SLOT_W - 8, SLOT_Y], [-SLOT_W + 8, SLOT_Y], [-SLOT_W, SLOT_Y - 8]], ZI - 1, L/2 + 1)
    shell = shell - slot
    # 后壁：USB-C 电源口 9×4.5，顶上过线缺口 6×6.25
    shell = shell - box(15.5, 24.5, 17.75, 22.25, -L/2 - 1, -ZI + 1)
    shell = shell - box(39, 45, 114, SPLIT + 1, -L/2 - 1, -ZI + 1)
    # 两侧进风百叶：4 扇窗 18×30，每扇 5 片 45° 斜片
    fins = []
    for sd in (-1, 1):
        for z0 in (-62, -40, 22, 44):
            shell = shell - box(sd*(W/2 - WT - 1), sd*(W/2 + 1), 18, 48, z0, z0 + 18)
            wall_slab = box(sd*(W/2 - WT), sd*(W/2), 17, 49, z0 - 1, z0 + 19)
            for j in range(5):
                f = Manifold.cube([3.3, 1.0, 18.0], True).rotate([0, 0, -sd*45]).translate([sd*(W/2 - WT/2), 21.5 + j*5.6, z0 + 9])
                fins.append(f ^ wall_slab)
    parts.append(shell); parts += fins
    # 支撑筋 4 条（4 mm 高）+ 后挡
    for x in (-30, -12, 12, 30): parts.append(box(x - .8, x + .8, FL, RIB, -78, 84.5))
    parts.append(box(-32, 32, FL, 12, -79.5, -77.5))
    # 后壁线槽：竖筋 + 3 个卡钩
    parts.append(box(37.2, 38.4, 34, SPLIT - 3, -ZI, -89.6))
    for y in (50, 80, 105): parts.append(box(38.4, 40.6, y, y + 3, -90.6, -89.6))
    # 分线板 2 个螺丝柱（后壁内面）
    for (x, y) in ((13.5, 10.5), (36.5, 27.5)):
        parts.append(cyl_z(2.9, 3.0, x, y, -ZI))
    # 插头座（右前角）：带插槽的块
    parts.append(box(41.5, XI, FL, 15, 71, 79.5))
    # 磁铁座 ×2 + 45° 斜撑
    for sd in (-1, 1):
        parts.append(box(sd*40, sd*XI, 112, SPLIT, 85, ZI))
        parts.append(gusset_corner(sd*XI, ZI, sd*40, 85, 112, 7.6))
    # 合页耳 ×4（每侧 2 个）+ 45° 斜撑
    for sd in (-1, 1):
        for x0 in (18, 30):
            xa, xb = sorted((sd*x0, sd*(x0 + 6)))
            parts.append(box(xa, xb, PIV_Y - 4, PIV_Y, PIV_Z, -L/2))
            parts.append(cyl_x(4, 6, xa, PIV_Y, PIV_Z))
            parts.append(prism_yz([[-(-L/2), PIV_Y - 6], [-(-L/2), PIV_Y], [-(PIV_Z - 4), PIV_Y]], xa, xb))  # u=-z
    body = union(parts)
    # 减：插头座插槽、磁铁孔、螺丝柱孔、合页孔、脚垫位、定位标记
    body = body - box(43.2, 45.8, 4.3, 13.3, 73, 80)
    for sd in (-1, 1):
        body = body - cyl_y(D_MAG/2, MAG_DEPTH + 1, sd*43.8, SPLIT - MAG_DEPTH, 88.8)
    for (x, y) in ((13.5, 10.5), (36.5, 27.5)):
        body = body - cyl_z(D_INSERT/2, INSERT_DEPTH, x, y, -ZI - (INSERT_DEPTH - 3.0))
    body = body - teardrop_x(D_HINGE, -W/2 - 1, W/2 + 1, apex_up=True).translate([0, PIV_Y, PIV_Z])
    for x in (-38, 38):
        for z in (-80, 80):
            body = body - cyl_y(5.2, 1.0, x, -0.01, z)
    for x in (-36, 36):
        body = body - box(x - 3.5, x + 3.5, FL - 0.6, FL + 0.1, -84.5, -83.5)
        body = body - box(x - .5, x + .5, FL - 0.6, FL + 0.1, -87.5, -80.5)
    return body

# ================= P2 顶盖外壳 =================
def build_shell():
    parts = []
    top = plate_from_xz(rrect_xz(-W/2, W/2, -L/2, L/2, 4.0), H - WT, WT)
    # 风扇格栅区 30.4×30.4：挖通，再放 1 mm 厚的格栅条 + 4 角螺丝垫
    top = top - box(-15.2, 15.2, H - WT - 1, H + 1, -15.2, 15.2)
    parts.append(top)
    for i in range(-3, 4): parts.append(box(-15.2, 15.2, H - 1, H, i*4.2 - .6, i*4.2 + .6))
    for sx in (-1, 1):
        for sz in (-1, 1):
            parts.append(box(sx*9.5, sx*15.2, H - 1, H, sz*9.5, sz*15.2))
    # 围边：两侧 + 后（带 6 mm 缺口）+ 前
    for sd in (-1, 1):
        xa, xb = sorted((sd*(W/2 - WT), sd*W/2))
        parts.append(box(xa, xb, SPLIT, H - WT + .1, -L/2, L/2))
    rear = box(-XI, XI, SPLIT, H - WT + .1, -L/2, -ZI) - box(39, 45, SPLIT - 1, 126, -L/2 - 1, -ZI + 1)
    parts.append(rear)
    parts.append(box(-XI, XI, SPLIT, H - WT + .1, ZI, L/2))
    # 止口（伸进底仓 2 mm）
    for sd in (-1, 1): parts.append(box(sd*46.2, sd*47.4, SPLIT - 2, SPLIT, -ZI + 1, 84))
    parts.append(box(-XI + 1, XI - 1, SPLIT - 2, SPLIT, -ZI + 1, -ZI + 2.2))
    parts.append(box(-38, 38, SPLIT - 2, SPLIT, ZI - 2.2, ZI - 1))
    # 磁铁座：角上整根立柱到顶板（倒着打印不需要支撑）
    for sd in (-1, 1): parts.append(box(sd*40, sd*XI, SPLIT, H - WT + .1, 85, ZI))
    # 安装板螺丝柱 ×4（Ø7，从顶板下来到 122.25）
    for x in (-38, 38):
        for z in (-80, 80): parts.append(cyl_y(3.5, H - WT - 122.25 + .1, x, 122.25, z))
    # 合页耳 ×2 + 斜撑（倒着打印时斜撑在耳朵"下面"）
    for sd in (-1, 1):
        xa, xb = sorted((sd*24, sd*30))
        parts.append(box(xa, xb, PIV_Y, PIV_Y + 4, PIV_Z, -L/2))
        parts.append(cyl_x(4, 6, xa, PIV_Y, PIV_Z))
        parts.append(prism_yz([[-(-L/2), PIV_Y], [-(-L/2), PIV_Y + 6], [-(PIV_Z - 4), PIV_Y]], xa, xb))
    sh = union(parts)
    # 外侧竖边 R4
    outer = box(-W/2 - 1, W/2 + 1, SPLIT - 3, H + 1, -L/2 - 1, L/2 + 1)
    sh = sh - (outer - plate_from_xz(rrect_xz(-W/2, W/2, -L/2, L/2, 4.0), SPLIT - 4, H - SPLIT + 6) - box(-W/2 + 5, W/2 - 5, SPLIT - 4, SPLIT, -ZI, ZI))
    # 减：风扇螺丝孔、磁铁孔、螺丝柱热熔孔、合页孔（泪滴朝世界 -y，即打印时朝上）
    for sx in (-1, 1):
        for sz in (-1, 1): sh = sh - cyl_y(D_FAN_SCREW/2, 5, sx*12, H - 3, sz*12)
    for sd in (-1, 1): sh = sh - cyl_y(D_MAG/2, MAG_DEPTH, sd*43.8, SPLIT - .01, 88.8)
    for x in (-38, 38):
        for z in (-80, 80): sh = sh - cyl_y(D_INSERT/2, INSERT_DEPTH, x, 122.25 - .01, z)
    sh = sh - teardrop_x(D_HINGE, -W/2 - 1, W/2 + 1, apex_up=False).translate([0, PIV_Y, PIV_Z])
    return sh

# ================= P3 摄像头安装板 =================
def build_plate():
    outline = [[-47.3, -92.3], [47.3, -92.3], [47.3, 84.5], [39.5, 84.5], [39.5, 92.3], [-39.5, 92.3], [-39.5, 84.5], [-47.3, 84.5]]
    pl = plate_from_xz(CrossSection([outline]), SPLIT, 2.0)
    parts = [pl]
    for x in (-25, 25):
        for z in (-11, 11): parts.append(cyl_y(2.9, 3.75, x, SPLIT + 2 - .01, z))
    parts.append(box(-44, 44, SPLIT + 2 - .01, 124, -45.8, -44.2))
    parts.append(box(-44, 44, SPLIT + 2 - .01, 124, 44.2, 45.8))
    p = union(parts)
    p = p - cyl_y(7.5, 10, 0, SPLIT - 1, 0)
    for i in range(12):
        a = i/12*2*math.pi
        p = p - cyl_y(1.7, 10, math.cos(a)*11.5, SPLIT - 1, math.sin(a)*11.5)
    for x in (-38, 38):
        for z in (-80, 80): p = p - cyl_y(D_M2_CLEAR/2, 10, x, SPLIT - 1, z)
    for x in (-25, 25):
        for z in (-11, 11): p = p - cyl_y(D_INSERT/2, INSERT_DEPTH, x, SPLIT + 2 + 3.75 - INSERT_DEPTH, z)
    return p

def to_trimesh(man, mapping):
    mesh = man.to_mesh()
    v = np.asarray(mesh.vert_properties)[:, :3].astype(np.float64)
    f = np.asarray(mesh.tri_verts).astype(np.int64)
    v2 = mapping(v)
    return trimesh.Trimesh(vertices=v2, faces=f, process=False)

def report(name, man, tm):
    vol = man.volume() if hasattr(man, 'volume') else man.get_volume()
    bb = tm.bounds
    return {'name': name, 'watertight': bool(tm.is_watertight), 'volume_cm3': round(vol/1000, 1),
            'weight_g_PETG': round(vol/1000*1.27, 0), 'size_mm': [round(float(x), 2) for x in (bb[1]-bb[0])],
            'tris': int(len(tm.faces)), 'genus_ok': man.status().name if hasattr(man, 'status') else 'n/a'}

if __name__ == '__main__':
    stats = []
    body = build_body(); shellm = build_shell(); plate = build_plate()
    # 打印朝向：P1 开口朝上；P2 顶面贴板（翻过来）；P3 平放
    tb = to_trimesh(body, lambda v: np.c_[v[:, 0], -v[:, 2], v[:, 1]])
    ts = to_trimesh(shellm, lambda v: np.c_[v[:, 0], v[:, 2], H - v[:, 1]])
    tp = to_trimesh(plate, lambda v: np.c_[v[:, 0], -v[:, 2], v[:, 1] - SPLIT])
    for t, fn in ((tb, 'P1_底仓_v23.stl'), (ts, 'P2_顶盖外壳_v23.stl'), (tp, 'P3_摄像头安装板_v23.stl')):
        t.export(os.path.join(OUT, fn))
    stats = [report('P1 底仓', body, tb), report('P2 顶盖外壳', shellm, ts), report('P3 摄像头安装板', plate, tp)]
    # 装配状态（世界坐标，Z 朝上），用来看整体
    asm = trimesh.util.concatenate([to_trimesh(body, lambda v: np.c_[v[:, 0], -v[:, 2], v[:, 1]]),
                                    to_trimesh(shellm, lambda v: np.c_[v[:, 0], -v[:, 2], v[:, 1]]),
                                    to_trimesh(plate, lambda v: np.c_[v[:, 0], -v[:, 2], v[:, 1]])])
    asm.export(os.path.join(OUT, '整机装配参考_v23.stl'))
    json.dump(stats, open(os.path.join(OUT, 'stats.json'), 'w'), ensure_ascii=False, indent=1)
    print(json.dumps(stats, ensure_ascii=False, indent=1))
