# -*- coding: utf-8 -*-
"""
BF-B 超轻型载人飞行器机架 v7 —— 按 2026-10-24 董总指定实拍精确复刻（读图修正版）
运行于 FreeCAD 1.0.2 headless (FreeCADCmd.exe)

读图结论（实拍：机身平躺于木托盘，纵轴左下→右上，左前上方俯视）：
  1. 主体 = 左右两条「泪滴形大侧梁弯管」，从机头尖一路上拱越座舱顶、再缓降至尾部落地。
     这两条管在照片里构成中央那个大"蛋形"轮廓，两条并行、端头收拢到机头。
  2. 尾部 = 一个**横向拱架**（垂直机身轴线，照片里正对镜头、能看穿），
     拱内一组大 X 撑，X 下方架一块**灰色钢搁板**（中间有小孔）。
  3. 底部 = 平底板 + 两侧纵梁 + 一排横梁；板上放两个黑色设备箱。
  4. 机头 = 多管汇聚于一根**开口横管**（照片可见管壁内孔）。
  5. 座舱侧面为大开口，仅少量长斜杆三角化。
  6. 两侧梁顶点后方各有一根纵向顶管，连到尾部横向拱架。
输入：外廓 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-b5"
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

Y_NOSE, Y_TAIL = -1100.0, 1100.0
XC = 340.0                # 侧梁 / 底纵梁中心线 x
BED_Z = 150.0             # 底纵梁中心线高
NX = 150.0                # 机头横向半宽
Z_APEX = 1060.0           # 泪滴顶点中心线高
Y_ARCH = 950.0            # 尾部横向拱架所在纵向位置

# ---- 泪滴侧梁侧视中心线 (y, z)：机头汇聚 → 越座舱顶 → 尾部落地 ----
HOOP_PTS = [
    (-1100.0, 190.0),
    (-1010.0, 205.0),
    ( -900.0, 250.0),
    ( -760.0, 350.0),
    ( -580.0, 520.0),
    ( -380.0, 730.0),
    ( -170.0, 930.0),
    (   30.0, 1040.0),
    (  210.0, Z_APEX),   # 顶点
    (  380.0, 1000.0),
    (  530.0, 860.0),
    (  660.0, 640.0),
    (  770.0, 400.0),
    (  870.0, 220.0),
    (  960.0, BED_Z),   # 后脚落到底纵梁
]

# 尾部横向拱架（X-Z 平面，y = Y_ARCH）：两腿外撇 + 圆弧顶
ARCH_XZ = [
    (-XC, BED_Z),
    (-XC, 430.0),
    (-320.0, 700.0),
    (-230.0, 890.0),
    (-110.0, 1010.0),
    (   0.0, 1040.0),
    ( 110.0, 1010.0),
    ( 230.0, 890.0),
    ( 320.0, 700.0),
    ( XC, 430.0),
    ( XC, BED_Z),
]

def hoop_x(y):
    if y <= -900.0:
        t = (y - Y_NOSE) / (-900.0 - Y_NOSE)
        return NX + (XC - NX) * t
    return XC

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 hoop_z(y):
    return _prof_z(HOOP_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(30, min(150, int(L_path / 30.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)

def hollow_rod(p1, p2, r_out, t):
    v = p2 - p1
    outer = Part.makeCylinder(r_out, v.Length, p1, v)
    inner = Part.makeCylinder(r_out - t, v.Length + 2, p1, v)
    return outer.cut(inner)

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, hole=None):
    b = Part.makeBox(x1 - x0, y1 - y0, z1 - z0, Vector(x0, y0, z0))
    if hole:
        hx, hy, hr = hole
        b = b.cut(Part.makeCylinder(hr, (z1 - z0) + 4, Vector(hx, hy, z0 - 2), Vector(0, 0, 1)))
    shapes.append(b)
    log("  [板] %s  %.0fx%.0f mm" % (tag, x1 - x0, y1 - y0))

# ============================================================
# ③ 机架杆件
# ============================================================
log("== 泪滴侧梁大弯管 ×2 ==")
for sgn, tag in ((-1, "左侧梁"), (1, "右侧梁")):
    pts = [(sgn * hoop_x(y), y, z) for (y, z) in HOOP_PTS]
    add_curve(pts, D_MAIN, "main", tag)

log("== 尾部横向拱架（弯管，垂直机身轴线）==")
add_curve([(x, Y_ARCH, z) for (x, z) in ARCH_XZ], D_MAIN, "main", "尾横拱")

log("== 拱内大 X 撑（横向平面内，照片尾段交叉斜杆）==")
add_straight((-300.0, Y_ARCH - 22.0, 230.0), (255.0, Y_ARCH - 22.0, 880.0), D_SUB, "sub")
add_straight(( 300.0, Y_ARCH - 22.0, 230.0), (-255.0, Y_ARCH - 22.0, 880.0), D_SUB, "sub")
add_straight((-320.0, Y_ARCH - 22.0, 560.0), (320.0, Y_ARCH - 22.0, 560.0), D_SUB, "sub")

log("== 顶点纵向后顶管 ×2（侧梁顶点 → 尾部横拱）==")
for sgn in (-1, 1):
    add_straight((sgn * XC, 210.0, Z_APEX), (sgn * 250.0, Y_ARCH, 900.0), D_MAIN, "main")

log("== 底纵梁 ×2 + 机头收拢段 ==")
for sgn in (-1, 1):
    add_straight((sgn * XC, -1000.0, BED_Z), (sgn * XC, Y_TAIL, BED_Z), D_MAIN, "main")
    add_straight((sgn * XC, -1000.0, BED_Z), (sgn * NX, -1090.0, 190.0), D_MAIN, "main")

log("== 机头开口横管（照片可见管壁内孔）==")
shapes.append(hollow_rod(Vector(-NX - 8, Y_NOSE, 190.0), Vector(NX + 8, Y_NOSE, 190.0), 26.0, 3.0))
draw.append(([Vector(-NX, Y_NOSE, 190.0), Vector(NX, Y_NOSE, 190.0)], 45.0, "main"))
rod_len["main"] += 2 * (NX + 8)
log("  [管头] 开口 φ45 / 壁 3 mm")

log("== 底部横梁（梯，φ30）==")
bed_cross_ys = [-800, -640, -480, -320, -160, 0, 160, 320, 480, 640, 800, 960]
for y in bed_cross_ys:
    add_straight((-XC, y, BED_Z), (XC, y, BED_Z), D_SUB, "sub")
log("  底部横梁 %d 根" % len(bed_cross_ys))

log("== 尾部封口横管 ==")
add_straight((-XC, Y_TAIL, BED_Z), (XC, Y_TAIL, BED_Z), D_MAIN, "main")

log("== 座舱侧三角化（每侧少量长斜杆）==")
for sgn in (-1, 1):
    x = sgn * XC
    add_straight((x, -820.0, BED_Z), (x, -660.0, 430.0), D_SUB, "sub")
    add_straight((x, -500.0, BED_Z), (x, -300.0, 780.0), D_SUB, "sub")
    add_straight((x,  120.0, BED_Z), (x,  330.0, 1010.0), D_SUB, "sub")
    add_straight((x,  380.0, BED_Z), (x,  120.0, 1010.0), D_SUB, "sub")

log("== 座舱内横撑 ==")
for y in (-300.0, 120.0, 420.0):
    add_straight((-XC, y, BED_Z), (XC, y, BED_Z), D_SUB, "sub")

log("== 平底板 + 尾部灰钢搁板 + 黑色设备箱 ==")
add_plate(-300.0, 300.0, -720.0, 760.0, BED_Z - 3.0, BED_Z, "平底板")
add_plate(-280.0, 280.0, 830.0, 1000.0, 430.0, 433.0, "尾部灰色钢搁板",
          hole=(0.0, 915.0, 9.0))
add_plate(-230.0, 230.0, 800.0, 1020.0, BED_Z, 400.0, "尾部设备箱")
add_plate(-210.0, 210.0, 300.0, 640.0, BED_Z, 370.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_v7")
o1 = doc.addObject("Part::Feature", "BF_B_Frame_750x2200x1100_v7")
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
# ============================================================
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 机架 v7 泪滴侧梁+尾横拱 · 等轴测 — 深色 φ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 已生成")

json.dump({
    "meta": {"name": "BF-B", "version": "v7-泪滴侧梁+尾横拱",
             "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": 4, "solids": len(shapes)},
    "hoop_profile": HOOP_PTS,
    "rear_arch_xz": ARCH_XZ,
}, open(os.path.join(OUT, "model.json"), "w", encoding="utf-8"), ensure_ascii=False)
log("=== DONE ===")
