"""Forge American Oval-Track Stock Car (G4 v1)
Production realistic 3D game asset in Blender 5.2.
"""
import bpy, bmesh, math, os, json, struct, subprocess, sys
from mathutils import Vector, Matrix
from math import sin, cos, pi, sqrt, exp, atan2

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 = 32
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 real paint colours (default first: [0.02, 0.10, 0.45] deep blue)
PAINTS = [
    ('DaytonaBlue',       [0.02, 0.10, 0.45]),
    ('VictoryRed',        [0.58, 0.02, 0.02]),
    ('SunocoYellow',      [0.82, 0.52, 0.01]),
    ('ChampionshipWhite', [0.78, 0.80, 0.78]),
    ('MidnightBlack',     [0.025, 0.028, 0.032]),
    ('PettyBlue',         [0.04, 0.35, 0.68]),
]

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):
    if o.data.materials:
        o.data.materials[0] = M[mat] if isinstance(mat, str) else mat
    else:
        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
            if uvs:
                uv.data[li].uv = uvs[idx]
            else:
                uv.data[li].uv = (((v.y * 0.35 + v.x * 0.25) % 0.65), ((v.z * 0.55 + abs(v.x) * 0.25) % 0.65))
    return add(o, mat)

def set_uv_box(o, u0, v0, u1, v1):
    if not o.data.uv_layers:
        o.data.uv_layers.new(name='UVMap')
    uv = o.data.uv_layers.active
    verts = [v.co for v in o.data.vertices]
    if not verts:
        return o
    xs = [v.x for v in verts]
    ys = [v.y for v in verts]
    zs = [v.z for v in verts]
    dx = max(1e-4, max(xs) - min(xs))
    dy = max(1e-4, max(ys) - min(ys))
    dz = max(1e-4, max(zs) - min(zs))
    for p in o.data.polygons:
        for li in p.loop_indices:
            v = o.data.vertices[o.data.loops[li].vertex_index].co
            tx = (v.x - min(xs)) / dx if dx >= dy else (v.y - min(ys)) / dy
            tz = (v.z - min(zs)) / dz
            uv.data[li].uv = (u0 + tx * (u1 - u0), v0 + tz * (v1 - v0))
    return o

def bevel(o, w=0.008, seg=2):
    md = o.modifiers.new('Bevel', 'BEVEL')
    md.width = w
    md.segments = seg
    md.limit_method = 'ANGLE'
    md.angle_limit = math.radians(30)
    return o

def cube(name, loc, scale, mat, r=0.008, seg=2):
    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 mat == 'Interior':
        set_uv_box(o, 0.05, 0.05, 0.60, 0.60)
    if r > 0:
        bevel(o, r, seg)
        md = o.modifiers.new('WN', 'WEIGHTED_NORMAL')
        md.keep_sharp = True
    return o

def cyl(name, loc, r, depth, mat, axis='X', n=32, bev=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
    activate(o)
    bpy.ops.object.transform_apply(location=False, rotation=True, scale=True)
    add(o, mat)
    if mat == 'Interior':
        set_uv_box(o, 0.05, 0.05, 0.60, 0.60)
    if bev > 0:
        bevel(o, bev, 1)
    return o

def line(name, pts, r, mat, cyclic=False, res=1, u_res=8):
    cu = bpy.data.curves.new(name, 'CURVE')
    cu.dimensions = '3D'
    cu.resolution_u = u_res
    cu.bevel_depth = r
    cu.bevel_resolution = res
    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')
    o = bpy.context.object
    if not o.data.uv_layers:
        o.data.uv_layers.new(name='UVMap')
    set_uv_box(o, 0.05, 0.05, 0.60, 0.60)
    return add(o, mat)

def linkbar(name, a, b, width, depth, mat, r=0.003):
    va, vb = Vector(a), Vector(b)
    mid = (va + vb) * 0.5
    length = (vb - va).length
    if length < 1e-5:
        return None
    o = cube(name, mid, (width, depth, length), mat, min(width, depth) * 0.25 if r else 0, 1)
    o.rotation_euler = (vb - va).to_track_quat('Z', 'Y').to_euler()
    return o

def tube_seg(name, a, b, r, mat, n=12):
    va, vb = Vector(a), Vector(b)
    mid = (va + vb) * 0.5
    length = (vb - va).length
    if length < 1e-5:
        return None
    bpy.ops.mesh.primitive_cylinder_add(vertices=n, radius=r, depth=length, location=mid)
    o = bpy.context.object
    o.name = name
    o.rotation_euler = (vb - va).to_track_quat('Z', 'Y').to_euler()
    activate(o)
    bpy.ops.object.transform_apply(location=False, rotation=True, scale=True)
    if mat == 'Interior':
        set_uv_box(o, 0.05, 0.05, 0.60, 0.60)
    return add(o, mat)

def surf(name, fn, nu, nv, mat, subsurf=0, solidify=0.0):
    vs, uv, fs = [], [], []
    u_max = 0.60 if mat == 'Interior' else 1.0
    for i in range(nu + 1):
        for j in range(nv + 1):
            p = fn(i / nu, j / nv)
            vs.append(p)
            uv.append((u_max * i / nu, u_max * j / nv))
    for i in range(nu):
        for j in range(nv):
            a = i * (nv + 1) + j
            fs.append((a, a + nv + 1, a + nv + 2, a + 1))
    o = mesh(name, vs, fs, mat, uv)
    if subsurf > 0:
        md = o.modifiers.new('Subsurf', 'SUBSURF')
        md.levels = subsurf
        md.render_levels = subsurf
    if solidify != 0.0:
        md = o.modifiers.new('Solidify', 'SOLIDIFY')
        md.thickness = abs(solidify)
        md.offset = -1 if solidify < 0 else 1
    return o

# Download / load CC0 Poly Haven scans
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', '2', '-A', 'Mozilla/5.0', url, '-o', p], check=True)
    return bpy.data.images.load(os.path.abspath(p), check_existing=True)

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

# Material builder & Cycles baker
def build_source_mat(name, color, metal, rough, asset, grain, tile=3.0):
    m = bpy.data.materials.new(name + '_src')
    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

    uv = N.new('ShaderNodeTexCoord')
    mp = N.new('ShaderNodeMapping')
    mp.inputs['Scale'].default_value = (tile, tile, 1)
    L.new(uv.outputs['UV'], mp.inputs['Vector'])

    tex = N.new('ShaderNodeTexImage')
    tex.image = SCAN[asset, 'diff']
    L.new(mp.outputs['Vector'], tex.inputs['Vector'])

    bw = N.new('ShaderNodeRGBToBW')
    L.new(tex.outputs['Color'], bw.inputs[0])

    ramp = N.new('ShaderNodeValToRGB')
    ramp.color_ramp.elements[0].position = 0.05
    ramp.color_ramp.elements[1].position = 0.85
    if name == 'Paint':
        ramp.color_ramp.elements[0].color = (0.85, 0.85, 0.86, 1)
        ramp.color_ramp.elements[1].color = (0.98, 0.98, 0.98, 1)
    elif name == 'Carbon':
        ramp.color_ramp.elements[0].color = (0.015, 0.017, 0.020, 1)
        ramp.color_ramp.elements[1].color = (0.065, 0.070, 0.078, 1)
    else:
        c0 = tuple(v * 0.72 for v in color) + (1,)
        c1 = tuple(min(1.0, v * 1.12) for v in color) + (1,)
        ramp.color_ramp.elements[0].color = c0
        ramp.color_ramp.elements[1].color = c1

    L.new(bw.outputs[0], ramp.inputs[0])
    base_out = ramp.outputs[0]

    noise = N.new('ShaderNodeTexNoise')
    noise.inputs['Scale'].default_value = 42.0
    noise.inputs['Detail'].default_value = 6.0
    noise.inputs['Roughness'].default_value = 0.68
    L.new(uv.outputs['UV'], noise.inputs['Vector'])

    if name == 'Paint':
        sep_uv = N.new('ShaderNodeSeparateXYZ')
        L.new(uv.outputs['UV'], sep_uv.inputs[0])
        grime_ramp = N.new('ShaderNodeValToRGB')
        grime_ramp.color_ramp.elements[0].position = 0.0
        grime_ramp.color_ramp.elements[0].color = (0.68, 0.67, 0.66, 1)
        grime_ramp.color_ramp.elements[1].position = 0.26
        grime_ramp.color_ramp.elements[1].color = (1.0, 1.0, 1.0, 1)
        L.new(sep_uv.outputs['Y'], grime_ramp.inputs[0])

        scuff_ramp = N.new('ShaderNodeValToRGB')
        scuff_ramp.color_ramp.elements[0].position = 0.65
        scuff_ramp.color_ramp.elements[0].color = (1.0, 1.0, 1.0, 1)
        scuff_ramp.color_ramp.elements[1].position = 0.80
        scuff_ramp.color_ramp.elements[1].color = (0.74, 0.74, 0.75, 1)
        L.new(noise.outputs['Fac'], scuff_ramp.inputs[0])

        mix1 = N.new('ShaderNodeMixRGB')
        mix1.blend_type = 'MULTIPLY'
        mix1.inputs[0].default_value = 0.65
        L.new(base_out, mix1.inputs[1])
        L.new(grime_ramp.outputs[0], mix1.inputs[2])

        mix2 = N.new('ShaderNodeMixRGB')
        mix2.blend_type = 'MULTIPLY'
        mix2.inputs[0].default_value = 0.50
        L.new(mix1.outputs[0], mix2.inputs[1])
        L.new(scuff_ramp.outputs[0], mix2.inputs[2])
        base_out = mix2.outputs[0]

    elif name == 'Interior':
        sep_uv = N.new('ShaderNodeSeparateXYZ')
        L.new(uv.outputs['UV'], sep_uv.inputs[0])
        gt_u = N.new('ShaderNodeMath')
        gt_u.operation = 'GREATER_THAN'
        gt_u.inputs[1].default_value = 0.70
        L.new(sep_uv.outputs['X'], gt_u.inputs[0])
        gt_v = N.new('ShaderNodeMath')
        gt_v.operation = 'GREATER_THAN'
        gt_v.inputs[1].default_value = 0.70
        L.new(sep_uv.outputs['Y'], gt_v.inputs[0])
        is_decal = N.new('ShaderNodeMath')
        is_decal.operation = 'MULTIPLY'
        L.new(gt_u.outputs[0], is_decal.inputs[0])
        L.new(gt_v.outputs[0], is_decal.inputs[1])

        decal_mix = N.new('ShaderNodeMixRGB')
        L.new(is_decal.outputs[0], decal_mix.inputs[0])
        L.new(base_out, decal_mix.inputs[1])
        decal_mix.inputs[2].default_value = (0.91, 0.91, 0.89, 1)
        base_out = decal_mix.outputs[0]

    L.new(base_out, p.inputs['Base Color'])

    nt = N.new('ShaderNodeTexImage')
    nt.image = SCAN[asset, 'nor_gl']
    nt.image.colorspace_settings.name = 'Non-Color'
    L.new(mp.outputs['Vector'], nt.inputs['Vector'])
    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'
    L.new(mp.outputs['Vector'], arm.inputs['Vector'])
    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 = max(0.04, rough - 0.07)
    L.new(sep.outputs[1], mul.inputs[0])

    wear = N.new('ShaderNodeMath')
    wear.operation = 'MULTIPLY_ADD'
    wear.inputs[1].default_value = 0.05
    wear.inputs[2].default_value = -0.025
    L.new(noise.outputs['Fac'], wear.inputs[0])

    summ = N.new('ShaderNodeMath')
    summ.operation = 'ADD'
    summ.use_clamp = True
    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, sep.outputs[0]

def create_headlamp_image():
    """Creates a realistic race car decal headlamp + taillight atlas for Lamps."""
    w, h = 512, 512
    im = bpy.data.images.new('Lamps_Source_Atlas', width=w, height=h, alpha=False)
    pixels = [0.0] * (w * h * 4)
    for y in range(h):
        v = y / h
        for x in range(w):
            u = x / w
            idx = (y * w + x) * 4
            if u < 0.5 and v >= 0.5:
                lu = u * 2.0
                lv = (v - 0.5) * 2.0
                border = min(lu, 1.0 - lu, lv, 1.0 - lv)
                if border < 0.045:
                    r, g, b = 0.03, 0.035, 0.04
                elif border < 0.085:
                    r, g, b = 0.58, 0.62, 0.66
                else:
                    if lu > 0.72:
                        flute = 0.82 + 0.18 * sin(lu * 150.0) * cos(lv * 90.0)
                        r, g, b = 0.98 * flute, 0.36 * flute, 0.015 * flute
                    else:
                        d1 = sqrt((lu - 0.30)**2 + ((lv - 0.50)*0.82)**2)
                        d2 = sqrt((lu - 0.56)**2 + ((lv - 0.50)*0.86)**2)
                        if d1 < 0.21:
                            ring = abs(d1 - 0.15)
                            glow = max(0.0, 1.0 - d1 / 0.21)
                            flute = 0.88 + 0.12 * sin(atan2(lv - 0.50, lu - 0.30) * 16.0)
                            if d1 < 0.065:
                                r, g, b = 1.0, 0.96, 0.85
                            elif ring < 0.020:
                                r, g, b = 0.12, 0.14, 0.16
                            else:
                                r, g, b = (0.76 + 0.24*glow)*flute, (0.78 + 0.20*glow)*flute, (0.82 + 0.15*glow)*flute
                        elif d2 < 0.16:
                            ring = abs(d2 - 0.115)
                            glow = max(0.0, 1.0 - d2 / 0.16)
                            flute = 0.88 + 0.12 * sin(atan2(lv - 0.50, lu - 0.56) * 12.0)
                            if d2 < 0.05:
                                r, g, b = 1.0, 0.94, 0.80
                            elif ring < 0.016:
                                r, g, b = 0.12, 0.14, 0.16
                            else:
                                r, g, b = (0.72 + 0.28*glow)*flute, (0.74 + 0.22*glow)*flute, (0.78 + 0.18*glow)*flute
                        else:
                            rib = 0.84 + 0.16 * sin(lv * 95.0)
                            r, g, b = 0.42 * rib, 0.45 * rib, 0.49 * rib
            elif u >= 0.5 and v >= 0.5:
                lu = (u - 0.5) * 2.0
                lv = (v - 0.5) * 2.0
                border = min(lu, 1.0 - lu, lv, 1.0 - lv)
                grid = 0.88 + 0.12 * sin(lu * 120.0) * sin(lv * 120.0)
                if border < 0.05:
                    r, g, b = 0.05, 0.01, 0.01
                elif lv < 0.22 and 0.18 < lu < 0.82:
                    r, g, b = 0.90 * grid, 0.25 * grid, 0.18 * grid
                else:
                    r, g, b = 0.76 * grid, 0.012 * grid, 0.015 * grid
            elif u < 0.5 and v < 0.5:
                r, g, b = 0.92, 0.92, 0.90
            else:
                r, g, b = 0.85, 0.08, 0.02
            pixels[idx:idx+4] = [r, g, b, 1.0]
    im.pixels = pixels
    return im

def bake_mat(name, color, metal, rough, asset, grain, base_sz=512, norm_sz=512, orm_sz=256, tile=3.0):
    src, ao_sock = build_source_mat(name, color, metal, rough, asset, grain, tile)
    N = src.node_tree.nodes
    L = src.node_tree.links
    p = N.get('Principled BSDF')
    out = N.get('Material Output')

    if name == 'Lamps':
        im_lamp = create_headlamp_image()
        tex = N.new('ShaderNodeTexImage')
        tex.image = im_lamp
        L.new(tex.outputs[0], p.inputs['Base Color'])

    bpy.ops.mesh.primitive_plane_add(size=1)
    ob = bpy.context.object
    ob.data.materials.append(src)
    activate(ob)

    target = N.new('ShaderNodeTexImage')
    N.active = target
    imgs = {}
    S.cycles.samples = 4
    S.render.bake.margin = 8

    for label, sz in [('Base', base_sz), ('Normal', norm_sz), ('ORM', orm_sz)]:
        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')
            L.new(ao_sock, combine.inputs[0])
            L.new(p.inputs['Roughness'].links[0].from_socket, combine.inputs[1])
            combine.inputs[2].default_value = metal
            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', 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.65
            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.6
        p.inputs['Coat Roughness'].default_value = 0.15
    elif name == 'Glass':
        p.inputs['Alpha'].default_value = 0.36
        m.surface_render_method = 'BLENDED'
        m.diffuse_color = (0.10, 0.14, 0.16, 0.36)
    elif name == 'Lamps':
        p.inputs['Emission Strength'].default_value = 0.45
        l.new(p.inputs['Base Color'].links[0].from_socket, p.inputs['Emission Color'])
        p.inputs['Coat Weight'].default_value = 0.85
        p.inputs['Coat Roughness'].default_value = 0.04

    M[name] = m
    print('BAKED', name, flush=True)

MAT_SPECS = [
    ('Paint',    (0.96, 0.96, 0.96), 0.12, 0.24, 'metal_plate',       0.02, 1024, 512, 512, 2.0),
    ('Rubber',   (0.032, 0.034, 0.036), 0.0, 0.82, 'rubber_tiles',    0.35, 512,  512, 256, 4.0),
    ('Metal',    (0.58, 0.60, 0.63), 0.88, 0.28, 'metal_plate',       0.12, 512,  512, 256, 3.0),
    ('Carbon',   (0.035, 0.038, 0.042), 0.15, 0.32, 'fabric_pattern_07', 0.45, 512, 512, 256, 12.0),
    ('Plastic',  (0.025, 0.027, 0.030), 0.05, 0.45, 'fabric_leather_01', 0.18, 512, 512, 256, 4.0),
    ('Interior', (0.085, 0.088, 0.095), 0.05, 0.45, 'leather_white',  0.18, 512,  512, 256, 3.0),
    ('Glass',    (0.06, 0.08, 0.095), 0.10, 0.06, 'metal_plate',      0.01, 128,  128, 128, 1.0),
    ('Lamps',    (1.0, 1.0, 1.0),    0.15, 0.12, 'metal_plate',       0.02, 512,  256, 256, 1.0),
]
for spec in MAT_SPECS:
    bake_mat(*spec)

# ==============================================================================
# PROPORTIONS & UNIFIED LONGITUDINAL PROFILE FUNCTIONS
# ==============================================================================
Y_FRONT_AXLE = -1.395
Y_REAR_AXLE  =  1.395
R_TIRE       =  0.365
R_ARCH       =  0.402

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]

WIDTH_PTS = [
    (-2.48, 0.74),
    (-2.28, 0.885),
    (-1.95, 0.948),
    (-1.395, 0.975),
    (-0.60, 0.968),
    ( 0.20, 0.968),
    ( 1.395, 0.975),
    ( 2.05, 0.945),
    ( 2.38, 0.895),
    ( 2.48, 0.85),
]
def width_y(y):
    return smooth_interp(y, WIDTH_PTS)

CROWN_PTS = [
    (-2.48, 0.455),
    (-2.30, 0.565),
    (-1.95, 0.695),
    (-1.395, 0.805),
    (-0.82, 0.865),
    ( 0.00, 0.875),
    ( 1.25, 0.895),
    ( 1.85, 0.885),
    ( 2.48, 0.845),
]
def crown_y(y):
    return smooth_interp(y, CROWN_PTS)

SHOULDER_PTS = [
    (-2.48, 0.425),
    (-2.30, 0.525),
    (-1.95, 0.655),
    (-1.395, 0.780),
    (-0.82, 0.845),
    ( 0.00, 0.855),
    ( 1.395, 0.885),
    ( 1.95, 0.865),
    ( 2.48, 0.825),
]
def shoulder_y(y):
    base = smooth_interp(y, SHOULDER_PTS)
    flare = 0.018 * exp(-((y - Y_FRONT_AXLE) / 0.48)**2) + 0.020 * exp(-((y - Y_REAR_AXLE) / 0.52)**2)
    return base + flare

def top_z(y, x):
    w = max(0.2, width_y(y))
    t = min(1.0, abs(x) / w)
    c = crown_y(y)
    s = shoulder_y(y)
    z = c + (s - c) * (t ** 2.0) + 0.006 * sin(pi * t)**2
    if -2.25 < y < -0.82 and abs(x) < 0.42:
        u_b = (y - (-2.25)) / (-0.82 - (-2.25))
        z += 0.016 * (cos(pi * (x / 0.42) * 0.5) ** 2) * (sin(pi * u_b) ** 0.8)
    return z

def arch_bottom(y):
    b = 0.145
    if y < -1.85:
        u = min(1.0, (-1.85 - y) / 0.43)
        b = 0.145 * (1.0 - u) + 0.105 * u
    elif y > 1.82:
        u = min(1.0, (y - 1.82) / 0.56)
        b = 0.145 * (1.0 - u) + 0.255 * u

    for yc in [Y_FRONT_AXLE, Y_REAR_AXLE]:
        d = abs(y - yc)
        if d < R_ARCH:
            arch_z = R_TIRE + sqrt(max(0.0, R_ARCH**2 - d*d))
            b = max(b, arch_z)
        elif d < R_ARCH + 0.04:
            t = (d - R_ARCH) / 0.04
            b = max(b, (1.0 - t) * R_TIRE + t * b)
    return b

def side_x(y, z):
    sh = shoulder_y(y)
    bot = 0.12
    f = max(0.0, min(1.0, (z - bot) / max(0.1, sh - bot)))
    w = width_y(y)
    arch_flare = 0.0
    for yc in [Y_FRONT_AXLE, Y_REAR_AXLE]:
        dist = sqrt((y - yc)**2 + (z - R_TIRE)**2)
        arch_flare += 0.014 * exp(-((dist - R_ARCH - 0.02) / 0.075)**2)
    door_smooth = exp(-((y - 0.05) / 0.65)**4)
    profile = (-0.012 * (1.0 - f)**2 + 0.010 * sin(pi * f**0.8) * (1.0 - 0.45 * door_smooth) - 0.022 * max(0.0, f - 0.82)**1.5)
    return w + profile + arch_flare * (1.0 - max(0.0, (f - 0.85) / 0.15))

def roof_width(y):
    return 0.685 - 0.025 * ((y - 0.15) / 1.1)**2

def roof_center_z(y):
    u = (y - (-0.22)) / (0.62 - (-0.22))
    u = max(0.0, min(1.0, u))
    return 1.245 + 0.047 * sin(pi * u)**0.85 - 0.015 * u

# ==============================================================================
# 1. UNIFIED BODY SHELL: FLANKS, CONTINUOUS BONNET & DECKLID, ARCHES
# ==============================================================================
# Body_Flank starts at y = -2.28 (where Front_Fascia ends) and ends at y = +2.38 (where Rear_Fascia starts)
for side in [-1, 1]:
    def hull_fn(u, v, s=side):
        y = -2.28 + (2.38 - (-2.28)) * u
        if v <= 0.65:
            t = v / 0.65
            z0 = arch_bottom(y)
            z1 = shoulder_y(y)
            z = z0 + (z1 - z0) * t
            x = side_x(y, z)
        else:
            t = (v - 0.65) / 0.35
            x_out = side_x(y, shoulder_y(y))
            x_in = 0.68
            x = x_out + (x_in - x_out) * t
            z = top_z(y, x)
        return (s * x, y, z)

    surf('Body_Flank', hull_fn, 92, 12, 'Paint', subsurf=1, solidify=-0.005)

    for is_front, yc in [(True, Y_FRONT_AXLE), (False, Y_REAR_AXLE)]:
        pts_lip = []
        for k in range(45):
            a = -0.32 + (pi + 0.64) * k / 44
            y = yc - R_ARCH * cos(a)
            z = R_TIRE + R_ARCH * sin(a)
            if z >= 0.13:
                xx = side_x(y, min(z, shoulder_y(y) - 0.02)) + 0.004
                pts_lip.append((side * xx, y, z))
        line('Fender_Lip', pts_lip, 0.007, 'Paint', cyclic=False, res=1)

        pts_inner_lip = [(x - side * 0.005, y, z - 0.004) for (x, y, z) in pts_lip]
        line('Fender_Rub_Edge', pts_inner_lip, 0.006, 'Plastic', cyclic=False, res=1)

        def liner_fn(u, v, s=side, yc=yc):
            a = -0.35 + (pi + 0.70) * u
            y = yc - (R_ARCH - 0.004) * cos(a)
            z = max(0.12, R_TIRE + (R_ARCH - 0.004) * sin(a))
            x_out = side_x(y, min(z, shoulder_y(y) - 0.02))
            x_in = 0.56
            return (s * (x_in + (x_out - x_in) * v), y, z)
        surf('Wheelhouse_Liner', liner_fn, 32, 3, 'Plastic')

        vs_bh = [(side * 0.565, yc, R_TIRE)]
        fs_bh = []
        for k in range(33):
            a = -0.35 + (pi + 0.70) * k / 32
            y = yc - R_ARCH * cos(a)
            z = max(0.12, R_TIRE + R_ARCH * sin(a))
            vs_bh.append((side * 0.565, y, z))
        for k in range(32):
            fs_bh.append((0, k + 1, k + 2) if side == 1 else (0, k + 2, k + 1))
        mesh('Wheelhouse_Bulkhead', vs_bh, fs_bh, 'Plastic')

# Single continuous Bonnet (from y = -2.28 to -0.80) & Rear Decklid (from y = 1.32 to 2.38)
for name_part, ya, yb in [('Bonnet_Full', -2.28, -0.80), ('Decklid_Full', 1.32, 2.38)]:
    def full_deck_fn(u, v, ya=ya, yb=yb):
        y = ya + (yb - ya) * u
        x = (2.0 * v - 1.0) * 0.685
        return (x, y, top_z(y, x))
    surf(name_part, full_deck_fn, 26, 16, 'Paint', subsurf=1, solidify=-0.004)

# Crisp hood & trunk shutlines
for side in [-1, 1]:
    hood_seam = [(side * 0.66, y, top_z(y, 0.66) + 0.0018) for y in [-2.18 + 1.32 * k / 20 for k in range(21)]]
    line('Bonnet_Side_Shutline', hood_seam, 0.0018, 'Plastic', cyclic=False, res=1)
hood_front_seam = [(x, -2.18 - 0.04 * (1.0 - (x/0.66)**2), top_z(-2.18 - 0.04 * (1.0 - (x/0.66)**2), x) + 0.0018) for x in [-0.66 + 1.32 * k / 24 for k in range(25)]]
line('Bonnet_Front_Shutline', hood_front_seam, 0.0018, 'Plastic', cyclic=False, res=1)

# ==============================================================================
# 2. GREENHOUSE: ROOF, A/B/C PILLARS, WINDSHIELD, BACKGLASS, SIDE WINDOWS
# ==============================================================================
def roof_surf_fn(u, v):
    y = -0.24 + 0.88 * u
    t = 2.0 * v - 1.0
    rw = roof_width(y)
    x = t * rw
    zc = roof_center_z(y)
    z = zc - 0.028 * (t ** 2)
    return (x, y, z)

surf('Roof_Crown', roof_surf_fn, 20, 16, 'Paint', subsurf=1, solidify=-0.005)

for side, x_flap in [(-1, -0.24), (1, 0.22)]:
    flap_z = roof_center_z(0.20) - 0.003
    pts_flap = [
        (x_flap - 0.11, 0.06, flap_z),
        (x_flap + 0.11, 0.08, flap_z),
        (x_flap + 0.10, 0.36, flap_z - 0.004),
        (x_flap - 0.12, 0.34, flap_z - 0.004),
    ]
    line('Roof_Flap_Seam', pts_flap, 0.002, 'Plastic', cyclic=True, res=1)

for side in [-1, 1]:
    rail_pts = []
    for k in range(21):
        u = k / 20
        y = -0.20 + 0.80 * u
        x = side * (roof_width(y) - 0.08)
        z = roof_center_z(y) - 0.020 + 0.008
        rail_pts.append((x, y, z))
    line('Roof_Aero_Rail', rail_pts, 0.005, 'Plastic', cyclic=False, res=1)

for side in [-1, 1]:
    def cpillar_fn(u, v, s=side):
        y = 0.58 + 0.80 * u
        x_in = (roof_width(0.58) - 0.06) * (1.0 - u) + 0.64 * u
        x_out = roof_width(0.58) * (1.0 - u) + (side_x(y, shoulder_y(y)) - 0.02) * u
        x = x_in + (x_out - x_in) * v
        z_in = roof_center_z(0.58) * (1.0 - u) + top_z(1.38, x_in) * u - 0.015 * sin(pi * u)
        z_out = (roof_center_z(0.58) - 0.03) * (1.0 - u) + shoulder_y(y) * u
        z = z_in + (z_out - z_in) * (v ** 1.3) + 0.012 * sin(pi * v)
        return (s * x, y, z)
    surf('C_Pillar_Sail', cpillar_fn, 16, 6, 'Paint', subsurf=1, solidify=-0.005)

    def apillar_fn(u, v, s=side):
        y = -0.82 + 0.60 * u
        x_in = 0.72 * (1.0 - u) + (roof_width(-0.22) - 0.05) * u
        x_out = 0.82 * (1.0 - u) + roof_width(-0.22) * u
        x = x_in + (x_out - x_in) * v
        z_in = (top_z(-0.82, 0.72) + 0.01) * (1.0 - u) + (roof_center_z(-0.22) - 0.025) * u + 0.015 * sin(pi * u)
        z_out = (shoulder_y(-0.82) + 0.02) * (1.0 - u) + (roof_center_z(-0.22) - 0.032) * u + 0.012 * sin(pi * u)
        return (s * x, y, z_in + (z_out - z_in) * v)
    surf('A_Pillar', apillar_fn, 14, 4, 'Paint', subsurf=1, solidify=-0.005)

    def cantrail_fn(u, v, s=side):
        y = -0.24 + 0.86 * u
        rw = roof_width(y)
        x = rw + 0.045 * v
        z = (roof_center_z(y) - 0.028) - 0.045 * v
        return (s * x, y, z)
    surf('Roof_Cantrail', cantrail_fn, 16, 4, 'Paint', subsurf=1, solidify=-0.005)

    b_pts = []
    for u in [0.0, 1.0]:
        y = 0.16 + 0.14 * u
        b_pts.append((side * (side_x(y, shoulder_y(y)) - 0.03), y, shoulder_y(y) - 0.01))
    for u in [1.0, 0.0]:
        y = 0.16 + 0.14 * u
        b_pts.append((side * (roof_width(y) + 0.03), y, roof_center_z(y) - 0.055))
    o_bp = mesh('B_Pillar_Post', b_pts, [(0, 1, 2, 3) if side == 1 else (0, 3, 2, 1)], 'Paint')
    md = o_bp.modifiers.new('Sol', 'SOLIDIFY')
    md.thickness = 0.012

def windshield_fn(u, v):
    t = 2.0 * v - 1.0
    y = -0.82 + 0.58 * u - 0.045 * (1.0 - t*t) * (1.0 - u)
    w = 0.73 * (1.0 - u) + (roof_width(-0.24) - 0.04) * u
    x = t * w
    z0 = top_z(-0.82, x) + 0.008
    z1 = roof_center_z(-0.24) - 0.025 * (t*t)
    z = z0 + (z1 - z0) * u + 0.022 * sin(pi * u) * (1.0 - 0.4 * t*t)
    return (x, y, z)

def backglass_fn(u, v):
    t = 2.0 * v - 1.0
    y = 0.60 + 0.76 * u + 0.035 * (1.0 - t*t) * u
    w = (roof_width(0.60) - 0.05) * (1.0 - u) + 0.65 * u
    x = t * w
    z0 = roof_center_z(0.60) - 0.025 * (t*t)
    z1 = top_z(1.36, x) + 0.006
    z = z0 + (z1 - z0) * u + 0.015 * sin(pi * u)
    return (x, y, z)

for gname, fn in [('Windshield', windshield_fn), ('Rear_Backglass', backglass_fn)]:
    surf(gname, fn, 16, 20, 'Glass')
    edge = (
        [fn(0, j/20) for j in range(21)] +
        [fn(i/16, 1) for i in range(1, 17)] +
        [fn(1, 1 - j/20) for j in range(1, 21)] +
        [fn(1 - i/16, 0) for i in range(1, 17)]
    )
    line(gname + '_Seal', edge, 0.009, 'Plastic', cyclic=True, res=1)

for x_bar in [-0.32, 0.0, 0.32]:
    u_val = 0.5 + x_bar / 1.40
    pts_brace = [windshield_fn(k/10, u_val) for k in range(11)]
    pts_brace_in = [(x, y + 0.008, z - 0.006) for (x, y, z) in pts_brace]
    line('Windshield_Brace', pts_brace_in, 0.004 if x_bar == 0 else 0.003, 'Metal', cyclic=False, res=1)

for v_strap in [0.32, 0.68]:
    pts_strap = [backglass_fn(k/10, v_strap) for k in range(11)]
    pts_strap_out = [(x, y - 0.002, z + 0.003) for (x, y, z) in pts_strap]
    line('Backglass_Strap', pts_strap_out, 0.0035, 'Metal', cyclic=False, res=1)

surf('Windshield_Cowl', lambda u, v: tuple((1.0 - u)*a + u*b for a, b in zip(windshield_fn(0, v), ((2*v - 1)*0.76, -0.86, top_z(-0.86, (2*v - 1)*0.76)))), 6, 20, 'Paint')

for side in [-1, 1]:
    def side_win_pt(u, v, s=side):
        y_bot = -0.74 + 0.90 * u
        y_top = -0.22 + 0.38 * u
        y = y_bot * (1.0 - v) + y_top * v
        z_bot = shoulder_y(y_bot) + 0.005
        z_top = roof_center_z(y_top) - 0.055
        z = z_bot + (z_top - z_bot) * v
        x_bot = side_x(y_bot, shoulder_y(y_bot)) - 0.025
        x_top = roof_width(y_top) + 0.032
        x = x_bot + (x_top - x_bot) * v
        return (s * x, y, z)

    win_border = (
        [side_win_pt(k/16, 0) for k in range(17)] +
        [side_win_pt(1, k/12) for k in range(1, 13)] +
        [side_win_pt(1 - k/16, 1) for k in range(1, 17)] +
        [side_win_pt(0, 1 - k/12) for k in range(1, 13)]
    )
    line('Door_Window_Frame', win_border, 0.009, 'Plastic', cyclic=True, res=1)

    g_pts = [
        side_win_pt(0.0, 0.0),
        side_win_pt(0.24, 0.0),
        side_win_pt(0.18, 0.42),
        side_win_pt(0.0, 0.35),
    ]
    mesh('Window_Front_Gusset', g_pts, [(0, 1, 2, 3) if side == 1 else (0, 3, 2, 1)], 'Metal')

    if side == 1:
        rod_bot = [side_win_pt(0.24 + 0.72 * k / 10, 0.05) for k in range(11)]
        rod_top = [side_win_pt(0.20 + 0.76 * k / 10, 0.94) for k in range(11)]
        line('Window_Net_Rod_Bot', rod_bot, 0.006, 'Metal', cyclic=False, res=1)
        line('Window_Net_Rod_Top', rod_top, 0.006, 'Metal', cyclic=False, res=1)
        p_latch = side_win_pt(0.20, 0.94)
        o_latch = cube('Window_Net_Latch', p_latch, (0.015, 0.045, 0.028), 'Lamps', 0.003)
        set_uv_box(o_latch, 0.55, 0.05, 0.95, 0.45)

        for kv in range(8):
            u_w = 0.24 + 0.70 * (kv / 7.0)
            p0 = side_win_pt(u_w, 0.05)
            p1 = side_win_pt(u_w * 0.95 + 0.03, 0.94)
            linkbar('Window_Net_VStrap', p0, p1, 0.004, 0.022, 'Plastic', r=0.001)
        for kh in range(6):
            v_w = 0.10 + 0.80 * (kh / 5.0)
            p0 = side_win_pt(0.23, v_w)
            p1 = side_win_pt(0.95, v_w)
            linkbar('Window_Net_HStrap', p0, p1, 0.004, 0.022, 'Plastic', r=0.001)
    else:
        surf('Passenger_Side_Glass', lambda u, v: side_win_pt(u, v, s=-1), 10, 8, 'Glass')

    def qwin_pt(u, v, s=side):
        y_bot = 0.30 + 0.52 * u
        y_top = 0.30 + 0.36 * u
        y = y_bot * (1.0 - v) + y_top * v - 0.035 * (u**2) * sin(pi * v)
        z_bot = shoulder_y(y_bot) + 0.008 + 0.025 * (u**2)
        z_top = (roof_center_z(min(0.62, y_top)) - 0.055) - 0.065 * (u**1.6)
        z = z_bot + (z_top - z_bot) * v
        x_bot = side_x(y_bot, shoulder_y(y_bot)) - 0.028
        x_top = roof_width(min(0.62, y_top)) + 0.035 + 0.08 * u
        x = x_bot + (x_top - x_bot) * v
        return (s * x, y, z)

    surf('Quarter_Window_Glass', lambda u, v, s=side: qwin_pt(u, v, s), 10, 8, 'Glass')
    q_border = (
        [qwin_pt(k/10, 0) for k in range(11)] +
        [qwin_pt(1, k/8) for k in range(1, 9)] +
        [qwin_pt(1 - k/10, 1) for k in range(1, 11)] +
        [qwin_pt(0, 1 - k/8) for k in range(1, 9)]
    )
    line('Quarter_Window_Seal', q_border, 0.008, 'Plastic', cyclic=True, res=1)

    for v_duct, u_duct in [(0.62, 0.38), (0.28, 0.45)]:
        p_d = qwin_pt(u_duct, v_duct, side)
        d_pts = [
            (p_d[0] + side*0.003, p_d[1] - 0.07, p_d[2]),
            (p_d[0] + side*0.003, p_d[1] + 0.06, p_d[2] + 0.032),
            (p_d[0] + side*0.003, p_d[1] + 0.06, p_d[2] - 0.032),
        ]
        line('NACA_Duct_Lip', d_pts, 0.004, 'Plastic', cyclic=True, res=1)
        mesh('NACA_Duct_Pocket', [(x - side*0.004, y, z) for (x, y, z) in d_pts], [(0, 1, 2) if side == 1 else (0, 2, 1)], 'Plastic')

# ==============================================================================
# 3. SEAMLESS ROUNDED FRONT FASCIA, SPLITTER, GRILLES & FLAT HEADLIGHT DECALS
# ==============================================================================
# Front_Fascia uses exact same 0.65 / 0.35 vertical parameterization as Body_Flank at y = -2.28!
def front_cap_pt(u, v):
    t = 2.0 * v - 1.0
    s_ang = sin(t * pi * 0.5)
    a_ang = abs(s_ang)
    y_edge = -2.28
    # Smoothly sweep from nose centre (y = -2.495) to exact fender join (y = -2.28 at |t|=1)
    y_low = -2.495 + (2.495 - 2.28) * (a_ang ** 1.85) - 0.014 * sin(pi * min(1.0, u / 0.65)) * (1.0 - a_ang**2)
    z0 = 0.105
    z1 = shoulder_y(y_low)
    if u <= 0.65:
        fu = u / 0.65
        z = z0 + (z1 - z0) * fu
        x = s_ang * side_x(y_edge, z0 + (shoulder_y(y_edge) - z0) * fu)
        y = y_low
    else:
        fu = (u - 0.65) / 0.35
        x_out = side_x(y_edge, shoulder_y(y_edge))
        x_in = 0.685
        x_max = x_out + (x_in - x_out) * fu
        x = s_ang * x_max
        y = y_low * (1.0 - fu) + y_edge * fu
        z = top_z(y, x)
    return (x, y, z)

surf('Front_Fascia', front_cap_pt, 16, 48, 'Paint', subsurf=1, solidify=-0.005)

def front_nose_y(x, z):
    x_ref = side_x(-2.28, min(shoulder_y(-2.28), max(0.105, z)))
    a_ang = min(1.0, abs(x) / max(0.2, x_ref))
    fu = max(0.0, min(1.0, (z - 0.105) / max(0.1, shoulder_y(-2.35) - 0.105)))
    return -2.495 + (2.495 - 2.28) * (a_ang ** 1.85) - 0.014 * sin(pi * fu) * (1.0 - a_ang**2)

def splitter_blade_fn(u, v):
    t = 2.0 * v - 1.0
    x = t * 0.90
    y_back = front_nose_y(x, 0.11) + 0.10
    y_front = front_nose_y(x, 0.11) - 0.042 * (1.0 - 0.20 * t*t)
    y = y_front + (y_back - y_front) * u
    z = 0.082
    return (x, y, z)

o_split = surf('Front_Splitter_Blade', splitter_blade_fn, 6, 44, 'Carbon', solidify=0.016)
bevel(o_split, 0.003, 1)

def splitter_collar_fn(u, v):
    t = 2.0 * v - 1.0
    x = t * 0.89
    z = 0.092 + 0.055 * u
    y = front_nose_y(x, z) - 0.010
    return (x, y, z)

surf('Front_Splitter_Collar', splitter_collar_fn, 4, 44, 'Plastic', solidify=-0.005)

for k in range(15):
    t = -0.82 + 1.64 * (k / 14.0)
    y_r = front_nose_y(t, 0.122) - 0.014
    cyl('Splitter_Rivet', (t, y_r, 0.122), 0.006, 0.006, 'Metal', axis='Y', n=12, bev=0.001)

# Upper & Lower Grilles
def upper_grille_fn(u, v):
    t = 2.0 * v - 1.0
    x = t * 0.27 * (1.0 - 0.08 * u)
    z = 0.345 + 0.068 * u
    y = front_nose_y(x, z) - 0.007
    return (x, y, z)

surf('Upper_Grille_Back', upper_grille_fn, 6, 18, 'Plastic')
ug_border = (
    [ (x, front_nose_y(x, 0.345) - 0.011, 0.345) for x in [-0.27 + 0.54*k/16 for k in range(17)] ] +
    [ (0.25, front_nose_y(0.25, 0.413) - 0.011, 0.413) ] +
    [ (x, front_nose_y(x, 0.413) - 0.011, 0.413) for x in [0.25 - 0.50*k/16 for k in range(17)] ] +
    [ (-0.27, front_nose_y(-0.27, 0.345) - 0.011, 0.345) ]
)
line('Upper_Grille_Bezel', ug_border, 0.007, 'Plastic', cyclic=True, res=1)
for k in range(-8, 9):
    xg = k * 0.028
    line('Upper_Grille_Mesh_V', [(xg, front_nose_y(xg, 0.348)-0.010, 0.348), (xg, front_nose_y(xg, 0.410)-0.010, 0.410)], 0.002, 'Metal', res=1)
for zg in [0.365, 0.382, 0.398]:
    line('Upper_Grille_Mesh_H', [(xg, front_nose_y(xg, zg)-0.011, zg) for xg in [-0.25 + 0.50*j/12 for j in range(13)]], 0.002, 'Metal', res=1)

def lower_grille_fn(u, v):
    t = 2.0 * v - 1.0
    x = t * 0.30
    z = 0.165 + 0.115 * u
    y = front_nose_y(x, z) - 0.007
    return (x, y, z)

surf('Lower_Grille_Back', lower_grille_fn, 6, 18, 'Plastic')
lg_border = (
    [ (x, front_nose_y(x, 0.165) - 0.012, 0.165) for x in [-0.30 + 0.60*k/16 for k in range(17)] ] +
    [ (0.30, front_nose_y(0.30, 0.280) - 0.012, 0.280) ] +
    [ (x, front_nose_y(x, 0.280) - 0.012, 0.280) for x in [0.30 - 0.60*k/16 for k in range(17)] ] +
    [ (-0.30, front_nose_y(-0.30, 0.165) - 0.012, 0.165) ]
)
line('Lower_Grille_Bezel', lg_border, 0.008, 'Plastic', cyclic=True, res=1)
for k in range(-10, 11):
    xg = k * 0.028
    line('Lower_Grille_Bar_V', [(xg, front_nose_y(xg, 0.168)-0.010, 0.168), (xg, front_nose_y(xg, 0.277)-0.010, 0.277)], 0.0022, 'Metal', res=1)
for zg in [0.195, 0.222, 0.250]:
    line('Lower_Grille_Bar_H', [(xg, front_nose_y(xg, zg)-0.011, zg) for xg in [-0.29 + 0.58*j/12 for j in range(13)]], 0.0022, 'Metal', res=1)

for side in [-1, 1]:
    bd_pts = []
    for x_loc, z_loc in [(0.36, 0.165), (0.52, 0.170), (0.52, 0.275), (0.36, 0.278)]:
        bd_pts.append((side * x_loc, front_nose_y(x_loc, z_loc) - 0.011, z_loc))
    line('Brake_Duct_Bezel', bd_pts, 0.007, 'Plastic', cyclic=True, res=1)
    mesh('Brake_Duct_Back', [(x, y + 0.003, z) for (x, y, z) in bd_pts], [(0, 1, 2, 3) if side == 1 else (0, 3, 2, 1)], 'Plastic')
    for xv in [0.40, 0.44, 0.48]:
        line('Brake_Duct_Vane', [(side*xv, front_nose_y(xv, 0.168)-0.010, 0.168), (side*xv, front_nose_y(xv, 0.274)-0.010, 0.274)], 0.0025, 'Metal', res=1)

# Flat Headlight Shapes wrapping around the front corner onto the upper bumper shoulder
for side in [-1, 1]:
    def hl_surf_fn(u, v, s=side):
        x = 0.33 + 0.49 * u
        z_mid = 0.375 + 0.075 * (u ** 1.1)
        half_h = (0.025 + 0.048 * sin(pi * (u ** 0.70))) * (1.0 - 0.18 * u)
        z = z_mid + (2.0 * v - 1.0) * half_h
        y = front_nose_y(x, z) - 0.008
        return (s * x, y, z)

    vs_hl, uv_hl, fs_hl = [], [], []
    nu_hl, nv_hl = 18, 10
    for i in range(nu_hl + 1):
        for j in range(nv_hl + 1):
            u = i / nu_hl
            v = j / nv_hl
            vs_hl.append(hl_surf_fn(u, v, side))
            uv_hl.append((0.02 + 0.46 * u, 0.52 + 0.46 * v))
    for i in range(nu_hl):
        for j in range(nv_hl):
            a = i * (nv_hl + 1) + j
            if side == 1:
                fs_hl.append((a, a + nv_hl + 1, a + nv_hl + 2, a + 1))
            else:
                fs_hl.append((a, a + 1, a + nv_hl + 2, a + nv_hl + 1))
    mesh('Headlight_Flat_Decal', vs_hl, fs_hl, 'Lamps', uv_hl)

    hl_edge = (
        [hl_surf_fn(i/nu_hl, 0, side) for i in range(nu_hl + 1)] +
        [hl_surf_fn(1, j/nv_hl, side) for j in range(1, nv_hl + 1)] +
        [hl_surf_fn(1 - i/nu_hl, 1, side) for i in range(1, nu_hl + 1)] +
        [hl_surf_fn(0, 1 - j/nv_hl, side) for j in range(1, nv_hl + 1)]
    )
    line('Headlight_Decal_Trim', hl_edge, 0.0028, 'Plastic', cyclic=True, res=1)

# Hood Pins & Vents
for x_pin, y_pin in [(-0.54, -2.06), (-0.20, -2.16), (0.20, -2.16), (0.54, -2.06)]:
    z_pin = top_z(y_pin, x_pin) + 0.003
    cyl('Hood_Pin_Plate', (x_pin, y_pin, z_pin), 0.016, 0.003, 'Metal', axis='Z', n=16, bev=0.0005)
    cyl('Hood_Pin_Post', (x_pin, y_pin, z_pin + 0.005), 0.0035, 0.010, 'Metal', axis='Z', n=10, bev=0.0005)
    pts_ring = [(x_pin + 0.012*cos(2*pi*k/12), y_pin + 0.012*sin(2*pi*k/12), z_pin + 0.006) for k in range(12)]
    line('Hood_Pin_Ring', pts_ring, 0.0015, 'Metal', cyclic=True, res=1)

for side in [-1, 1]:
    y_lv, x_lv = -1.55, side * 0.34
    lv_border = [
        (x_lv - side*0.08, y_lv - 0.14, top_z(y_lv - 0.14, x_lv - side*0.08) + 0.002),
        (x_lv + side*0.08, y_lv - 0.14, top_z(y_lv - 0.14, x_lv + side*0.08) + 0.002),
        (x_lv + side*0.09, y_lv + 0.14, top_z(y_lv + 0.14, x_lv + side*0.09) + 0.002),
        (x_lv - side*0.07, y_lv + 0.14, top_z(y_lv + 0.14, x_lv - side*0.07) + 0.002),
    ]
    line('Hood_Vent_Trim', lv_border, 0.003, 'Plastic', cyclic=True, res=1)
    for kl in range(4):
        yl = y_lv - 0.10 + kl * 0.065
        zl = top_z(yl, x_lv) + 0.003
        line('Hood_Louvre_Slats', [(x_lv - side*0.065, yl, zl), (x_lv + side*0.075, yl, zl)], 0.0025, 'Plastic', res=1)

# ==============================================================================
# 4. SIDE SKIRTS, SIDE EXHAUST, JACK POINTS & MULTI-RING BLANK NUMBER CIRCLES
# ==============================================================================
for side in [-1, 1]:
    def skirt_fn(u, v, s=side):
        y = -0.985 + 1.970 * u
        z = 0.068 + 0.082 * v
        x = side_x(y, 0.15) + 0.006 - 0.006 * (1.0 - v)
        return (s * x, y, z)
    o_sk = surf('Side_Skirt', skirt_fn, 28, 4, 'Plastic', solidify=-0.006)
    bevel(o_sk, 0.002, 1)

    for kr in range(11):
        yr = -0.90 + 1.80 * (kr / 10.0)
        xr = side * (side_x(yr, 0.15) + 0.009)
        cyl('Skirt_Rivet', (xr, yr, 0.132), 0.005, 0.004, 'Metal', axis='X', n=10, bev=0.001)

    xj = side * (side_x(0.08, 0.15) + 0.009)
    o_jp = cube('Jack_Point_Marker', (xj, 0.08, 0.115), (0.006, 0.024, 0.065), 'Lamps', 0.001)
    set_uv_box(o_jp, 0.55, 0.05, 0.95, 0.45)
    cyl('Jack_Post_Tube', (side * (side_x(0.08, 0.15) - 0.01), 0.08, 0.075), 0.014, 0.04, 'Metal', axis='X', n=14, bev=0.002)

    yc_num, zc_num, r_num = 0.05, 0.485, 0.335
    vs_nd = [(side * (side_x(yc_num, zc_num) + 0.007), yc_num, zc_num)]
    uv_nd = [(0.85, 0.85)]
    fs_nd = []
    n_ang, n_rad = 48, 6
    ring_pts = []
    for ir in range(1, n_rad + 1):
        fr = ir / n_rad
        rad = r_num * fr
        for ia in range(n_ang):
            a = 2.0 * pi * ia / n_ang
            yn = yc_num + rad * cos(a)
            zn = zc_num + rad * sin(a)
            xn = side * (side_x(yn, zn) + 0.007)
            vs_nd.append((xn, yn, zn))
            uv_nd.append((0.85 + 0.10 * fr * cos(a), 0.85 + 0.10 * fr * sin(a)))
            if ir == n_rad:
                ring_pts.append((side * (side_x(yn, zn) + 0.009), yn, zn))
    for ia in range(n_ang):
        nxt = (ia + 1) % n_ang
        fs_nd.append((0, ia + 1, nxt + 1) if side == 1 else (0, nxt + 1, ia + 1))
    for ir in range(n_rad - 1):
        base0 = 1 + ir * n_ang
        base1 = 1 + (ir + 1) * n_ang
        for ia in range(n_ang):
            nxt = (ia + 1) % n_ang
            a, b, c, d = base0 + ia, base0 + nxt, base1 + nxt, base1 + ia
            fs_nd.append((a, d, c, b) if side == 1 else (a, b, c, d))
    mesh('Door_Number_Circle_Blank', vs_nd, fs_nd, 'Interior', uv_nd)
    line('Door_Number_Circle_Ring', ring_pts, 0.012, 'Plastic', cyclic=True, res=1)

yc_rnum, r_rnum = 0.18, 0.34
vs_rnd = [(0.0, yc_rnum, roof_center_z(yc_rnum) + 0.006)]
uv_rnd = [(0.85, 0.85)]
fs_rnd = []
rring_pts = []
for ir in range(1, n_rad + 1):
    fr = ir / n_rad
    rad = r_rnum * fr
    for ia in range(n_ang):
        a = 2.0 * pi * ia / n_ang
        xr = rad * sin(a)
        yr = yc_rnum + rad * cos(a)
        rw = roof_width(yr)
        t_r = xr / rw
        zr = roof_center_z(yr) - 0.028 * (t_r ** 2) + 0.006
        vs_rnd.append((xr, yr, zr))
        uv_rnd.append((0.85 + 0.10 * fr * cos(a), 0.85 + 0.10 * fr * sin(a)))
        if ir == n_rad:
            rring_pts.append((xr, yr, zr + 0.002))
for ia in range(n_ang):
    nxt = (ia + 1) % n_ang
    fs_rnd.append((0, ia + 1, nxt + 1))
for ir in range(n_rad - 1):
    base0 = 1 + ir * n_ang
    base1 = 1 + (ir + 1) * n_ang
    for ia in range(n_ang):
        nxt = (ia + 1) % n_ang
        a, b, c, d = base0 + ia, base0 + nxt, base1 + nxt, base1 + ia
        fs_rnd.append((a, d, c, b))
mesh('Roof_Number_Circle_Blank', vs_rnd, fs_rnd, 'Interior', uv_rnd)
line('Roof_Number_Circle_Ring', rring_pts, 0.011, 'Plastic', cyclic=True, res=1)

for side in [-1, 1]:
    if side == -1:
        for y_ex in [0.72, 0.81]:
            x_ex = side * (side_x(y_ex, 0.15) - 0.005)
            cyl('Side_Exhaust_Pipe', (x_ex - side*0.04, y_ex, 0.095), 0.034, 0.10, 'Metal', axis='X', n=24, bev=0.002)
            cyl('Side_Exhaust_Bore', (x_ex - side*0.002, y_ex, 0.095), 0.028, 0.02, 'Plastic', axis='X', n=20, bev=0.001)
    if side == 1:
        xf = side * (side_x(1.98, 0.74) + 0.004)
        cyl('Fuel_Filler_Bezel', (xf, 1.98, 0.74), 0.046, 0.008, 'Metal', axis='X', n=24, bev=0.002)
        cyl('Fuel_Filler_Cap', (xf + 0.002, 1.98, 0.74), 0.032, 0.008, 'Plastic', axis='X', n=20, bev=0.001)

# ==============================================================================
# 5. SEAMLESS ROUNDED REAR FASCIA, TALL REAR SPOILER, TAILLIGHTS & DIFFUSER
# ==============================================================================
def rear_cap_pt(u, v):
    t = 2.0 * v - 1.0
    s_ang = sin(t * pi * 0.5)
    a_ang = abs(s_ang)
    y_edge = 2.38
    y = 2.505 - (2.505 - 2.38) * (a_ang ** 2.0) - 0.010 * sin(pi * min(1.0, u / 0.65)) * (1.0 - a_ang**2)
    z0 = 0.255
    z1 = shoulder_y(y)
    if u <= 0.65:
        fu = u / 0.65
        z = z0 + (z1 - z0) * fu
        x = s_ang * side_x(y_edge, z0 + (shoulder_y(y_edge) - z0) * fu)
    else:
        fu = (u - 0.65) / 0.35
        x_out = side_x(y_edge, shoulder_y(y_edge))
        x_in = 0.685
        x_max = x_out + (x_in - x_out) * fu
        x = s_ang * x_max
        y_top = 2.38 + 0.125 * (1.0 - fu) * (1.0 - a_ang**2)
        y = y_top
        z = top_z(y, x)
    return (x, y, z)

surf('Rear_Fascia', rear_cap_pt, 14, 40, 'Paint', subsurf=1, solidify=-0.005)

def rear_tail_y(x, z):
    x_ref = side_x(2.38, min(shoulder_y(2.38), max(0.255, z)))
    a_ang = min(1.0, abs(x) / max(0.2, x_ref))
    fu = max(0.0, min(1.0, (z - 0.255) / max(0.1, shoulder_y(2.38) - 0.255)))
    return 2.505 - (2.505 - 2.38) * (a_ang ** 2.0) - 0.010 * sin(pi * fu) * (1.0 - a_ang**2)

cube('Rear_Bumper_Step', (0.0, 2.495, 0.44), (1.36, 0.035, 0.085), 'Paint', r=0.012)

def spoiler_blade_fn(u, v):
    t = 2.0 * v - 1.0
    x = t * 0.78
    z_base = top_z(2.38, x) - 0.005
    h_sp = 0.145 * (1.0 - 0.08 * (t ** 4))
    z = z_base + h_sp * u
    y = 2.385 - 0.025 * (t ** 2) + 0.068 * u
    return (x, y, z)

o_sp = surf('Tall_Rear_Spoiler', spoiler_blade_fn, 6, 36, 'Carbon', solidify=0.010)
bevel(o_sp, 0.0025, 1)

def spoiler_collar_fn(u, v):
    t = 2.0 * v - 1.0
    x = t * 0.78
    z_base = top_z(2.38, x) - 0.004
    return (x, 2.380 - 0.025 * (t**2) + 0.018 * u, z_base + 0.032 * u)

surf('Spoiler_Base_Collar', spoiler_collar_fn, 3, 36, 'Plastic', solidify=0.006)

for kr in range(8):
    xr = -0.68 + 1.36 * (kr / 7.0)
    zr = top_z(2.38, xr) + 0.014
    yr = 2.380 - 0.025 * ((xr / 0.78)**2) - 0.006
    cyl('Spoiler_Rivet', (xr, yr, zr), 0.0055, 0.006, 'Metal', axis='Y', n=10, bev=0.001)

for xs in [-0.52, -0.18, 0.18, 0.52]:
    t_s = xs / 0.78
    z_b = top_z(2.38, xs)
    p_top = (xs, 2.385 - 0.025*(t_s**2) + 0.055, z_b + 0.115)
    p_bot = (xs, 2.445 - 0.025*(t_s**2), z_b + 0.005)
    tube_seg('Spoiler_Support_Strut', p_top, p_bot, 0.0045, 'Metal', n=10)
    cyl('Spoiler_Strut_Bracket', p_top, 0.008, 0.010, 'Metal', axis='X', n=10, bev=0.001)

for side in [-1, 1]:
    def tl_surf_fn(u, v, s=side):
        x = 0.54 + 0.30 * u
        z_bot = 0.625 + 0.010 * u
        z_top = 0.795 - 0.025 * (u ** 1.5)
        z = z_bot + (z_top - z_bot) * v
        y_rear = rear_tail_y(x, z) + 0.008
        return (s * x, y_rear, z)

    vs_tl, uv_tl, fs_tl = [], [], []
    nu_tl, nv_tl = 14, 8
    for i in range(nu_tl + 1):
        for j in range(nv_tl + 1):
            u = i / nu_tl
            v = j / nv_tl
            vs_tl.append(tl_surf_fn(u, v, side))
            uv_tl.append((0.52 + 0.46 * u, 0.52 + 0.46 * v))
    for i in range(nu_tl):
        for j in range(nv_tl):
            a = i * (nv_tl + 1) + j
            if side == 1:
                fs_tl.append((a, a + 1, a + nv_tl + 2, a + nv_tl + 1))
            else:
                fs_tl.append((a, a + nv_tl + 1, a + nv_tl + 2, a + 1))
    mesh('Taillight_Lens', vs_tl, fs_tl, 'Lamps', uv_tl)

    tl_edge = (
        [tl_surf_fn(i/nu_tl, 0, side) for i in range(nu_tl + 1)] +
        [tl_surf_fn(1, j/nv_tl, side) for j in range(1, nv_tl + 1)] +
        [tl_surf_fn(1 - i/nu_tl, 1, side) for i in range(1, nu_tl + 1)] +
        [tl_surf_fn(0, 1 - j/nv_tl, side) for j in range(1, nv_tl + 1)]
    )
    line('Taillight_Bezel', tl_edge, 0.003, 'Plastic', cyclic=True, res=1)

# Underbody Chassis Pan, Rear Fuel Cell / Diffuser Housing & Suspension Struts
cube('Underbody_Pan', (0.0, 0.0, 0.135), (1.42, 4.40, 0.04), 'Plastic', r=0.008)
cube('Rear_Fuel_Cell_Box', (0.0, 1.95, 0.24), (0.98, 0.65, 0.18), 'Plastic', r=0.015)
cube('Rear_Under_Diffuser', (0.0, 2.28, 0.22), (1.16, 0.36, 0.09), 'Plastic', r=0.012)
for xd in [-0.45, -0.22, 0.0, 0.22, 0.45]:
    cube('Diffuser_Fin', (xd, 2.32, 0.19), (0.012, 0.28, 0.075), 'Metal', r=0.002)
cyl('Rear_Live_Axle_Tube', (0.0, Y_REAR_AXLE, R_TIRE), 0.042, 1.35, 'Metal', axis='X', n=16, bev=0.002)
for side in [-1, 1]:
    tube_seg('Rear_Trailing_Arm', (side*0.48, 1.05, 0.20), (side*0.48, 2.25, 0.23), 0.016, 'Metal', n=10)

# ==============================================================================
# 6. FULL NASCAR TUBULAR ROLL CAGE & RACE INTERIOR
# ==============================================================================
cube('Interior_Floor_Tub', (0.0, -0.05, 0.18), (1.52, 1.95, 0.04), 'Interior', r=0.008)
cube('Transmission_Tunnel', (0.0, -0.05, 0.28), (0.28, 1.85, 0.20), 'Interior', r=0.03)
cube('Front_Firewall', (0.0, -0.82, 0.52), (1.50, 0.06, 0.62), 'Interior', r=0.01)
cube('Rear_Bulkhead', (0.0, 0.88, 0.56), (1.48, 0.06, 0.68), 'Interior', r=0.01)

for side in [-1, 1]:
    cube('Inner_Door_Card', (side * 0.78, -0.15, 0.48), (0.03, 1.35, 0.52), 'Interior', r=0.006)

cube('Race_Dashboard', (0.0, -0.62, 0.76), (1.42, 0.34, 0.14), 'Carbon', r=0.025)
cube('Instrument_Binnacle', (0.38, -0.52, 0.83), (0.38, 0.18, 0.11), 'Plastic', r=0.02)
for xg, rg in [(0.26, 0.038), (0.38, 0.048), (0.50, 0.038)]:
    cyl('Tach_Bezel', (xg, -0.425, 0.83), rg, 0.012, 'Metal', axis='Y', n=20, bev=0.002)
    cyl('Tach_Face', (xg, -0.420, 0.83), rg - 0.006, 0.008, 'Plastic', axis='Y', n=16, bev=0.001)
cube('Center_Switch_Panel', (0.05, -0.48, 0.72), (0.22, 0.12, 0.16), 'Carbon', r=0.01)
for ks in range(5):
    cyl('Toggle_Switch', (-0.02 + ks*0.035, -0.415, 0.74), 0.005, 0.018, 'Metal', axis='Y', n=8, bev=0.001)
tube_seg('Shifter_Lever', (0.12, -0.18, 0.36), (0.15, -0.24, 0.68), 0.012, 'Metal', n=10)
bpy.ops.mesh.primitive_uv_sphere_add(segments=14, ring_count=10, radius=0.028, location=(0.15, -0.24, 0.70))
add(bpy.context.object, 'Plastic')

sx, sy, sz = 0.38, -0.05, 0.32
cube('Race_Seat_Base', (sx, sy, sz), (0.46, 0.48, 0.10), 'Interior', r=0.025)
o_sb = cube('Race_Seat_Back', (sx, sy + 0.18, sz + 0.32), (0.44, 0.10, 0.62), 'Interior', r=0.025)
o_sb.rotation_euler[0] = -0.14
for bside in [-1, 1]:
    cube('Seat_Thigh_Bolster', (sx + bside*0.21, sy - 0.02, sz + 0.08), (0.07, 0.44, 0.14), 'Interior', r=0.02)
    o_rb = cube('Seat_Rib_Bolster', (sx + bside*0.21, sy + 0.12, sz + 0.30), (0.08, 0.22, 0.36), 'Interior', r=0.02)
    o_rb.rotation_euler[0] = -0.14
    cube('Seat_Halo_Ear', (sx + bside*0.20, sy + 0.12, sz + 0.56), (0.07, 0.28, 0.16), 'Interior', r=0.02)
cube('Seat_Headrest_Pad', (sx, sy + 0.21, sz + 0.56), (0.34, 0.08, 0.18), 'Interior', r=0.02)

r_cage = 0.022
b_hoop = [
    (-0.68, 0.22, 0.22),
    (-0.68, 0.22, 0.82),
    (-0.56, 0.22, 1.18),
    ( 0.56, 0.22, 1.18),
    ( 0.68, 0.22, 0.82),
    ( 0.68, 0.22, 0.22),
]
line('RollCage_Main_Hoop', b_hoop, r_cage, 'Metal', cyclic=False, res=1)
tube_seg('RollCage_Main_Diag', (-0.54, 0.22, 1.16), (0.66, 0.22, 0.32), r_cage * 0.9, 'Metal', n=12)
tube_seg('RollCage_Harness_Bar', (-0.66, 0.22, 0.76), (0.66, 0.22, 0.76), r_cage * 0.9, 'Metal', n=12)

for side in [-1, 1]:
    a_bar = [
        (side * 0.56, 0.22, 1.18),
        (side * 0.55, -0.22, 1.17),
        (side * 0.66, -0.68, 0.82),
        (side * 0.68, -0.72, 0.22),
    ]
    line('RollCage_A_Bar', a_bar, r_cage, 'Metal', cyclic=False, res=1)
    tube_seg('RollCage_A_Gusset', (side * 0.55, -0.26, 1.14), (side * 0.63, -0.56, 0.90), r_cage * 0.75, 'Metal', n=10)

    for z_db in [0.34, 0.50, 0.66]:
        db_pts = [
            (side * 0.68, -0.70, z_db),
            (side * 0.73, -0.24, z_db),
            (side * 0.68,  0.22, z_db),
        ]
        line('RollCage_Door_Bar', db_pts, r_cage * 0.88, 'Metal', cyclic=False, res=1)
    for y_sp in [-0.42, -0.06]:
        tube_seg('RollCage_Door_Post', (side * 0.71, y_sp, 0.32), (side * 0.71, y_sp, 0.68), r_cage * 0.75, 'Metal', n=8)

    tube_seg('RollCage_Rear_Strut', (side * 0.52, 0.22, 1.16), (side * 0.50, 1.18, 0.82), r_cage, 'Metal', n=12)
    tube_seg('RollCage_Quarter_Cross', (side * 0.66, 0.22, 0.80), (side * 0.50, 0.85, 0.94), r_cage * 0.85, 'Metal', n=10)

tube_seg('RollCage_Header_Bar', (-0.55, -0.22, 1.17), (0.55, -0.22, 1.17), r_cage, 'Metal', n=12)
tube_seg('RollCage_Roof_Diag', (-0.52, -0.20, 1.17), (0.52, 0.20, 1.18), r_cage * 0.8, 'Metal', n=10)
tube_seg('RollCage_Dash_Bar', (-0.66, -0.68, 0.80), (0.66, -0.68, 0.80), r_cage * 0.9, 'Metal', n=12)
tube_seg('RollCage_Rear_X1', (-0.50, 0.28, 1.12), (0.48, 1.12, 0.84), r_cage * 0.8, 'Metal', n=10)
tube_seg('RollCage_Rear_X2', ( 0.50, 0.28, 1.12), (-0.48, 1.12, 0.84), r_cage * 0.8, 'Metal', n=10)

# ==============================================================================
# 7. WHEELS (Wheel_FL, Wheel_FR, Wheel_RL, Wheel_RR) & STEERING WHEEL
# ==============================================================================
def lathe_x(name, xc, yc, zc, profile, mat, n=56):
    vs, fs, uv = [], [], []
    for i in range(n):
        a = 2.0 * pi * i / n
        ca, sa = cos(a), sin(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_pts = len(profile)
    for i in range(n):
        ni = (i + 1) % n
        for j in range(m_pts - 1):
            a = i * m_pts + j
            b = ni * m_pts + j
            fs.append((a, b, b + 1, a + 1))
    return mesh(name, vs, fs, mat, uv)

for is_front, yc in [(True, Y_FRONT_AXLE), (False, Y_REAR_AXLE)]:
    for side in [-1, 1]:
        w_name = 'Wheel_' + ('F' if is_front else 'R') + ('L' if side == 1 else 'R')
        current = w_name
        xc = side * 0.815
        zc = R_TIRE

        tire_prof = [
            (-0.145, 0.202),
            (-0.162, 0.235),
            (-0.168, 0.285),
            (-0.158, 0.335),
            (-0.135, 0.358),
            (-0.095, 0.3645),
            ( 0.000, 0.3650),
            ( 0.095, 0.3645),
            ( 0.135, 0.358),
            ( 0.158, 0.335),
            ( 0.168, 0.285),
            ( 0.162, 0.235),
            ( 0.145, 0.202),
        ]
        lathe_x(w_name + '_Slick_Tire', xc, yc, zc, tire_prof, 'Rubber', n=56)

        for r_rib in [0.255, 0.312]:
            lathe_x(w_name + '_Sidewall_Rib', xc, yc, zc, [
                (side * 0.165, r_rib - 0.003),
                (side * 0.169, r_rib),
                (side * 0.165, r_rib + 0.003),
            ], 'Rubber', n=48)

        s = side
        rim_prof = [
            (-s * 0.135, 0.195),
            ( s * 0.055, 0.195),
            ( s * 0.085, 0.202),
            ( s * 0.138, 0.202),
            ( s * 0.150, 0.210),
            ( s * 0.144, 0.214),
            ( s * 0.122, 0.205),
            ( s * 0.088, 0.190),
            ( s * 0.096, 0.148),
            ( s * 0.118, 0.115),
            ( s * 0.118, 0.072),
            ( s * 0.098, 0.055),
            ( s * 0.098, 0.025),
        ]
        if s == -1:
            rim_prof = list(reversed(rim_prof))
        lathe_x(w_name + '_Steel_Rim', xc, yc, zc, rim_prof, 'Metal', n=48)

        for kh in range(10):
            ah = 2.0 * pi * kh / 10.0
            yh = yc + 0.145 * sin(ah)
            zh = zc + 0.145 * cos(ah)
            xh = xc + s * 0.105
            cyl(w_name + '_DHole_Pocket', (xh, yh, zh), 0.021, 0.014, 'Plastic', axis='X', n=14, bev=0.001)
            pts_fl = [(xh + s*0.005, yh + 0.022*sin(2*pi*q/14), zh + 0.022*cos(2*pi*q/14)) for q in range(14)]
            line(w_name + '_DHole_Flange', pts_fl, 0.0035, 'Metal', cyclic=True, res=1)

        cyl(w_name + '_Hub_Ring', (xc + s * 0.112, yc, zc), 0.052, 0.024, 'Metal', axis='X', n=24, bev=0.002)
        cyl(w_name + '_Dust_Cap', (xc + s * 0.118, yc, zc), 0.036, 0.028, 'Plastic', axis='X', n=20, bev=0.002)

        for kl in range(5):
            al = 2.0 * pi * kl / 5.0
            yl = yc + 0.078 * sin(al)
            zl = zc + 0.078 * cos(al)
            cyl(w_name + '_Lug_Socket', (xc + s * 0.116, yl, zl), 0.012, 0.008, 'Plastic', axis='X', n=10, bev=0.001)
            cyl(w_name + '_Lug_Nut', (xc + s * 0.123, yl, zl), 0.0085, 0.016, 'Metal', axis='X', n=6, bev=0.001)

        cyl(w_name + '_Valve_Stem', (xc + s * 0.132, yc + 0.192, zc), 0.0045, 0.020, 'Metal', axis='Z', n=8, bev=0.0005)
        cyl(w_name + '_Brake_Rotor', (xc + s * 0.055, yc, zc), 0.172, 0.024, 'Metal', axis='X', n=36, bev=0.002)
        cyl(w_name + '_Rotor_Hat', (xc + s * 0.072, yc, zc), 0.095, 0.020, 'Plastic', axis='X', n=24, bev=0.002)
        cal_y = yc + (0.125 if is_front else -0.125)
        o_cal = cube(w_name + '_Brake_Caliper', (xc + s * 0.058, cal_y, zc + 0.02), (0.068, 0.085, 0.145), 'Lamps', r=0.012)
        set_uv_box(o_cal, 0.55, 0.05, 0.95, 0.45)

# SteeringWheel node
current = 'SteeringWheel'
SW_HUB = Vector((0.38, -0.34, 0.79))
sw_tilt = 0.24
pts_sw = []
for k in range(48):
    a = 2.0 * pi * k / 48.0
    dx = 0.165 * cos(a)
    dz = 0.165 * sin(a)
    dy = dz * sin(sw_tilt)
    dz_t = dz * cos(sw_tilt)
    pts_sw.append((SW_HUB.x + dx, SW_HUB.y + 0.035 + dy, SW_HUB.z + dz_t))
line('Steering_Rim', pts_sw, 0.016, 'Interior', cyclic=True, res=1)
pts_stripe = pts_sw[10:15]
o_str = line('Steering_Center_Stripe', pts_stripe, 0.0175, 'Lamps', cyclic=False, res=1)
set_uv_box(o_str, 0.55, 0.05, 0.95, 0.45)

cyl('Steering_Hub_Boss', SW_HUB, 0.048, 0.065, 'Metal', axis='Y', n=20, bev=0.002)
for a_sp in [0.0, pi, 1.5 * pi]:
    dx = 0.150 * cos(a_sp)
    dz = 0.150 * sin(a_sp)
    p_rim = (SW_HUB.x + dx, SW_HUB.y + 0.035 + dz * sin(sw_tilt), SW_HUB.z + dz * cos(sw_tilt))
    linkbar('Steering_Spoke', SW_HUB, p_rim, 0.026, 0.008, 'Metal', r=0.002)

current = 'Body'
tube_seg('Steering_Column_Shaft', (0.38, -0.65, 0.71), SW_HUB, 0.022, 'Metal', n=12)

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

# ==============================================================================
# 8. CONSOLIDATE INTO THE 6 MANDATED MESH NODES, SET PIVOTS & ENFORCE BUDGET
# ==============================================================================
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_WARN', 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_'):
        yc = Y_FRONT_AXLE if name[6] == 'F' else Y_REAR_AXLE
        xc = 0.815 if name.endswith('L') else -0.815
        origin = (xc, yc, R_TIRE)
    elif name == 'SteeringWheel':
        origin = tuple(SW_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)

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-11]
    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)

if raw_tris > 106000:
    body_ob = next(o for o in final if o.name == 'Body')
    activate(body_ob)
    md = body_ob.modifiers.new('BudgetDecimate', 'DECIMATE')
    md.ratio = max(0.1, (98000 - wheel_tris) / max(1, (raw_tris - wheel_tris)))
    bpy.ops.object.modifier_apply(modifier=md.name)

# Fit exact bounding box dimensions:
world_verts = [o.matrix_world @ v.co for o in final for v in o.data.vertices]
min_x = min(v.x for v in world_verts)
max_x = max(v.x for v in world_verts)
min_y = min(v.y for v in world_verts)
max_y = max(v.y for v in world_verts)
min_z = min(v.z for v in world_verts)
max_z = max(v.z for v in world_verts)
print('PRE_FIT_BBOX', (min_x, max_x), (min_y, max_y), (min_z, max_z), flush=True)

for o in final:
    inv = o.matrix_world.inverted()
    for v in o.data.vertices:
        w = o.matrix_world @ v.co
        if w.y < -1.85 and min_y < -1.85:
            w.y = -1.85 + (w.y + 1.85) * ((2.54 - 1.85) / (-min_y - 1.85))
        elif w.y > 1.85 and max_y > 1.85:
            w.y = 1.85 + (w.y - 1.85) * ((2.54 - 1.85) / (max_y - 1.85))
        if abs(w.x) > 0.85 and max_x > 0.85:
            s_x = 1.0 if w.x > 0 else -1.0
            w.x = s_x * (0.85 + (abs(w.x) - 0.85) * ((0.99 - 0.85) / (max_x - 0.85)))
        if w.z > 0.85 and max_z > 0.85:
            w.z = 0.85 + (w.z - 0.85) * ((1.30 - 0.85) / (max_z - 0.85))
        w.z = max(0.0, w.z)
        v.co = inv @ w
    o.data.update()

sockets = {
    'Seat_Driver':  ( 0.38,  0.02, 0.38),
    'Door_L':       ( 0.99, -0.10, 0.52),
    'Door_R':       (-0.99, -0.10, 0.52),
    'Exhaust':      (-0.985, 0.765, 0.095),
    'Light_Head_L': ( 0.56, -2.42, 0.42),
    'Light_Head_R': (-0.56, -2.42, 0.42),
    'Light_Tail_L': ( 0.70,  2.47, 0.71),
    'Light_Tail_R': (-0.70,  2.47, 0.71),
}
for s_name, pos in sockets.items():
    o = bpy.data.objects.new(s_name, None)
    o.empty_display_size = 0.12
    S.collection.objects.link(o)
    o.location = pos
    final.append(o)

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

verts_gl = [gl(o.matrix_world @ v.co) for o in final if o.type == 'MESH' for v in o.data.vertices]
bbox = {
    'min': [min(v[i] for v in verts_gl) for i in range(3)],
    'max': [max(v[i] for v in verts_gl) 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': 2.79,
    'paints': [{'name': n, 'linear_rgb': c} for n, c in PAINTS],
}
with open(os.path.join(ROOT, 'nodes.json'), 'w') as f:
    json.dump(manifest, f, indent=2)

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

glb_path = os.path.join(ROOT, 'stock-car-g4.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,
)

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:]
for mat in doc.get('materials', []):
    if mat.get('name') == 'Paint':
        mat.setdefault('pbrMetallicRoughness', {})['baseColorFactor'] = PAINTS[0][1] + [1.0]
        ext = mat.setdefault('extensions', {})
        ext['KHR_materials_clearcoat'] = {'clearcoatFactor': 1.0, 'clearcoatRoughnessFactor': 0.05}
        if 'KHR_materials_clearcoat' not in doc.setdefault('extensionsUsed', []):
            doc['extensionsUsed'].append('KHR_materials_clearcoat')
    elif mat.get('name') == 'Glass':
        mat['alphaMode'] = 'BLEND'
        mat['doubleSided'] = True

js = json.dumps(doc, separators=(',', ':')).encode('utf-8')
js += b' ' * ((-len(js)) % 4)
with open(glb_path, 'wb') as f:
    f.write(struct.pack('<III', 0x46546C67, 2, 12 + 8 + len(js) + len(binary)))
    f.write(struct.pack('<II', len(js), 0x4E4F534A))
    f.write(js)
    f.write(binary)

glb_mb = round(os.path.getsize(glb_path) / 1048576.0, 2)
print('EXPORT_READY', 'TRIS=', tris, 'MB=', glb_mb, 'BBOX=', bbox, flush=True)

# ==============================================================================
# 9. RENDERING: final.png, side.png, clay.png, paints.png
# ==============================================================================
p_bsdf = M['Paint'].node_tree.nodes.get('Principled BSDF')
l_paint = M['Paint'].node_tree.links
src_sock = p_bsdf.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_bsdf.inputs['Base Color'])

world = bpy.data.worlds.new('Studio_Daylight')
world.use_nodes = True
bg = world.node_tree.nodes['Background']
bg.inputs[0].default_value = (0.72, 0.76, 0.82, 1.0)
bg.inputs[1].default_value = 0.65
S.world = world

floor_mat = bpy.data.materials.new('Floor_Mat')
floor_mat.use_nodes = True
fp = floor_mat.node_tree.nodes.get('Principled BSDF')
fp.inputs['Base Color'].default_value = (0.38, 0.39, 0.41, 1.0)
fp.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 = 'Studio_Floor'
ground.data.materials.append(floor_mat)

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

add_area_light('Key_Overhead', (2.5, -3.8, 5.5), 480, 6.0, (0, 0, 0.5), 'RECTANGLE', 4.0)
add_area_light('Side_Fill',    (5.2, -1.0, 2.2), 320, 6.0, (0, 0, 0.55), 'RECTANGLE', 2.0)
add_area_light('Front_Fill',   (-3.5, -4.8, 2.6), 240, 4.5, (0, -0.5, 0.5), 'RECTANGLE', 2.0)
add_area_light('Rear_Rim',     (2.5,  4.8, 3.8), 320, 5.0, (0, 0.5, 0.6), 'RECTANGLE', 2.5)

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 set_camera(pos, target, lens=54, 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_img(name, w, h, samples=32):
    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
set_camera((4.8, -5.8, 2.15), (0.0, -0.05, 0.58), lens=54)
render_img('final.png', 1920, 1080, samples=44)

# 2. clay.png
clay_mat = bpy.data.materials.new('Clay_Diagnostic')
clay_mat.use_nodes = True
cp = clay_mat.node_tree.nodes.get('Principled BSDF')
cp.inputs['Base Color'].default_value = (0.45, 0.45, 0.46, 1.0)
cp.inputs['Roughness'].default_value = 0.62
S.view_layers[0].material_override = clay_mat
render_img('clay.png', 1920, 1080, samples=32)
S.view_layers[0].material_override = None

# 3. side.png
set_camera((7.0, 0.0, 0.65), (0.0, 0.0, 0.65), ortho=5.55)
render_img('side.png', 1920, 1080, samples=32)

# 4. paints.png
set_camera((4.8, -5.8, 2.15), (0.0, -0.05, 0.58), lens=54)
frames = []
for pname, pcol in PAINTS:
    mix_paint.inputs[2].default_value = (*pcol, 1.0)
    fn = f'paint_{pname}.png'
    render_img(fn, 640, 360, samples=16)
    im = bpy.data.images.load(os.path.join(ROOT, fn))
    frames.append(list(im.pixels))
    bpy.data.images.remove(im)

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

with open(os.path.join(ROOT, 'NOTES.md'), 'w') as f:
    f.write('American oval-track stock car (5.08 x 1.98 x 1.30 m, wheelbase 2.79 m); welded-door sedan race body, blank number circles, tall rear spoiler, splitter, window net, roll cage, single containment seat, 5-lug steel wheels, slicks, flat headlight decals.\n')
    f.write(f'{tris:,} triangles across 6 mesh nodes (Body, Wheel_FL/FR/RL/RR, SteeringWheel) and 8 empty gameplay sockets.\n')
    f.write('8 materials: Paint (clearcoat 1.0, neutral detail texture, default factor [0.02, 0.10, 0.45]), Rubber, Metal, Carbon, Plastic, Interior, Glass (BLEND), Lamps (emissive).\n')
    f.write(f'Cycles OptiX baked textures: Paint_Base 1024 px, other bases/normals <= 512 px, ORM maps <= 512 px; GLB size {glb_mb} MB.\n')
    f.write('Poly Haven CC0 ids: metal_plate, fabric_leather_01, rubber_tiles, fabric_pattern_07, leather_white (https://polyhaven.com/license).\n')
    f.write('Differences from reference: original teardrop-wedge headlight decal graphics, dual-slot upper/lower grilles and bonnet extractor vents, default deep blue paint with 6 colour variants, tyre lettering omitted per race rules.\n')

print('BUILD_COMPLETE', flush=True)
