import bpy
import bmesh
import json
import math
import os
import struct
import urllib.request
from math import sin, cos, pi, sqrt, exp, atan2
from mathutils import Vector, Matrix

ROOT = os.path.dirname(os.path.abspath(__file__))
os.chdir(ROOT)
os.makedirs(os.path.join(ROOT, 'textures'), exist_ok=True)

# Reset to clean empty scene
bpy.ops.wm.read_factory_settings(use_empty=True)
S = bpy.context.scene
S.unit_settings.system = 'METRIC'
S.unit_settings.scale_length = 1.0

# Configure Cycles with GPU (OptiX / CUDA)
S.render.engine = 'CYCLES'
S.cycles.samples = 16
S.cycles.use_denoising = True
try:
    prefs = bpy.context.preferences.addons['cycles'].preferences
    for backend in ('OPTIX', 'CUDA'):
        try:
            prefs.compute_device_type = backend
            prefs.get_devices()
            devs = [d for d in prefs.devices if d.type != 'CPU']
            if devs:
                for d in prefs.devices:
                    d.use = (d.type != 'CPU')
                S.cycles.device = 'GPU'
                print('GPU_BACKEND', backend, [d.name for d in devs], flush=True)
                break
        except Exception:
            pass
except Exception as e:
    print('GPU_SETUP_FALLBACK', str(e), flush=True)

# ==============================================================================
# 1. DOWNLOAD / LOAD CC0 POLY HAVEN TEXTURES & CYCLES BAKE (8 MATERIALS MAX)
# ==============================================================================
POLY_IDS = ['metal_plate', 'asphalt_02', 'denim_fabric', 'fabric_leather_02']


def fetch_polyhaven(tid, map_kind, res='1k'):
    fn = os.path.join(ROOT, 'textures', f'{tid}_{map_kind}_{res}.jpg')
    if not os.path.exists(fn) or os.path.getsize(fn) < 1024:
        url = f'https://dl.polyhaven.org/file/ph-assets/Textures/jpg/{res}/{tid}/{tid}_{map_kind}_{res}.jpg'
        urllib.request.urlretrieve(url, fn)
    im = bpy.data.images.load(fn, check_existing=True)
    im.colorspace_settings.name = 'sRGB' if map_kind == 'diff' else 'Non-Color'
    return im


# Six linear RGB paints (default [0.10, 0.20, 0.55] Electric Blue first per Paint Contract)
PAINTS = [
    ('ElectricBlue',    [0.10, 0.20, 0.55]),
    ('TitaniumSilver',  [0.36, 0.39, 0.43]),
    ('CrimsonProto',    [0.58, 0.025, 0.018]),
    ('PhotonEmerald',   [0.018, 0.24, 0.11]),
    ('TurbineOrange',   [0.72, 0.24, 0.015]),
    ('StealthGunmetal', [0.035, 0.040, 0.048]),
]

M = {}


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


def bake_material(name, base_rgb, metal, rough, ph_id, scan_strength, res_main, res_orm):
    src = bpy.data.materials.new(name + '_Src')
    src.use_nodes = True
    n = src.node_tree.nodes
    l = src.node_tree.links
    p = n.get('Principled BSDF')
    out = n.get('Material Output')
    p.inputs['Metallic'].default_value = metal

    tc = n.new('ShaderNodeTexCoord')
    mp = n.new('ShaderNodeMapping')
    mp.inputs['Scale'].default_value = (8.0, 8.0, 8.0) if name == 'CarbonFibre' else (4.0, 4.0, 4.0)
    l.new(tc.outputs['UV'], mp.inputs['Vector'])

    td = n.new('ShaderNodeTexImage')
    td.image = fetch_polyhaven(ph_id, 'diff')
    tn = n.new('ShaderNodeTexImage')
    tn.image = fetch_polyhaven(ph_id, 'nor_gl')
    ta = n.new('ShaderNodeTexImage')
    ta.image = fetch_polyhaven(ph_id, 'arm')
    for tnode in (td, tn, ta):
        l.new(mp.outputs['Vector'], tnode.inputs['Vector'])

    # Convert Poly Haven diffuse to grayscale detail factor so dark materials stay deep dark!
    bw = n.new('ShaderNodeRGBToBW')
    l.new(td.outputs['Color'], bw.inputs['Color'])

    noise_grime = n.new('ShaderNodeTexNoise')
    noise_grime.inputs['Scale'].default_value = 8.5
    noise_grime.inputs['Detail'].default_value = 6.0
    noise_grime.inputs['Roughness'].default_value = 0.65
    l.new(tc.outputs['UV'], noise_grime.inputs['Vector'])

    noise_wear = n.new('ShaderNodeTexNoise')
    noise_wear.inputs['Scale'].default_value = 48.0
    noise_wear.inputs['Detail'].default_value = 4.0
    l.new(tc.outputs['UV'], noise_wear.inputs['Vector'])

    if name == 'Paint':
        # Paint contract: base colour texture holds ONLY light neutral detail (near white with grey grime, dust and wear baked in)
        ramp_grime = n.new('ShaderNodeValToRGB')
        ramp_grime.color_ramp.elements[0].position = 0.15
        ramp_grime.color_ramp.elements[0].color = (0.78, 0.79, 0.81, 1.0)
        ramp_grime.color_ramp.elements[1].position = 0.55
        ramp_grime.color_ramp.elements[1].color = (0.98, 0.98, 0.99, 1.0)
        l.new(noise_grime.outputs['Fac'], ramp_grime.inputs['Fac'])

        scan_ramp = n.new('ShaderNodeValToRGB')
        scan_ramp.color_ramp.elements[0].position = 0.0
        scan_ramp.color_ramp.elements[0].color = (0.86, 0.87, 0.89, 1.0)
        scan_ramp.color_ramp.elements[1].position = 1.0
        scan_ramp.color_ramp.elements[1].color = (1.00, 1.00, 1.00, 1.0)
        l.new(bw.outputs['Val'], scan_ramp.inputs['Fac'])

        mul_p = n.new('ShaderNodeMixRGB')
        mul_p.blend_type = 'MULTIPLY'
        mul_p.inputs['Fac'].default_value = 0.65
        l.new(ramp_grime.outputs['Color'], mul_p.inputs[1])
        l.new(scan_ramp.outputs['Color'], mul_p.inputs[2])
        l.new(mul_p.outputs['Color'], p.inputs['Base Color'])
    elif name == 'CarbonFibre':
        chk = n.new('ShaderNodeTexChecker')
        chk.inputs['Scale'].default_value = 110.0
        chk.inputs['Color1'].default_value = (0.015, 0.017, 0.020, 1.0)
        chk.inputs['Color2'].default_value = (0.042, 0.046, 0.052, 1.0)
        l.new(mp.outputs['Vector'], chk.inputs['Vector'])

        scan_ramp = n.new('ShaderNodeValToRGB')
        scan_ramp.color_ramp.elements[0].color = (0.75, 0.75, 0.78, 1.0)
        scan_ramp.color_ramp.elements[1].color = (1.15, 1.15, 1.18, 1.0)
        l.new(bw.outputs['Val'], scan_ramp.inputs['Fac'])

        mul_c = n.new('ShaderNodeMixRGB')
        mul_c.blend_type = 'MULTIPLY'
        mul_c.inputs['Fac'].default_value = 0.85
        l.new(chk.outputs['Color'], mul_c.inputs[1])
        l.new(scan_ramp.outputs['Color'], mul_c.inputs[2])
        l.new(mul_c.outputs['Color'], p.inputs['Base Color'])
    else:
        col_ramp = n.new('ShaderNodeValToRGB')
        low_f = 1.0 - scan_strength
        high_f = 1.0 + scan_strength * 0.5
        col_ramp.color_ramp.elements[0].position = 0.15
        col_ramp.color_ramp.elements[0].color = (*(c * low_f for c in base_rgb), 1.0)
        col_ramp.color_ramp.elements[1].position = 0.85
        col_ramp.color_ramp.elements[1].color = (*(min(1.0, c * high_f) for c in base_rgb), 1.0)
        l.new(bw.outputs['Val'], col_ramp.inputs['Fac'])

        wear_ramp = n.new('ShaderNodeValToRGB')
        wear_ramp.color_ramp.elements[0].position = 0.20
        wear_ramp.color_ramp.elements[0].color = (0.76, 0.76, 0.78, 1.0)
        wear_ramp.color_ramp.elements[1].position = 0.75
        wear_ramp.color_ramp.elements[1].color = (1.05, 1.05, 1.05, 1.0)
        l.new(noise_grime.outputs['Fac'], wear_ramp.inputs['Fac'])

        mul_w = n.new('ShaderNodeMixRGB')
        mul_w.blend_type = 'MULTIPLY'
        mul_w.inputs['Fac'].default_value = 0.45
        l.new(col_ramp.outputs['Color'], mul_w.inputs[1])
        l.new(wear_ramp.outputs['Color'], mul_w.inputs[2])
        l.new(mul_w.outputs['Color'], p.inputs['Base Color'])

    # Roughness & ARM extraction
    sep_arm = n.new('ShaderNodeSeparateColor')
    l.new(ta.outputs['Color'], sep_arm.inputs['Color'])
    math_r = n.new('ShaderNodeMath')
    math_r.operation = 'MULTIPLY_ADD'
    math_r.inputs[1].default_value = 0.12 if name != 'Paint' else 0.05
    math_r.inputs[2].default_value = rough
    l.new(sep_arm.outputs['Green'], math_r.inputs[0])
    l.new(math_r.outputs['Value'], p.inputs['Roughness'])

    # Normal map + micro bump
    nm_node = n.new('ShaderNodeNormalMap')
    nm_node.inputs['Strength'].default_value = 0.03 if name == 'Paint' else (0.35 if name == 'CarbonFibre' else 0.22)
    l.new(tn.outputs['Color'], nm_node.inputs['Color'])

    bump = n.new('ShaderNodeBump')
    bump.inputs['Strength'].default_value = 0.02 if name == 'Paint' else 0.10
    bump.inputs['Distance'].default_value = 0.001
    l.new(noise_wear.outputs['Fac'], bump.inputs['Height'])
    l.new(nm_node.outputs['Normal'], bump.inputs['Normal'])
    l.new(bump.outputs['Normal'], p.inputs['Normal'])

    # Bake plane
    bpy.ops.mesh.primitive_plane_add(size=2.0)
    ob = bpy.context.object
    ob.data.materials.append(src)
    activate(ob)

    baked_imgs = {}
    for map_label, r_px in [('Base', res_main), ('Normal', res_main), ('ORM', res_orm)]:
        img = bpy.data.images.new(f'{name}_{map_label}', width=r_px, height=r_px, alpha=False)
        img.colorspace_settings.name = 'sRGB' if map_label == 'Base' else 'Non-Color'
        inode = n.new('ShaderNodeTexImage')
        inode.image = img
        for nd in n:
            nd.select = False
        inode.select = True
        n.active = inode

        if map_label == 'Base':
            em = n.new('ShaderNodeEmission')
            l.new(p.inputs['Base Color'].links[0].from_socket, em.inputs['Color'])
            l.new(em.outputs['Emission'], out.inputs['Surface'])
            bpy.ops.object.bake(type='EMIT')
            l.new(p.outputs['BSDF'], out.inputs['Surface'])
            n.remove(em)
        elif map_label == 'Normal':
            bpy.ops.object.bake(type='NORMAL')
        else:
            em = n.new('ShaderNodeEmission')
            comb = n.new('ShaderNodeCombineColor')
            l.new(sep_arm.outputs['Red'], comb.inputs['Red'])
            l.new(math_r.outputs['Value'], comb.inputs['Green'])
            comb.inputs['Blue'].default_value = metal
            l.new(comb.outputs['Color'], em.inputs['Color'])
            l.new(em.outputs['Emission'], out.inputs['Surface'])
            bpy.ops.object.bake(type='EMIT')
            l.new(p.outputs['BSDF'], out.inputs['Surface'])
            n.remove(em)

        img_path = os.path.join(ROOT, 'textures', f'{name}_{map_label}.png')
        img.filepath_raw = img_path
        img.file_format = 'PNG'
        img.save()
        baked_imgs[map_label] = img

    bpy.data.objects.remove(ob, do_unlink=True)
    bpy.data.materials.remove(src, do_unlink=True)

    # Create final clean glTF-ready PBR material
    mat = bpy.data.materials.new(name)
    mat.use_nodes = True
    mn = mat.node_tree.nodes
    ml = mat.node_tree.links
    mp_bsdf = mn.get('Principled BSDF')

    for map_label, img in baked_imgs.items():
        t = mn.new('ShaderNodeTexImage')
        t.image = img
        t.label = map_label
        if map_label == 'Base':
            ml.new(t.outputs['Color'], mp_bsdf.inputs['Base Color'])
        elif map_label == 'Normal':
            nm = mn.new('ShaderNodeNormalMap')
            nm.inputs['Strength'].default_value = 0.04 if name == 'Paint' else 0.45
            ml.new(t.outputs['Color'], nm.inputs['Color'])
            ml.new(nm.outputs['Normal'], mp_bsdf.inputs['Normal'])
        elif map_label == 'ORM':
            sep = mn.new('ShaderNodeSeparateColor')
            ml.new(t.outputs['Color'], sep.inputs['Color'])
            ml.new(sep.outputs['Green'], mp_bsdf.inputs['Roughness'])
            ml.new(sep.outputs['Blue'], mp_bsdf.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 = mn.new('ShaderNodeGroup')
            gn.node_tree = group
            ml.new(sep.outputs['Red'], gn.inputs['Occlusion'])

    if name == 'Paint':
        mp_bsdf.inputs['Coat Weight'].default_value = 1.0
        mp_bsdf.inputs['Coat Roughness'].default_value = 0.05
    elif name == 'CarbonFibre':
        mp_bsdf.inputs['Coat Weight'].default_value = 0.65
        mp_bsdf.inputs['Coat Roughness'].default_value = 0.12

    M[name] = mat
    print('BAKED_MATERIAL', name, flush=True)


# Bake the 6 opaque PBR materials (Game Weight: Paint 1024px/512px ORM, others 512px/256px ORM)
# Notice the rich, dark realistic values for CarbonFibre, MattePlastic, Rubber, and Interior matching sheet.jpg swatches!
MAT_SPECS = [
    ('Paint',        (0.96, 0.97, 0.98),   0.38, 0.16, 'metal_plate',       0.12, 1024, 512),
    ('CarbonFibre',  (0.024, 0.026, 0.030), 0.25, 0.22, 'denim_fabric',     0.28, 512,  256),
    ('MetalChrome',  (0.76, 0.80, 0.85),   0.92, 0.15, 'metal_plate',       0.20, 512,  256),
    ('MattePlastic', (0.022, 0.024, 0.028), 0.06, 0.62, 'asphalt_02',       0.22, 512,  256),
    ('Rubber',       (0.016, 0.017, 0.019), 0.00, 0.72, 'asphalt_02',       0.25, 512,  256),
    ('Interior',     (0.032, 0.034, 0.040), 0.02, 0.74, 'fabric_leather_02', 0.30, 512, 256),
]
for spec in MAT_SPECS:
    bake_material(*spec)

# Material 7: "Glass" (alpha BLEND, smoked dark tint, low roughness, transparent canopy & engine cover)
glass_mat = bpy.data.materials.new('Glass')
glass_mat.use_nodes = True
if hasattr(glass_mat, 'surface_render_method'):
    glass_mat.surface_render_method = 'BLENDED'
elif hasattr(glass_mat, 'blend_method'):
    glass_mat.blend_method = 'BLEND'
gp = glass_mat.node_tree.nodes.get('Principled BSDF')
gp.inputs['Base Color'].default_value = (0.025, 0.035, 0.048, 1.0)
gp.inputs['Metallic'].default_value = 0.12
gp.inputs['Roughness'].default_value = 0.035
gp.inputs['IOR'].default_value = 1.52
gp.inputs['Alpha'].default_value = 0.42
M['Glass'] = glass_mat

# Material 8: "Lamps" (Cycles-baked multi-band emissive atlas:
#   band 0 (u in 0.00..0.25): Warm White Headlights [1.0, 0.95, 0.84]
#   band 1 (u in 0.25..0.50): Glowing Turbine Intake Orange / Amber Indicators [1.0, 0.30, 0.015]
#   band 2 (u in 0.50..0.75): Glowing Rear Thruster Cyan-Blue Inner Ring [0.03, 0.76, 1.00]
#   band 3 (u in 0.75..1.00): Tail & Brake Light Red [1.0, 0.02, 0.015]
lamps_src = bpy.data.materials.new('Lamps_Src')
lamps_src.use_nodes = True
ln = lamps_src.node_tree.nodes
ll = lamps_src.node_tree.links
l_out = ln.get('Material Output')
l_em = ln.new('ShaderNodeEmission')
l_tc = ln.new('ShaderNodeTexCoord')
l_sep = ln.new('ShaderNodeSeparateXYZ')
ll.new(l_tc.outputs['UV'], l_sep.inputs['Vector'])

l_ramp = ln.new('ShaderNodeValToRGB')
l_ramp.color_ramp.interpolation = 'CONSTANT'
l_ramp.color_ramp.elements[0].position = 0.00
l_ramp.color_ramp.elements[0].color = (1.00, 0.95, 0.84, 1.0)  # Warm White
l_ramp.color_ramp.elements[1].position = 0.25
l_ramp.color_ramp.elements[1].color = (1.00, 0.28, 0.01, 1.0)  # Glowing Turbine Orange / Amber
e2 = l_ramp.color_ramp.elements.new(0.50)
e2.color = (0.02, 0.76, 1.00, 1.0)  # Thruster Cyan-Blue
e3 = l_ramp.color_ramp.elements.new(0.75)
e3.color = (1.00, 0.02, 0.01, 1.0)  # Tail Light Red
ll.new(l_sep.outputs['X'], l_ramp.inputs['Fac'])
ll.new(l_ramp.outputs['Color'], l_em.inputs['Color'])
ll.new(l_em.outputs['Emission'], l_out.inputs['Surface'])

bpy.ops.mesh.primitive_plane_add(size=2.0)
lob = bpy.context.object
lob.data.materials.append(lamps_src)
activate(lob)
lamp_img = bpy.data.images.new('Lamps_Base', width=256, height=64, alpha=False)
lamp_img.colorspace_settings.name = 'sRGB'
linode = ln.new('ShaderNodeTexImage')
linode.image = lamp_img
for nd in ln:
    nd.select = False
linode.select = True
ln.active = linode
bpy.ops.object.bake(type='EMIT')
lamp_img.filepath_raw = os.path.join(ROOT, 'textures', 'Lamps_Base.png')
lamp_img.file_format = 'PNG'
lamp_img.save()
bpy.data.objects.remove(lob, do_unlink=True)
bpy.data.materials.remove(lamps_src, do_unlink=True)

lamps_mat = bpy.data.materials.new('Lamps')
lamps_mat.use_nodes = True
lmn = lamps_mat.node_tree.nodes
lml = lamps_mat.node_tree.links
lp_bsdf = lmn.get('Principled BSDF')
lt_node = lmn.new('ShaderNodeTexImage')
lt_node.image = lamp_img
lt_node.interpolation = 'Closest'
lml.new(lt_node.outputs['Color'], lp_bsdf.inputs['Base Color'])
lml.new(lt_node.outputs['Color'], lp_bsdf.inputs['Emission Color'])
lp_bsdf.inputs['Emission Strength'].default_value = 5.5
lp_bsdf.inputs['Roughness'].default_value = 0.10
M['Lamps'] = lamps_mat

LAMP_BANDS = {'white': 0.125, 'orange': 0.375, 'cyan': 0.625, 'red': 0.875}


def set_lamp_band(obj, band='white'):
    u_val = LAMP_BANDS.get(band, 0.125)
    if not obj.data.uv_layers:
        obj.data.uv_layers.new(name='UVMap')
    uv_data = obj.data.uv_layers.active.data
    for poly in obj.data.polygons:
        for li in poly.loop_indices:
            uv_data[li].uv = (u_val, 0.5)
    return obj


# ==============================================================================
# 2. GEOMETRY HELPERS & NODE COLLECTION GROUPS
# ==============================================================================
groups = {
    'Body': [],
    'Wheel_FL': [],
    'Wheel_FR': [],
    'Wheel_RL': [],
    'Wheel_RR': [],
    'SteeringWheel': [],
}
current_group = 'Body'


def smooth_interp(x, knots):
    if x <= knots[0][0]:
        return knots[0][1]
    if x >= knots[-1][0]:
        return knots[-1][1]
    for i in range(len(knots) - 1):
        a, va = knots[i]
        b, vb = knots[i + 1]
        if a <= x <= b:
            t = (x - a) / (b - a)
            m0 = (vb - knots[max(0, i - 1)][1]) / (b - knots[max(0, i - 1)][0])
            m1 = (knots[min(len(knots) - 1, i + 2)][1] - va) / (knots[min(len(knots) - 1, i + 2)][0] - a)
            return (
                (2 * t**3 - 3 * t**2 + 1) * va
                + (t**3 - 2 * t**2 + t) * (b - a) * m0
                + (-2 * t**3 + 3 * t**2) * vb
                + (t**3 - t**2) * (b - a) * m1
            )
    return knots[-1][1]


def reg(obj, mat_name):
    me = obj.data
    if me and hasattr(me, 'materials'):
        me.materials.clear()
        me.materials.append(M[mat_name])
        for p in me.polygons:
            p.use_smooth = True
        if not me.uv_layers:
            me.uv_layers.new(name='UVMap')
            uv_layer = me.uv_layers.active.data
            for poly in me.polygons:
                nrm = poly.normal
                for li in poly.loop_indices:
                    co = me.vertices[me.loops[li].vertex_index].co
                    if abs(nrm.z) >= abs(nrm.x) and abs(nrm.z) >= abs(nrm.y):
                        uv_layer[li].uv = (co.x * 0.5 + 0.5, co.y * 0.25 + 0.5)
                    elif abs(nrm.x) >= abs(nrm.y):
                        uv_layer[li].uv = (co.y * 0.25 + 0.5, co.z * 0.5 + 0.2)
                    else:
                        uv_layer[li].uv = (co.x * 0.5 + 0.5, co.z * 0.5 + 0.2)
    groups[current_group].append(obj)
    return obj


def add_bevel(obj, width=0.004, segs=1):
    md = obj.modifiers.new('Bevel', 'BEVEL')
    md.width = width
    md.segments = segs
    md.limit_method = 'ANGLE'
    md.angle_limit = 0.55
    return obj


def add_solidify(obj, thickness=0.010):
    md = obj.modifiers.new('Solidify', 'SOLIDIFY')
    md.thickness = thickness
    md.offset = 0.0
    md.use_even_offset = True
    return obj


def mesh_from_vf(name, verts, faces, mat_name, uvs=None, th=0.0, bev=0.0, bev_segs=1):
    me = bpy.data.meshes.new(name)
    me.from_pydata(verts, [], faces)
    me.update()
    if uvs and len(uvs) == len(verts):
        uv_layer = me.uv_layers.new(name='UVMap').data
        for poly in me.polygons:
            for li in poly.loop_indices:
                vi = me.loops[li].vertex_index
                uv_layer[li].uv = uvs[vi]
    obj = bpy.data.objects.new(name, me)
    S.collection.objects.link(obj)
    reg(obj, mat_name)
    if th > 0.0:
        add_solidify(obj, th)
    if bev > 0.0:
        add_bevel(obj, bev, bev_segs)
    return obj


def surf_grid(name, fn, nu, nv, mat_name, closed_u=False, closed_v=False, th=0.0, bev=0.0, flip=False):
    verts = []
    uvs = []
    faces = []
    u_steps = nu if closed_u else nu + 1
    v_steps = nv if closed_v else nv + 1
    for i in range(u_steps):
        u = i / nu
        for j in range(v_steps):
            v = j / nv
            verts.append(fn(u, v))
            uvs.append((u, v))
    for i in range(nu):
        i1 = (i + 1) % u_steps if closed_u else i + 1
        for j in range(nv):
            j1 = (j + 1) % v_steps if closed_v else j + 1
            a = i * v_steps + j
            b = i1 * v_steps + j
            c = i1 * v_steps + j1
            d = i * v_steps + j1
            faces.append((a, d, c, b) if flip else (a, b, c, d))
    return mesh_from_vf(name, verts, faces, mat_name, uvs=uvs, th=th, bev=bev)


def box(name, loc, dims, mat_name, bev=0.004, rot=(0.0, 0.0, 0.0)):
    bpy.ops.mesh.primitive_cube_add(size=1.0, location=loc, rotation=rot)
    o = bpy.context.object
    o.name = name
    o.scale = (dims[0] * 0.5, dims[1] * 0.5, dims[2] * 0.5)
    bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
    reg(o, mat_name)
    if bev > 0:
        add_bevel(o, bev, 1)
    return o


def cyl(name, loc, rad, depth, mat_name, axis='Z', verts=20, bev=0.002):
    rot = {'X': (0.0, pi * 0.5, 0.0), 'Y': (pi * 0.5, 0.0, 0.0), 'Z': (0.0, 0.0, 0.0)}[axis]
    bpy.ops.mesh.primitive_cylinder_add(vertices=verts, radius=rad, depth=depth, location=loc, rotation=rot)
    o = bpy.context.object
    o.name = name
    reg(o, mat_name)
    if bev > 0:
        add_bevel(o, bev, 1)
    return o


def spline_tube(name, pts, rad, mat_name, cyclic=False, res_u=6, bev_res=1, rad_end=None):
    cu = bpy.data.curves.new(name + '_Curve', 'CURVE')
    cu.dimensions = '3D'
    cu.resolution_u = res_u
    cu.bevel_depth = rad
    cu.bevel_resolution = bev_res
    cu.use_fill_caps = True
    use_nurbs = len(pts) >= 3
    sp = cu.splines.new('NURBS' if use_nurbs else 'POLY')
    sp.points.add(len(pts) - 1)
    for i, pt in enumerate(pts):
        sp.points[i].co = (pt[0], pt[1], pt[2], 1.0)
        if rad_end is not None and len(pts) > 1:
            t = i / (len(pts) - 1)
            sp.points[i].radius = (1.0 - t) + t * (rad_end / max(1e-5, rad))
    sp.use_cyclic_u = cyclic
    if use_nurbs:
        sp.use_endpoint_u = not cyclic
        sp.order_u = min(4, len(pts))
    ob = bpy.data.objects.new(name, cu)
    S.collection.objects.link(ob)
    activate(ob)
    bpy.ops.object.convert(target='MESH')
    o = bpy.context.object
    o.name = name
    return reg(o, mat_name)


def strut_bar(name, p0, p1, r0, r1, mat_name, segs=8):
    v0, v1 = Vector(p0), Vector(p1)
    dv = v1 - v0
    length = dv.length
    if length < 1e-5:
        return None
    mid = (v0 + v1) * 0.5
    bpy.ops.mesh.primitive_cone_add(vertices=segs, radius1=r0, radius2=r1, depth=length, location=mid)
    o = bpy.context.object
    o.name = name
    o.rotation_euler = dv.to_track_quat('Z', 'Y').to_euler()
    return reg(o, mat_name)


# ==============================================================================
# 3. SCULPTED FUTURISTIC WEDGE BODY SHELL ("Paint")
# ==============================================================================
# Dimensions: Length 4.70 m (Y in [-2.35, +2.35]), Width 2.10 m (X in [-1.05, +1.05]),
# Height 1.10 m (Z in [0.00, +1.10]), Wheelbase 2.90 m (Y_F = -1.45, Y_R = +1.45).
# Front wheel radius = 0.370 m, Rear wheel radius = 0.385 m.

Y_FRONT_AXLE = -1.45
Y_REAR_AXLE = 1.45
R_WHEEL_F = 0.370
R_WHEEL_R = 0.385


def body_half_width(y):
    return smooth_interp(y, [
        (-2.30, 0.54),
        (-2.15, 0.86),
        (-1.85, 0.98),
        (-1.45, 1.035),  # Front wheel arch flare
        (-0.98, 0.97),
        (-0.45, 0.93),   # Taut door flank
        (0.15,  0.88),   # Waist ahead of side turbine intake
        (0.65,  0.98),   # Flare behind side turbine intake
        (1.10,  1.035),
        (1.45,  1.050),  # Rear wheel haunch flare (2.10 m full width)
        (1.95,  0.99),
        (2.26,  0.91),
    ])


def center_spine_z(y):
    return smooth_interp(y, [
        (-2.30, 0.34),   # Sharp wedge nose leading edge
        (-2.05, 0.43),
        (-1.60, 0.52),   # Low plunging central hood V-channel
        (-1.12, 0.61),   # Base of windshield cowl
        (-0.55, 0.64),
        (0.10,  0.66),
        (0.60,  0.76),   # Forward edge of rear engine bay
        (1.25,  0.77),
        (1.85,  0.81),
        (2.26,  0.83),   # Rear fascia upper deck
    ])


def fender_crest_z(y):
    return smooth_interp(y, [
        (-2.30, 0.35),
        (-2.00, 0.56),
        (-1.45, 0.775),  # Muscular front wheel arch crest
        (-0.92, 0.68),
        (-0.30, 0.70),   # Door shoulder beltline
        (0.30,  0.76),   # Sweeping side shoulder over turbine intake
        (0.90,  0.85),
        (1.45,  0.905),  # Muscular rear haunch crest
        (1.95,  0.865),
        (2.26,  0.83),
    ])


def surface_top_z(x, y):
    w = max(0.20, body_half_width(y))
    t = min(1.0, abs(x) / (w * 0.97))
    zc = center_spine_z(y)
    zf = fender_crest_z(y)
    fender_blend = (t ** 2.1) * (3.0 - 2.0 * (t ** 1.05))
    crown = 0.022 * sin(pi * t)
    z = zc + (zf - zc) * fender_blend + crown
    if t > 0.88:
        z -= 0.022 * ((t - 0.88) / 0.12) ** 2
    return z


def wheel_arch_cut_z(y):
    z_sill = smooth_interp(y, [
        (-2.30, 0.33),   # Nose overhang above front radiator grille mouth!
        (-2.02, 0.31),
        (-1.90, 0.13),
        (-1.00, 0.135),
        (0.00,  0.135),
        (0.95,  0.145),
        (1.92,  0.18),
        (2.26,  0.39),
    ])
    df = abs(y - Y_FRONT_AXLE)
    if df < 0.405:
        z_sill = max(z_sill, R_WHEEL_F + sqrt(max(0.0, 0.405**2 - df**2)))
    dr = abs(y - Y_REAR_AXLE)
    if dr < 0.420:
        z_sill = max(z_sill, R_WHEEL_R + sqrt(max(0.0, 0.420**2 - dr**2)))
    return z_sill


# Build continuous, smooth sculpted wedge body shell (NO jagged quad deletions!)
N_Y = 128
Y_START = -2.30
Y_END = 2.26
shell_verts = []
shell_uvs = []
shell_faces = []
K_SIDE = 7
K_TOP = 20
K_TOTAL = K_SIDE + (K_TOP + 1) + K_SIDE  # 35 vertices per cross-section ring

for i in range(N_Y + 1):
    y = Y_START + (Y_END - Y_START) * (i / N_Y)
    w = body_half_width(y)
    zf = fender_crest_z(y)
    z_low = wheel_arch_cut_z(y)

    # Smooth Coke-bottle side door channel feeding into the exposed side turbine intake (Y in [-0.90, +0.64])
    intake_pinch = 0.0
    if -0.90 < y < 0.66:
        tp = (y - (-0.90)) / 1.56
        scoop_depth = 0.24 * exp(-(((y - 0.36) / 0.22) ** 2))
        intake_pinch = 0.08 * sin(pi * tp) + scoop_depth

    ring = []
    # Right side wall (-X): from bottom arch/sill up to fender shoulder
    for j in range(K_SIDE):
        t = j / K_SIDE
        ang = t * pi * 0.5
        undercut = intake_pinch * (sin(pi * t) ** 0.78)
        # Sculpted diagonal character wedge ridge along the lower door skin
        ridge = 0.0
        if -0.88 < y < 0.22:
            t_ridge = 0.22 + 0.38 * ((y - (-0.88)) / 1.10)
            ridge = 0.016 * exp(-(((t - t_ridge) / 0.14) ** 2))
        x = -w * (0.94 + 0.06 * sin(ang)) + undercut - ridge
        z = z_low + (zf - 0.022 - z_low) * (t ** 0.92)
        ring.append((x, y, z))

    # Upper deck / hood / fender-to-fender profile (-X to +X)
    for j in range(K_TOP + 1):
        t = -1.0 + 2.0 * (j / K_TOP)
        x = w * 0.97 * t
        z = surface_top_z(x, y)
        # Smooth depression for cockpit interior tub strictly inside the canopy footprint (-0.98 < y < 0.44, |x| < 0.60)
        if -0.98 < y < 0.44 and abs(x) < 0.60:
            cockpit_dip = 0.32 * sin(pi * ((y - (-0.98)) / 1.42)) * (1.0 - (abs(x) / 0.60) ** 2)
            z -= cockpit_dip
        # Smooth depression for rear hexagonal engine bay (0.60 < y < 1.64, |x| < 0.35)
        elif 0.60 < y < 1.64 and abs(x) < 0.35:
            eng_dip = 0.11 * sin(pi * ((y - 0.60) / 1.04)) * (1.0 - (abs(x) / 0.35) ** 2)
            z -= eng_dip
        ring.append((x, y, z))

    # Left side wall (+X): from fender shoulder down to bottom arch/sill
    for j in range(1, K_SIDE + 1):
        t = 1.0 - (j / K_SIDE)
        ang = t * pi * 0.5
        undercut = intake_pinch * (sin(pi * t) ** 0.78)
        ridge = 0.0
        if -0.88 < y < 0.22:
            t_ridge = 0.22 + 0.38 * ((y - (-0.88)) / 1.10)
            ridge = 0.016 * exp(-(((t - t_ridge) / 0.14) ** 2))
        x = w * (0.94 + 0.06 * sin(ang)) - undercut + ridge
        z = z_low + (zf - 0.022 - z_low) * (t ** 0.92)
        ring.append((x, y, z))

    for j, pt in enumerate(ring):
        shell_verts.append(pt)
        shell_uvs.append((j / (K_TOTAL - 1), i / N_Y))

for i in range(N_Y):
    for j in range(K_TOTAL - 1):
        a = i * K_TOTAL + j
        b = a + 1
        c = (i + 1) * K_TOTAL + j + 1
        d = (i + 1) * K_TOTAL + j
        shell_faces.append((a, b, c, d))

mesh_from_vf('Main_Wedge_Body_Shell', shell_verts, shell_faces, 'Paint', uvs=shell_uvs, th=0.006)

# Dark MattePlastic Inner Wheelhouse Tubs clamped strictly below outer fender skin so they NEVER poke through!
for is_front, y_ax, r_arch in [(True, Y_FRONT_AXLE, 0.382), (False, Y_REAR_AXLE, 0.398)]:
    z_ax = R_WHEEL_F if is_front else R_WHEEL_R
    for side in [-1, 1]:
        def wheelhouse_fn(u, v, s=side, ya=y_ax, za=z_ax, ra=r_arch):
            ang = -0.10 * pi + 1.20 * pi * u
            y_w = ya - ra * cos(ang)
            x_outer = body_half_width(y_w) * 0.89
            x_w = s * (0.58 + (x_outer - 0.58) * v)
            z_raw = max(0.14, za + ra * sin(ang))
            z_cap = surface_top_z(x_w, y_w) - 0.032
            return (x_w, y_w, min(z_raw, z_cap))

        surf_grid(f'Wheelhouse_Liner_{is_front}_{side}', wheelhouse_fn, 18, 4, 'MattePlastic', th=0.004)
        cyl(f'Wheelhouse_Inner_Wall_{is_front}_{side}', (side * 0.59, y_ax, z_ax), r_arch * 0.90, 0.015, 'MattePlastic', 'X', 22, 0.0)

# Crisp Panel Seam Lines, Side Skirt & Triangular Front Fender Extractor ("VENT" in sheet.jpg!)
for side in [-1, 1]:
    bonnet_seam = []
    for k in range(12):
        yk = -2.22 + 1.12 * (k / 11.0)
        xk = side * (0.18 + 0.36 * (k / 11.0))
        bonnet_seam.append((xk, yk, surface_top_z(xk, yk) + 0.002))
    spline_tube(f'Bonnet_Panel_Seam_{side}', bonnet_seam, 0.0025, 'MattePlastic', res_u=6, bev_res=1)

    # Flush Triangular Carbon Fibre Front Fender Extractor Panel behind front wheel ("VENT" arrow in sheet.jpg)
    def fender_vent_fn(u, v, s=side):
        y_top = -1.03
        y_bot_f = -1.04
        y_bot_r = -0.68
        y = (1.0 - v) * (y_bot_f + (y_bot_r - y_bot_f) * u) + v * (y_top + 0.12 * u)
        z = 0.120 + 0.32 * v
        w_y = body_half_width(y) * 0.952 - 0.012 * (1.0 - u)
        return (s * w_y, y, z)

    surf_grid(f'Front_Fender_Carbon_Extractor_{side}', fender_vent_fn, 6, 6, 'CarbonFibre', th=0.010, bev=0.002)

    # Full-length sculpted Carbon Fibre Side Skirt rocker panel along bottom sill (Z = 0.10 to 0.15)
    def side_skirt_fn(u, v, s=side):
        y = -1.04 + 2.08 * u
        z = 0.100 + 0.048 * v
        w_s = body_half_width(y) * 0.962 + 0.020 * (1.0 - v)
        return (s * w_s, y, z)

    surf_grid(f'Carbon_Side_Rocker_Skirt_{side}', side_skirt_fn, 18, 4, 'CarbonFibre', th=0.012, bev=0.003)


# ==============================================================================
# 4. FRONT SPLITTER, 3-CHAMBER FRONT GRILLE, BONNET V-VENT & ORIGINAL HEADLIGHTS
# ==============================================================================
# 1. Full-width Carbon Fibre Front Splitter (Y = -2.35 to -1.75, Z = 0.088 to 0.115)
def splitter_fn(u, v):
    x = (2.0 * u - 1.0) * 1.00
    ax = abs(x)
    y_front = -2.35 + 0.13 * (ax ** 1.7)
    y_rear = -1.78 + 0.06 * (ax ** 1.4)
    y = y_front + (y_rear - y_front) * v
    spoon = 0.016 * exp(-((x / 0.26) ** 2)) * (1.0 - v)
    z = 0.092 + spoon + 0.010 * v
    return (x, y, z)


surf_grid('Front_Carbon_Splitter', splitter_fn, 28, 5, 'CarbonFibre', th=0.015, bev=0.003)
box('Underbody_Carbon_Floor_Tray', (0.0, 0.0, 0.116), (1.70, 3.70, 0.020), 'MattePlastic', 0.003)

# 2. Recessed 3-Chamber Front Grille Cavity under the Wedge Nose Overhang
def front_cavity_fn(u, v):
    x = (2.0 * u - 1.0) * 0.90
    ax = abs(x)
    y = -2.14 + 0.13 * (ax ** 1.5)
    z = 0.105 + 0.24 * v
    return (x, y, z)


surf_grid('Front_Grille_Dark_Backing', front_cavity_fn, 20, 4, 'MattePlastic', th=0.010)

for k in range(5):
    zk = 0.135 + k * 0.042
    spline_tube(f'Front_Grille_Mesh_H_{k}', [
        (-0.86, -2.04, zk), (-0.45, -2.15, zk), (0.0, -2.19, zk), (0.45, -2.15, zk), (0.86, -2.04, zk)
    ], 0.0035, 'MattePlastic', res_u=6, bev_res=1)

# Sculpted Painted Front Intake Surrounds & Angled Splitter Struts (matching Front & 3/4 views of sheet.jpg!)
for side in [-1, 1]:
    strut_pts = [
        (side * 0.21, -2.30, 0.105),
        (side * 0.27, -2.27, 0.220),
        (side * 0.34, -2.23, 0.335),
    ]
    spline_tube(f'Front_Bumper_Diagonal_Strut_{side}', strut_pts, 0.018, 'Paint', res_u=6, bev_res=2)

    # Smooth raked C-shaped outer bumper aero surround wrapping smoothly from the nose edge to the front fender arch
    c_surround_pts = [
        (side * 0.22, -2.30, 0.110),
        (side * 0.58, -2.22, 0.115),
        (side * 0.88, -2.10, 0.125),
        (side * 0.94, -1.96, 0.220),
        (side * 0.88, -2.04, 0.335),
        (side * 0.56, -2.18, 0.335),
    ]
    spline_tube(f'Front_Intake_C_Surround_{side}', c_surround_pts, 0.018, 'Paint', res_u=8, bev_res=2)

    # Smooth painted cheek fairing bridging outer intake C-surround to the front wheel arch
    def bumper_cheek_fn(u, v, s=side):
        z = 0.115 + 0.225 * v
        y_in = -2.08 + 0.04 * v
        y_out = -1.86
        y = y_in + (y_out - y_in) * u
        w_y = body_half_width(y) * 0.955
        x_in = 0.88 - 0.02 * v
        x = s * (x_in + (w_y - x_in) * u)
        return (x, y, z)

    surf_grid(f'Front_Bumper_Outer_Fairing_{side}', bumper_cheek_fn, 6, 5, 'Paint', th=0.012, bev=0.002)

    # Low sleek Carbon Fibre splitter corner fin
    def corner_winglet_fn(u, v, s=side):
        y = -2.12 + 0.22 * u
        z = 0.095 + 0.085 * v * u
        x = s * (0.95 + 0.035 * u)
        return (x, y, z)

    surf_grid(f'Splitter_Corner_Winglet_{side}', corner_winglet_fn, 5, 3, 'CarbonFibre', th=0.008, bev=0.002)

cyl('Nose_Blank_Shield_Badge', (0.0, -2.12, surface_top_z(0.0, -2.12) + 0.004), 0.020, 0.004, 'MetalChrome', 'Z', 6, 0.001)

# 3. Smoothly Contoured Bonnet V-Vent Cowl & Windshield Base Apron (seals windshield base 100%!)
def bonnet_cowl_fn(u, v):
    y = -1.52 + 0.54 * v
    hw = 0.14 + 0.42 * (v ** 1.2)
    x = (2.0 * u - 1.0) * hw
    z = surface_top_z(x, y) + 0.002
    return (x, y, z)


surf_grid('Bonnet_V_Vent_Panel', bonnet_cowl_fn, 14, 10, 'MattePlastic', th=0.006, bev=0.002)

for sx_norm in [-0.62, -0.22, 0.22, 0.62]:
    strake_pts = []
    for k in range(6):
        vk = 0.08 + 0.78 * (k / 5.0)
        yk = -1.52 + 0.54 * vk
        hw = 0.14 + 0.42 * (vk ** 1.2)
        xk = sx_norm * hw
        strake_pts.append((xk, yk, surface_top_z(xk, yk) + 0.010))
    spline_tube(f'Bonnet_Aero_Strake_{sx_norm}', strake_pts, 0.0065, 'CarbonFibre', res_u=6, bev_res=1)

# 4. ORIGINAL HEADLIGHT DESIGN: Flush Fender-Contoured Dual-Chevron Laser Projector Clusters
for side in [-1, 1]:
    def hl_point(u, v, s=side, z_off=0.0):
        y = -2.06 + 0.42 * v - 0.05 * u
        x_in = 0.52 + 0.10 * v
        x_out = 0.78 + 0.08 * v
        x = s * (x_in + (x_out - x_in) * u)
        z = surface_top_z(x, y) + z_off
        return (x, y, z)

    surf_grid(f'Headlight_Flush_Housing_{side}', lambda u, v, s=side: hl_point(u, v, s, 0.002), 6, 8, 'MattePlastic', th=0.004, bev=0.002)

    hl_perim = (
        [hl_point(u / 4.0, 0.0, side, 0.004) for u in range(5)]
        + [hl_point(1.0, v / 4.0, side, 0.004) for v in range(1, 5)]
        + [hl_point(1.0 - u / 4.0, 1.0, side, 0.004) for u in range(1, 5)]
        + [hl_point(0.0, 1.0 - v / 4.0, side, 0.004) for v in range(1, 4)]
    )
    spline_tube(f'Headlight_Perimeter_Trim_{side}', hl_perim, 0.0055, 'CarbonFibre', cyclic=True, res_u=6, bev_res=1)

    for k in range(3):
        vk = 0.22 + k * 0.28
        p_pod = hl_point(0.48, vk, side, 0.008)
        cyl(f'Headlight_Projector_Bezel_{side}_{k}', p_pod, 0.018, 0.022, 'MetalChrome', 'Y', 14, 0.002)
        em_pod = cyl(f'Headlight_Projector_Core_{side}_{k}', (p_pod[0], p_pod[1] - 0.008, p_pod[2]), 0.013, 0.010, 'Lamps', 'Y', 12, 0.001)
        set_lamp_band(em_pod, 'white')

    drl_pts = [hl_point(0.15, 0.08, side, 0.007), hl_point(0.82, 0.14, side, 0.007), hl_point(0.85, 0.88, side, 0.007)]
    drl = spline_tube(f'Headlight_DRL_Blade_{side}', drl_pts, 0.0065, 'Lamps', res_u=6, bev_res=1)
    set_lamp_band(drl, 'white')

    ind_pts = [hl_point(0.90, 0.22, side, 0.006), hl_point(0.92, 0.82, side, 0.006)]
    ind = spline_tube(f'Headlight_Amber_Strip_{side}', ind_pts, 0.0045, 'Lamps', res_u=4, bev_res=1)
    set_lamp_band(ind, 'orange')

    surf_grid(f'Headlight_Glass_Cover_{side}', lambda u, v, s=side: hl_point(u, v, s, 0.011), 6, 8, 'Glass')


# ==============================================================================
# 5. TEARDROP CANOPY COCKPIT ("Glass" + "Paint" Roof Spine & Pillars)
# ==============================================================================
def canopy_roof_z(y):
    return smooth_interp(y, [
        (-1.14, 0.605),  # Meets bonnet cowl flush with zero gap!
        (-0.72, 0.905),
        (-0.18, 1.096),  # Canopy roof peak (1.10 m total vehicle height)
        (0.28,  1.020),
        (0.68,  0.865),
        (0.96,  0.790),
    ])


def canopy_half_w(y):
    return smooth_interp(y, [
        (-1.14, 0.64),
        (-0.68, 0.67),
        (-0.15, 0.63),
        (0.35,  0.53),
        (0.68,  0.41),
        (0.96,  0.28),
    ])


def canopy_pt(u, v, z_off=0.0):
    y = -1.14 + 2.10 * v
    w_top = canopy_half_w(y)
    w_base = min(body_half_width(y) * 0.82, w_top + 0.13)
    z_base = fender_crest_z(y) - 0.016
    z_top = canopy_roof_z(y)
    au = abs(u)
    ang = (1.0 - au) * (pi * 0.5)
    x = (w_top + (w_base - w_top) * (au ** 1.3)) * (1.0 if u >= 0 else -1.0) * (au ** 0.88)
    z = z_base + max(0.0, z_top - z_base) * (sin(ang) ** 0.76) + z_off
    return (x, y, z)


# 1. Smooth Continuous Wrap-Around Smoked Glass Canopy Dome ("Glass")
def canopy_glass_fn(u, v):
    return canopy_pt((2.0 * u - 1.0) * 0.96, 0.00 + 0.76 * v, 0.0)


surf_grid('Canopy_Wraparound_Glass_Dome', canopy_glass_fn, 28, 32, 'Glass')

# Lower windshield ceramic-frit base seal strip ("MattePlastic")
windshield_base_seal = [canopy_pt(-0.92 + 1.84 * (k / 16.0), 0.01, 0.002) for k in range(17)]
spline_tube('Canopy_Windshield_Base_Seal', windshield_base_seal, 0.012, 'MattePlastic', res_u=8, bev_res=1)

# 2. Smooth Painted Canopy Roof Cap & Rear Teardrop Buttresses ("Paint")
def canopy_roof_cap_fn(u, v):
    v_actual = 0.36 + 0.64 * v
    width_frac = 0.42 + 0.56 * (v ** 1.2)
    return canopy_pt((2.0 * u - 1.0) * width_frac, v_actual, 0.003)


surf_grid('Canopy_Painted_Roof_Buttress', canopy_roof_cap_fn, 18, 20, 'Paint', th=0.007, bev=0.002)

for side in [-1, 1]:
    # Painted A-pillar arch starting cleanly at v=0.04 inside the windshield seal
    a_pillar_pts = [canopy_pt(side * (0.62 - 0.18 * t), 0.04 + 0.66 * t, 0.003) for t in [i / 10.0 for i in range(11)]]
    spline_tube(f'Canopy_A_Pillar_Arch_{side}', a_pillar_pts, 0.014, 'Paint', res_u=8, bev_res=1)

    # Dark MattePlastic window lower sill seal & hexagonal rear window frame
    sill_pts = [
        canopy_pt(side * 0.96, 0.02, 0.003),
        canopy_pt(side * 0.96, 0.25, 0.003),
        canopy_pt(side * 0.96, 0.52, 0.003),
        canopy_pt(side * 0.72, 0.68, 0.003),
        canopy_pt(side * 0.46, 0.42, 0.004),
    ]
    spline_tube(f'Canopy_Window_Seal_Frame_{side}', sill_pts, 0.010, 'MattePlastic', res_u=8, bev_res=1)

    # Sleek blade-style side mirrors on twin carbon stalks
    spline_tube(f'Side_Mirror_Stalk_{side}', [
        (side * 0.74, -0.72, 0.70),
        (side * 0.88, -0.68, 0.76),
        (side * 0.95, -0.64, 0.77),
    ], 0.010, 'CarbonFibre', res_u=6, bev_res=1)
    box(f'Side_Mirror_Aero_Housing_{side}', (side * 0.96, -0.62, 0.775), (0.13, 0.15, 0.048), 'Paint', 0.010, rot=(0.0, 0.0, side * 0.18))
    box(f'Side_Mirror_Reflective_Glass_{side}', (side * 0.96, -0.545, 0.775), (0.10, 0.008, 0.034), 'MetalChrome', 0.002, rot=(0.0, 0.0, side * 0.18))

# Central pantograph aero windshield wiper
spline_tube('Aero_Windshield_Wiper_Arm', [canopy_pt(0.06, 0.03, 0.006), canopy_pt(0.01, 0.18, 0.006), canopy_pt(-0.03, 0.33, 0.006)], 0.0055, 'MattePlastic', res_u=6, bev_res=1)
spline_tube('Aero_Windshield_Wiper_Blade', [canopy_pt(-0.10, 0.12, 0.008), canopy_pt(0.0, 0.24, 0.008), canopy_pt(0.08, 0.35, 0.008)], 0.0045, 'Rubber', res_u=6, bev_res=1)


# ==============================================================================
# 6. EXPOSED GLOWING SIDE TURBINE INTAKES ("GLOWING TURBINE INTAKE" in sheet.jpg)
# ==============================================================================
# Look at sheet.jpg:
# - Behind the door (Y = +0.16 to +0.66, Z = 0.20 to 0.72), a thick, smooth black carbon
#   crescent scoop cowl wraps the rear edge of the side intake.
# - Inside the crescent scoop sits a curved radial jet turbine compressor with short radial
#   glowing orange-amber blades fanning around a recessed dark turbine cone!
for side in [-1, 1]:
    # 1. Thick Sculpted Black Carbon Crescent Scoop Cowl ("CarbonFibre")
    def crescent_cowl_fn(u, v, s=side):
        ang = -0.48 * pi + 0.96 * pi * u
        z = 0.46 + 0.27 * sin(ang)
        # Backward-leaning crescent curve matching sheet.jpg Side & 3/4 views
        tilt_y = -0.09 * sin(ang)
        y_lead = 0.38 + tilt_y + 0.14 * (1.0 - cos(ang))
        y_trail = 0.54 + tilt_y + 0.18 * (1.0 - cos(ang))
        y = y_lead + (y_trail - y_lead) * v
        x_in = 0.80 + 0.12 * cos(ang)
        x_out = 0.95 + 0.04 * cos(ang)
        x = s * (x_in + (x_out - x_in) * (v ** 0.65))
        return (x, y, z)

    surf_grid(f'Side_Turbine_Crescent_Bezel_{side}', crescent_cowl_fn, 22, 6, 'CarbonFibre', th=0.022, bev=0.005)

    # 2. Recessed Dark Intake Tunnel Backing Wall ("MattePlastic")
    def intake_tunnel_fn(u, v, s=side):
        ang = -0.46 * pi + 0.92 * pi * u
        z = 0.46 + 0.25 * sin(ang)
        tilt_y = -0.08 * sin(ang)
        y = 0.12 + 0.36 * v + tilt_y
        x = s * (0.68 + 0.14 * (v ** 1.3) + 0.04 * cos(ang))
        return (x, y, z)

    surf_grid(f'Side_Turbine_Dark_Tunnel_{side}', intake_tunnel_fn, 18, 6, 'MattePlastic', th=0.005)

    # 3. Curved Glowing Orange-Amber Radial Turbine Ring & 18 Radial Compressor Blades
    # Instead of a flat wall of horizontal stripes, we build a curved crescent ring of radial glowing
    # orange turbine blades ("Lamps" orange) alternating with dark titanium blades ("CarbonFibre")!
    def radial_turbine_pt(ang_t, r_frac, s=side):
        # ang_t in [-1, +1] around the crescent arc; r_frac in [0, 1] from inner hub to outer cowl lip
        ang = ang_t * 0.44 * pi
        r_z = (0.10 + 0.15 * r_frac)
        r_y = (0.07 + 0.13 * r_frac)
        z = 0.46 + r_z * sin(ang)
        tilt_y = -0.07 * sin(ang)
        y = 0.26 + tilt_y + r_y * (1.0 - 0.35 * cos(ang))
        x = s * (0.73 + 0.14 * r_frac + 0.04 * cos(ang))
        return (x, y, z)

    # Glowing orange-amber outer & mid turbine rings inside the scoop
    glow_band = surf_grid(
        f'Side_Turbine_Glowing_Ring_{side}',
        lambda u, v, s=side: radial_turbine_pt(2.0 * u - 1.0, 0.18 + 0.80 * v, s),
        20, 6, 'Lamps', th=0.005
    )
    set_lamp_band(glow_band, 'orange')

    # 17 Radial Twisted Compressor Blades fanning across the glowing orange ring (creates the exact radial turbine look of sheet.jpg!)
    for b_idx in range(17):
        at = -0.94 + 1.88 * (b_idx / 16.0)
        p_in = radial_turbine_pt(at, 0.08, side)
        p_mid = radial_turbine_pt(at + 0.03, 0.55, side)
        p_out = radial_turbine_pt(at + 0.06, 0.98, side)
        blade_pts = [
            (p_in[0] + side * 0.008, p_in[1] - 0.006, p_in[2]),
            (p_mid[0] + side * 0.012, p_mid[1] - 0.008, p_mid[2]),
            (p_out[0] + side * 0.010, p_out[1] - 0.006, p_out[2]),
        ]
        spline_tube(f'Side_Turbine_Radial_Blade_{side}_{b_idx}', blade_pts, 0.009, 'CarbonFibre', res_u=5, bev_res=1)

    # Dark recessed turbine hub spinner at the forward focus of the radial blades
    p_hub = radial_turbine_pt(0.0, 0.05, side)
    cyl(f'Side_Turbine_Hub_Cone_{side}', (p_hub[0] + side * 0.01, p_hub[1] - 0.02, p_hub[2]), 0.075, 0.06, 'MattePlastic', 'X', 18, 0.003)


# ==============================================================================
# 7. FLOATING REAR AERO BLADES ("AERO BLADE") & LOW-SLUNG ACTIVE REAR WING
# ==============================================================================
# 1. Floating C-shaped Rear Aero Blades over & behind the rear wheels ("AERO BLADE" arrow in sheet.jpg!)
for side in [-1, 1]:
    def aero_blade_fn(u, v, s=side):
        y = 1.12 + 1.16 * u
        z_top = smooth_interp(y, [
            (1.12, 0.85),
            (1.45, 0.922),
            (1.90, 0.86),
            (2.28, 0.74),
        ])
        z_bot = smooth_interp(y, [
            (1.12, 0.79),
            (1.45, 0.845),
            (1.90, 0.68),
            (2.28, 0.44),
        ])
        z = z_bot + (z_top - z_bot) * v
        x_in = 0.84 + 0.06 * sin(pi * u)
        x_out = 1.045 - 0.05 * (u ** 1.6)
        x = s * (x_in + (x_out - x_in) * (0.3 + 0.7 * v))
        return (x, y, z)

    surf_grid(f'Floating_Rear_Aero_Blade_{side}', aero_blade_fn, 22, 6, 'CarbonFibre', th=0.016, bev=0.004)

    for u_s in [0.20, 0.55, 0.88]:
        pb = aero_blade_fn(u_s, 0.5, side)
        p_in = (pb[0] - side * 0.06, pb[1] - 0.02, pb[2] - 0.03)
        strut_bar(f'Aero_Blade_Strut_{side}_{u_s}', p_in, pb, 0.009, 0.009, 'MetalChrome', 8)

    # Lower Rear Carbon Fender Skirt Valence behind the rear wheel (seals the lower rear corner behind the rear wheel 100%!)
    def rear_lower_valence_fn(u, v, s=side):
        y = 1.82 + 0.46 * u
        z_bot = 0.115 + 0.065 * u
        z_top = 0.39 + 0.04 * u
        z = z_bot + (z_top - z_bot) * v
        w_y = body_half_width(y) * 0.955 - 0.03 * u * (1.0 - v)
        return (s * w_y, y, z)

    surf_grid(f'Rear_Lower_Carbon_Valence_{side}', rear_lower_valence_fn, 8, 5, 'CarbonFibre', th=0.014, bev=0.003)

# 2. Low-Slung Active Swept Carbon Rear Wing (Z = 0.95 to 1.01 m, below the 1.10 m canopy peak just like sheet.jpg!)
def rear_wing_main_fn(u, v):
    x = (2.0 * u - 1.0) * 0.88
    ax = abs(2.0 * u - 1.0)
    sweep_y = 0.14 * (ax ** 1.6)
    y = 1.78 + sweep_y + 0.28 * v
    camber = 0.018 * sin(pi * v) + 0.026 * v
    dihedral = -0.024 * (ax ** 2.0)
    z = 0.965 + camber + dihedral
    return (x, y, z)


surf_grid('Active_Rear_Wing_Airfoil', rear_wing_main_fn, 28, 6, 'CarbonFibre', th=0.015, bev=0.004)

for side in [-1, 1]:
    def wing_tip_fn(u, v, s=side):
        y = 1.88 + 0.30 * u
        z = 0.91 + 0.095 * v
        x = s * (0.87 + 0.03 * v - 0.01 * u)
        return (x, y, z)

    surf_grid(f'Rear_Wing_Endplate_{side}', wing_tip_fn, 8, 5, 'CarbonFibre', th=0.010, bev=0.003)

    pylon_pts = [
        (side * 0.24, 1.64, 0.80),
        (side * 0.24, 1.76, 0.92),
        (side * 0.24, 1.88, 0.985),
        (side * 0.24, 1.98, 0.975),
    ]
    spline_tube(f'Rear_Wing_Swan_Pylon_{side}', pylon_pts, 0.015, 'CarbonFibre', res_u=8, bev_res=1)
    strut_bar(f'Rear_Wing_Actuator_{side}', (side * 0.24, 1.74, 0.81), (side * 0.24, 1.94, 0.97), 0.009, 0.007, 'MetalChrome', 8)


# ==============================================================================
# 8. VISIBLE HYBRID-TURBINE ENGINE BAY & REAR DECK VENTS (Top View of sheet.jpg)
# ==============================================================================
engine_bay_frame = [
    (-0.32, 0.58, 0.77),
    (0.32,  0.58, 0.77),
    (0.38,  1.10, 0.79),
    (0.30,  1.66, 0.81),
    (-0.30, 1.66, 0.81),
    (-0.38, 1.10, 0.79),
]
spline_tube('Engine_Bay_Perimeter_Bezel', engine_bay_frame, 0.020, 'MattePlastic', cyclic=True, res_u=8, bev_res=1)
box('Engine_Bay_Lower_Cavity_Pan', (0.0, 1.12, 0.65), (0.72, 1.10, 0.03), 'MattePlastic', 0.003)

for side in [-1, 1]:
    cyl(f'Engine_Titanium_Plenum_{side}', (side * 0.14, 1.05, 0.71), 0.055, 0.76, 'MetalChrome', 'Y', 18, 0.002)
    for c_idx in range(4):
        yc = 0.74 + c_idx * 0.19
        box(f'Engine_Cylinder_Coil_Pack_{side}_{c_idx}', (side * 0.14, yc, 0.755), (0.085, 0.055, 0.035), 'CarbonFibre', 0.003)
        strut_bar(f'Engine_Intake_Runner_{side}_{c_idx}', (side * 0.04, yc, 0.73), (side * 0.24, yc, 0.71), 0.016, 0.016, 'MetalChrome', 10)
    spline_tube(f'Engine_Braided_Feed_Line_{side}', [
        (side * 0.16, 0.65, 0.72),
        (side * 0.22, 1.15, 0.74),
        (side * 0.26, 1.62, 0.72),
    ], 0.010, 'MetalChrome', res_u=6, bev_res=1)

# Structural Carbon X-Brace across the engine bay (Top View of sheet.jpg!)
strut_bar('Engine_Bay_Carbon_XBrace_1', (-0.32, 0.72, 0.775), (0.30, 1.54, 0.805), 0.020, 0.020, 'CarbonFibre', 8)
strut_bar('Engine_Bay_Carbon_XBrace_2', (0.32, 0.72, 0.775), (-0.30, 1.54, 0.805), 0.020, 0.020, 'CarbonFibre', 8)
box('Engine_Bay_XBrace_Center_Hub', (0.0, 1.13, 0.792), (0.14, 0.18, 0.026), 'MetalChrome', 0.004)

# Smoked glass viewing window over engine bay
def engine_glass_fn(u, v):
    y = 0.60 + 0.96 * v
    hw = 0.30 - 0.04 * abs(2.0 * v - 1.0)
    x = (2.0 * u - 1.0) * hw
    z = 0.785 + 0.024 * v + 0.010 * (1.0 - (2.0 * u - 1.0) ** 2)
    return (x, y, z)


surf_grid('Engine_Bay_Glass_Window', engine_glass_fn, 6, 8, 'Glass')

# Trapezoidal rear haunch extractor vents flanking the engine bay (Top View of sheet.jpg)
for side in [-1, 1]:
    def haunch_vent_fn(u, v, s=side):
        y = 1.08 + 0.34 * v
        x = s * (0.45 + 0.20 * u)
        z = surface_top_z(x, y) + 0.003
        return (x, y, z)

    surf_grid(f'Rear_Haunch_Vent_Panel_{side}', haunch_vent_fn, 4, 4, 'MattePlastic', th=0.006, bev=0.002)
    for k in range(3):
        vk = 0.22 + k * 0.28
        p0 = haunch_vent_fn(0.1, vk, side)
        p1 = haunch_vent_fn(0.9, vk + 0.05, side)
        strut_bar(f'Rear_Haunch_Vent_Slat_{side}_{k}', p0, p1, 0.006, 0.006, 'CarbonFibre', 8)

# Billet alloy race fuel filler cap with 6 hex bolts
cyl('Race_Fuel_Filler_Flange', (0.62, 0.74, surface_top_z(0.62, 0.74) + 0.004), 0.038, 0.008, 'MetalChrome', 'Z', 18, 0.001)
cyl('Race_Fuel_Filler_Cap', (0.62, 0.74, surface_top_z(0.62, 0.74) + 0.008), 0.026, 0.008, 'CarbonFibre', 'Z', 14, 0.001)


# ==============================================================================
# 9. TWIN REAR THRUSTER NOZZLES ("REAR THRUSTER NOZZLE"), TAIL LIGHTS & DIFFUSER
# ==============================================================================
box('Rear_Fascia_Recessed_Backplate', (0.0, 2.20, 0.61), (1.74, 0.05, 0.40), 'MattePlastic', 0.005)
rear_stadium_frame = [
    (-0.86, 2.25, 0.42),
    (0.86,  2.25, 0.42),
    (0.88,  2.24, 0.79),
    (0.0,   2.25, 0.82),
    (-0.88, 2.24, 0.79),
]
spline_tube('Rear_Fascia_Stadium_Surround', rear_stadium_frame, 0.022, 'CarbonFibre', cyclic=True, res_u=8, bev_res=2)

for k in range(7):
    zk = 0.46 + k * 0.050
    strut_bar(f'Rear_Fascia_Mesh_HBar_{k}', (-0.84, 2.22, zk), (0.84, 2.22, zk), 0.003, 0.003, 'MattePlastic', 6)

# ORIGINAL REAR LIGHT DESIGN: Continuous Glowing Red Upper Arch + Vertical Fin Blades + Cyan Lower Accent
tail_red_arch = [
    (-0.87, 2.26, 0.46),
    (-0.86, 2.26, 0.78),
    (-0.45, 2.27, 0.81),
    (0.0,   2.27, 0.815),
    (0.45,  2.27, 0.81),
    (0.86,  2.26, 0.78),
    (0.87,  2.26, 0.46),
]
tail_blade = spline_tube('Rear_Tail_Light_Red_Perimeter', tail_red_arch, 0.012, 'Lamps', res_u=10, bev_res=1)
set_lamp_band(tail_blade, 'red')

for side in [-1, 1]:
    for k in range(3):
        zk = 0.54 + k * 0.075
        brk = box(f'Rear_Brake_Chevron_{side}_{k}', (side * 0.78, 2.25, zk), (0.085, 0.018, 0.020), 'Lamps', 0.002)
        set_lamp_band(brk, 'red')
    r_ind = box(f'Rear_Amber_Indicator_{side}', (side * 0.76, 2.25, 0.455), (0.11, 0.018, 0.015), 'Lamps', 0.002)
    set_lamp_band(r_ind, 'orange')

cyan_lower = [
    (-0.84, 2.26, 0.43),
    (-0.52, 2.26, 0.43),
    (-0.42, 2.26, 0.49),
    (0.0,   2.26, 0.50),
    (0.42,  2.26, 0.49),
    (0.52,  2.26, 0.43),
    (0.84,  2.26, 0.43),
]
cyan_strip = spline_tube('Rear_Lower_Cyan_Accent_Line', cyan_lower, 0.007, 'Lamps', res_u=8, bev_res=1)
set_lamp_band(cyan_strip, 'cyan')

# TWIN REAR THRUSTER NOZZLES ("REAR THRUSTER NOZZLE" with glowing inner ring in Lamps material!)
for side in [-1, 1]:
    nx = side * 0.285
    nz = 0.615
    cyl(f'Thruster_Outer_Shroud_Barrel_{side}', (nx, 2.23, nz), 0.158, 0.18, 'MetalChrome', 'Y', 28, 0.003)
    cyl(f'Thruster_Actuator_Collar_Ring_{side}', (nx, 2.26, nz), 0.166, 0.035, 'CarbonFibre', 'Y', 28, 0.002)

    # 16 Variable-Geometry Afterburner Nozzle Petals ("MetalChrome") extending to Y = +2.34
    for p_idx in range(16):
        ang0 = 2.0 * pi * (p_idx / 16.0)
        ang1 = 2.0 * pi * ((p_idx + 0.82) / 16.0)
        r_base = 0.160
        r_tip = 0.138
        p_verts = [
            (nx + r_base * cos(ang0), 2.23,  nz + r_base * sin(ang0)),
            (nx + r_base * cos(ang1), 2.23,  nz + r_base * sin(ang1)),
            (nx + r_tip * cos(ang1),  2.340, nz + r_tip * sin(ang1)),
            (nx + r_tip * cos(ang0),  2.340, nz + r_tip * sin(ang0)),
        ]
        mesh_from_vf(f'Thruster_Nozzle_Petal_{side}_{p_idx}', p_verts, [(0, 1, 2, 3)], 'MetalChrome', th=0.010, bev=0.002)

    # GLOWING INNER RING ("Lamps" material, cyan-blue band) + concentric inner plasma ring
    for r_glow, y_glow, th_glow in [(0.122, 2.320, 0.018), (0.094, 2.280, 0.014)]:
        ring_pts = [
            (nx + r_glow * cos(2.0 * pi * k / 24.0), y_glow, nz + r_glow * sin(2.0 * pi * k / 24.0))
            for k in range(24)
        ]
        g_ring = spline_tube(f'Thruster_Glowing_Inner_Ring_{side}_{r_glow}', ring_pts, th_glow, 'Lamps', cyclic=True, res_u=8, bev_res=2)
        set_lamp_band(g_ring, 'cyan')

    g_core = cyl(f'Thruster_Glowing_Core_Disc_{side}', (nx, 2.21, nz), 0.116, 0.02, 'Lamps', 'Y', 24, 0.002)
    set_lamp_band(g_core, 'cyan')
    cyl(f'Thruster_Center_Exhaust_Cone_{side}', (nx, 2.24, nz), 0.046, 0.09, 'MetalChrome', 'Y', 16, 0.002)

# Sleek Integrated Rear Underbody Carbon Diffuser (Y = +1.88 to +2.33, Z = 0.11 to 0.36)
def rear_diffuser_fn(u, v):
    x = (2.0 * u - 1.0) * 0.86
    y = 1.88 + 0.44 * v
    arch = 0.035 * cos(pi * (2.0 * u - 1.0) * 1.5) ** 2
    z = 0.120 + 0.21 * (v ** 1.25) + arch * v
    return (x, y, z)


surf_grid('Rear_Underbody_Carbon_Diffuser', rear_diffuser_fn, 24, 6, 'CarbonFibre', th=0.014, bev=0.003)

for x_strake in [-0.70, -0.42, -0.16, 0.16, 0.42, 0.70]:
    def strake_fn(u, v, xs=x_strake):
        y = 1.94 + 0.36 * u
        z_top = 0.14 + 0.19 * u
        z_bot = 0.105 + 0.04 * u
        z = z_bot + (z_top - z_bot) * v
        return (xs, y, z)
    surf_grid(f'Rear_Diffuser_Strake_{x_strake}', strake_fn, 6, 4, 'CarbonFibre', th=0.012, bev=0.002)

box('Rear_Center_Beacon_Housing', (0.0, 2.29, 0.23), (0.065, 0.04, 0.085), 'CarbonFibre', 0.003)
beacon = box('Rear_Center_Beacon_Emitter', (0.0, 2.31, 0.23), (0.045, 0.015, 0.065), 'Lamps', 0.002)
set_lamp_band(beacon, 'red')


# ==============================================================================
# 10. BELIEVABLE RACE INTERIOR (Visible through Tinted Canopy Glass!)
# ==============================================================================
box('Cockpit_Carbon_Monocoque_Floor', (0.0, -0.32, 0.21), (1.34, 1.54, 0.032), 'CarbonFibre', 0.004)
box('Cockpit_Center_Tunnel', (0.0, -0.28, 0.34), (0.20, 1.38, 0.22), 'CarbonFibre', 0.014)
box('Cockpit_Rear_Bulkhead', (0.0, 0.44, 0.54), (1.24, 0.07, 0.60), 'CarbonFibre', 0.010)

def dash_fn(u, v):
    x = (2.0 * u - 1.0) * 0.62
    y = -0.92 + 0.32 * v
    driver_hood = 0.050 * exp(-(((x - 0.32) / 0.18) ** 2)) * sin(pi * v)
    z = 0.57 + 0.10 * sin(pi * v * 0.85) + driver_hood
    return (x, y, z)


surf_grid('Cockpit_Wraparound_Dashboard', dash_fn, 20, 6, 'Interior', th=0.022, bev=0.004)

box('Driver_HUD_Binnacle', (0.32, -0.62, 0.69), (0.26, 0.16, 0.085), 'CarbonFibre', 0.008)
hud_scr = box('Driver_HUD_Display_Screen', (0.32, -0.54, 0.69), (0.20, 0.010, 0.060), 'Lamps', 0.002)
set_lamp_band(hud_scr, 'cyan')
box('Center_Switch_Console', (0.0, -0.52, 0.51), (0.18, 0.34, 0.12), 'CarbonFibre', 0.008, rot=(-0.38, 0.0, 0.0))

for side in [-1, 1]:
    box(f'Cockpit_Door_Card_{side}', (side * 0.66, -0.25, 0.46), (0.050, 1.12, 0.32), 'Interior', 0.010)
    box(f'Cockpit_Door_Armrest_{side}', (side * 0.62, -0.22, 0.47), (0.065, 0.58, 0.050), 'CarbonFibre', 0.006)

# Twin Sculpted Carbon/Alcantara Racing Bucket Seats (Driver at X = +0.32, Co-driver at X = -0.32)
for sx in [-0.32, 0.32]:
    box(f'Seat_Cushion_Base_{sx}', (sx, -0.10, 0.28), (0.40, 0.46, 0.08), 'Interior', 0.020)
    box(f'Seat_Backrest_Shell_{sx}', (sx, 0.16, 0.55), (0.40, 0.08, 0.54), 'CarbonFibre', 0.018, rot=(0.24, 0.0, 0.0))
    box(f'Seat_Backrest_Pad_{sx}', (sx, 0.13, 0.54), (0.32, 0.06, 0.46), 'Interior', 0.016, rot=(0.24, 0.0, 0.0))
    box(f'Seat_Headrest_Cushion_{sx}', (sx, 0.23, 0.82), (0.22, 0.08, 0.15), 'Interior', 0.016, rot=(0.15, 0.0, 0.0))
    for b_side in [-1, 1]:
        box(f'Seat_Thigh_Bolster_{sx}_{b_side}', (sx + b_side * 0.18, -0.10, 0.34), (0.065, 0.44, 0.13), 'Interior', 0.014)
        box(f'Seat_Shoulder_Wing_{sx}_{b_side}', (sx + b_side * 0.20, 0.14, 0.66), (0.065, 0.18, 0.20), 'CarbonFibre', 0.012)
        spline_tube(f'Seat_Harness_Strap_{sx}_{b_side}', [
            (sx + b_side * 0.08, 0.25, 0.76),
            (sx + b_side * 0.08, 0.10, 0.56),
            (sx + b_side * 0.04, -0.04, 0.34),
        ], 0.012, 'Rubber', res_u=5, bev_res=1)
    cyl(f'Seat_Harness_Camlock_Buckle_{sx}', (sx, -0.04, 0.345), 0.025, 0.016, 'MetalChrome', 'Z', 12, 0.002)

# Dark Matte/Carbon Roll Cage Hoop behind seats (subtle, realistic race cage)
roll_hoop = [
    (-0.54, 0.34, 0.26),
    (-0.52, 0.36, 0.74),
    (-0.34, 0.38, 0.94),
    (0.34,  0.38, 0.94),
    (0.52,  0.36, 0.74),
    (0.54,  0.34, 0.26),
]
spline_tube('Cockpit_Roll_Cage_Main_Hoop', roll_hoop, 0.018, 'MattePlastic', res_u=8, bev_res=1)


# ==============================================================================
# 11. STEERING WHEEL NODE ("SteeringWheel", Origin at Hub (+0.32, -0.38, 0.64))
# ==============================================================================
SW_HUB = Vector((0.32, -0.38, 0.64))
strut_bar('Steering_Column_Shaft', (0.32, -0.64, 0.58), SW_HUB, 0.024, 0.022, 'CarbonFibre', 12)
cyl('Steering_Quick_Release_Collar', (0.32, -0.42, 0.63), 0.026, 0.042, 'MetalChrome', 'Y', 14, 0.002)

current_group = 'SteeringWheel'
SW_TILT = -0.24


def sw_local(dx, dy, dz):
    ry = dy * cos(SW_TILT) - dz * sin(SW_TILT)
    rz = dy * sin(SW_TILT) + dz * cos(SW_TILT)
    return (SW_HUB.x + dx, SW_HUB.y + ry, SW_HUB.z + rz)


sw_hub_cyl = cyl('Steering_Center_Boss', sw_local(0.0, 0.0, 0.0), 0.036, 0.034, 'MetalChrome', 'Y', 16, 0.002)
sw_hub_cyl.rotation_euler[0] = SW_TILT
sw_plate = box('Steering_Carbon_Yoke_Plate', sw_local(0.0, 0.008, 0.0), (0.25, 0.020, 0.13), 'CarbonFibre', 0.006)
sw_plate.rotation_euler[0] = SW_TILT
sw_disp = box('Steering_OLED_Display', sw_local(0.0, 0.018, 0.012), (0.10, 0.008, 0.052), 'Lamps', 0.002)
sw_disp.rotation_euler[0] = SW_TILT
set_lamp_band(sw_disp, 'cyan')

for side in [-1, 1]:
    yoke_grip = [
        sw_local(side * 0.082, 0.006, 0.065),
        sw_local(side * 0.132, 0.010, 0.046),
        sw_local(side * 0.142, 0.012, 0.000),
        sw_local(side * 0.132, 0.010, -0.050),
        sw_local(side * 0.082, 0.006, -0.062),
    ]
    spline_tube(f'Steering_Yoke_Grip_{side}', yoke_grip, 0.016, 'Interior', res_u=8, bev_res=2)
    paddle = box(f'Steering_Shift_Paddle_{side}', sw_local(side * 0.102, -0.020, 0.010), (0.040, 0.006, 0.072), 'MetalChrome', 0.002)
    paddle.rotation_euler[0] = SW_TILT
    for dial_z in [-0.022, 0.024]:
        dial = cyl(f'Steering_Thumb_Rotary_{side}_{dial_z}', sw_local(side * 0.070, 0.018, dial_z), 0.010, 0.010, 'MetalChrome', 'Y', 10, 0.001)
        dial.rotation_euler[0] = SW_TILT

current_group = 'Body'


# ==============================================================================
# 12. LARGE WHEELS WITH DARK TURBINE-BLADE RIMS, SLICK TYRES & BRAKES (Wheel_FL..Wheel_RR)
# ==============================================================================
def lathe_wheel_x(name, xc, yc, zc, profile, mat_name, n_seg=56):
    verts = []
    uvs = []
    faces = []
    m_pts = len(profile)
    for i in range(n_seg):
        ang = 2.0 * pi * (i / n_seg)
        sa, ca = sin(ang), cos(ang)
        for j, (dx, rad) in enumerate(profile):
            verts.append((xc + dx, yc + rad * sa, zc + rad * ca))
            uvs.append((i / n_seg, j / max(1, m_pts - 1)))
    for i in range(n_seg):
        i1 = (i + 1) % n_seg
        for j in range(m_pts - 1):
            a = i * m_pts + j
            b = i1 * m_pts + j
            c = i1 * m_pts + j + 1
            d = i * m_pts + j + 1
            faces.append((a, b, c, d))
    return mesh_from_vf(name, verts, faces, mat_name, uvs=uvs)


WHEEL_SPECS = [
    ('Wheel_FL',  1,  0.865, Y_FRONT_AXLE, R_WHEEL_F, 0.155),
    ('Wheel_FR', -1, -0.865, Y_FRONT_AXLE, R_WHEEL_F, 0.155),
    ('Wheel_RL',  1,  0.855, Y_REAR_AXLE,  R_WHEEL_R, 0.175),
    ('Wheel_RR', -1, -0.855, Y_REAR_AXLE,  R_WHEEL_R, 0.175),
]

for w_name, side, xc, yc, r_tyre, hw in WHEEL_SPECS:
    current_group = w_name
    zc = r_tyre
    r_rim = r_tyre * 0.765  # Large low-profile turbine rim matching sheet.jpg!

    # 1. Slick Race Tyre ("Rubber" - deep black, curved shoulder, no lettering!)
    tyre_profile = [
        (-hw * 0.88, r_rim - 0.004),
        (-hw * 0.96, r_rim + 0.015),
        (-hw * 1.00, r_rim + (r_tyre - r_rim) * 0.50),
        (-hw * 0.96, r_tyre - 0.018),
        (-hw * 0.84, r_tyre - 0.005),
        (-hw * 0.58, r_tyre - 0.001),
        (-hw * 0.22, r_tyre),
        (0.0,        r_tyre),
        (hw * 0.22,  r_tyre),
        (hw * 0.58,  r_tyre - 0.001),
        (hw * 0.84,  r_tyre - 0.005),
        (hw * 0.96,  r_tyre - 0.018),
        (hw * 1.00,  r_rim + (r_tyre - r_rim) * 0.50),
        (hw * 0.96,  r_rim + 0.015),
        (hw * 0.88,  r_rim - 0.004),
    ]
    lathe_wheel_x(f'{w_name}_Slick_Tyre', xc, yc, zc, tyre_profile, 'Rubber', n_seg=56)

    # 2. Dark Gunmetal/Carbon Outer Rim Barrel & Thin Machined Alloy Lip Ring (matching sheet.jpg!)
    out_x = side * (hw * 0.90)
    in_x = -side * (hw * 0.88)
    rim_profile = [
        (in_x,                   r_rim + 0.002),
        (in_x + side * 0.016,    r_rim - 0.014),
        (out_x - side * 0.028,   r_rim - 0.012),
        (out_x - side * 0.006,   r_rim - 0.002),
        (out_x + side * 0.003,   r_rim + 0.005),
        (out_x - side * 0.005,   r_rim + 0.008),
        (out_x - side * 0.024,   r_rim - 0.018),
    ]
    lathe_wheel_x(f'{w_name}_Rim_Barrel', xc, yc, zc, rim_profile, 'CarbonFibre', n_seg=48)

    # Thin machined silver outer flange highlight ring
    lip_ring = [
        (out_x - side * 0.002, r_rim + 0.001),
        (out_x + side * 0.005, r_rim + 0.005),
        (out_x - side * 0.002, r_rim + 0.009),
    ]
    lathe_wheel_x(f'{w_name}_Rim_Machined_Lip', xc, yc, zc, lip_ring, 'MetalChrome', n_seg=48)

    # 3. 12 Twisted Directional Dark Turbine Rim Blades ("TURBINE RIM" in sheet.jpg!)
    hub_x = xc + out_x - side * 0.040
    rim_face_x = xc + out_x - side * 0.006
    for b_idx in range(12):
        ang0 = 2.0 * pi * (b_idx / 12.0)
        ang_outer = ang0 + 0.34  # Swept directional turbine twist
        r_in = 0.054
        r_mid = (r_rim + 0.054) * 0.52
        r_out = r_rim - 0.008
        blade_v = [
            (hub_x,                         yc + r_in * sin(ang0 - 0.08),        zc + r_in * cos(ang0 - 0.08)),
            (hub_x + side * 0.012,          yc + r_in * sin(ang0 + 0.10),        zc + r_in * cos(ang0 + 0.10)),
            (rim_face_x + side * 0.003,     yc + r_mid * sin(ang0 + 0.24),       zc + r_mid * cos(ang0 + 0.24)),
            (rim_face_x - side * 0.012,     yc + r_mid * sin(ang0 + 0.04),       zc + r_mid * cos(ang0 + 0.04)),
            (rim_face_x + side * 0.002,     yc + r_out * sin(ang_outer + 0.12),  zc + r_out * cos(ang_outer + 0.12)),
            (rim_face_x - side * 0.014,     yc + r_out * sin(ang_outer - 0.04),  zc + r_out * cos(ang_outer - 0.04)),
        ]
        blade_f = [(0, 1, 2, 3), (3, 2, 4, 5)]
        mesh_from_vf(f'{w_name}_Turbine_Blade_{b_idx}', blade_v, blade_f, 'CarbonFibre', th=0.011, bev=0.002)

        # Secondary dark sub-spoke between main turbine blades
        p_hub = (hub_x + side * 0.006, yc + 0.058 * sin(ang0 + 0.14), zc + 0.058 * cos(ang0 + 0.14))
        p_rim = (rim_face_x - side * 0.004, yc + (r_rim - 0.012) * sin(ang_outer - 0.08), zc + (r_rim - 0.012) * cos(ang_outer - 0.08))
        strut_bar(f'{w_name}_Secondary_Spoke_{b_idx}', p_hub, p_rim, 0.008, 0.010, 'MattePlastic', 8)

    # 4. Central Hub Cap & Centre-Lock Ring (matching dark turbine hub in sheet.jpg)
    cyl(f'{w_name}_Center_Hub_Bowl', (hub_x, yc, zc), 0.062, 0.032, 'MattePlastic', 'X', 24, 0.002)
    cyl(f'{w_name}_Center_Lock_Ring', (hub_x + side * 0.016, yc, zc), 0.042, 0.018, 'MetalChrome', 'X', 20, 0.002)
    cyl(f'{w_name}_Center_Lock_Cap', (hub_x + side * 0.022, yc, zc), 0.032, 0.016, 'CarbonFibre', 'X', 16, 0.002)

    # 5. Large Brake Disc Rotor, Bell & 6-Piston Racing Caliper visible behind turbine blades
    disc_x = xc - side * 0.024
    r_disc = r_rim * 0.76
    cyl(f'{w_name}_Brake_Disc_Rotor', (disc_x, yc, zc), r_disc, 0.024, 'MetalChrome', 'X', 32, 0.002)
    cyl(f'{w_name}_Brake_Mounting_Bell', (disc_x + side * 0.012, yc, zc), r_disc * 0.54, 0.022, 'MattePlastic', 'X', 20, 0.002)
    cal_y = yc + (0.14 if yc < 0 else -0.14)
    box(f'{w_name}_Racing_Brake_Caliper', (disc_x + side * 0.006, cal_y, zc + 0.02), (0.065, 0.088, 0.175), 'MetalChrome', 0.008)

current_group = 'Body'
print('GEOMETRY_BUILT', {k: len(v) for k, v in groups.items()}, flush=True)


# ==============================================================================
# 13. CONSOLIDATE MESH NODES, SET EXACT PIVOTS, ENFORCE TRI BUDGET & EXPORT GLB
# ==============================================================================
final_objects = []
for node_name, obj_list in groups.items():
    for o in obj_list:
        activate(o)
        for md in list(o.modifiers):
            try:
                bpy.ops.object.modifier_apply(modifier=md.name)
            except Exception as e:
                print('MOD_APPLY_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 obj_list:
        o.select_set(True)
    bpy.context.view_layer.objects.active = obj_list[0]
    bpy.ops.object.join()
    joined = bpy.context.object
    joined.name = node_name

    if node_name == 'Wheel_FL':
        pivot = (0.865, Y_FRONT_AXLE, R_WHEEL_F)
    elif node_name == 'Wheel_FR':
        pivot = (-0.865, Y_FRONT_AXLE, R_WHEEL_F)
    elif node_name == 'Wheel_RL':
        pivot = (0.855, Y_REAR_AXLE, R_WHEEL_R)
    elif node_name == 'Wheel_RR':
        pivot = (-0.855, Y_REAR_AXLE, R_WHEEL_R)
    elif node_name == 'SteeringWheel':
        pivot = tuple(SW_HUB)
    else:
        pivot = (0.0, 0.0, 0.0)

    S.cursor.location = pivot
    activate(joined)
    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_objects.append(joined)

# Triangulate and clean degenerate sliver faces
for o in final_objects:
    bm = bmesh.new()
    bm.from_mesh(o.data)
    bmesh.ops.triangulate(bm, faces=list(bm.faces))
    slivers = [f for f in bm.faces if f.calc_area() < 1e-11]
    if slivers:
        bmesh.ops.delete(bm, geom=slivers, context='FACES_ONLY')
    bm.to_mesh(o.data)
    bm.free()
    o.data.update()

# Enforce strict 60k..110k triangle budget (target ~92,000 tris if raw exceeds 106,000)
raw_tris = sum(len(o.data.polygons) for o in final_objects)
print('RAW_TRIANGLES', raw_tris, flush=True)
if raw_tris > 106000:
    body_obj = next(o for o in final_objects if o.name == 'Body')
    other_tris = raw_tris - len(body_obj.data.polygons)
    target_body_tris = max(35000, 92000 - other_tris)
    ratio = min(0.98, target_body_tris / max(1, len(body_obj.data.polygons)))
    activate(body_obj)
    dec = body_obj.modifiers.new('BudgetDecimate', 'DECIMATE')
    dec.ratio = ratio
    bpy.ops.object.modifier_apply(modifier=dec.name)
    bm = bmesh.new()
    bm.from_mesh(body_obj.data)
    bmesh.ops.triangulate(bm, faces=list(bm.faces))
    bm.to_mesh(body_obj.data)
    bm.free()
    body_obj.data.update()

# Calibrate exact outer bounding box to 4.70 m long ([-2.35, +2.35]), 2.10 m wide ([-1.05, +1.05]), 1.10 m high ([0.00, +1.10])
# while keeping wheelbase fixed at 2.90 m (-1.45 and +1.45) and tyres touching Z = 0.000 m!
all_world_verts = [o.matrix_world @ v.co for o in final_objects for v in o.data.vertices]
cur_ymin = min(v.y for v in all_world_verts)
cur_ymax = max(v.y for v in all_world_verts)
cur_xmax = max(abs(v.x) for v in all_world_verts)
cur_zmax = max(v.z for v in all_world_verts)


def fit_longitudinal_y(y):
    if y < Y_FRONT_AXLE:
        return Y_FRONT_AXLE + (y - Y_FRONT_AXLE) * ((-2.35 - Y_FRONT_AXLE) / (cur_ymin - Y_FRONT_AXLE))
    elif y > Y_REAR_AXLE:
        return Y_REAR_AXLE + (y - Y_REAR_AXLE) * ((2.35 - Y_REAR_AXLE) / (cur_ymax - Y_REAR_AXLE))
    return y


for o in final_objects:
    inv_mw = 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_longitudinal_y(w.y)
            w.z = max(0.004, w.z * (1.100 / cur_zmax))
        else:
            w.z = max(0.000, w.z)
        w.x = w.x * (1.050 / cur_xmax)
        v.co = inv_mw @ w
    o.data.update()

# Add the 8 required Empty gameplay nodes
EMPTY_SOCKETS = {
    'Seat_Driver':  (0.32,  -0.10, 0.34),
    'Door_L':       (1.04,  -0.15, 0.54),
    'Door_R':       (-1.04, -0.15, 0.54),
    'Exhaust':      (0.00,   2.33, 0.615),
    'Light_Head_L': (0.72,  -1.92, 0.54),
    'Light_Head_R': (-0.72, -1.92, 0.54),
    'Light_Tail_L': (0.82,   2.26, 0.72),
    'Light_Tail_R': (-0.82,  2.26, 0.72),
}
for s_name, s_pos in EMPTY_SOCKETS.items():
    emp = bpy.data.objects.new(s_name, None)
    S.collection.objects.link(emp)
    emp.location = s_pos
    final_objects.append(emp)


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


gltf_verts = [to_gltf_coords(o.matrix_world @ v.co) for o in final_objects if o.type == 'MESH' for v in o.data.vertices]
bbox = {
    'min': [round(min(v[i] for v in gltf_verts), 6) for i in range(3)],
    'max': [round(max(v[i] for v in gltf_verts), 6) for i in range(3)],
}
total_tris = sum(len(o.data.polygons) for o in final_objects if o.type == 'MESH')
print('TOTAL_TRIANGLES', total_tris, 'BBOX', bbox, flush=True)

manifest = {
    'contract': 'Forge Garage v2',
    'units': 'metres',
    'axes': {'front': '+Z', 'up': '+Y', 'left': '+X'},
    'nodes': {o.name: {'position': to_gltf_coords(o.location), 'type': o.type} for o in final_objects},
    'bounding_box': bbox,
    'triangles': total_tris,
    'wheelbase': 2.90,
    '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 future-racer.glb (selected asset nodes only, no cameras/lights/helpers)
bpy.ops.object.select_all(action='DESELECT')
for o in final_objects:
    o.select_set(True)

glb_path = os.path.join(ROOT, 'future-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.10, 0.20, 0.55, 1.0],
# KHR_materials_clearcoat (1.0, roughness 0.05), and Glass alphaMode = BLEND
blob = open(glb_path, 'rb').read()
json_len = struct.unpack_from('<I', blob, 12)[0]
doc = json.loads(blob[20:20 + json_len])
bin_chunk = blob[20 + json_len:]

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', []):
    mname = mat.get('name', '')
    if mname == 'Paint':
        mat.setdefault('pbrMetallicRoughness', {})['baseColorFactor'] = PAINTS[0][1] + [1.0]
        mat.setdefault('extensions', {})['KHR_materials_clearcoat'] = {
            'clearcoatFactor': 1.0,
            'clearcoatRoughnessFactor': 0.05,
        }
    elif mname == 'Glass':
        mat['alphaMode'] = 'BLEND'
        pbr = mat.setdefault('pbrMetallicRoughness', {})
        pbr['baseColorFactor'] = [0.025, 0.035, 0.048, 0.42]
        pbr['roughnessFactor'] = 0.035
        pbr['metallicFactor'] = 0.12

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

glb_mb = round(os.path.getsize(glb_path) / (1024 * 1024), 2)
print('GLB_EXPORTED', glb_path, f'{glb_mb} MB', flush=True)


# ==============================================================================
# 14. STUDIO DAYLIGHT EVIDENCE RENDERS (final.png, side.png, clay.png, paints.png)
# ==============================================================================
# Wire Multiply node on Paint shader so Cycles renders the exact baseColorFactor * neutral detail texture
p_paint = M['Paint'].node_tree.nodes.get('Principled BSDF')
l_paint = M['Paint'].node_tree.links
base_tex_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['Fac'].default_value = 1.0
mix_paint.inputs[2].default_value = (*PAINTS[0][1], 1.0)
l_paint.new(base_tex_sock, mix_paint.inputs[1])
l_paint.new(mix_paint.outputs['Color'], p_paint.inputs['Base Color'])

# Soft Studio Daylight World & Neutral Studio Floor (matching sheet.jpg studio backdrop)
world = bpy.data.worlds.new('SoftDaylight')
world.use_nodes = True
world.node_tree.nodes['Background'].inputs[0].default_value = (0.76, 0.77, 0.79, 1.0)
world.node_tree.nodes['Background'].inputs[1].default_value = 0.38
S.world = world

floor_mat = bpy.data.materials.new('StudioFloor')
floor_mat.use_nodes = True
fp = floor_mat.node_tree.nodes.get('Principled BSDF')
fp.inputs['Base Color'].default_value = (0.62, 0.63, 0.64, 1.0)
fp.inputs['Roughness'].default_value = 0.82
bpy.ops.mesh.primitive_plane_add(size=200.0, location=(0.0, 0.0, -0.001))
ground = bpy.context.object
ground.name = 'StudioGround'
ground.data.materials.append(floor_mat)


def add_area_light(name, loc, energy, size, target, shape='RECTANGLE', size_y=2.0, color=(1.0, 0.99, 0.97)):
    ld = bpy.data.lights.new(name, 'AREA')
    ld.energy = energy
    ld.shape = shape
    ld.size = size
    if shape == 'RECTANGLE':
        ld.size_y = size_y
    ld.color = color
    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()
    return ob


add_area_light('OverheadSoftbox', (0.6, -1.2, 5.8), 460.0, 7.0, (0.0, 0.0, 0.42), 'RECTANGLE', 3.8)
add_area_light('LeftKeyStrip', (4.8, -2.0, 2.5), 360.0, 6.0, (0.0, -0.1, 0.42), 'RECTANGLE', 1.6)
add_area_light('RightFillStrip', (-4.8, -1.8, 2.3), 240.0, 5.5, (0.0, -0.1, 0.42), 'RECTANGLE', 1.6)
add_area_light('FrontLowFill', (1.4, -5.5, 1.3), 180.0, 3.5, (0.0, -1.4, 0.32), 'DISK')
add_area_light('RearRimBacklight', (-2.2, 5.0, 3.2), 300.0, 4.5, (0.0, 0.8, 0.52), 'DISK')

# Warm orange point lights nested inside the left & right side turbine intakes
# so the glowing turbine core casts authentic warm light on the surrounding intake scoop & ground!
turbine_lights = []
for side in [-1, 1]:
    pld = bpy.data.lights.new(f'TurbineGlowLight_{side}', 'POINT')
    pld.energy = 32.0
    pld.color = (1.0, 0.30, 0.02)
    pld.shadow_soft_size = 0.12
    pob = bpy.data.objects.new(f'TurbineGlowLight_{side}', pld)
    S.collection.objects.link(pob)
    pob.location = (side * 0.86, 0.32, 0.46)
    turbine_lights.append(pob)

cam_data = bpy.data.cameras.new('RenderCam')
cam_obj = bpy.data.objects.new('RenderCam', cam_data)
S.collection.objects.link(cam_obj)
S.camera = cam_obj


def set_camera(pos, target, lens=50.0, ortho_scale=None):
    cam_obj.location = pos
    cam_obj.rotation_euler = (Vector(target) - cam_obj.location).to_track_quat('-Z', 'Y').to_euler()
    cam_data.type = 'ORTHO' if ortho_scale else 'PERSP'
    cam_data.lens = lens
    if ortho_scale:
        cam_data.ortho_scale = ortho_scale


def render_image(filename, 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, filename)
    bpy.ops.render.render(write_still=True)
    print('RENDERED', filename, flush=True)


# 1. final.png (1920x1080 Cycles render, 3/4 front view matching sheet.jpg angle with full nose & rear margin!)
hero_cam_pos = (4.20, -4.95, 1.88)
hero_cam_tgt = (0.0, -0.05, 0.42)
set_camera(hero_cam_pos, hero_cam_tgt, lens=50.0)
render_image('final.png', 1920, 1080, samples=44)

# 2. clay.png (same view as final, plain grey material)
for pob in turbine_lights:
    pob.hide_render = True
clay_mat = bpy.data.materials.new('ClayOverride')
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.56
S.view_layers[0].material_override = clay_mat
render_image('clay.png', 1920, 1080, samples=32)
S.view_layers[0].material_override = None
for pob in turbine_lights:
    pob.hide_render = False

# 3. side.png (orthographic side view matching sheet.jpg orientation: nose pointing LEFT!)
set_camera((7.5, 0.0, 0.52), (0.0, 0.0, 0.52), ortho_scale=5.35)
render_image('side.png', 1920, 960, samples=36)

# 4. paints.png (3x2 grid of the same 3/4 view in all six paints)
set_camera(hero_cam_pos, hero_cam_tgt, lens=50.0)
tile_buffers = []
for p_name, p_rgb in PAINTS:
    mix_paint.inputs[2].default_value = (*p_rgb, 1.0)
    tile_fn = f'_paint_tile_{p_name}.png'
    render_image(tile_fn, 640, 360, samples=16)
    tile_path = os.path.join(ROOT, tile_fn)
    im = bpy.data.images.load(tile_path)
    tile_buffers.append(list(im.pixels))
    bpy.data.images.remove(im)
    try:
        os.remove(tile_path)
    except OSError:
        pass

grid_img = bpy.data.images.new('PaintsGrid', width=1920, height=720, alpha=True)
grid_pix = [0.0] * (1920 * 720 * 4)
for idx, buf in enumerate(tile_buffers):
    col_i = idx % 3
    row_i = 1 - (idx // 3)
    for y_px in range(360):
        dst_start = ((row_i * 360 + y_px) * 1920 + col_i * 640) * 4
        src_start = y_px * 640 * 4
        grid_pix[dst_start:dst_start + 640 * 4] = buf[src_start:src_start + 640 * 4]
grid_img.pixels.foreach_set(grid_pix)
grid_img.filepath_raw = os.path.join(ROOT, 'paints.png')
grid_img.file_format = 'PNG'
grid_img.save()
mix_paint.inputs[2].default_value = (*PAINTS[0][1], 1.0)

# 5. Write exact 6-line NOTES.md
with open(os.path.join(ROOT, 'NOTES.md'), 'w') as f:
    f.write('Futuristic concept race car (4.70 m long, 2.10 m wide, 1.10 m high, 2.90 m wheelbase) with low wedge body, wrap-around glass canopy, glowing side turbine intakes, floating rear aero blades, turbine-blade rims with slick tyres and twin rear thruster nozzles.\n')
    f.write(f'{total_tris:,} triangles across 6 mesh nodes (Body, Wheel_FL, Wheel_FR, Wheel_RL, Wheel_RR, SteeringWheel) and 8 empty gameplay nodes.\n')
    f.write('8 PBR materials: Paint (clearcoat 1.0, neutral grime/wear detail + baseColorFactor [0.10, 0.20, 0.55]), CarbonFibre, MetalChrome, MattePlastic, Rubber, Interior, Glass (alpha BLEND), Lamps (emissive white/orange/cyan/red).\n')
    f.write(f'Texture sizes: Paint Base/Normal 1024 px (ORM 512 px), other Base/Normal 512 px (ORM 256 px), Lamps emissive 256x64 px; exported future-racer.glb is {glb_mb} MB.\n')
    f.write('Poly Haven CC0 texture IDs used: metal_plate, asphalt_02, denim_fabric, fabric_leather_02 (https://polyhaven.com/license).\n')
    f.write('Differences from reference: original dual-chevron headlight clusters, hexagonal side window line and continuous rear LED arch per originality rule, blank tyres/panels with no text, and default Paint set to electric blue [0.10, 0.20, 0.55] with metallic grey included in paints.png.\n')

print('ALL_DELIVERABLES_COMPLETE', flush=True)
