# -*- coding: utf-8 -*-
"""
BF-B 超轻型载人飞行器机架 v5 —— 按 2026-10-24 董总指定实拍复刻
运行于 FreeCAD 1.0.2 headless (FreeCADCmd.exe)

对照本张实拍的关键结构（与 v4 单根贯通上弦的区别）：
  1. 双泪滴拱：前拱（越过座舱，前腿顺机头上弦下探收拢）+ 后拱（稍低，独立落在床身），
     两拱之间顶部用纵向连梁连接 —— 拱间通透，能看到穿。
  2. 尾部方舱：后拱之后是矩形设备舱，尾部顶装水平设备板（实拍黑色铝板），
     舱内座椅背板 + 低位电池托板，共 3 块板。
  3. 机头低收：上弦从座舱顶前顺着长机头缓缓下收到鼻尖，φ30 鼻环。
  4. 梯式床身 + 侧面密集三角撑 + 侧中纵梁。
输入：外廓 750 x 2200 x 1100，主骨架 φ45，次骨架 φ30，6061
输出：FCStd / STEP / STL / preview-mini.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-b3"
if not os.path.isdir(OUT):
    os.makedirs(OUT)

# ============================================================
# ① 参数
# ============================================================
D_MAIN, D_SUB = 45.0, 30.0
T_MAIN, T_SUB = 2.0, 2.0
RHO_AL = 2.70e-3          # g/mm3
HW = 375.0                # 半宽（外廓）
XC = 352.0                # 侧框拱管中心线 x
XB = 340.0                # 床身纵梁中心线 x
BED_Z = 112.0             # 床身纵梁中心线高
NOSE_Y = -1050.0          # 鼻尖纵向位置
NX = 110.0                # 鼻环半宽
TAIL_Y = 1080.0           # 尾端
DECK_Z = 620.0            # 尾部设备舱顶（尾顶板面）

# ---- 前拱侧视中心线（y,z）：鼻尖低进 → 越座舱顶 → 后腿落到床身 ----
HOOP_A_PTS = [
    (-1050.0,  300.0),   # 鼻尖（与上弦/底梁收弯汇合）
    ( -950.0,  335.0),
    ( -800.0,  445.0),
    ( -580.0,  640.0),
    ( -320.0,  925.0),
    ( -100.0, 1058.0),
    (   60.0, 1077.0),   # 顶点（1100-管半径22.5≈1077）
    (  230.0, 1045.0),
    (  370.0,  905.0),
    (  460.0,  670.0),
    (  515.0,  380.0),
    (  545.0,  BED_Z),   # 后脚落床身
]
# ---- 后拱侧视中心线：独立拱，双脚落床身，顶点稍低 ----
HOOP_B_PTS = [
    (  585.0,  BED_Z),
    (  625.0,  400.0),
    (  695.0,  745.0),
    (  775.0,  955.0),
    (  855.0,  992.0),   # 后拱顶点
    (  935.0,  928.0),
    (  995.0,  745.0),
    ( 1032.0,  480.0),
    ( 1052.0,  240.0),
    ( 1065.0,  BED_Z),
]
# 拱间顶部连梁（纵向前拱顶 → 后拱顶）
LINK_Y0, LINK_Z0 =   60.0, 1070.0
LINK_Y1, LINK_Z1 =  855.0,  985.0
# 侧中纵梁（机头段 → 尾部）
RAIL_Y0, RAIL_Z0 = -880.0, 380.0
RAIL_Y1, RAIL_Z1 = 1060.0, 470.0

# 前拱 x(y)：鼻尖处 NX → y=-700 处收到 XC，之后恒 XC
def hoopA_x(y):
    if y <= -700.0:
        t = (y - NOSE_Y) / (-700.0 - NOSE_Y)
        return NX + (XC - NX) * t
    return XC

# 底梁机头收弯段 x(y)
BED_BEND0_Y = -700.0
def bed_x(y):
    if y <= BED_BEND0_Y:
        t = (y - NOSE_Y) / (BED_BEND0_Y - NOSE_Y)
        return NX + (XB - NX) * t
    return XB

def _prof_z(pts, y):
    for i in range(len(pts) - 1):
        (y0, z0), (y1, z1) = pts[i], pts[i + 1]
        if min(y0, y1) <= y <= max(y0, y1):
            t = (y - y0) / (y1 - y0)
            return z0 + t * (z1 - z0)
    return None

def hoopA_z(y):
    return _prof_z(HOOP_A_PTS, y)

def hoopB_z(y):
    return _prof_z(HOOP_B_PTS, y)

# ============================================================
# ② 造型工具
# ============================================================
def spline_of(pts):
    sp = Part.BSplineCurve()
    sp.interpolate(pts)
    return sp

def bent_tube(pts, r, tag=""):
    """弯管 = 样条逐截面圆放样（Loft）。比 makePipeShell 稳：不会产生远端飞点。
    放样失败则退化为分段圆柱+节点球。"""
    sp = spline_of([Vector(*p) for p in pts])
    L_path = sp.length(sp.FirstParameter, sp.LastParameter)
    n = max(24, min(90, int(L_path / 40.0)))
    us = [sp.FirstParameter + (sp.LastParameter - sp.FirstParameter) * i / float(n) for i in range(n + 1)]
    samp = [sp.value(u) for u in us]
    try:
        secs = []
        for i, u in enumerate(us):
            p = sp.value(u)
            t = sp.tangent(u)[0]
            if i == 0:
                t = (sp.value(u + 1e-3) - p)
            elif i == n:
                t = (p - sp.value(u - 1e-3))
            secs.append(Part.Wire([Part.makeCircle(r, p, t)]))
        solid = Part.makeLoft(secs, True, False)
        if solid.ShapeType != "Solid":
            solid = Part.Solid(solid)
        if not solid.isValid() or solid.Volume <= 0:
            raise RuntimeError("invalid loft")
        vol_expect = math.pi * r * r * L_path
        if solid.Volume < vol_expect * 0.75 or solid.Volume > vol_expect * 1.3:
            raise RuntimeError("loft volume off: %.0f vs %.0f" % (solid.Volume, vol_expect))
        bb = solid.BoundBox
        if bb.XLength > 1600 or bb.YLength > 2600 or bb.ZLength > 1600:
            raise RuntimeError("loft bbox blown: %.0f/%.0f/%.0f" % (bb.XLength, bb.YLength, bb.ZLength))
        log("  [弯管] %s 放样 OK  体积 %.1f cm3  长度 %.0f mm" % (tag, solid.Volume / 1000.0, L_path))
        return solid, samp, L_path
    except Exception as e:
        log("  [弯管] %s 改用分段直管(%s)" % (tag, e))
        segs = []
        for i in range(len(samp) - 1):
            a, b = samp[i], samp[i + 1]
            segs.append(Part.makeCylinder(r, (b - a).Length, a, b - a))
        for p in samp[1:-1]:
            segs.append(Part.makeSphere(r, p))
        L = sum((samp[i + 1] - samp[i]).Length for i in range(len(samp) - 1))
        return Part.makeCompound(segs), samp, L

def rod(p1, p2, r):
    v = p2 - p1
    return Part.makeCylinder(r, v.Length, p1, v)

shapes = []
draw = []
rod_len = {"main": 0.0, "sub": 0.0}

def add_straight(p1, p2, d, kind):
    r = d / 2.0
    shapes.append(rod(Vector(*p1), Vector(*p2), r))
    draw.append(([Vector(*p1), Vector(*p2)], d, kind))
    rod_len["main" if kind == "main" else "sub"] += (Vector(*p2) - Vector(*p1)).Length

def add_curve(pts3d, d, kind, tag):
    r = d / 2.0
    vs = [Vector(*p) for p in pts3d]
    shape, samp, L = bent_tube(vs, r, tag)
    shapes.append(shape)
    draw.append((samp, d, kind))
    rod_len["main" if kind == "main" else "sub"] += L

def add_plate(x0, x1, y0, y1, z0, z1, tag):
    """设备安装板（薄板盒）"""
    b = Part.makeBox(x1 - x0, y1 - y0, z1 - z0, Vector(x0, y0, z0))
    shapes.append(b)
    log("  [板] %s  %.0fx%.0f mm" % (tag, x1 - x0, y1 - y0))

# ============================================================
# ③ 机架杆件
# ============================================================
log("== 前拱（泪滴弯管 ×2：鼻尖→座舱顶→床身）==")
for sgn, tag in ((-1, "左前拱"), (1, "右前拱")):
    pts = [(sgn * hoopA_x(y), y, z) for (y, z) in HOOP_A_PTS]
    add_curve(pts, D_MAIN, "main", tag)

log("== 后拱（独立拱 ×2：双脚落床身）==")
for sgn, tag in ((-1, "左后拱"), (1, "右后拱")):
    pts = [(sgn * XC, y, z) for (y, z) in HOOP_B_PTS]
    add_curve(pts, D_MAIN, "main", tag)

log("== 拱间顶部连梁 ×2（前拱顶→后拱顶）==")
for sgn in (-1, 1):
    add_straight((sgn * XC, LINK_Y0, LINK_Z0), (sgn * XC, LINK_Y1, LINK_Z1), D_MAIN, "main")

log("== 侧中纵梁 ×2（机头段→尾部）==")
for sgn in (-1, 1):
    add_straight((sgn * XC, RAIL_Y0, RAIL_Z0), (sgn * XC, RAIL_Y1, RAIL_Z1), D_SUB, "sub")

log("== 底梁（长直段圆柱 + 机头收弯短样条 ×2）==")
for sgn, tag in ((-1, "左底梁"), (1, "右底梁")):
    add_straight((sgn * XB, BED_BEND0_Y, BED_Z), (sgn * XB, TAIL_Y, BED_Z), D_MAIN, "main")
    curve = [(sgn * XB, BED_BEND0_Y, BED_Z),
             (sgn * 300.0, -930.0, 135.0),
             (sgn * 190.0, -1000.0, 165.0),
             (sgn * NX, NOSE_Y, 200.0)]
    add_curve(curve, D_MAIN, "main", tag)

log("== 鼻环（φ30：上拱 + 底封管）==")
add_curve([(-NX, NOSE_Y, 200.0), (-60.0, NOSE_Y, 325.0), (0.0, NOSE_Y, 385.0),
           (60.0, NOSE_Y, 325.0), (NX, NOSE_Y, 200.0)], D_SUB, "sub", "鼻环上拱")
add_straight((-NX, NOSE_Y, 197.0), (NX, NOSE_Y, 197.0), D_SUB, "sub")

log("== 床身横梁（梯，φ30）==")
bed_cross_ys = [-700, -570, -440, -310, -180, -50, 80, 210, 340, 470, 660, 800, 940]
for y in bed_cross_ys:
    add_straight((-XB, y, BED_Z), (XB, y, BED_Z), D_SUB, "sub")
log("  床身横梁 %d 根" % len(bed_cross_ys))

log("== 尾部方舱（设备舱）==")
for sgn in (-1, 1):
    add_straight((sgn * XC, TAIL_Y, BED_Z), (sgn * XC, TAIL_Y, DECK_Z), D_MAIN, "main")     # 尾立柱
    add_straight((sgn * XB, 940.0, BED_Z), (sgn * XC, TAIL_Y, DECK_Z - 10.0), D_SUB, "sub") # 舱内侧斜杆
    add_straight((sgn * XC, 855.0, BED_Z), (sgn * XC, 855.0, hoopB_z(855.0)), D_SUB, "sub") # 舱前立柱（后拱顶下方）
add_straight((-XC, TAIL_Y, DECK_Z), (XC, TAIL_Y, DECK_Z), D_MAIN, "main")                    # 尾顶横
add_straight((-XC, TAIL_Y, BED_Z), (XC, TAIL_Y, BED_Z), D_MAIN, "main")                      # 尾底横
add_straight((-XC, 950.0, 300.0), (XC, 950.0, 300.0), D_SUB, "sub")                          # 尾中横
add_straight((0.0, TAIL_Y, BED_Z), (0.0, TAIL_Y, DECK_Z), D_SUB, "sub")                      # 尾面中立柱

log("== 拱间横框（底横 + 顶横，φ30）==")
for y in (-400.0, -100.0, 350.0):
    add_straight((-XB, y, BED_Z), (XB, y, BED_Z), D_SUB, "sub")
    zt = hoopA_z(y)
    if zt and zt > 500.0:
        add_straight((-XC, y, zt), (XC, y, zt), D_SUB, "sub")
# 前后拱之间的顶横
for y in (620.0, 740.0):
    zb = hoopB_z(y)
    if zb:
        add_straight((-XC, y, zb), (XC, y, zb), D_SUB, "sub")

log("== 座舱贯穿大 X（两拱之间侧向长斜管）==")
for sgn in (-1, 1):
    add_straight((sgn * XB, 545.0, BED_Z), (sgn * XC, LINK_Y0, LINK_Z0 - 8.0), D_SUB, "sub")
    add_straight((sgn * XC, 545.0, BED_Z), (sgn * XC, LINK_Y0, LINK_Z0 - 8.0), D_SUB, "sub")

log("== 侧面斜杆 / 立柱（每格一杆，座舱格 X）==")
for sgn in (-1, 1):
    # 机头格：底弯段 → 前拱前腿
    add_straight((sgn * bed_x(-880.0), -880.0, BED_Z), (sgn * XC, -700.0, hoopA_z(-700.0)), D_SUB, "sub")
    add_straight((sgn * bed_x(-790.0), -790.0, BED_Z), (sgn * XC, -950.0, hoopA_z(-950.0)), D_SUB, "sub")
    # 座舱前格：上斜杆 + 竖柱
    add_straight((sgn * XB, -545.0, BED_Z), (sgn * XC, -320.0, hoopA_z(-320.0)), D_SUB, "sub")
    add_straight((sgn * XC, -545.0, BED_Z), (sgn * XC, -545.0, 380.0), D_SUB, "sub")
    # 座舱格：X（开口区，大交叉）
    add_straight((sgn * XB, -180.0, BED_Z), (sgn * XC, 230.0, hoopA_z(230.0)), D_SUB, "sub")
    add_straight((sgn * XB, 350.0, BED_Z), (sgn * XC, -100.0, hoopA_z(-100.0)), D_SUB, "sub")
    # 后格（前拱脚—后拱脚之间）：X + 竖柱
    add_straight((sgn * XB, 545.0, BED_Z), (sgn * XC, 800.0, hoopB_z(800.0)), D_SUB, "sub")
    add_straight((sgn * XB, 940.0, BED_Z), (sgn * XC, 660.0, hoopB_z(660.0)), D_SUB, "sub")
    # 侧中纵梁与前拱/床身之间的短撑
    add_straight((sgn * XC, 100.0, RAIL_Z0 + 90.0), (sgn * XC, 100.0, hoopA_z(100.0)), D_SUB, "sub")

log("== 设备安装板 ×3（6061 板 3mm，实拍黑色铝板位置）==")
add_plate(-340.0, 340.0, 900.0, 1078.0, DECK_Z, DECK_Z + 3.0, "尾部顶板")
add_plate(-320.0, 320.0, 545.0, 548.0, 320.0, 600.0, "座椅背板")
add_plate(-240.0, 240.0, -80.0, 320.0, 240.0, 243.0, "电池托板")

frame_shape = Part.makeCompound(shapes)
bb = frame_shape.BoundBox
log("[机架] 实体数 = %d   BBox = %.0f x %.0f x %.0f mm"
    % (len(shapes), bb.XLength, bb.YLength, bb.ZLength))

mass = (rod_len["main"] * math.pi * ((D_MAIN / 2) ** 2 - (D_MAIN / 2 - T_MAIN) ** 2)
        + rod_len["sub"] * math.pi * ((D_SUB / 2) ** 2 - (D_SUB / 2 - T_SUB) ** 2)) * RHO_AL
log("[机架] φ45 总长 %.1f m   φ30 总长 %.1f m   净重 ≈ %.2f kg（不含设备板）"
    % (rod_len["main"] / 1000, rod_len["sub"] / 1000, mass / 1000))

# ============================================================
# ④ 导出
# ============================================================
doc = App.newDocument("BF_B_frame_v5")
o1 = doc.addObject("Part::Feature", "BF_B_Frame_750x2200x1100_v5")
o1.Shape = frame_shape
doc.recompute()
doc.saveAs(os.path.join(OUT, "BF-B-frame.FCStd"))
try:
    Part.export([o1], os.path.join(OUT, "BF-B-frame.step"))
    log("[导出] STEP ok")
except Exception as e:
    log("[导出] STEP 失败", e)
try:
    import Mesh, MeshPart
    m = MeshPart.meshFromShape(Shape=o1.Shape, LinearDeflection=1.0,
                               AngularDeflection=0.6, Relative=False)
    m.write(os.path.join(OUT, "bframe.stl"))
    log("[导出] STL ok  面数 %d" % m.CountFacets)
except Exception as e:
    log("[导出] STL 失败", e)

# ============================================================
# ⑤ 等轴测线框 SVG（viewer 降级图）
# ============================================================
C_MAIN, C_SUB, C_INK = "#2b3a45", "#9aa9b4", "#42545f"
W_SVG, H_SVG = 680, 430

def iso(p):
    c30, s30 = math.cos(math.radians(30)), math.sin(math.radians(30))
    return (p.x - p.y) * c30, (p.x + p.y) * s30 - p.z

allp = []
for pts, d, k in draw:
    allp.extend(pts)
proj = [iso(p) for p in allp]
xs = [q[0] for q in proj]; ys = [q[1] for q in proj]
x0, x1, y0, y1 = min(xs), max(xs), min(ys), max(ys)
sc = min((W_SVG - 50) / (x1 - x0), (H_SVG - 60) / (y1 - y0)) * 0.95
ox = (W_SVG - (x1 - x0) * sc) / 2 - x0 * sc
oy = (H_SVG - 40 - (y1 - y0) * sc) / 2 + 30 - y0 * sc

s = ['<svg viewBox="0 0 %d %d" xmlns="http://www.w3.org/2000/svg" font-family="Segoe UI,Microsoft YaHei,sans-serif">' % (W_SVG, H_SVG)]
s.append('<rect width="%d" height="%d" fill="#eef2f4"/>' % (W_SVG, H_SVG))
s.append('<text x="14" y="22" font-size="13" font-weight="700" fill="%s">BF-B 机架 v5 双拱版 · 等轴测 — 深色 φ45 主骨架 / 浅灰 φ30 撑杆</text>' % C_INK)
segs = []
for pts, d, k in draw:
    for i in range(len(pts) - 1):
        a = iso(pts[i]); b = iso(pts[i + 1])
        segs.append((a[0] * sc + ox, a[1] * sc + oy, b[0] * sc + ox, b[1] * sc + oy, d, k))
segs.sort(key=lambda t: t[0] + t[1])
for ax, ay, bx, by, d, k in segs:
    s.append('<line x1="%.1f" y1="%.1f" x2="%.1f" y2="%.1f" stroke="%s" stroke-width="%.2f" stroke-linecap="round" opacity="0.92"/>'
             % (ax, ay, bx, by, C_MAIN if k == "main" else C_SUB, max(0.8, d * sc * 0.8)))
s.append('</svg>')
open(os.path.join(OUT, "preview-mini.svg"), "w", encoding="utf-8").write("".join(s))
log("[SVG] preview-mini.svg 已生成")

# ============================================================
# ⑥ model.json
# ============================================================
json.dump({
    "meta": {"name": "BF-B", "version": "v5-双泪滴拱",
             "type": "实拍复刻弯管桁架机架（含设备板）",
             "total": [750, 2200, 1100], "d_main": D_MAIN, "d_sub": D_SUB,
             "len_main_m": round(rod_len["main"] / 1000, 1),
             "len_sub_m": round(rod_len["sub"] / 1000, 1),
             "frame_mass_kg": round(mass / 1000, 2),
             "plates": 3, "solids": len(shapes)},
    "hoop_a_profile": HOOP_A_PTS,
    "hoop_b_profile": HOOP_B_PTS,
}, open(os.path.join(OUT, "model.json"), "w", encoding="utf-8"), ensure_ascii=False)
log("=== DONE ===")
