"""Forge Formula Racer: modern open-wheel single-seater ground-effect formula race car.
Blender 5.2 headless: blender -b --python-exit-code 1 --python build_formula-racer.py
Authoring coordinates: +X left, -Y front, +Z up; glTF v2 export: +X left, +Y up, +Z front.
Real dimensions: 5.60 m long, 2.00 m wide, 0.95 m high, wheelbase 3.60 m.
"""
import bpy, bmesh, math, os, json, struct, subprocess
from mathutils import Vector, Matrix
from math import sin, cos, pi, sqrt, exp

ROOT = os.path.dirname(os.path.abspath(__file__))
os.chdir(ROOT)
bpy.ops.wm.read_factory_settings(use_empty=True)

S = bpy.context.scene
S.render.engine = 'CYCLES'
S.cycles.samples = 28
S.cycles.use_denoising = True
prefs = bpy.context.preferences.addons['cycles'].preferences
prefs.compute_device_type = 'OPTIX'
prefs.get_devices()
for d in prefs.devices:
    d.use = (d.type == 'OPTIX')
S.cycles.device = 'GPU'
S.render.image_settings.file_format = 'PNG'
S.view_settings.view_transform = 'AgX'
S.render.film_transparent = False
os.makedirs('textures', exist_ok=True)

# Six realistic formula livery paint colours in linear RGB (mandatory default race red first)
PAINTS = [
    ('ScuderiaRed',  [0.75, 0.05, 0.02]),
    ('CobaltNight',  [0.018, 0.055, 0.22]),
    ('PapayaOrange', [0.82, 0.22, 0.015]),
    ('BritishGreen', [0.012, 0.15, 0.085]),
    ('SilverArrow',  [0.52, 0.56, 0.62]),
    ('AlpineBlue',   [0.022, 0.24, 0.70]),
]

M = {}
groups = {'Body': []}
current = 'Body'


def activate(o):
    bpy.ops.object.select_all(action='DESELECT')
    o.select_set(True)
    bpy.context.view_layer.objects.active = o


def add(o, mat):
    o.data.materials.append(M[mat] if isinstance(mat, str) else mat)
    for f in o.data.polygons:
        f.use_smooth = True
    groups.setdefault(current, []).append(o)
    return o


def mesh(name, vs, fs, mat, uvs=None):
    me = bpy.data.meshes.new(name)
    me.from_pydata(vs, [], fs)
    me.update()
    o = bpy.data.objects.new(name, me)
    S.collection.objects.link(o)
    uv = me.uv_layers.new(name='UVMap')
    for p in me.polygons:
        for li in p.loop_indices:
            idx = me.loops[li].vertex_index
            v = me.vertices[idx].co
            uv.data[li].uv = uvs[idx] if (uvs and idx < len(uvs)) else (v.y * 0.55 + v.x * 0.25, v.z * 0.75 + v.x * 0.30)
    return add(o, mat)


def bevel(o, w=0.005, seg=1):
    md = o.modifiers.new('Bevel', 'BEVEL')
    md.width = w
    md.segments = min(seg, 2)
    md.limit_method = 'ANGLE'
    md.angle_limit = 0.55
    return o


def solidify(o, th=0.008, offset=0.0, rim=True):
    md = o.modifiers.new('Solidify', 'SOLIDIFY')
    md.thickness = th
    md.offset = offset
    md.use_rim = rim
    return o


def cube(name, loc, scale, mat, r=0.005, seg=1):
    bpy.ops.mesh.primitive_cube_add(size=1, location=loc)
    o = bpy.context.object
    o.name = name
    o.scale = scale
    activate(o)
    bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
    add(o, mat)
    if r > 0:
        bevel(o, r, seg)
        md = o.modifiers.new('WeightedNormal', 'WEIGHTED_NORMAL')
        md.keep_sharp = True
        md.weight = 40
    return o


def cyl(name, loc, r, depth, mat, axis='X', n=24, b=0.002):
    bpy.ops.mesh.primitive_cylinder_add(vertices=n, radius=r, depth=depth, location=loc)
    o = bpy.context.object
    o.name = name
    if axis == 'X':
        o.rotation_euler[1] = pi / 2
    elif axis == 'Y':
        o.rotation_euler[0] = pi / 2
    add(o, mat)
    if b > 0:
        bevel(o, b, 1)
    return o


def line(name, pts, r, mat, cyclic=False, res=1):
    cu = bpy.data.curves.new(name, 'CURVE')
    cu.dimensions = '3D'
    cu.resolution_u = 6
    cu.bevel_depth = r
    cu.bevel_resolution = min(res, 1)
    cu.use_fill_caps = True
    sp = cu.splines.new('POLY')
    sp.points.add(len(pts) - 1)
    for p, v in zip(sp.points, pts):
        p.co = (*v, 1)
    sp.use_cyclic_u = cyclic
    ob = bpy.data.objects.new(name, cu)
    S.collection.objects.link(ob)
    activate(ob)
    bpy.ops.object.convert(target='MESH')
    return add(bpy.context.object, mat)


def spline_tube(name, pts, r, mat, cyclic=False, res_u=10, bev_res=2):
    cu = bpy.data.curves.new(name, 'CURVE')
    cu.dimensions = '3D'
    cu.resolution_u = res_u
    cu.bevel_depth = r
    cu.bevel_resolution = bev_res
    cu.use_fill_caps = True
    sp = cu.splines.new('BEZIER')
    sp.bezier_points.add(len(pts) - 1)
    for bp, v in zip(sp.bezier_points, pts):
        bp.co = v
        bp.handle_left_type = 'AUTO'
        bp.handle_right_type = 'AUTO'
    sp.use_cyclic_u = cyclic
    ob = bpy.data.objects.new(name, cu)
    S.collection.objects.link(ob)
    activate(ob)
    bpy.ops.object.convert(target='MESH')
    return add(bpy.context.object, mat)


def linkbar(name, a, b, width, depth, mat, r=None):
    va, vb = Vector(a), Vector(b)
    mid = (va + vb) * 0.5
    length = (vb - va).length
    rad = r if r is not None else min(width, depth) * 0.28
    o = cube(name, mid, (width, depth, length), mat, rad, 1)
    o.rotation_euler = (vb - va).to_track_quat('Z', 'Y').to_euler()
    return o


def airfoil_strut(name, a, b, chord, thick, mat, up_hint=(0, 0, 1), n_prof=10):
    """Creates a streamlined teardrop/airfoil suspension strut or winglet between points a and b."""
    va, vb = Vector(a), Vector(b)
    axis = (vb - va)
    L = axis.length
    if L < 1e-5:
        return None
    z_dir = axis.normalized()
    up = Vector(up_hint).normalized()
    if abs(z_dir.dot(up)) > 0.95:
        up = Vector((0, 1, 0))
    x_dir = up.cross(z_dir).normalized()
    y_dir = z_dir.cross(x_dir).normalized()
    vs = []
    fs = []
    uvs = []
    prof = []
    for k in range(n_prof):
        ang = 2 * pi * k / n_prof
        u_c = 0.5 * (1 - cos(ang))
        sgn = 1 if sin(ang) >= 0 else -1
        t_val = sgn * 5.0 * (0.2969 * sqrt(max(1e-5, u_c)) - 0.1260 * u_c - 0.3516 * u_c**2 + 0.2843 * u_c**3 - 0.085 * u_c**4)
        prof.append(((u_c - 0.45) * chord, t_val * thick))
    rings = 3
    for i in range(rings):
        t = i / (rings - 1)
        scale = 0.82 if (i == 0 or i == rings - 1) else 1.0
        c = va.lerp(vb, t)
        for j, (py, px) in enumerate(prof):
            p = c + y_dir * (py * scale) + x_dir * (px * scale)
            vs.append(tuple(p))
            uvs.append((t, j / n_prof))
    for i in range(rings - 1):
        for j in range(n_prof):
            a0 = i * n_prof + j
            a1 = i * n_prof + (j + 1) % n_prof
            b0 = (i + 1) * n_prof + j
            b1 = (i + 1) * n_prof + (j + 1) % n_prof
            fs.append((a0, b0, b1, a1))
    fs.append(tuple(range(n_prof - 1, -1, -1)))
    base = (rings - 1) * n_prof
    fs.append(tuple(base + j for j in range(n_prof)))
    return mesh(name, vs, fs, mat, uvs)


def surf(name, fn, nu, nv, mat, th=0.0, bev=0.0, closed_u=False, closed_v=False):
    vs = []
    uv = []
    fs = []
    nu_pts = nu if closed_u else nu + 1
    nv_pts = nv if closed_v else nv + 1
    for i in range(nu_pts):
        u = i / nu
        for j in range(nv_pts):
            v = j / nv
            vs.append(fn(u, v))
            uv.append((u, v))
    for i in range(nu):
        i1 = (i + 1) % nu_pts if closed_u else i + 1
        for j in range(nv):
            j1 = (j + 1) % nv_pts if closed_v else j + 1
            a = i * nv_pts + j
            b = i1 * nv_pts + j
            c = i1 * nv_pts + j1
            d = i * nv_pts + j1
            fs.append((a, b, c, d))
    o = mesh(name, vs, fs, mat, uv)
    if th != 0.0:
        solidify(o, abs(th), -1.0 if th < 0 else 0.0, True)
    if bev > 0.0:
        bevel(o, bev, 1)
    return o


def smooth_interp(y, pts):
    slopes = []
    for i, (x, z) in enumerate(pts):
        if i == 0:
            m = (pts[1][1] - z) / (pts[1][0] - x)
        elif i == len(pts) - 1:
            m = (z - pts[i - 1][1]) / (x - pts[i - 1][0])
        else:
            l = (z - pts[i - 1][1]) / (x - pts[i - 1][0])
            r = (pts[i + 1][1] - z) / (pts[i + 1][0] - x)
            m = 0 if l * r <= 0 else 2 * l * r / (l + r)
        slopes.append(m)
    if y <= pts[0][0]:
        return pts[0][1]
    for i, ((a, v), (b, w)) in enumerate(zip(pts, pts[1:])):
        if y <= b:
            t = max(0.0, min(1.0, (y - a) / (b - a)))
            d = b - a
            return (2 * t**3 - 3 * t * t + 1) * v + (t**3 - 2 * t * t + t) * d * slopes[i] + (-2 * t**3 + 3 * t * t) * w + (t**3 - t * t) * d * slopes[i + 1]
    return pts[-1][1]


def bisect_livery_stripe(obj):
    """Slices a hull mesh cleanly along the diagonal livery planes from sheet.jpg and assigns Trim & Alloy materials flush with the surface."""
    for m_name in ['Trim', 'Alloy', 'Carbon']:
        if M[m_name].name not in [m.name for m in obj.data.materials]:
            obj.data.materials.append(M[m_name])
    idx_trim = [i for i, m in enumerate(obj.data.materials) if m.name == 'Trim'][0]
    idx_alloy = [i for i, m in enumerate(obj.data.materials) if m.name == 'Alloy'][0]

    activate(obj)
    for md in list(obj.modifiers):
        bpy.ops.object.modifier_apply(modifier=md.name)

    bm = bmesh.new()
    bm.from_mesh(obj.data)
    # Diagonal plane normal in (x, y, z): sweeps up and back along the sidepod & engine cover
    plane_no = Vector((0.0, 1.0, -1.35)).normalized()
    for d_val in [-0.08, -0.02, 0.24]:
        geom = list(bm.verts) + list(bm.edges) + list(bm.faces)
        bmesh.ops.bisect_plane(bm, geom=geom, dist=1e-5, plane_co=(0.0, d_val, 0.0), plane_no=plane_no)

    for f in bm.faces:
        c = f.calc_center_median()
        val = c.y - 1.35 * c.z
        if c.y > -0.25 and c.z > 0.11:
            if -0.08 <= val < -0.02:
                f.material_index = idx_alloy
            elif -0.02 <= val <= 0.24:
                f.material_index = idx_trim
    bm.to_mesh(obj.data)
    bm.free()
    obj.data.update()


# ==============================================================================
# 1. POLY HAVEN CC0 TEXTURES & CYCLES BAKING
# ==============================================================================
def download(asset, kind):
    p = f'textures/{asset}_{kind}_1k.jpg'
    if not os.path.exists(p):
        url = f'https://dl.polyhaven.org/file/ph-assets/Textures/jpg/1k/{asset}/{asset}_{kind}_1k.jpg'
        subprocess.run(['curl', '-fL', '--retry', '3', '-A', 'Mozilla/5.0', url, '-o', p], check=True)
    return bpy.data.images.load(os.path.abspath(p), check_existing=True)


SCAN = {}
for asset in ['metal_plate', 'fabric_leather_01', 'rubber_tiles']:
    for kind in ['diff', 'nor_gl', 'arm']:
        SCAN[asset, kind] = download(asset, kind)
print('INPUTS_READY', flush=True)


def source_mat(name, color, metal, rough, asset, grain):
    m = bpy.data.materials.new(name)
    m.use_nodes = True
    n = m.node_tree.nodes
    l = m.node_tree.links
    p = n.get('Principled BSDF')
    p.inputs['Metallic'].default_value = metal
    p.inputs['Roughness'].default_value = rough

    tex = n.new('ShaderNodeTexImage')
    tex.image = SCAN[asset, 'diff']
    bw = n.new('ShaderNodeRGBToBW')
    l.new(tex.outputs['Color'], bw.inputs[0])

    ramp = n.new('ShaderNodeValToRGB')
    ramp.color_ramp.elements[0].position = 0.0
    ramp.color_ramp.elements[1].position = 0.22
    low_mul = 0.94 if name.startswith('Paint') else 0.72
    for el, mul in zip(ramp.color_ramp.elements, [low_mul, 1.0]):
        el.color = (*(v * mul for v in color), 1.0)
    l.new(bw.outputs[0], ramp.inputs[0])

    if name.startswith('Carbon'):
        tc = n.new('ShaderNodeTexCoord')
        mp = n.new('ShaderNodeMapping')
        mp.inputs['Scale'].default_value = (46.0, 46.0, 46.0)
        mp.inputs['Rotation'].default_value = (0.0, 0.0, pi / 4)
        wv = n.new('ShaderNodeTexWave')
        wv.wave_type = 'BANDS'
        wv.inputs['Scale'].default_value = 20.0
        wv.inputs['Distortion'].default_value = 0.30
        l.new(tc.outputs['UV'], mp.inputs['Vector'])
        l.new(mp.outputs['Vector'], wv.inputs['Vector'])
        mix_c = n.new('ShaderNodeMixRGB')
        mix_c.blend_type = 'MULTIPLY'
        mix_c.inputs[0].default_value = 0.35
        l.new(ramp.outputs[0], mix_c.inputs[1])
        l.new(wv.outputs['Color'], mix_c.inputs[2])
        l.new(mix_c.outputs[0], p.inputs['Base Color'])
    else:
        noise_c = n.new('ShaderNodeTexNoise')
        noise_c.inputs['Scale'].default_value = 14.0
        noise_c.inputs['Detail'].default_value = 5.0
        noise_c.inputs['Roughness'].default_value = 0.65
        mix_c = n.new('ShaderNodeMixRGB')
        mix_c.blend_type = 'MULTIPLY'
        mix_c.inputs[0].default_value = 0.05 if name.startswith('Paint') else 0.14
        l.new(ramp.outputs[0], mix_c.inputs[1])
        l.new(noise_c.outputs['Color'], mix_c.inputs[2])
        l.new(mix_c.outputs[0], p.inputs['Base Color'])

    nt = n.new('ShaderNodeTexImage')
    nt.image = SCAN[asset, 'nor_gl']
    nt.image.colorspace_settings.name = 'Non-Color'
    nm = n.new('ShaderNodeNormalMap')
    nm.inputs['Strength'].default_value = grain
    l.new(nt.outputs['Color'], nm.inputs['Color'])
    l.new(nm.outputs[0], p.inputs['Normal'])

    arm = n.new('ShaderNodeTexImage')
    arm.image = SCAN[asset, 'arm']
    arm.image.colorspace_settings.name = 'Non-Color'
    sep = n.new('ShaderNodeSeparateColor')
    l.new(arm.outputs[0], sep.inputs[0])

    mul = n.new('ShaderNodeMath')
    mul.operation = 'MULTIPLY_ADD'
    mul.inputs[1].default_value = 0.14
    mul.inputs[2].default_value = rough - 0.07
    l.new(sep.outputs[1], mul.inputs[0])

    noise = n.new('ShaderNodeTexNoise')
    noise.inputs['Scale'].default_value = 140.0
    noise.inputs['Detail'].default_value = 3.0
    wear = n.new('ShaderNodeMath')
    wear.operation = 'MULTIPLY_ADD'
    wear.inputs[1].default_value = 0.04
    wear.inputs[2].default_value = -0.02
    l.new(noise.outputs['Fac'], wear.inputs[0])

    summ = n.new('ShaderNodeMath')
    summ.operation = 'ADD'
    l.new(mul.outputs[0], summ.inputs[0])
    l.new(wear.outputs[0], summ.inputs[1])
    l.new(summ.outputs[0], p.inputs['Roughness'])
    return m


def bake_mat(name, color, metal, rough, asset, grain, size=512):
    src = source_mat(name + '_source', color, metal, rough, asset, grain)
    if name == 'Lamps':
        im = bpy.data.images.new('LampLEDGrid', width=128, height=128, alpha=False)
        pix = []
        for y in range(128):
            for x in range(128):
                if x < 64:
                    gx = ((x % 12) - 5.5) / 5.5
                    gy = ((y % 24) - 11.5) / 11.5
                    d2 = gx * gx + gy * gy
                    b = max(0.18, min(1.0, 1.15 - d2 * 0.85))
                    pix.extend([0.98 * b, 0.025 * b, 0.012 * b, 1.0])
                else:
                    pix.extend([0.95, 0.02, 0.01, 1.0])
        im.pixels = pix
        tex = src.node_tree.nodes.new('ShaderNodeTexImage')
        tex.image = im
        src.node_tree.links.new(tex.outputs[0], src.node_tree.nodes.get('Principled BSDF').inputs['Base Color'])

    bpy.ops.mesh.primitive_plane_add(size=1)
    ob = bpy.context.object
    ob.data.materials.append(src)
    n = src.node_tree.nodes
    l = src.node_tree.links
    p = n.get('Principled BSDF')
    out = n.get('Material Output')
    target = n.new('ShaderNodeTexImage')
    n.active = target

    imgs = {}
    S.cycles.samples = 4
    S.render.bake.margin = 8
    for label, sz in [('Base', size), ('Normal', 512), ('ORM', 256)]:
        img = bpy.data.images.new(name + '_' + label, width=sz, height=sz, alpha=False)
        target.image = img
        if label != 'Base':
            img.colorspace_settings.name = 'Non-Color'
        if label == 'Base':
            emit = n.new('ShaderNodeEmission')
            l.new(p.inputs['Base Color'].links[0].from_socket, emit.inputs[0])
            l.new(emit.outputs[0], out.inputs['Surface'])
            bpy.ops.object.bake(type='EMIT')
            l.new(p.outputs['BSDF'], out.inputs['Surface'])
        elif label == 'Normal':
            bpy.ops.object.bake(type='NORMAL')
        else:
            emit = n.new('ShaderNodeEmission')
            combine = n.new('ShaderNodeCombineColor')
            combine.inputs[0].default_value = 0.94
            combine.inputs[2].default_value = metal
            l.new(p.inputs['Roughness'].links[0].from_socket, combine.inputs[1])
            l.new(combine.outputs[0], emit.inputs[0])
            l.new(emit.outputs[0], out.inputs['Surface'])
            bpy.ops.object.bake(type='EMIT')
        img.filepath_raw = os.path.join(ROOT, 'textures', f'{name}_{label}.png')
        img.file_format = 'PNG'
        img.save()
        imgs[label] = img

    bpy.data.objects.remove(ob, do_unlink=True)
    m = bpy.data.materials.new(name)
    m.use_nodes = True
    n = m.node_tree.nodes
    l = m.node_tree.links
    p = n.get('Principled BSDF')
    for label in imgs:
        t = n.new('ShaderNodeTexImage')
        t.image = imgs[label]
        t.label = label
        if label == 'Base':
            l.new(t.outputs[0], p.inputs['Base Color'])
        elif label == 'Normal':
            nm = n.new('ShaderNodeNormalMap')
            nm.inputs['Strength'].default_value = 0.55
            l.new(t.outputs[0], nm.inputs['Color'])
            l.new(nm.outputs[0], p.inputs['Normal'])
        else:
            sep = n.new('ShaderNodeSeparateColor')
            l.new(t.outputs[0], sep.inputs[0])
            l.new(sep.outputs[1], p.inputs['Roughness'])
            l.new(sep.outputs[2], p.inputs['Metallic'])
            group = bpy.data.node_groups.get('glTF Material Output')
            if not group:
                group = bpy.data.node_groups.new('glTF Material Output', 'ShaderNodeTree')
                group.interface.new_socket(name='Occlusion', in_out='INPUT', socket_type='NodeSocketFloat')
            gn = n.new('ShaderNodeGroup')
            gn.node_tree = group
            l.new(sep.outputs[0], gn.inputs['Occlusion'])

    if name == 'Paint':
        p.inputs['Coat Weight'].default_value = 1.0
        p.inputs['Coat Roughness'].default_value = 0.05
    elif name == 'Carbon':
        p.inputs['Coat Weight'].default_value = 0.45
        p.inputs['Coat Roughness'].default_value = 0.14
    elif name == 'Lamps':
        p.inputs['Emission Strength'].default_value = 4.5
        l.new(p.inputs['Base Color'].links[0].from_socket, p.inputs['Emission Color'])
    M[name] = m
    print('BAKED', name, flush=True)


MAT_SPECS = [
    ('Paint',    (0.96, 0.96, 0.96), 0.16, 0.24, 'metal_plate',       0.008, 1024),
    ('Carbon',   (0.036, 0.039, 0.044), 0.18, 0.26, 'rubber_tiles',   0.28,  512),
    ('Rubber',   (0.026, 0.028, 0.031), 0.00, 0.74, 'fabric_leather_01', 0.32, 512),
    ('Alloy',    (0.76, 0.79, 0.83), 0.88, 0.20, 'metal_plate',       0.02,  512),
    ('Brake',    (0.16, 0.17, 0.20), 0.82, 0.40, 'metal_plate',       0.22,  512),
    ('Trim',     (0.80, 0.83, 0.86), 0.22, 0.30, 'metal_plate',       0.04,  512),
    ('Interior', (0.040, 0.038, 0.042), 0.00, 0.78, 'fabric_leather_01', 0.18, 512),
    ('Lamps',    (0.95, 0.02, 0.01), 0.10, 0.16, 'metal_plate',       0.01,  128),
]
for spec in MAT_SPECS:
    bake_mat(*spec)


def lamp_uv(o, region='grid'):
    if not o.data.uv_layers:
        o.data.uv_layers.new(name='UVMap')
    uv_layer = o.data.uv_layers.active.data
    for poly in o.data.polygons:
        for li in poly.loop_indices:
            v = o.data.vertices[o.data.loops[li].vertex_index].co
            if region == 'grid':
                uv_layer[li].uv = (0.25 + v.x * 2.2, 0.5 + v.z * 2.2)
            else:
                uv_layer[li].uv = (0.75, 0.5)
    return o


# ==============================================================================
# 2. SCULPTED MONOCOQUE, NOSE CONE, ENGINE COVER & AIRBOX (PAINT + CARBON + TRIM)
# ==============================================================================
def mono_top_z(y):
    return smooth_interp(y, [
        (-2.86, 0.175),
        (-2.60, 0.255),
        (-2.20, 0.380),
        (-1.80, 0.505),
        (-1.25, 0.585),
        (-0.72, 0.625),
        (-0.15, 0.618),
        (-0.02, 0.655),
        (0.08,  0.858),  # Airbox roll-hoop crown
        (0.35,  0.815),  # Taut sloping engine cover spine matching sheet.jpg!
        (0.85,  0.705),
        (1.35,  0.585),
        (1.80,  0.495),
        (2.15,  0.440),
    ])


def mono_bot_z(y):
    return smooth_interp(y, [
        (-2.86, 0.135),
        (-2.55, 0.165),
        (-2.15, 0.215),
        (-1.80, 0.235),
        (-1.25, 0.170),
        (-0.80, 0.075),
        (0.00,  0.070),
        (1.50,  0.075),
        (2.15,  0.260),
    ])


def mono_half_w(y):
    return smooth_interp(y, [
        (-2.86, 0.068),
        (-2.55, 0.115),
        (-2.15, 0.175),
        (-1.80, 0.225),
        (-1.20, 0.285),
        (-0.68, 0.345),
        (-0.10, 0.355),
        (0.20,  0.315),
        (0.75,  0.235),
        (1.35,  0.155),
        (1.80,  0.105),
        (2.15,  0.068),
    ])


# Build Nose Cone & Forward Monocoque (-2.86 to -0.68)
def nose_shell_fn(u, v):
    y = -2.86 + (-0.68 - (-2.86)) * u
    w = mono_half_w(y)
    zt = mono_top_z(y)
    zb = mono_bot_z(y)
    ang = (v * 2.0 - 1.0) * pi
    sa = sin(ang)
    ca = cos(ang)
    rx = abs(sa)**0.78 * (1 if sa >= 0 else -1)
    rz = 0.5 + 0.5 * (abs(ca)**0.82 * (1 if ca >= 0 else -1))
    x = w * rx
    z = zb + (zt - zb) * rz
    if ca > 0.35:
        z += 0.008 * (1.0 - (rx)**2)
    return (x, y, z)


nose_obj = surf('Nose_And_Front_Monocoque', nose_shell_fn, 52, 32, 'Paint', closed_v=True)
# Assign Trim material directly to the blank nose roundel polygons on the upper nose deck (100% flush!)
nose_obj.data.materials.append(M['Trim'])
for poly in nose_obj.data.polygons:
    c = poly.center
    if -2.65 <= c.y <= -1.95 and c.z >= mono_top_z(c.y) - 0.022 and abs(c.x) <= mono_half_w(c.y) * 0.52:
        poly.material_index = 1

# Nose tip carbon cap
surf('Nose_Tip_Cap', lambda u, v: (
    mono_half_w(-2.86 + 0.10 * u) * sin((2 * v - 1) * pi) * (0.3 + 0.7 * u)**0.6,
    -2.89 + 0.13 * u,
    0.155 + 0.025 * u + 0.024 * cos((2 * v - 1) * pi) * (0.3 + 0.7 * u)**0.6
), 10, 20, 'Carbon', closed_v=True)

# Twin under-nose front wing mounting pillars
for side in [-1, 1]:
    airfoil_strut('Front_Wing_Pillar', (side * 0.080, -2.62, 0.095), (side * 0.080, -2.52, 0.220), 0.18, 0.018, 'Carbon', up_hint=(1, 0, 0))

# Nose & Monocoque chamfer accent lines (silver-chrome Alloy stripe matching sheet.jpg)
for side in [-1, 1]:
    stripe_pts = []
    for k in range(32):
        u = k / 31
        y = -2.78 + 2.12 * u
        w = mono_half_w(y) * 0.84
        z = mono_bot_z(y) + (mono_top_z(y) - mono_bot_z(y)) * 0.82 + 0.004
        stripe_pts.append((side * w, y, z))
    line('Nose_Shoulder_Accent', stripe_pts, 0.0055, 'Alloy')

# Pitot tube & antenna array on front bulkhead
cyl('Pitot_Mast_Base', (0.0, -1.45, mono_top_z(-1.45) + 0.01), 0.010, 0.025, 'Alloy', 'Z', 12)
line('Pitot_Sensor_Tube', [(0.0, -1.45, mono_top_z(-1.45) + 0.02), (0.0, -1.45, mono_top_z(-1.45) + 0.095), (0.0, -1.51, mono_top_z(-1.45) + 0.095)], 0.003, 'Alloy')
for sx in [-0.06, 0.06]:
    line('Telemetry_Blade_Antenna', [(sx, -1.18, mono_top_z(-1.18)), (sx, -1.16, mono_top_z(-1.18) + 0.065)], 0.0025, 'Carbon')


# ==============================================================================
# 3. OPEN COCKPIT TUB, DRIVER SEAT, HARNESS, PEDALS & ENGINE COVER
# ==============================================================================
def rear_spine_fn(u, v):
    y = -0.72 + (2.15 - (-0.72)) * u
    w_top = mono_half_w(y)
    zt = mono_top_z(y)
    zb = max(0.18, mono_bot_z(y))
    in_cockpit = (-0.68 < y < -0.01)
    ang = v * pi
    if in_cockpit:
        t_v = v
        x = 0.255 + (w_top - 0.255) * sin(t_v * pi * 0.85)
        z = zt - (zt - zb) * (t_v**1.15)
    else:
        fin_boost = 0.0
        if y > 0.15 and v < 0.18:
            fin_boost = 0.038 * sin(pi * min(1.0, (y - 0.15) / 1.75)) * (1.0 - v / 0.18)**2
        x = w_top * (sin(ang * 0.88)**0.78)
        z = (zt + fin_boost) - ((zt + fin_boost) - zb) * (v**1.25)
    return (x, y, z)


for side in [-1, 1]:
    sp_obj = surf('Monocoque_And_Engine_Spine', lambda u, v, s=side: (
        s * rear_spine_fn(u, v)[0],
        rear_spine_fn(u, v)[1],
        rear_spine_fn(u, v)[2]
    ), 56, 20, 'Paint', th=0.007)
    bisect_livery_stripe(sp_obj)

# Carbon dorsal shark-fin blade on top of the engine cover spine (y = +0.20 to +1.85)
def dorsal_fin_fn(u, v):
    y = 0.20 + 1.65 * u
    z_base = mono_top_z(y) - 0.01
    z_top = z_base + (0.068 + 0.045 * u) * sin(pi * u)**0.55
    z = z_base + (z_top - z_base) * v
    return (0.0, y, z)


surf('Engine_Cover_Dorsal_Fin', dorsal_fin_fn, 24, 4, 'Carbon', th=0.008, bev=0.002)

# Sculpted Forward-Overhanging Airbox Torpedo Pod & Intake Scoop (matches sheet.jpg side view!)
def airbox_pod_fn(u, v):
    # Extends from y = -0.06 (overhangingintake lip) back to y = +0.36 (blending into spine)
    y = -0.06 + 0.42 * u
    ang = (2 * v - 1) * pi
    sa, ca = sin(ang), cos(ang)
    taper = 1.0 - 0.28 * (u**1.5)
    w_a = 0.135 * (0.80 - 0.20 * ca) * taper
    h_a = 0.092 * taper
    x = w_a * sa
    z = (0.768 - 0.015 * u) + h_a * ca
    return (x, y, z)


surf('Airbox_Torpedo_Cowling', airbox_pod_fn, 14, 24, 'Paint', closed_v=True)

airbox_ring = [airbox_pod_fn(0.0, k / 28) for k in range(28)]
line('Airbox_Carbon_Lip', airbox_ring, 0.015, 'Carbon', cyclic=True, res=2)

surf('Airbox_Intake_Throat', lambda u, v: (
    airbox_ring[int(u * 27)][0] * (1.0 - 0.35 * v),
    -0.06 + 0.22 * v,
    airbox_ring[int(u * 27)][2] * (1.0 - 0.04 * v)
), 27, 4, 'Carbon', closed_u=True)
cube('Airbox_Center_Splitter', (0.0, -0.01, 0.768), (0.010, 0.10, 0.15), 'Carbon', 0.003)

# Iconic F1 T-Cam & Onboard Camera Housing atop Airbox (z = 0.86..0.95 m, right above airbox brow as in sheet.jpg!)
linkbar('TCam_Vertical_Mast', (0.0, 0.02, 0.852), (0.0, 0.04, 0.922), 0.024, 0.065, 'Carbon', 0.005)
cube('TCam_Horizontal_Crossbar', (0.0, 0.02, 0.932), (0.26, 0.085, 0.028), 'Carbon', 0.008)
cyl('TCam_Center_Pod', (0.0, 0.00, 0.934), 0.018, 0.12, 'Carbon', 'Y', 18)
cyl('TCam_Front_Lens', (0.0, -0.062, 0.934), 0.012, 0.008, 'Alloy', 'Y', 14)
for side in [-1, 1]:
    cyl('TCam_Wingtip_Pod', (side * 0.125, 0.02, 0.932), 0.012, 0.08, 'Carbon', 'Y', 14)
    cyl('TCam_Wingtip_Lens', (side * 0.125, -0.022, 0.932), 0.008, 0.006, 'Alloy', 'Y', 14)

# Open Cockpit Carbon Tub (visible from above!)
def cockpit_tub_fn(u, v):
    y = -0.70 + 0.72 * u
    ang = (2 * v - 1) * (pi * 0.52)
    w = 0.250 * (0.82 + 0.18 * sin(pi * u))
    x = w * sin(ang)
    z = 0.14 + 0.47 * (1.0 - cos(ang))
    return (x, y, z)


surf('Cockpit_Carbon_Tub', cockpit_tub_fn, 18, 14, 'Carbon', th=0.012)
cube('Cockpit_Front_Bulkhead', (0.0, -0.70, 0.36), (0.44, 0.03, 0.42), 'Carbon', 0.008)
cube('Cockpit_Rear_Bulkhead', (0.0, 0.01, 0.38), (0.48, 0.04, 0.46), 'Carbon', 0.010)

coaming_pts = []
for k in range(33):
    a = -pi * 0.92 + 1.84 * pi * k / 32
    y = -0.34 + 0.35 * cos(a)
    x = 0.258 * sin(a)
    z = mono_top_z(y) + 0.008
    coaming_pts.append((x, y, z))
line('Cockpit_Carbon_Collar', coaming_pts, 0.018, 'Carbon')

bolster_pts = []
for k in range(21):
    a = -pi * 0.68 + 1.36 * pi * k / 20
    y = -0.30 + 0.29 * cos(a)
    x = 0.225 * sin(a)
    z = mono_top_z(y) - 0.012
    bolster_pts.append((x, y, z))
line('Cockpit_Head_Padding_Bolster', bolster_pts, 0.034, 'Interior')

wind_pts = [(-0.22 + 0.44 * k / 16, -0.68 + 0.06 * (1 - ((k - 8) / 8)**2), 0.635 + 0.026 * (1 - ((k - 8) / 8)**2)) for k in range(17)]
line('Cockpit_Wind_Deflector', wind_pts, 0.009, 'Carbon')

# Sculpted Reclined Formula Bucket Seat inside Cockpit
def seat_fn(u, v):
    y = -0.44 + 0.43 * u
    z_center = 0.165 + 0.36 * (u**1.35)
    w_seat = 0.195 * (1.0 - 0.18 * u)
    s = 2 * v - 1
    x = w_seat * s
    bolster = 0.055 * (s**4) * (1.0 - 0.25 * u)
    z = z_center + bolster
    y -= 0.035 * (s**2)
    return (x, y, z)


surf('Formula_Bucket_Seat', seat_fn, 14, 12, 'Interior', th=0.018, bev=0.004)
cube('Seat_Headrest_Pad', (0.0, -0.02, 0.54), (0.19, 0.045, 0.12), 'Interior', 0.015)

# 6-Point Racing Harness Straps & Central Quick-Release Buckle
cyl('Harness_Central_Buckle', (0.0, -0.23, 0.265), 0.026, 0.016, 'Alloy', 'Z', 16)
for side in [-1, 1]:
    shoulder_pts = [
        (side * 0.085, -0.03, 0.495),
        (side * 0.075, -0.12, 0.385),
        (side * 0.035, -0.22, 0.272),
    ]
    line('Harness_Shoulder_Strap', shoulder_pts, 0.014, 'Interior')
    cube('Harness_Adjuster_Clasp', (side * 0.075, -0.12, 0.390), (0.042, 0.024, 0.012), 'Alloy', 0.003)
    lap_pts = [
        (side * 0.175, -0.26, 0.215),
        (side * 0.095, -0.24, 0.255),
        (side * 0.030, -0.23, 0.268),
    ]
    line('Harness_Lap_Strap', lap_pts, 0.012, 'Interior')

# Footwell Pedals
for side, px in [(-1, -0.065), (1, 0.065)]:
    linkbar('Pedal_Arm', (px, -0.66, 0.16), (px, -0.62, 0.27), 0.014, 0.022, 'Alloy')
    o = cube('Pedal_Faceplate', (px, -0.61, 0.28), (0.055, 0.010, 0.095), 'Alloy', 0.004)
    o.rotation_euler[0] = -0.25


# ==============================================================================
# 4. SAFETY HALO OVER OPEN COCKPIT (CARBON)
# ==============================================================================
halo_hoop_pts = [
    (-0.255, -0.01, 0.635),
    (-0.275, -0.18, 0.705),
    (-0.230, -0.44, 0.745),
    (-0.120, -0.63, 0.755),
    (0.0,    -0.695, 0.758),
    (0.120,  -0.63, 0.755),
    (0.230,  -0.44, 0.745),
    (0.275,  -0.18, 0.705),
    (0.255,  -0.01, 0.635),
]
spline_tube('Safety_Halo_Main_Hoop', halo_hoop_pts, 0.027, 'Carbon', cyclic=False, res_u=14, bev_res=3)

halo_wing_pts = [
    (-0.245, -0.10, 0.720),
    (-0.185, -0.32, 0.756),
    (0.0,    -0.46, 0.768),
    (0.185,  -0.32, 0.756),
    (0.245,  -0.10, 0.720),
]
spline_tube('Halo_Upper_Aero_Fairing', halo_wing_pts, 0.009, 'Carbon', cyclic=False, res_u=10, bev_res=2)

airfoil_strut('Safety_Halo_Front_Pillar', (0.0, -0.755, 0.618), (0.0, -0.685, 0.756), 0.100, 0.034, 'Carbon', up_hint=(1, 0, 0), n_prof=12)
cube('Halo_Apex_Fairing', (0.0, -0.685, 0.750), (0.088, 0.115, 0.048), 'Carbon', 0.018)
cube('Halo_Base_Mount_Foot', (0.0, -0.755, 0.625), (0.076, 0.110, 0.032), 'Carbon', 0.012)
for side in [-1, 1]:
    cube('Halo_Rear_Mount_Fairing', (side * 0.255, -0.01, 0.635), (0.068, 0.120, 0.045), 'Carbon', 0.015)

# Aero Wing Mirrors on Dual Cantilever Stalks
for side in [-1, 1]:
    airfoil_strut('Mirror_Inboard_Stalk', (side * 0.31, -0.52, 0.595), (side * 0.51, -0.47, 0.625), 0.055, 0.010, 'Carbon', up_hint=(0, 0, 1))
    airfoil_strut('Mirror_Vertical_Stay', (side * 0.48, -0.46, 0.535), (side * 0.51, -0.47, 0.620), 0.042, 0.008, 'Carbon', up_hint=(1, 0, 0))
    cube('Mirror_Aero_Housing', (side * 0.535, -0.465, 0.630), (0.135, 0.095, 0.052), 'Paint', 0.018)
    cube('Mirror_Carbon_Bezel', (side * 0.535, -0.422, 0.630), (0.122, 0.016, 0.044), 'Carbon', 0.006)
    cube('Mirror_Reflective_Glass', (side * 0.535, -0.415, 0.630), (0.110, 0.006, 0.036), 'Alloy', 0.003)


# ==============================================================================
# 5. SIDEPODS WITH RADIATOR INLETS, UNDERCUT & DEEP COOLING GILL LOUVRES
# ==============================================================================
def sidepod_outer_x(y):
    return smooth_interp(y, [
        (-0.58, 0.685),
        (-0.35, 0.765),
        (-0.05, 0.780),
        (0.35,  0.735),
        (0.85,  0.585),
        (1.35,  0.365),
        (1.90,  0.145),
    ])


def sidepod_top_z(y):
    return smooth_interp(y, [
        (-0.58, 0.555),
        (-0.30, 0.578),
        (0.10,  0.568),
        (0.60,  0.525),
        (1.15,  0.455),
        (1.90,  0.385),
    ])


def sidepod_bot_z(y):
    return smooth_interp(y, [
        (-0.58, 0.275),
        (-0.25, 0.185),
        (0.15,  0.110),
        (0.80,  0.090),
        (1.45,  0.115),
        (1.90,  0.220),
    ])


for side in [-1, 1]:
    def sidepod_hull_fn(u, v, s=side):
        y = -0.58 + (1.90 - (-0.58)) * u
        x_in = max(0.10, mono_half_w(y) - 0.04)
        x_out = sidepod_outer_x(y)
        zt = sidepod_top_z(y)
        zb = sidepod_bot_z(y)
        z_mid = zb + 0.58 * (zt - zb)
        if v <= 0.55:
            tv = v / 0.55
            x = x_in + (x_out - x_in) * (sin(tv * pi * 0.5)**0.72)
            channel = -0.022 * sin(pi * tv)**2 * sin(pi * min(1.0, max(0.0, (u - 0.12) / 0.65)))
            z = zt - (zt - z_mid) * (tv**2.2) + channel
        else:
            tv = (v - 0.55) / 0.45
            x_floor = x_in + 0.14 * (1.0 - u * 0.6)
            x = x_out - (x_out - x_floor) * (tv**1.35)
            z = z_mid - (z_mid - zb) * (sin(tv * pi * 0.5)**0.85)
        return (s * x, y, z)

    sp_hull = surf('Sidepod_Sculpted_Body', sidepod_hull_fn, 54, 24, 'Paint', th=0.007)
    bisect_livery_stripe(sp_hull)

    # Sidepod Forward Radiator Inlet Mouth, Carbon Lip & Internal Heat-Exchanger Core
    inlet_ring = []
    for k in range(32):
        a = 2 * pi * k / 32
        sa, ca = sin(a), cos(a)
        rx = abs(sa)**0.68 * (1 if sa >= 0 else -1)
        rz = abs(ca)**0.72 * (1 if ca >= 0 else -1)
        x = side * (0.495 + 0.185 * rx)
        z = 0.425 + 0.118 * rz
        y = -0.585 - 0.045 * max(0.0, ca) + 0.025 * min(0.0, ca)
        inlet_ring.append((x, y, z))
    line('Sidepod_Radiator_Inlet_Lip', inlet_ring, 0.016, 'Carbon', cyclic=True, res=2)

    surf('Sidepod_Inlet_Duct_Wall', lambda u, v, ring=inlet_ring: (
        ring[int(u * 31)][0] * (1.0 - 0.08 * v),
        ring[int(u * 31)][1] + 0.28 * v,
        ring[int(u * 31)][2] * (1.0 - 0.05 * v)
    ), 31, 4, 'Carbon', closed_u=True)

    rad = cube('Sidepod_Radiator_Core', (side * 0.495, -0.42, 0.425), (0.31, 0.035, 0.19), 'Brake', 0.006)
    rad.rotation_euler = (0.35, 0.0, side * 0.18)
    for k in range(6):
        z_f = 0.355 + k * 0.026
        line('Radiator_Fin_Louver', [
            (side * 0.355, -0.45 + k * 0.006, z_f),
            (side * 0.635, -0.39 + k * 0.006, z_f)
        ], 0.0025, 'Alloy')

    # 7 Prominent Sculpted Cooling Gill Louvres along Upper Sidepod Flank (clearly visible in side & 3/4 views like sheet.jpg!)
    for g in range(7):
        u_g = 0.26 + g * 0.045
        p_top = sidepod_hull_fn(u_g, 0.18, side)
        p_mid = sidepod_hull_fn(u_g + 0.008, 0.36, side)
        p_bot = sidepod_hull_fn(u_g + 0.016, 0.54, side)
        # Curved carbon gill louver blade standing slightly proud (+0.009 m) of the sidepod shoulder to catch light and cast crisp gill shadows
        gill_pts = [
            (p_top[0] + side * 0.006, p_top[1], p_top[2] + 0.006),
            (p_mid[0] + side * 0.011, p_mid[1], p_mid[2] + 0.008),
            (p_bot[0] + side * 0.006, p_bot[1], p_bot[2] + 0.004),
        ]
        spline_tube('Cooling_Gill_Louver_Vane', gill_pts, 0.0075, 'Carbon', res_u=6, bev_res=1)


# ==============================================================================
# 6. FLAT FLOOR, SWEPT BARGEBOARD SHIELDS, VENTURI FENCES & REAR DIFFUSER
# ==============================================================================
surf('T_Tray_Front_Splitter', lambda u, v: (
    (2 * v - 1) * (0.18 + 0.22 * u),
    -1.38 + 0.63 * u,
    0.046 + 0.012 * u
), 8, 12, 'Carbon', th=0.012, bev=0.003)
linkbar('Splitter_Stay_Rod', (0.0, -1.32, 0.052), (0.0, -1.25, 0.185), 0.008, 0.008, 'Alloy')


def floor_half_w(y):
    return smooth_interp(y, [
        (-0.88, 0.64),
        (-0.45, 0.805),
        (0.20,  0.815),
        (0.95,  0.790),
        (1.40,  0.700),
        (1.72,  0.620),
    ])


for side in [-1, 1]:
    def floor_fn(u, v, s=side):
        y = -0.88 + (1.72 - (-0.88)) * u
        w = floor_half_w(y)
        x = s * w * v
        scroll = 0.0
        if v > 0.80:
            tv = (v - 0.80) / 0.20
            scroll = 0.038 * sin(tv * pi * 0.65) * sin(pi * u)**0.5
        z = 0.052 + scroll
        return (x, y, z)

    surf('Flat_Carbon_Floor', floor_fn, 36, 12, 'Carbon', th=0.014, bev=0.003)

    edge_pts = []
    for k in range(28):
        u = 0.08 + 0.78 * k / 27
        y = -0.88 + (1.72 - (-0.88)) * u
        w = floor_half_w(y) + 0.018
        z = 0.072 + 0.022 * sin(pi * k / 27)
        edge_pts.append((side * w, y, z))
    line('Floor_Edge_Scroll_Lip', edge_pts, 0.009, 'Carbon')
    linkbar('Floor_Edge_Tie_Stay', (side * 0.52, 1.05, 0.34), (side * 0.77, 1.15, 0.075), 0.007, 0.007, 'Alloy')

    def barge_shield_fn(u, v, s=side):
        y = -0.95 + 1.00 * u
        z_top = smooth_interp(y, [(-0.95, 0.255), (-0.55, 0.215), (-0.15, 0.145), (0.05, 0.085)])
        z = 0.054 + (z_top - 0.054) * v
        w_base = smooth_interp(y, [(-0.95, 0.54), (-0.55, 0.72), (-0.15, 0.78), (0.05, 0.77)])
        x = s * (w_base - 0.045 * v * (1.0 - 0.5 * u))
        return (x, y, z)

    surf('Swept_Venturi_Bargeboard_Shield', barge_shield_fn, 20, 6, 'Carbon', th=0.010, bev=0.003)

    for f_idx, x_frac in enumerate([0.44, 0.58, 0.72]):
        def fence_fn(u, v, s=side, xf=x_frac, idx=f_idx):
            y = -0.88 + 0.42 * u
            w_base = floor_half_w(y) * xf
            x = s * (w_base + 0.045 * (u**1.3))
            z_top = (0.215 - 0.022 * idx) * (1.0 - 0.32 * u)
            z = 0.052 + (z_top - 0.052) * v
            return (x, y, z)
        surf('Venturi_Tunnel_Fence', fence_fn, 8, 4, 'Carbon', th=0.007, bev=0.002)

# Rear Venturi Diffuser (+1.68 to +2.53)
for side in [-1, 1]:
    def diffuser_fn(u, v, s=side):
        y = 1.68 + (2.53 - 1.68) * u
        w = 0.62 - 0.06 * u
        x = s * w * v
        tunnel_arch = sin(max(0.0, min(1.0, (v - 0.14) / 0.82)) * pi)**0.75
        z_roof = 0.055 + (0.285 * (u**1.25)) * tunnel_arch + 0.065 * u * (1.0 - tunnel_arch)
        if v > 0.94:
            z_roof = 0.075 + 0.14 * u
        return (x, y, z_roof)

    surf('Rear_Venturi_Diffuser', diffuser_fn, 18, 14, 'Carbon', th=0.012, bev=0.003)

    for x_st in [0.17, 0.34, 0.50]:
        def strake_fn(u, v, s=side, xs=x_st):
            y = 1.74 + 0.76 * u
            z_top = 0.065 + 0.255 * (u**1.25)
            z_bot = max(0.060, z_top - (0.075 + 0.055 * u))
            return (s * xs, y, z_bot + (z_top - z_bot) * v)
        surf('Diffuser_Vertical_Strake', strake_fn, 10, 4, 'Carbon', th=0.008, bev=0.002)

for side in [-1, 1]:
    gurney_pts = [diffuser_fn(1.0, k / 12, side) for k in range(2, 12)]
    line('Diffuser_Exit_Gurney_Flap', gurney_pts, 0.008, 'Carbon')


# ==============================================================================
# 7. MULTI-ELEMENT FRONT WING, ENDPLATES & CONNECTED WHEEL WAKE DEFLECTORS
# ==============================================================================
for side in [-1, 1]:
    for elem in range(4):
        def fwing_elem_fn(u, v, s=side, e=elem):
            x_start = 0.0 if e == 0 else 0.102
            x_end = 0.982
            x = x_start + (x_end - x_start) * u
            sweep_y = 0.26 * (u**1.45)
            gull_z = -0.025 * sin(u * pi * 0.85) + 0.018 * (u**2)
            y_lead_base = -2.99 + e * 0.125 + sweep_y
            chord = (0.21 - e * 0.026) * (1.0 - 0.18 * (u**2))
            y = y_lead_base + chord * v
            z_lead = 0.078 + e * 0.032 + gull_z
            flap_rise = (0.022 + e * 0.018) * (1.0 - 0.25 * (u**2))
            camber = 0.010 * sin(pi * v)
            z = z_lead + flap_rise * v + camber
            return (s * x, y, z)

        surf(f'Front_Wing_Element_{elem+1}', fwing_elem_fn, 24, 6, 'Carbon', th=0.012 - elem * 0.0015, bev=0.002)

    for x_br in [0.22, 0.46, 0.70, 0.90]:
        u_b = (x_br - 0.102) / (0.982 - 0.102)
        for e in range(3):
            p0 = fwing_elem_fn(u_b, 0.85, side, e)
            p1 = fwing_elem_fn(u_b, 0.15, side, e + 1)
            linkbar('Front_Wing_Slot_Bracket', p0, p1, 0.006, 0.014, 'Alloy')

    def fwing_endplate_fn(u, v, s=side):
        y_front = -2.72 + 0.12 * (v**1.4)
        y_rear = -2.20 - 0.06 * sin(pi * v)
        y = y_front + (y_rear - y_front) * u
        z = 0.068 + (0.275 - 0.045 * (1.0 - u)) * v
        x = s * (0.976 + 0.016 * (v**1.6) - 0.008 * sin(pi * u))
        return (x, y, z)

    surf('Front_Wing_Endplate', fwing_endplate_fn, 14, 10, 'Carbon', th=0.012, bev=0.003)

    surf('Front_Wing_Footplate', lambda u, v, s=side: (
        s * (0.955 + 0.043 * v),
        -2.70 + 0.48 * u,
        0.066 + 0.016 * sin(pi * u) * v
    ), 10, 4, 'Carbon', th=0.008, bev=0.002)

    canard_pts = [
        (side * 0.992, -2.58, 0.165),
        (side * 0.996, -2.42, 0.195),
        (side * 0.994, -2.26, 0.238),
    ]
    line('Front_Wing_Diveplane_Canard', canard_pts, 0.011, 'Carbon')

    def wheel_deflector_fn(u, v, s=side):
        ang = -0.55 + 0.95 * u
        r_arc = 0.372 * (0.62 + 0.38 * (1.0 - (1.0 - u)**3)) if u < 0.25 else 0.376
        y = -1.80 - r_arc * sin(ang)
        z = 0.360 + r_arc * cos(ang)
        x_in = 0.650 + 0.020 * u
        x_out = 0.785 + 0.025 * sin(pi * u)
        x = s * (x_in + (x_out - x_in) * v)
        z += 0.014 * sin(pi * v)
        return (x, y, z)

    surf('Front_Wheel_Wake_Deflector', wheel_deflector_fn, 16, 6, 'Carbon', th=0.010, bev=0.0025)
    airfoil_strut('Wake_Deflector_Mount_Stalk', (side * 0.715, -1.80, 0.515), (side * 0.715, -1.80, 0.728), 0.14, 0.018, 'Carbon', up_hint=(1, 0, 0))


# ==============================================================================
# 8. MULTI-ELEMENT REAR WING, SWAN-NECK PYLON, TAIL CONE, EXHAUST & RAIN LIGHT
# ==============================================================================
def tail_cone_fn(u, v):
    y = 1.80 + 0.56 * u
    ang = (2 * v - 1) * pi
    sa, ca = sin(ang), cos(ang)
    w = 0.145 * (1.0 - 0.42 * u)
    h = 0.135 * (1.0 - 0.38 * u)
    x = w * sa
    z = (0.435 - 0.020 * u) + h * ca
    return (x, y, z)


surf('Rear_Gearbox_Carbon_Tail_Cone', tail_cone_fn, 14, 20, 'Carbon', closed_v=True)

for sx in [-0.036, 0.036]:
    pylon_pts = [
        (sx, 1.88, 0.485),
        (sx, 2.02, 0.625),
        (sx, 2.16, 0.775),
        (sx, 2.26, 0.830),
        (sx, 2.32, 0.760),
    ]
    spline_tube('Rear_Wing_Swan_Neck_Pylon', pylon_pts, 0.012, 'Carbon', res_u=10, bev_res=2)

cube('DRS_Actuator_Pod', (0.0, 2.28, 0.830), (0.048, 0.22, 0.046), 'Carbon', 0.015)
linkbar('DRS_Control_Link', (0.0, 2.37, 0.830), (0.0, 2.47, 0.850), 0.010, 0.010, 'Alloy')

for beam_idx in range(2):
    def beam_wing_fn(u, v, b=beam_idx):
        x = (2 * u - 1) * 0.455
        span_t = abs(2 * u - 1)
        y = 2.15 + b * 0.13 + (0.14 - b * 0.02) * v + 0.04 * (span_t**2)
        z = 0.435 + b * 0.045 + 0.038 * v - 0.018 * (span_t**2)
        return (x, y, z)
    surf(f'Rear_Beam_Wing_{beam_idx+1}', beam_wing_fn, 18, 4, 'Carbon', th=0.010, bev=0.002)


def rwing_main_fn(u, v):
    x = (2 * u - 1) * 0.505
    span_t = abs(2 * u - 1)
    spoon_z = 0.028 * (span_t**1.6)
    sweep_y = 0.055 * (span_t**2)
    y = 2.18 + sweep_y + 0.23 * v
    z = 0.700 + spoon_z - 0.018 * sin(pi * v) + 0.048 * (v**1.4)
    return (x, y, z)


surf('Rear_Wing_Mainplane', rwing_main_fn, 24, 7, 'Carbon', th=0.014, bev=0.003)


def rwing_drs_flap_fn(u, v):
    x = (2 * u - 1) * 0.498
    span_t = abs(2 * u - 1)
    spoon_z = 0.022 * (span_t**1.6)
    sweep_y = 0.055 * (span_t**2)
    y = 2.40 + sweep_y + 0.145 * v
    z = 0.768 + spoon_z + 0.095 * (v**1.15)
    if span_t > 0.85:
        z -= 0.022 * ((span_t - 0.85) / 0.15)**2 * v
    return (x, y, z)


surf('Rear_Wing_DRS_Flap', rwing_drs_flap_fn, 24, 6, 'Carbon', th=0.011, bev=0.0025)
linkbar('Rear_Wing_Slot_Separator', (0.0, 2.38, 0.745), (0.0, 2.41, 0.785), 0.008, 0.016, 'Alloy')

for side in [-1, 1]:
    def rwing_endplate_fn(u, v, s=side):
        z = 0.340 + 0.535 * v
        t_pinch = 1.0 / (1.0 + exp(-10.0 * (v - 0.48)))
        top_roll = -0.014 * max(0.0, (v - 0.88) / 0.12)**2
        x = s * (0.455 + 0.053 * t_pinch + top_roll)
        y_front = 2.14 + 0.06 * (1.0 - v) + 0.05 * max(0.0, (v - 0.85) / 0.15)**2
        y_rear = 2.55 - 0.08 * (1.0 - v)**1.8
        y = y_front + (y_rear - y_front) * u
        return (x, y, z)

    surf('Rear_Wing_Endplate', rwing_endplate_fn, 10, 16, 'Carbon', th=0.012, bev=0.003)

    cyl('DRS_Flap_Pivot_Pin', (side * 0.505, 2.41, 0.790), 0.012, 0.024, 'Alloy', 'X', 12)

    for k in range(3):
        v0 = 0.52 + k * 0.12
        p_a = rwing_endplate_fn(0.25, v0, side)
        p_b = rwing_endplate_fn(0.85, v0 + 0.08, side)
        line('Rear_Endplate_Upwash_Strake', [(p_a[0] + side * 0.008, p_a[1], p_a[2]), (p_b[0] + side * 0.008, p_b[1], p_b[2])], 0.0045, 'Carbon')

    led_pts = []
    for k in range(11):
        vk = 0.36 + 0.48 * k / 10
        pe = rwing_endplate_fn(1.0, vk, side)
        led_pts.append((pe[0], pe[1] + 0.006, pe[2]))
    lamp_uv(line('Rear_Endplate_LED_Tail_Strip', led_pts, 0.0075, 'Lamps'), 'blade')

# Central Exhaust Tailpipe & Twin Wastegate Pipes
exhaust_prof = [(2.15, 0.054), (2.49, 0.058), (2.51, 0.056), (2.51, 0.046), (2.18, 0.044)]
ex_vs = []
ex_fs = []
for k in range(32):
    a = 2 * pi * k / 32
    for y_e, r_e in exhaust_prof:
        ex_vs.append((r_e * cos(a), y_e, 0.415 + r_e * sin(a)))
for k in range(32):
    for j in range(len(exhaust_prof) - 1):
        a0 = k * len(exhaust_prof) + j
        b0 = ((k + 1) % 32) * len(exhaust_prof) + j
        ex_fs.append((a0, b0, b0 + 1, a0 + 1))
mesh('Central_Exhaust_Tailpipe', ex_vs, ex_fs, 'Alloy')
cyl('Exhaust_Inner_Soot', (0.0, 2.36, 0.415), 0.044, 0.02, 'Rubber', 'Y', 20, 0)
for side in [-1, 1]:
    cyl('Wastegate_Pipe_Outer', (side * 0.072, 2.39, 0.395), 0.019, 0.18, 'Brake', 'Y', 16, 0.002)
    cyl('Wastegate_Pipe_Inner', (side * 0.072, 2.47, 0.395), 0.014, 0.02, 'Rubber', 'Y', 12, 0)

# Rear Crash Structure & FIA Rain Light under Rear Wing
cube('Rear_Crash_Attenuator_Box', (0.0, 2.34, 0.255), (0.105, 0.42, 0.165), 'Carbon', 0.014)
cube('Rain_Light_Carbon_Bezel', (0.0, 2.545, 0.255), (0.086, 0.028, 0.138), 'Carbon', 0.008)
rain_lens = cube('Rain_Light_Emissive_Matrix', (0.0, 2.556, 0.255), (0.064, 0.012, 0.114), 'Lamps', 0.004)
lamp_uv(rain_lens, 'grid')
for rx in [-0.018, 0.0, 0.018]:
    for rz in [-0.040, -0.020, 0.0, 0.020, 0.040]:
        led_dome = cyl('Rain_Light_LED_Dome', (rx, 2.562, 0.255 + rz), 0.0065, 0.006, 'Lamps', 'Y', 10, 0.001)
        lamp_uv(led_dome, 'blade')


# ==============================================================================
# 9. PUSH-ROD SUSPENSION ARMS, UPRIGHTS, BRAKE DUCTS & CALIPERS (FRONT & REAR)
# ==============================================================================
for side in [-1, 1]:
    # --- FRONT PUSH-ROD SUSPENSION (Axle at y = -1.80) ---
    p_fu_hub = (side * 0.725, -1.80, 0.485)
    p_fu_fwd = (side * 0.205, -2.06, 0.495)
    p_fu_aft = (side * 0.235, -1.48, 0.485)
    airfoil_strut('Front_Upper_Wishbone_Fwd', p_fu_fwd, p_fu_hub, 0.065, 0.014, 'Carbon')
    airfoil_strut('Front_Upper_Wishbone_Aft', p_fu_aft, p_fu_hub, 0.065, 0.014, 'Carbon')
    cyl('Front_Upper_Balljoint', p_fu_hub, 0.018, 0.024, 'Alloy', 'Z', 12)

    p_fl_hub = (side * 0.735, -1.80, 0.225)
    p_fl_fwd = (side * 0.195, -2.02, 0.225)
    p_fl_aft = (side * 0.245, -1.42, 0.215)
    airfoil_strut('Front_Lower_Wishbone_Fwd', p_fl_fwd, p_fl_hub, 0.072, 0.015, 'Carbon')
    airfoil_strut('Front_Lower_Wishbone_Aft', p_fl_aft, p_fl_hub, 0.072, 0.015, 'Carbon')
    cyl('Front_Lower_Balljoint', p_fl_hub, 0.018, 0.024, 'Alloy', 'Z', 12)

    # Diagonal Front PUSH-ROD
    p_f_push_low = (side * 0.705, -1.80, 0.245)
    p_f_push_high = (side * 0.205, -1.82, 0.545)
    airfoil_strut('Front_Push_Rod_Strut', p_f_push_low, p_f_push_high, 0.040, 0.016, 'Carbon')
    linkbar('Front_Push_Rod_End_Top', p_f_push_high, (side * 0.265, -1.82, 0.508), 0.020, 0.020, 'Alloy')
    linkbar('Front_Push_Rod_End_Bot', p_f_push_low, (side * 0.655, -1.80, 0.278), 0.020, 0.020, 'Alloy')

    airfoil_strut('Front_Steering_Tie_Rod', (side * 0.215, -1.91, 0.365), (side * 0.715, -1.89, 0.365), 0.048, 0.013, 'Carbon')

    cyl('Front_Brake_Cooling_Drum', (side * 0.765, -1.80, 0.360), 0.205, 0.120, 'Carbon', 'X', 32, 0.004)
    cube('Front_Brake_Inlet_Scoop', (side * 0.705, -1.92, 0.360), (0.048, 0.14, 0.19), 'Carbon', 0.010)
    cube('Front_Brake_Scoop_Opening', (side * 0.705, -1.985, 0.360), (0.032, 0.02, 0.15), 'Rubber', 0.004)
    cube('Front_Brake_Caliper', (side * 0.815, -1.645, 0.360), (0.068, 0.085, 0.175), 'Brake', 0.014)
    for z_b in [-0.05, 0.0, 0.05]:
        cyl('Front_Caliper_Bridge_Bolt', (side * 0.815, -1.605, 0.360 + z_b), 0.008, 0.065, 'Alloy', 'X', 10)

    # --- REAR PUSH-ROD SUSPENSION (Axle at y = +1.80) ---
    p_ru_hub = (side * 0.675, 1.80, 0.485)
    p_ru_fwd = (side * 0.185, 1.40, 0.475)
    p_ru_aft = (side * 0.135, 1.98, 0.465)
    airfoil_strut('Rear_Upper_Wishbone_Fwd', p_ru_fwd, p_ru_hub, 0.068, 0.015, 'Carbon')
    airfoil_strut('Rear_Upper_Wishbone_Aft', p_ru_aft, p_ru_hub, 0.068, 0.015, 'Carbon')
    cyl('Rear_Upper_Balljoint', p_ru_hub, 0.018, 0.024, 'Alloy', 'Z', 12)

    p_rl_hub = (side * 0.685, 1.80, 0.220)
    p_rl_fwd = (side * 0.195, 1.44, 0.215)
    p_rl_aft = (side * 0.135, 2.00, 0.215)
    airfoil_strut('Rear_Lower_Wishbone_Fwd', p_rl_fwd, p_rl_hub, 0.075, 0.016, 'Carbon')
    airfoil_strut('Rear_Lower_Wishbone_Aft', p_rl_aft, p_rl_hub, 0.075, 0.016, 'Carbon')
    cyl('Rear_Lower_Balljoint', p_rl_hub, 0.018, 0.024, 'Alloy', 'Z', 12)

    # Diagonal Rear PUSH-ROD
    p_r_push_low = (side * 0.655, 1.80, 0.245)
    p_r_push_high = (side * 0.165, 1.48, 0.545)
    airfoil_strut('Rear_Push_Rod_Strut', p_r_push_low, p_r_push_high, 0.042, 0.016, 'Carbon')
    linkbar('Rear_Push_Rod_End_Top', p_r_push_high, (side * 0.225, 1.52, 0.508), 0.020, 0.020, 'Alloy')
    linkbar('Rear_Push_Rod_End_Bot', p_r_push_low, (side * 0.605, 1.76, 0.278), 0.020, 0.020, 'Alloy')

    linkbar('Rear_Driveshaft_Axle', (side * 0.155, 1.80, 0.345), (side * 0.685, 1.80, 0.360), 0.032, 0.032, 'Brake')
    cyl('Rear_CV_Boot_Inboard', (side * 0.195, 1.80, 0.348), 0.030, 0.055, 'Rubber', 'X', 14)
    cyl('Rear_CV_Boot_Outboard', (side * 0.645, 1.80, 0.358), 0.030, 0.055, 'Rubber', 'X', 14)
    airfoil_strut('Rear_Toe_Control_Link', (side * 0.145, 1.92, 0.335), (side * 0.675, 1.89, 0.345), 0.048, 0.013, 'Carbon')

    cyl('Rear_Brake_Cooling_Drum', (side * 0.725, 1.80, 0.360), 0.205, 0.120, 'Carbon', 'X', 32, 0.004)
    cube('Rear_Brake_Inlet_Scoop', (side * 0.645, 1.68, 0.360), (0.052, 0.14, 0.18), 'Carbon', 0.010)
    for wz in [0.24, 0.30, 0.36]:
        cube('Rear_Brake_Duct_Winglet', (side * 0.635, 1.88, wz), (0.085, 0.11, 0.008), 'Carbon', 0.002)
    cube('Rear_Brake_Caliper', (side * 0.765, 1.645, 0.360), (0.068, 0.085, 0.175), 'Brake', 0.014)
    for z_b in [-0.05, 0.0, 0.05]:
        cyl('Rear_Caliper_Bridge_Bolt', (side * 0.765, 1.605, 0.360 + z_b), 0.008, 0.065, 'Alloy', 'X', 10)


# ==============================================================================
# 10. EXPOSED WHEELS, SLICK TYRES, AERO RIMS & BRAKE DISCS (Wheel_FL..Wheel_RR)
# ==============================================================================
def lathe_x(name, xc, yc, zc, profile, mat, n=56):
    vs = []
    fs = []
    uv = []
    for i in range(n):
        a = 2 * pi * i / n
        sa, ca = sin(a), cos(a)
        for j, (dx, r) in enumerate(profile):
            vs.append((xc + dx, yc + r * sa, zc + r * ca))
            uv.append((i / n, j / max(1, len(profile) - 1)))
    m = len(profile)
    for i in range(n):
        i1 = (i + 1) % n
        for j in range(m - 1):
            a0 = i * m + j
            b0 = i1 * m + j
            fs.append((a0, b0, b0 + 1, a0 + 1))
    return mesh(name, vs, fs, mat, uv)


for front, yc, half_w, xc_abs in [(True, -1.80, 0.165, 0.835), (False, 1.80, 0.205, 0.795)]:
    for side in [-1, 1]:
        current = 'Wheel_' + ('F' if front else 'R') + ('L' if side == 1 else 'R')
        xc = side * xc_abs
        zc = 0.360
        hw = half_w

        tyre_prof = [
            (-hw * 0.88, 0.232),
            (-hw * 0.96, 0.252),
            (-hw * 1.00, 0.285),
            (-hw * 0.98, 0.322),
            (-hw * 0.90, 0.346),
            (-hw * 0.76, 0.357),
            (-hw * 0.55, 0.360),
            (0.0,        0.360),
            (hw * 0.55,  0.360),
            (hw * 0.76,  0.357),
            (hw * 0.90,  0.346),
            (hw * 0.98,  0.322),
            (hw * 1.00,  0.285),
            (hw * 0.96,  0.252),
            (hw * 0.88,  0.232),
        ]
        lathe_x('Formula_Slick_Tyre', xc, yc, zc, tyre_prof, 'Rubber', 56)

        for s_face in [-1, 1]:
            for r_ring in [0.268, 0.308]:
                lathe_x('Sidewall_Mould_Ring', xc, yc, zc, [
                    (s_face * (hw * 0.992), r_ring - 0.002),
                    (s_face * (hw * 1.003), r_ring),
                    (s_face * (hw * 0.992), r_ring + 0.002),
                ], 'Rubber', 40)

        outer_x = side * (hw * 0.90)
        inner_x = -side * (hw * 0.88)
        lathe_x('Rim_Outer_Rolled_Flange', xc, yc, zc, [
            (outer_x - side * 0.018, 0.224),
            (outer_x - side * 0.004, 0.232),
            (outer_x + side * 0.004, 0.239),
            (outer_x - side * 0.003, 0.243),
            (outer_x - side * 0.014, 0.238),
        ], 'Alloy', 48)
        lathe_x('Rim_Inner_Barrel', xc, yc, zc, [
            (inner_x, 0.234),
            (inner_x + side * 0.02, 0.222),
            (outer_x - side * 0.018, 0.224),
        ], 'Carbon', 40)

        dish_prof = [
            (outer_x - side * 0.006, 0.228),
            (outer_x + side * 0.002, 0.205),
            (outer_x - side * 0.008, 0.192),
            (outer_x - side * 0.004, 0.145),
            (outer_x - side * 0.016, 0.118),
            (outer_x - side * 0.014, 0.068),
        ]
        lathe_x('Aero_Wheel_Cover_Dish', xc, yc, zc, dish_prof, 'Carbon', 48)
        lathe_x('Wheel_Cover_Step_Ring', xc, yc, zc, [
            (outer_x - side * 0.003, 0.142),
            (outer_x + side * 0.001, 0.147),
            (outer_x - side * 0.003, 0.152),
        ], 'Brake', 40)

        for k in range(8):
            a = 2 * pi * k / 8
            p0 = (xc + outer_x - side * 0.010, yc + 0.072 * sin(a), zc + 0.072 * cos(a))
            p1 = (xc + outer_x - side * 0.004, yc + 0.195 * sin(a), zc + 0.195 * cos(a))
            linkbar('Wheel_Cover_Aero_Rib', p0, p1, 0.010, 0.014, 'Carbon')

        cyl('Wheel_Center_Hub_Barrel', (xc + outer_x - side * 0.014, yc, zc), 0.068, 0.024, 'Brake', 'X', 28)
        cyl('Center_Lock_Flange', (xc + outer_x - side * 0.003, yc, zc), 0.048, 0.018, 'Alloy', 'X', 24)
        cyl('Center_Lock_Nut_Cone', (xc + outer_x + side * 0.005, yc, zc), 0.035, 0.020, 'Brake', 'X', 12)
        cyl('Center_Lock_Spindle_Bore', (xc + outer_x + side * 0.012, yc, zc), 0.020, 0.010, 'Alloy', 'X', 16)
        for k in range(6):
            a = 2 * pi * k / 6
            cube('Center_Lock_Grip_Rib', (xc + outer_x + side * 0.004, yc + 0.038 * sin(a), zc + 0.038 * cos(a)), (0.016, 0.007, 0.010), 'Alloy', 0.002)

        disc_x = xc - side * (hw * 0.18)
        cyl('Carbon_Ceramic_Brake_Disc', (disc_x, yc, zc), 0.188, 0.026, 'Brake', 'X', 36, 0.003)
        cyl('Brake_Disc_Mounting_Bell', (disc_x + side * 0.012, yc, zc), 0.112, 0.022, 'Alloy', 'X', 24, 0.002)
        for k in range(8):
            a = 2 * pi * k / 8
            cyl('Brake_Bell_Bobbins', (disc_x + side * 0.022, yc + 0.106 * sin(a), zc + 0.106 * cos(a)), 0.006, 0.008, 'Alloy', 'X', 8, 0.001)

current = 'Body'


# ==============================================================================
# 11. FORMULA BUTTERFLY STEERING WHEEL (SteeringWheel node, origin at hub!)
# ==============================================================================
hub = Vector((0.0, -0.495, 0.555))
linkbar('Steering_Column_Shaft', (0.0, -0.68, 0.510), hub, 0.028, 0.028, 'Carbon')
cyl('Steering_Quick_Release_Collar', (0.0, -0.525, 0.548), 0.024, 0.045, 'Alloy', 'Y', 16)

current = 'SteeringWheel'
tilt = -0.26


def sw_pt(dx, dy, dz):
    ry = dy * cos(tilt) - dz * sin(tilt)
    rz = dy * sin(tilt) + dz * cos(tilt)
    return (hub.x + dx, hub.y + ry, hub.z + rz)


sw_plate = cube('Steering_Carbon_Faceplate', sw_pt(0.0, 0.0, 0.0), (0.255, 0.026, 0.145), 'Carbon', 0.012)
sw_plate.rotation_euler[0] = tilt
sw_scr = cube('Steering_Telemetry_Display', sw_pt(0.0, 0.013, 0.018), (0.115, 0.008, 0.064), 'Trim', 0.004)
sw_scr.rotation_euler[0] = tilt
sw_bezel = cube('Steering_Display_Bezel', sw_pt(0.0, 0.010, 0.018), (0.128, 0.010, 0.074), 'Carbon', 0.005)
sw_bezel.rotation_euler[0] = tilt

sw_leds = cube('Steering_RPM_Shift_LEDs', sw_pt(0.0, 0.013, 0.062), (0.110, 0.008, 0.010), 'Lamps', 0.002)
sw_leds.rotation_euler[0] = tilt
lamp_uv(sw_leds, 'blade')

for side in [-1, 1]:
    grip_pts = [
        sw_pt(side * 0.088, 0.002, 0.068),
        sw_pt(side * 0.132, 0.005, 0.052),
        sw_pt(side * 0.142, 0.008, 0.005),
        sw_pt(side * 0.135, 0.005, -0.052),
        sw_pt(side * 0.085, 0.002, -0.068),
    ]
    spline_tube('Steering_Butterfly_Grip', grip_pts, 0.018, 'Interior', cyclic=False, res_u=8, bev_res=2)
    thumb_pts = [
        sw_pt(side * 0.128, 0.005, 0.022),
        sw_pt(side * 0.095, 0.004, 0.018),
        sw_pt(side * 0.085, 0.002, 0.055),
    ]
    spline_tube('Steering_Thumb_Hook', thumb_pts, 0.011, 'Carbon', cyclic=False, res_u=6, bev_res=1)
    pad = cube('Steering_Shift_Paddle', sw_pt(side * 0.105, -0.026, 0.015), (0.048, 0.008, 0.078), 'Alloy', 0.003)
    pad.rotation_euler[0] = tilt

for rx, rz in [(-0.048, -0.028), (0.0, -0.036), (0.048, -0.028), (-0.078, 0.032), (0.078, 0.032)]:
    enc = cyl('Steering_Rotary_Dial', sw_pt(rx, 0.016, rz), 0.013, 0.014, 'Alloy', 'Y', 12, 0.002)
    enc.rotation_euler[0] = tilt

current = 'Body'
print('GEOMETRY_COMPLETE', sum(len(a) for a in groups.values()), flush=True)


# ==============================================================================
# 12. CONSOLIDATE MESH NODES, SET EXACT PIVOTS, FIT EXACT BBOX & EXPORT GLB
# ==============================================================================
final = []
for name, objects in groups.items():
    for o in objects:
        activate(o)
        for md in list(o.modifiers):
            try:
                bpy.ops.object.modifier_apply(modifier=md.name)
            except Exception as e:
                print('MODIFIER', o.name, str(e))
        bpy.ops.object.transform_apply(location=False, rotation=True, scale=True)
    bpy.ops.object.select_all(action='DESELECT')
    for o in objects:
        o.select_set(True)
    bpy.context.view_layer.objects.active = objects[0]
    bpy.ops.object.join()
    o = bpy.context.object
    o.name = name

    if name.startswith('Wheel_'):
        is_front = (name[6] == 'F')
        yc = -1.80 if is_front else 1.80
        xc_abs = 0.835 if is_front else 0.795
        xc = xc_abs if name.endswith('L') else -xc_abs
        origin = (xc, yc, 0.360)
    elif name == 'SteeringWheel':
        origin = tuple(hub)
    else:
        origin = (0.0, 0.0, 0.0)

    S.cursor.location = origin
    activate(o)
    bpy.ops.object.origin_set(type='ORIGIN_CURSOR')

    bpy.ops.object.mode_set(mode='EDIT')
    bpy.ops.mesh.select_all(action='SELECT')
    bpy.ops.mesh.normals_make_consistent(inside=False)
    bpy.ops.object.mode_set(mode='OBJECT')
    final.append(o)

print('ASSEMBLY_COMPLETE', flush=True)

# Triangulate and remove zero-area slivers
for o in final:
    bm = bmesh.new()
    bm.from_mesh(o.data)
    bmesh.ops.triangulate(bm, faces=list(bm.faces))
    bad = [f for f in bm.faces if f.calc_area() < 1e-12]
    if bad:
        bmesh.ops.delete(bm, geom=bad, context='FACES_ONLY')
    bm.to_mesh(o.data)
    bm.free()
    o.data.update()

raw_tris = sum(len(o.data.polygons) for o in final)
wheel_tris = sum(len(o.data.polygons) for o in final if o.name != 'Body')
print('RAW_TRIANGLES', raw_tris, 'MOVING_PARTS', wheel_tris, flush=True)

# Calibrate exact outer bounding box to 5.60 m long ([-3.00, +2.60]), 2.00 m wide ([-1.00, +1.00]), 0.95 m high ([0.00, +0.95])
worldverts = [o.matrix_world @ v.co for o in final for v in o.data.vertices]
ymin = min(v.y for v in worldverts)
ymax = max(v.y for v in worldverts)
xmax = max(abs(v.x) for v in worldverts)
zmax = max(v.z for v in worldverts)


def fit_y(y):
    if y < -1.80:
        return -1.80 + (y - (-1.80)) * (-1.20 / (ymin - (-1.80)))
    elif y > 1.80:
        return 1.80 + (y - 1.80) * (0.80 / (ymax - 1.80))
    return y


for o in final:
    inv = o.matrix_world.inverted()
    is_wheel = o.name.startswith('Wheel_')
    for v in o.data.vertices:
        w = o.matrix_world @ v.co
        if not is_wheel:
            w.y = fit_y(w.y)
        w.x = w.x * (1.000 / xmax)
        if not is_wheel:
            w.z = max(0.002, w.z * (0.950 / zmax))
        else:
            w.z = max(0.000, w.z)
        v.co = inv @ w
    o.data.update()

# Empty gameplay sockets mandated by contract
sockets = {
    'Seat_Driver':  (0.0,   -0.22,  0.240),
    'Door_L':       (0.52,  -0.30,  0.640),
    'Door_R':       (-0.52, -0.30,  0.640),
    'Exhaust':      (0.0,    2.51,  0.415),
    'Light_Head_L': (0.55,  -2.62,  0.180),
    'Light_Head_R': (-0.55, -2.62,  0.180),
    'Light_Tail_L': (0.505,  2.55,  0.620),
    'Light_Tail_R': (-0.505, 2.55,  0.620),
}
for name, pos in sockets.items():
    o = bpy.data.objects.new(name, None)
    S.collection.objects.link(o)
    o.location = pos
    final.append(o)


def gl(v):
    return [round(v[0], 6), round(v[2], 6), round(-v[1], 6)]


verts = [o.matrix_world @ v.co for o in final if o.type == 'MESH' for v in o.data.vertices]
bbox = {
    'min': [min(gl(v)[i] for v in verts) for i in range(3)],
    'max': [max(gl(v)[i] for v in verts) for i in range(3)],
}
tris = sum(len(p.vertices) - 2 for o in final if o.type == 'MESH' for p in o.data.polygons)
manifest = {
    'contract': 'Forge Garage v2',
    'units': 'metres',
    'axes': {'front': '+Z', 'up': '+Y', 'left': '+X'},
    'nodes': {o.name: {'position': gl(o.location), 'type': o.type} for o in final},
    'bounding_box': bbox,
    'triangles': tris,
    'wheelbase': 3.60,
    'paints': [{'name': n, 'linear_rgb': c} for n, c in PAINTS],
}
with open('nodes.json', 'w') as f:
    json.dump(manifest, f, indent=2)

bpy.ops.object.select_all(action='DESELECT')
for o in final:
    o.select_set(True)

glb_path = os.path.join(ROOT, 'formula-racer.glb')
bpy.ops.export_scene.gltf(
    filepath=glb_path,
    export_format='GLB',
    use_selection=True,
    export_yup=True,
    export_apply=True,
    export_materials='EXPORT',
    export_image_format='AUTO',
    export_texcoords=True,
    export_normals=True,
    export_tangents=True,
    export_cameras=False,
    export_lights=False,
)

# Patch glTF JSON chunk to guarantee Paint baseColorFactor = [0.75, 0.05, 0.02, 1.0] and KHR_materials_clearcoat
blob = open(glb_path, 'rb').read()
jl = struct.unpack_from('<I', blob, 12)[0]
doc = json.loads(blob[20:20 + jl])
binary = blob[20 + jl:]
ext_used = doc.setdefault('extensionsUsed', [])
if 'KHR_materials_clearcoat' not in ext_used:
    ext_used.append('KHR_materials_clearcoat')
for mat in doc.get('materials', []):
    if mat.get('name') == 'Paint':
        mat.setdefault('pbrMetallicRoughness', {})['baseColorFactor'] = PAINTS[0][1] + [1.0]
        mat.setdefault('extensions', {})['KHR_materials_clearcoat'] = {
            'clearcoatFactor': 1.0,
            'clearcoatRoughnessFactor': 0.05,
        }
js = json.dumps(doc, separators=(',', ':')).encode()
js += b' ' * ((-len(js)) % 4)
open(glb_path, 'wb').write(
    struct.pack('<III', 0x46546C67, 2, 12 + 8 + len(js) + len(binary))
    + struct.pack('<II', len(js), 0x4E4F534A)
    + js
    + binary
)
glb_mb = round(os.path.getsize(glb_path) / 1048576, 2)
print('EXPORT_READY', tris, os.path.getsize(glb_path), bbox, flush=True)


# ==============================================================================
# 13. STUDIO DAYLIGHT EVIDENCE RENDERS (final.png, side.png, clay.png, paints.png)
# ==============================================================================
p_paint = M['Paint'].node_tree.nodes.get('Principled BSDF')
l_paint = M['Paint'].node_tree.links
src_sock = p_paint.inputs['Base Color'].links[0].from_socket
mix_paint = M['Paint'].node_tree.nodes.new('ShaderNodeMixRGB')
mix_paint.blend_type = 'MULTIPLY'
mix_paint.inputs[0].default_value = 1.0
mix_paint.inputs[2].default_value = (*PAINTS[0][1], 1.0)
l_paint.new(src_sock, mix_paint.inputs[1])
l_paint.new(mix_paint.outputs[0], p_paint.inputs['Base Color'])

world = bpy.data.worlds.new('SoftDaylight')
world.use_nodes = True
world.node_tree.nodes['Background'].inputs[0].default_value = (0.68, 0.74, 0.84, 1.0)
world.node_tree.nodes['Background'].inputs[1].default_value = 0.55
S.world = world

floor = bpy.data.materials.new('StudioTrackFloor')
floor.use_nodes = True
pp = floor.node_tree.nodes.get('Principled BSDF')
pp.inputs['Base Color'].default_value = (0.20, 0.22, 0.24, 1.0)
pp.inputs['Roughness'].default_value = 0.82
bpy.ops.mesh.primitive_plane_add(size=2000, location=(0, 0, -0.002))
ground = bpy.context.object
ground.name = 'RenderGround'
ground.data.materials.append(floor)


def area(name, loc, power, size, target, shape='DISK', size_y=None):
    data = bpy.data.lights.new(name, 'AREA')
    data.energy = power
    data.shape = shape
    data.size = size
    if size_y:
        data.size_y = size_y
    ob = bpy.data.objects.new(name, data)
    S.collection.objects.link(ob)
    ob.location = loc
    ob.rotation_euler = (Vector(target) - ob.location).to_track_quat('-Z', 'Y').to_euler()


area('SkyKey', (2.5, -4.5, 6.5), 480, 6.0, (0, -0.2, 0.35))
area('SideReflectionStrip', (-4.8, -0.5, 2.8), 340, 6.5, (0, 0, 0.45), 'RECTANGLE', 1.8)
area('LeftReflectionStrip', (4.8, -0.5, 2.8), 320, 6.5, (0, 0, 0.45), 'RECTANGLE', 1.8)
area('RearRimLight', (2.8, 5.2, 3.8), 360, 4.5, (0, 0.4, 0.45))
area('FrontFill', (-2.5, -5.5, 2.2), 180, 3.5, (0, -1.5, 0.30))

cam_data = bpy.data.cameras.new('Camera')
cam = bpy.data.objects.new('Camera', cam_data)
S.collection.objects.link(cam)
S.camera = cam
cam_data.clip_end = 5000


def camera(pos, target, lens=48, ortho=None):
    cam.location = pos
    cam.rotation_euler = (Vector(target) - cam.location).to_track_quat('-Z', 'Y').to_euler()
    cam_data.type = 'ORTHO' if ortho else 'PERSP'
    cam_data.lens = lens
    if ortho:
        cam_data.ortho_scale = ortho


def render(name, w, h, samples=28):
    S.render.resolution_x = w
    S.render.resolution_y = h
    S.render.resolution_percentage = 100
    S.cycles.samples = samples
    S.render.filepath = os.path.join(ROOT, name)
    bpy.ops.render.render(write_still=True)
    print('RENDERED', name, flush=True)


# 1. final.png (1920x1080 Cycles render, 3/4 front hero view matching sheet.jpg, generous margin around full front wing)
hero_pos = (-4.85, -5.85, 1.92)
hero_tgt = (0.0, -0.45, 0.36)
camera(hero_pos, hero_tgt, 48)
render('final.png', 1920, 1080, 40)

# 2. clay.png (same view as final, plain grey material)
clay = bpy.data.materials.new('ClayDiagnostic')
clay.use_nodes = True
clay.node_tree.nodes.get('Principled BSDF').inputs['Base Color'].default_value = (0.44, 0.44, 0.44, 1.0)
clay.node_tree.nodes.get('Principled BSDF').inputs['Roughness'].default_value = 0.62
S.view_layers[0].material_override = clay
render('clay.png', 1920, 1080, 28)
S.view_layers[0].material_override = None

# 3. side.png (orthographic side view matching sheet.jpg orientation: nose pointing LEFT!)
camera((8.0, -0.20, 0.475), (0.0, -0.20, 0.475), ortho=6.15)
render('side.png', 1920, 960, 28)

# 4. paints.png (3x2 grid of the same 3/4 view in all six paints)
camera(hero_pos, hero_tgt, 48)
frames = []
for name, col in PAINTS:
    mix_paint.inputs[2].default_value = (*col, 1.0)
    fn = f'paint_{name}.png'
    render(fn, 640, 360, 16)
    im = bpy.data.images.load(os.path.join(ROOT, fn))
    frames.append(list(im.pixels))
    bpy.data.images.remove(im)

out = bpy.data.images.new('SixPaintVariants', width=1920, height=720, alpha=True)
pix = [0.0] * (1920 * 720 * 4)
for k, buf in enumerate(frames):
    col_idx = k % 3
    row_idx = 1 - k // 3
    for y in range(360):
        start = ((row_idx * 360 + y) * 1920 + col_idx * 640) * 4
        pix[start:start + 640 * 4] = buf[y * 640 * 4:(y + 1) * 640 * 4]
out.pixels.foreach_set(pix)
out.filepath_raw = os.path.join(ROOT, 'paints.png')
out.file_format = 'PNG'
out.save()
mix_paint.inputs[2].default_value = (*PAINTS[0][1], 1.0)

# 5. Write exact 6-line NOTES.md
with open('NOTES.md', 'w') as f:
    f.write('Modern open-wheel ground-effect formula race car (5.60 m x 2.00 m x 0.95 m, 3.60 m wheelbase) with open cockpit, safety halo, multi-element wings, sidepods, flat floor, diffuser, push-rod suspension and rain light.\n')
    f.write(f'{tris:,} triangles across 6 mesh nodes (Body, Wheel_FL, Wheel_FR, Wheel_RL, Wheel_RR, SteeringWheel) and 9 empty gameplay sockets.\n')
    f.write('8 Cycles-baked PBR materials: Paint (clearcoat 1.0, neutral detail + 6 linear RGB variants), Carbon, Rubber, Alloy, Brake, Trim, Interior, Lamps.\n')
    f.write(f'Texture sizes: Paint Base 1024 px, other Base/Normal maps 512 px (Lamps Base 128 px), ORM maps 256 px; exported formula-racer.glb is {glb_mb} MB.\n')
    f.write('Poly Haven CC0 texture IDs used: metal_plate, fabric_leather_01, rubber_tiles (https://polyhaven.com/license).\n')
    f.write('Differences from reference: number panels and tyres are left completely blank (no digits or text per race car contract), and default Paint is Scuderia Red [0.75, 0.05, 0.02] with midnight blue included in paints.png.\n')

print('BUILD_COMPLETE', flush=True)
