# -*- coding: utf-8 -*-
"""
BF-X4 超轻型载人飞行器 —— 机架 + 外壳 参数化自动建模
运行于 FreeCAD 1.0.2 headless (FreeCADCmd.exe)

输入参数（董总实测）：
  外廓 750 x 2200 x 1100 mm
  主骨架 φ45 管 / 非主骨架 φ30 管 / 材质 6061 航空铝
输出：
  FCStd（可继续编辑）、STEP（通用 CAD）、STL（3D 打印）
  等轴测线框 SVG、model.json
"""

import FreeCAD as App
import Part
from FreeCAD import Vector
import os, math, json, sys

def log(*a):
    print(*a)
    sys.stdout.flush()

OUT = "C:/Users/DEYI/WorkBuddy/2026-09-20-12-27-14/bf-3d"
if not os.path.isdir(OUT):
    os.makedirs(OUT)

# ============================================================
# ① 设计参数
# ============================================================
TOTAL_W, TOTAL_L, TOTAL_H = 750.0, 2200.0, 1100.0   # 机架外廓
D_MAIN, D_SUB = 45.0, 30.0                          # 主管 / 次管外径
T_MAIN, T_SUB = 2.0, 2.0                            # 管壁厚（估）
RHO_AL = 2.70e-3                                    # 6061 密度 g/mm3
WALL_T = 2.0                                        # 外壳壁厚
GAP_SIDE, GAP_V = 6.0, 3.0                          # 外壳内壁与机架间隙

# 机架站位 (y, 半宽, 底高 z_bot, 顶高 z_top)
ST = [
    (-1100.0, 150.0, 320.0,  620.0),   # 机头端点
    ( -820.0, 245.0, 200.0,  790.0),   # 机头段
    ( -480.0, 340.0,  60.0,  990.0),   # 机头-座舱过渡
    ( -380.0, 375.0,   0.0, 1100.0),   # 前防滚拱（顶 = 全高 1100）
    (  300.0, 375.0,   0.0, 1090.0),   # 后防滚拱
    (  620.0, 340.0,  60.0,  950.0),   # 尾段过渡
    (  900.0, 285.0, 150.0,  760.0),   # 尾段
    ( 1050.0, 250.0, 220.0,  620.0),   # 尾端方盒
]
TOP_SHRINK = 0.86        # 顶部宽度收缩，形成梯形截面
ARCH_IDX = (3, 4)        # 防滚拱站位（用主管）

# ============================================================
# ② 机架：节点 → 杆件
# ============================================================
def st_nodes(st):
    y, hw, zb, zt = st
    tw = hw * TOP_SHRINK
    return {
        'BL': Vector(-hw, y, zb), 'BR': Vector(hw, y, zb),
        'TL': Vector(-tw, y, zt), 'TR': Vector(tw, y, zt),
    }

NODES = [st_nodes(s) for s in ST]
rods = []   # (p1, p2, dia, kind)

# 纵梁（4 根，主管）
for i in range(len(ST) - 1):
    for k in ('BL', 'BR', 'TL', 'TR'):
        rods.append((NODES[i][k], NODES[i + 1][k], D_MAIN, 'main'))

# 横向框（每站位 4 根）
for i, n in enumerate(NODES):
    d = D_MAIN if i in ARCH_IDX else D_SUB
    rods.append((n['BL'], n['BR'], d, 'frame'))
    rods.append((n['TL'], n['TR'], d, 'frame'))
    rods.append((n['BL'], n['TL'], d, 'frame'))
    rods.append((n['BR'], n['TR'], d, 'frame'))

# 斜撑（每个间隔 6 根，次管）
for i in range(len(ST) - 1):
    a, b = NODES[i], NODES[i + 1]
    rods.append((a['BL'], b['TL'], D_SUB, 'brace'))
    rods.append((a['BR'], b['TR'], D_SUB, 'brace'))
    rods.append((a['BL'], b['BR'], D_SUB, 'brace'))
    rods.append((a['BR'], b['BL'], D_SUB, 'brace'))
    rods.append((a['TL'], b['TR'], D_SUB, 'brace'))
    rods.append((a['TR'], b['TL'], D_SUB, 'brace'))

rod_shapes = []
rod_data = []
for p1, p2, d, kind in rods:
    v = p2 - p1
    L = v.Length
    if L < 1e-6:
        continue
    rod_shapes.append(Part.makeCylinder(d / 2.0, L, p1, v))
    rod_data.append({"p1": [p1.x, p1.y, p1.z], "p2": [p2.x, p2.y, p2.z],
                     "d": d, "kind": kind})

frame_shape = Part.makeCompound(rod_shapes)
log("[机架] 杆件数 =", len(rod_shapes))
log("[机架] BBox =", frame_shape.BoundBox)

# 管件重量
mass = 0.0
len_main = len_sub = 0.0
for p1, p2, d, kind in rods:
    L = (p2 - p1).Length
    t = T_MAIN if abs(d - D_MAIN) < 1e-6 else T_SUB
    area = math.pi * ((d / 2.0) ** 2 - (d / 2.0 - t) ** 2)      # mm2
    mass += area * L * RHO_AL                                    # g
    if abs(d - D_MAIN) < 1e-6:
        len_main += L
    else:
        len_sub += L
log("[机架] φ45 管总长 = %.1f m    φ30 管总长 = %.1f m" % (len_main / 1000, len_sub / 1000))
log("[机架] 管件净重 ≈ %.2f kg" % (mass / 1000.0))

# ============================================================
# ③ 外壳：沿 Y 采样截面 → 放样 → 抽壳
# ============================================================
def interp_frame(y):
    ys = [s[0] for s in ST]
    if y <= ys[0]:
        return ST[0][1], ST[0][2], ST[0][3]
    if y >= ys[-1]:
        return ST[-1][1], ST[-1][2], ST[-1][3]
    for i in range(len(ST) - 1):
        if ys[i] <= y <= ys[i + 1]:
            t = (y - ys[i]) / (ys[i + 1] - ys[i])
            return (ST[i][1] + t * (ST[i + 1][1] - ST[i][1]),
                    ST[i][2] + t * (ST[i + 1][2] - ST[i][2]),
                    ST[i][3] + t * (ST[i + 1][3] - ST[i][3]))
    return ST[-1][1], ST[-1][2], ST[-1][3]

def shell_section(y):
    hw, zb, zt = interp_frame(y)
    a = hw + GAP_SIDE
    b = (zt - zb) / 2.0 + GAP_V
    zc = (zb + zt) / 2.0
    return a, b, zc

def end_scale(y):
    """前端收口，尾部保持"""
    if y <= -1105.0:
        return 0.32
    if y < -1020.0:
        return 0.32 + (y + 1105.0) / 85.0 * 0.68
    return 1.0

def rrect_pts(a, b, R, seg_line=4, seg_arc=10):
    """圆角矩形采样点（逆时针），a/b 为半宽/半高"""
    R = min(R, a * 0.9, b * 0.9)
    pts = []
    def line(p, q, n):
        return [(p[0] + (q[0] - p[0]) * i / n, p[1] + (q[1] - p[1]) * i / n) for i in range(n)]
    def arc(cx, cy, r, a0, a1, n):
        return [(cx + r * math.cos(a0 + (a1 - a0) * i / n),
                 cy + r * math.sin(a0 + (a1 - a0) * i / n)) for i in range(n)]
    pts += line((a, -b + R), (a, b - R), seg_line)
    pts += arc(a - R, b - R, R, 0, math.pi / 2, seg_arc)
    pts += line((-a + R, b), (a - R, b), seg_line)
    pts += arc(-a + R, b - R, R, math.pi / 2, math.pi, seg_arc)
    pts += line((-a, b - R), (-a, -b + R), seg_line)
    pts += arc(-a + R, -b + R, R, math.pi, 1.5 * math.pi, seg_arc)
    pts += line((a - R, -b), (-a + R, -b), seg_line)
    pts += arc(a - R, -b + R, R, 1.5 * math.pi, 2 * math.pi, seg_arc)
    return pts

def make_section_wire(y, a, b, zc, R_ratio=0.42):
    R = min(a, b) * R_ratio
    pts = [Vector(px, y, zc + pz) for px, pz in rrect_pts(a, b, R)]
    return Part.makePolygon(pts, True)

YS = []
y = -1120.0
while y <= 1050.0 + 1e-6:
    YS.append(round(y, 3))
    y += 45.0

wires_out, wires_in, sec_rec = [], [], []
for y in YS:
    a, b, zc = shell_section(y)
    s = end_scale(y)
    a, b = a * s, b * s
    wires_in.append(make_section_wire(y, a, b, zc))
    wires_out.append(make_section_wire(y, a + WALL_T, b + WALL_T, zc))
    sec_rec.append({"y": y, "a": a, "b": b, "zc": zc, "R": min(a, b) * 0.42})

log("[外壳] 截面数 =", len(YS))
loft_out = Part.makeLoft(wires_out, True, False, False)
loft_in = Part.makeLoft(wires_in, True, False, False)
log("[外壳] 外放样体积 = %.1f L" % (loft_out.Volume / 1e6))
shell_shape = loft_out.cut(loft_in)
log("[外壳] 壳体体积 = %.3f L   表面积 ≈ %.3f m2"
    % (shell_shape.Volume / 1e6, shell_shape.Area / 1e6))

# ============================================================
# ④ 导出 FCStd / STEP / STL
# ============================================================
doc = App.newDocument("BF_X4")
o1 = doc.addObject("Part::Feature", "Frame_750x2200x1100_phi45_phi30")
o1.Shape = frame_shape
o2 = doc.addObject("Part::Feature", "Shell_2mm")
o2.Shape = shell_shape
doc.recompute()

def save_all(obj, name):
    doc.saveAs(os.path.join(OUT, "BF-X4-%s.FCStd" % name))
    try:
        Part.export([obj], os.path.join(OUT, "BF-X4-%s.step" % name))
        log("[导出] STEP ok", name)
    except Exception as e:
        log("[导出] STEP 失败", name, e)
    try:
        import Mesh
        Mesh.export([obj], os.path.join(OUT, "BF-X4-%s.stl" % name))
        log("[导出] STL ok", name)
    except Exception as e:
        log("[导出] STL 失败", name, e)

save_all(o1, "frame")
save_all(o2, "shell")
doc.saveAs(os.path.join(OUT, "BF-X4-assembly.FCStd"))

# ============================================================
# ⑤ 线框 SVG（等轴测 + 侧视 + 俯视）
# ============================================================
C_MAIN, C_SUB, C_SHELL, C_INK = "#2b3a45", "#98a8b4", "#0f8a6a", "#42545f"
W_SVG = 680

def iso(p):
    c30, s30 = math.cos(math.radians(30)), math.sin(math.radians(30))
    return (p[0] - p[1]) * c30, (p[0] + p[1]) * s30 - p[2]

def plan(p):   # 俯视 (x, y) -> 屏幕 (x, -y)
    return p[0], -p[1]

def side(p):   # 侧视 (y, z) -> 屏幕 (-y, -z)
    return -p[1], -p[2]

def collect(proj, pts):
    out = [proj(p) for p in pts]
    xs = [q[0] for q in out]; ys = [q[1] for q in out]
    return out, min(xs), max(xs), min(ys), max(ys)

def rod_segs_screen(T):
    segs = []
    for p1, p2, d, kind in rods:
        a = T(Vector(*p1)); b = T(Vector(*p2))
        segs.append((a, b, d, kind))
    return segs

# 外壳纵向 4 条脊线 + 若干截面轮廓
def shell_guides():
    top, bot, left, right = [], [], [], []
    for r in sec_rec:
        top.append((0.0, r["y"], r["zc"] + r["b"]))
        bot.append((0.0, r["y"], r["zc"] - r["b"]))
        left.append((-r["a"], r["y"], r["zc"]))
        right.append((r["a"], r["y"], r["zc"]))
    return top, bot, left, right

GUIDES = shell_guides()
SEC_PICK = [0, 8, 16, 24, 32, 40, len(sec_rec) - 1]

def shell_section_pts(r):
    pts = rrect_pts(r["a"], r["b"], r["R"], 4, 8)
    return [Vector(px, r["y"], r["zc"] + pz) for px, pz in pts]

def svg_block(proj, height, title, scale_pad=0.94):
    allp = []
    for p1, p2, d, k in rods:
        allp.append(Vector(*p1)); allp.append(Vector(*p2))
    for g in GUIDES:
        for q in g:
            allp.append(Vector(*q))
    out, x0, x1, y0, y1 = collect(proj, allp)
    w, h = (x1 - x0) or 1.0, (y1 - y0) or 1.0
    sc = min((W_SVG - 60) / w, (height - 70) / h) * scale_pad
    ox = (W_SVG - w * sc) / 2 - x0 * sc
    oy = (height - 46 - h * sc) / 2 + 34 - y0 * sc
    def T(p):
        X, Y = proj(p)
        return X * sc + ox, Y * sc + oy
    s = ['<g>']
    s.append('<text x="14" y="20" font-size="13" font-weight="700" fill="%s">%s</text>' % (C_INK, title))
    # 外壳：填充 + 纵向脊线
    for g in GUIDES:
        pts = " ".join("%.1f,%.1f" % T(Vector(*q)) for q in g)
        s.append('<polyline points="%s" fill="none" stroke="%s" stroke-width="1.1" opacity="0.75"/>' % (pts, C_SHELL))
    for i in SEC_PICK:
        r = sec_rec[i]
        pts = " ".join("%.1f,%.1f" % T(p) for p in shell_section_pts(r))
        s.append('<polygon points="%s" fill="none" stroke="%s" stroke-width="0.9" opacity="0.55"/>' % (pts, C_SHELL))
    # 机架杆件（按深度粗略排序）
    segs = sorted(rod_segs_screen(T), key=lambda t: t[0][0] + t[0][1] + t[1][0] + t[1][1])
    for a, b, d, kind in segs:
        col = C_MAIN if kind == 'main' else C_SUB
        lw = max(1.0, d * sc * 0.95)
        s.append('<line x1="%.1f" y1="%.1f" x2="%.1f" y2="%.1f" stroke="%s" stroke-width="%.2f" stroke-linecap="round"/>'
                 % (a[0], a[1], b[0], b[1], col, lw))
    s.append('</g>')
    return "".join(s)

H1, H2, H3 = 400, 250, 200
svg = ['<svg viewBox="0 0 680 %d" xmlns="http://www.w3.org/2000/svg" '
       'font-family="Segoe UI,Microsoft YaHei,sans-serif">' % (H1 + H2 + H3 + 10)]
svg.append(svg_block(iso, H1, "① 等轴测（3/4 视角）— 深色 φ45 主管 / 浅灰 φ30 桁条 / 翡翠色外壳轮廓"))
svg.append('<g transform="translate(0,%d)">' % H1)
svg.append(svg_block(side, H2, "② 侧视（Y-Z）— 总长 2200 / 总高 1100"))
svg.append('</g>')
svg.append('<g transform="translate(0,%d)">' % (H1 + H2))
svg.append(svg_block(plan, H3, "③ 俯视（X-Y）— 总宽 750"))
svg.append('</g>')
svg.append('</svg>')
svg_txt = "".join(svg)
open(os.path.join(OUT, "preview-wireframe.svg"), "w", encoding="utf-8").write(svg_txt)
log("[SVG] 已生成 preview-wireframe.svg  %.1f KB" % (len(svg_txt) / 1024))

# ============================================================
# ⑥ model.json
# ============================================================
json.dump({
    "meta": {"name": "BF-X4", "total": [TOTAL_W, TOTAL_L, TOTAL_H],
             "d_main": D_MAIN, "d_sub": D_SUB,
             "frame_mass_kg": round(mass / 1000.0, 2),
             "shell_volume_L": round(shell_shape.Volume / 1e6, 3)},
    "stations": ST,
    "rods": rod_data,
    "sections": sec_rec,
}, open(os.path.join(OUT, "model.json"), "w", encoding="utf-8"), ensure_ascii=False)
log("[JSON] 已生成 model.json")
log("=== DONE ===")
