# -*- coding: utf-8 -*-
"""
BF-B 超轻型载人飞行器机架 —— 实拍复刻参数化建模（弯管版）
运行于 FreeCAD 1.0.2 headless (FreeCADCmd.exe)

参照董总提供的两张实拍（泪滴形防滚拱机架）：
  · 主拱：鹅蛋形弯管，前腿落地接床身，越顶后顺滑下接尾部方盒顶
  · 机头：两侧管向前收锥汇拢 + 鼻环，前方 V 形斜撑
  · 底部：平梯式床身（双纵梁 + 密横梁）
  · 尾部：方盒（四角立柱 + 顶框 + 中轨 + 斜撑）
  · 侧面：床身与拱/顶管之间 X 形三角撑
输入：外廓 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-b2"
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_MAIN = 352.0           # 拱/顶管中心线 x（375-22.5）
XC_BED = 340.0            # 床身纵梁中心线 x
BED_Z = 112.0             # 床身纵梁中心线高
TAIL_Z0, TAIL_Z1 = 112.0, 642.0   # 尾部方盒底/顶（中心线）
TAIL_Y0, TAIL_Y1 = 680.0, 1080.0

# 主拱中心线（侧视 y,z；x=±XC_MAIN）—— 鹅蛋形：前腿微前凸，顶点靠前 1/3，后段下滑接尾盒顶
ARCH_PTS = [
    (-980.0, 132.0),
    (-1015.0, 330.0),
    (-1018.0, 520.0),
    (-975.0, 730.0),
    (-890.0, 905.0),
    (-760.0, 1030.0),
    (-570.0, 1078.0),   # 顶点（全高 1100 - 管半径）
    (-330.0, 1070.0),
    (-80.0, 1010.0),
    (170.0, 920.0),
    (400.0, 800.0),
    (545.0, 700.0),
    (640.0, 642.0),     # 接尾部方盒顶前角
]

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

def bent_tube(pts, r, tag=""):
    """样条扫掠弯管；样条长度超差或扫掠失败则退化为分段圆柱+节点球。返回 (shape, 采样点列, 长度)"""
    poly_len = sum((Vector(*pts[i + 1]) - Vector(*pts[i])).Length for i in range(len(pts) - 1))
    sp = spline_of([Vector(*p) for p in pts])
    path = Part.Wire([sp.toShape()])
    t0 = sp.tangent(sp.FirstParameter)[0]
    prof = Part.Wire([Part.makeCircle(r, Vector(*pts[0]), t0)])
    try:
        solid = path.makePipeShell([prof], True, False)
        if solid.ShapeType != "Solid":
            solid = Part.Solid(solid)
        if not solid.isValid() or solid.Volume <= 0:
            raise RuntimeError("invalid sweep")
        if path.Length > poly_len * 1.08:
            raise RuntimeError("spline overshoot: %.0f >> %.0f" % (path.Length, poly_len))
        us = [sp.FirstParameter + (sp.LastParameter - sp.FirstParameter) * i / 40.0 for i in range(41)]
        samp = [sp.value(u) for u in us]
        log("  [弯管] %s 扫掠 OK  体积 %.1f cm3  长度 %.0f mm" % (tag, solid.Volume / 1000.0, path.Length))
        return solid, samp, path.Length
    except Exception as e:
        log("  [弯管] %s 改用分段直管(%s)" % (tag, e))
        # 线性细分（不依赖样条，杜绝过冲）
        samp = []
        for i in range(len(pts) - 1):
            a, b = Vector(*pts[i]), Vector(*pts[i + 1])
            n = max(2, int((b - a).Length / 45.0))
            for k in range(n):
                samp.append(a + (b - a) * (k / float(n)))
        samp.append(Vector(*pts[-1]))
        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 = []            # (pts3d, dia, kind) 供 SVG
rod_len = {"main": 0.0, "sub": 0.0}   # mm

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 arch_z(y):
    """拱中心线在纵向 y 处的高度（按 ARCH_PTS 线性插值）"""
    pts = ARCH_PTS
    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 pts[-1][1]

NOSE_Y = -1090.0
NOSE_TIP_X, NOSE_TIP_Z = 85.0, 320.0     # 机头尖端（半宽, 高）

log("== 主拱（弯管，前腿落在底梁上）==")
for sgn, tag in ((-1, "左拱"), (1, "右拱")):
    pts = [(sgn * XC_MAIN, y, z) for (y, z) in ARCH_PTS]
    add_curve(pts, D_MAIN, "main", tag)

log("== 顶部纵梁（拱尾→尾盒顶后角）==")
for sgn in (-1, 1):
    add_straight((sgn * XC_MAIN, 640.0, 642.0), (sgn * XC_MAIN, TAIL_Y1, TAIL_Z1 - 2.0), D_MAIN, "main")

log("== 底梁（下弦：尾端长直段[圆柱] + 前端上翘收锥弯管至机头尖）==")
for sgn, tag in ((-1, "左底梁"), (1, "右底梁")):
    # 长直段不用样条（避免插值过冲），直接圆柱
    add_straight((sgn * XC_BED, 1090.0, BED_Z), (sgn * XC_BED, -880.0, BED_Z), D_MAIN, "main")
    # 机头弯头：短样条，路径全部短段
    curve = [(sgn * XC_BED, -880.0, BED_Z),
             (sgn * XC_BED, -950.0, 128.0),
             (sgn * 280.0, -1045.0, 200.0),
             (sgn * 170.0, -1082.0, 275.0),
             (sgn * NOSE_TIP_X, NOSE_Y, NOSE_TIP_Z)]
    add_curve(curve, D_MAIN, "main", tag)

log("== 鼻环（连接左右机头尖）==")
add_curve([(-NOSE_TIP_X, NOSE_Y, NOSE_TIP_Z),
           (0.0, NOSE_Y, 440.0),
           (NOSE_TIP_X, NOSE_Y, NOSE_TIP_Z)], D_SUB, "sub", "鼻环")

log("== 床身横梁（梯）==")
bed_cross_ys = [-800, -670, -540, -410, -280, -150, -20, 110, 240, 370, 500, 630, 760, 890, 1020]
for y in bed_cross_ys:
    add_straight((-XC_BED, y, BED_Z), (XC_BED, y, BED_Z), D_SUB, "sub")
log("  床身横梁 %d 根" % len(bed_cross_ys))

log("== 尾部方盒 ==")
for sx in (-1, 1):
    for y in (TAIL_Y0, TAIL_Y1):
        add_straight((sx * XC_MAIN, y, TAIL_Z0), (sx * XC_MAIN, y, TAIL_Z1), D_MAIN, "main")     # 四角立柱
    add_straight((sx * XC_MAIN, TAIL_Y0, 380.0), (sx * XC_MAIN, TAIL_Y1, 380.0), D_SUB, "sub")   # 侧中轨
    add_straight((sx * XC_MAIN, TAIL_Y1, TAIL_Z0), (sx * XC_MAIN, TAIL_Y0, 380.0), D_SUB, "sub") # 侧斜撑
add_straight((-XC_MAIN, TAIL_Y0, TAIL_Z1), (XC_MAIN, TAIL_Y0, TAIL_Z1), D_MAIN, "main")           # 顶前横
add_straight((-XC_MAIN, TAIL_Y1, TAIL_Z1), (XC_MAIN, TAIL_Y1, TAIL_Z1), D_MAIN, "main")           # 顶后横
add_straight((-XC_BED, 1090.0, BED_Z), (XC_BED, 1090.0, BED_Z), D_MAIN, "main")                   # 底后横
add_straight((0.0, TAIL_Y1, TAIL_Z0), (0.0, TAIL_Y1, TAIL_Z1), D_SUB, "sub")                      # 尾面中立柱

log("== 机头区撑杆（端点全部落在拱/底梁中心线上）==")
for sgn in (-1, 1):
    # 拱前腿三角撑：底梁 → 拱（y=-975 处 z=730）
    add_straight((sgn * XC_BED, -780.0, BED_Z), (sgn * XC_MAIN, -975.0, arch_z(-975.0)), D_SUB, "sub")
    # 机头尖 → 拱前腿（长 V 撑，落点 y=-940）
    add_straight((sgn * NOSE_TIP_X, NOSE_Y, NOSE_TIP_Z + 25.0),
                 (sgn * XC_MAIN, -940.0, arch_z(-940.0)), D_SUB, "sub")
    # 底梁 → 机头尖（短撑）
    add_straight((sgn * XC_BED, -850.0, BED_Z),
                 (sgn * (NOSE_TIP_X + 20.0), -1085.0, NOSE_TIP_Z - 30.0), D_SUB, "sub")

log("== 侧面 X 撑（拱后段—底梁）与立柱 ==")
for sgn in (-1, 1):
    for (ya, yb) in ((-300.0, 100.0), (350.0, 640.0)):
        add_straight((sgn * XC_BED, ya, BED_Z), (sgn * XC_MAIN, yb, arch_z(yb)), D_SUB, "sub")
        add_straight((sgn * XC_BED, yb, BED_Z), (sgn * XC_MAIN, ya, arch_z(ya)), D_SUB, "sub")
    add_straight((sgn * XC_MAIN, 300.0, BED_Z), (sgn * XC_MAIN, 300.0, arch_z(300.0)), D_SUB, "sub")  # 中立柱

log("== 座舱内横撑（端点在拱中心线上）==")
add_straight((-XC_MAIN, -1000.0, arch_z(-1000.0)), (XC_MAIN, -1000.0, arch_z(-1000.0)), D_SUB, "sub")
add_straight((-XC_MAIN, -950.0, arch_z(-950.0)), (XC_MAIN, -950.0, arch_z(-950.0)), D_SUB, "sub")

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")
o1 = doc.addObject("Part::Feature", "BF_B_Frame_750x2200x1100")
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 机架 · 等轴测 — 深色 φ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", "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),
             "solids": len(shapes)},
    "arch_profile": ARCH_PTS,
}, open(os.path.join(OUT, "model.json"), "w", encoding="utf-8"), ensure_ascii=False)
log("=== DONE ===")
