#!/usr/bin/env python3
"""
build_sports-coupe-g4.py
Complete procedural build, texture download, Cycles bake, GLB export, and rendering
for the Forge asset library: low two-door front-engine sports coupe / grand tourer (sports-coupe-g4).
"""

import os
import sys
import json
import math
import urllib.request
import bpy
import bmesh
from mathutils import Vector, Matrix

# ---------------------------------------------------------------------------
# CONFIG & PATHS
# ---------------------------------------------------------------------------
OUT_DIR = os.path.dirname(os.path.abspath(__file__))
TEX_DIR = os.path.join(OUT_DIR, "textures")
BAKE_DIR = os.path.join(OUT_DIR, "baked")
os.makedirs(TEX_DIR, exist_ok=True)
os.makedirs(BAKE_DIR, exist_ok=True)

# Poly Haven CC0 textures used (recorded in NOTES.md)
POLYHAVEN_IDS = [
    "metal_plate",
    "asphalt_02",
    "rubber_tiles",
    "fabric_leather_02",
]

# Paint colours (linear RGB triples, default yellow first as required by Paint contract)
PAINTS = [
    [0.85, 0.55, 0.02],  # Default: Solar Yellow
    [0.10, 0.11, 0.12],  # Gunmetal Stealth Grey (sheet reference tone)
    [0.62, 0.03, 0.03],  # Rosso Corsa Crimson
    [0.02, 0.11, 0.40],  # Deep Le Mans Blue
    [0.03, 0.20, 0.07],  # British Racing Green
    [0.72, 0.74, 0.77],  # Liquid Silver Metallic
]

# Vehicle target metrics:
# Length: 4.45 m (-2.225 to +2.225 in Z)
# Width:  1.92 m (-0.960 to +0.960 in X)
# Height: 1.27 m (0.000 to +1.270 in Y)
# Wheelbase: 2.55 m (Z = -1.275 rear axle, Z = +1.275 front axle)
WHEELBASE = 2.55
Z_FRONT_AXLE = 1.275
Z_REAR_AXLE = -1.275
WHEEL_RADIUS = 0.355
WHEEL_WIDTH_F = 0.255
WHEEL_WIDTH_R = 0.285
X_WHEEL_F = 0.805
X_WHEEL_R = 0.815


# ---------------------------------------------------------------------------
# 1. DOWNLOAD POLY HAVEN TEXTURES
# ---------------------------------------------------------------------------
def download_polyhaven_textures():
    for ph_id in POLYHAVEN_IDS:
        for map_type in ["diff", "nor_gl", "arm"]:
            fname = f"{ph_id}_{map_type}_1k.jpg"
            fpath = os.path.join(TEX_DIR, fname)
            if not os.path.exists(fpath) or os.path.getsize(fpath) < 1000:
                url = f"https://dl.polyhaven.org/file/ph-assets/Textures/jpg/1k/{ph_id}/{fname}"
                print(f"[Download] {url} -> {fpath}")
                req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
                with urllib.request.urlopen(req, timeout=30) as resp, open(fpath, "wb") as out:
                    out.write(resp.read())


# ---------------------------------------------------------------------------
# 2. SCENE RESET & GPU SETUP
# ---------------------------------------------------------------------------
def reset_scene():
    bpy.ops.wm.read_factory_settings(use_empty=True)
    scene = bpy.context.scene
    scene.unit_settings.system = 'METRIC'
    scene.unit_settings.scale_length = 1.0

    scene.render.engine = 'CYCLES'
    prefs = bpy.context.preferences
    cprefs = prefs.addons['cycles'].preferences
    cprefs.refresh_devices()
    for dev_type in ('OPTIX', 'CUDA', 'HIP', 'ONEAPI'):
        try:
            cprefs.compute_device_type = dev_type
            for d in cprefs.devices:
                d.use = True
            if any(d.use for d in cprefs.devices if d.type != 'CPU'):
                print(f"[Cycles] Using GPU device type: {dev_type}")
                break
        except Exception:
            pass
    scene.cycles.device = 'GPU'
    scene.cycles.samples = 64
    scene.cycles.use_denoising = True


# ---------------------------------------------------------------------------
# 3. PROCEDURAL + POLYHAVEN MATERIALS & CYCLES BAKING
# ---------------------------------------------------------------------------
def load_image(path, non_color=False):
    img = bpy.data.images.load(path, check_existing=True)
    if non_color:
        img.colorspace_settings.name = 'Non-Color'
    return img


def build_source_and_final_materials():
    scene = bpy.context.scene

    bpy.ops.mesh.primitive_plane_add(size=2.0, location=(0, 0, -50))
    bake_plane = bpy.context.active_object
    bake_plane.name = "_BakePlane"

    def bake_material_maps(mat_name, setup_fn, res_col=1024, res_orm=512):
        mat = bpy.data.materials.new(name=f"_src_{mat_name}")
        mat.use_nodes = True
        nodes = mat.node_tree.nodes
        links = mat.node_tree.links
        nodes.clear()

        bsdf, out_node = setup_fn(nodes, links)
        bake_plane.data.materials.clear()
        bake_plane.data.materials.append(mat)
        bpy.context.view_layer.objects.active = bake_plane
        bake_plane.select_set(True)

        prev_samples = scene.cycles.samples
        scene.cycles.samples = 16

        img_col = bpy.data.images.new(f"{mat_name}_BaseColor", width=res_col, height=res_col, alpha=False)
        emit_node = nodes.new('ShaderNodeEmission')
        if bsdf.inputs['Base Color'].is_linked:
            links.new(bsdf.inputs['Base Color'].links[0].from_socket, emit_node.inputs['Color'])
        else:
            emit_node.inputs['Color'].default_value = bsdf.inputs['Base Color'].default_value
        links.new(emit_node.outputs['Emission'], out_node.inputs['Surface'])

        tex_target = nodes.new('ShaderNodeTexImage')
        tex_target.image = img_col
        for n in nodes:
            n.select = False
        tex_target.select = True
        nodes.active = tex_target

        bpy.ops.object.bake(type='EMIT')
        col_path = os.path.join(BAKE_DIR, f"{mat_name}_BaseColor.png")
        img_col.filepath_raw = col_path
        img_col.file_format = 'PNG'
        img_col.save()

        links.new(bsdf.outputs['BSDF'], out_node.inputs['Surface'])

        img_nor = bpy.data.images.new(f"{mat_name}_Normal", width=res_col, height=res_col, alpha=False)
        img_nor.colorspace_settings.name = 'Non-Color'
        tex_target.image = img_nor
        nodes.active = tex_target
        bpy.ops.object.bake(type='NORMAL')
        nor_path = os.path.join(BAKE_DIR, f"{mat_name}_Normal.png")
        img_nor.filepath_raw = nor_path
        img_nor.file_format = 'PNG'
        img_nor.save()

        img_orm = bpy.data.images.new(f"{mat_name}_ORM", width=res_orm, height=res_orm, alpha=False)
        img_orm.colorspace_settings.name = 'Non-Color'
        comb = nodes.new('ShaderNodeCombineColor')
        comb.inputs['Red'].default_value = 0.92
        if bsdf.inputs['Roughness'].is_linked:
            links.new(bsdf.inputs['Roughness'].links[0].from_socket, comb.inputs['Green'])
        else:
            comb.inputs['Green'].default_value = bsdf.inputs['Roughness'].default_value
        if bsdf.inputs['Metallic'].is_linked:
            links.new(bsdf.inputs['Metallic'].links[0].from_socket, comb.inputs['Blue'])
        else:
            comb.inputs['Blue'].default_value = bsdf.inputs['Metallic'].default_value

        for n in nodes:
            if n.type == 'TEX_IMAGE' and n.image and '_arm_' in n.image.name:
                sep = nodes.new('ShaderNodeSeparateColor')
                links.new(n.outputs['Color'], sep.inputs['Color'])
                links.new(sep.outputs['Red'], comb.inputs['Red'])
                break

        links.new(comb.outputs['Color'], emit_node.inputs['Color'])
        links.new(emit_node.outputs['Emission'], out_node.inputs['Surface'])
        tex_target.image = img_orm
        nodes.active = tex_target
        bpy.ops.object.bake(type='EMIT')
        orm_path = os.path.join(BAKE_DIR, f"{mat_name}_ORM.png")
        img_orm.filepath_raw = orm_path
        img_orm.file_format = 'PNG'
        img_orm.save()

        scene.cycles.samples = prev_samples
        return img_col, img_nor, img_orm

    # --- Setup 1: Paint ---
    def setup_paint_src(nodes, links):
        out = nodes.new('ShaderNodeOutputMaterial')
        bsdf = nodes.new('ShaderNodeBsdfPrincipled')
        links.new(bsdf.outputs['BSDF'], out.inputs['Surface'])

        tex_coord = nodes.new('ShaderNodeTexCoord')
        mapping = nodes.new('ShaderNodeMapping')
        mapping.inputs['Scale'].default_value = (4.0, 4.0, 4.0)
        links.new(tex_coord.outputs['UV'], mapping.inputs['Vector'])

        t_metal_diff = nodes.new('ShaderNodeTexImage')
        t_metal_diff.image = load_image(os.path.join(TEX_DIR, "metal_plate_diff_1k.jpg"))
        links.new(mapping.outputs['Vector'], t_metal_diff.inputs['Vector'])

        t_metal_arm = nodes.new('ShaderNodeTexImage')
        t_metal_arm.image = load_image(os.path.join(TEX_DIR, "metal_plate_arm_1k.jpg"), non_color=True)
        links.new(mapping.outputs['Vector'], t_metal_arm.inputs['Vector'])

        t_asph_diff = nodes.new('ShaderNodeTexImage')
        t_asph_diff.image = load_image(os.path.join(TEX_DIR, "asphalt_02_diff_1k.jpg"))
        links.new(mapping.outputs['Vector'], t_asph_diff.inputs['Vector'])

        t_metal_nor = nodes.new('ShaderNodeTexImage')
        t_metal_nor.image = load_image(os.path.join(TEX_DIR, "metal_plate_nor_gl_1k.jpg"), non_color=True)
        links.new(mapping.outputs['Vector'], t_metal_nor.inputs['Vector'])

        noise = nodes.new('ShaderNodeTexNoise')
        noise.inputs['Scale'].default_value = 10.0
        noise.inputs['Detail'].default_value = 8.0
        links.new(mapping.outputs['Vector'], noise.inputs['Vector'])

        mix_a = nodes.new('ShaderNodeMix')
        mix_a.data_type = 'RGBA'
        mix_a.inputs['Factor'].default_value = 0.5
        links.new(t_metal_diff.outputs['Color'], mix_a.inputs['A'])
        links.new(t_asph_diff.outputs['Color'], mix_a.inputs['B'])

        bw = nodes.new('ShaderNodeRGBToBW')
        links.new(mix_a.outputs['Result'], bw.inputs['Color'])

        ramp = nodes.new('ShaderNodeValToRGB')
        ramp.color_ramp.elements[0].position = 0.0
        ramp.color_ramp.elements[0].color = (0.76, 0.76, 0.77, 1.0)
        ramp.color_ramp.elements[1].position = 1.0
        ramp.color_ramp.elements[1].color = (0.98, 0.98, 0.98, 1.0)
        links.new(bw.outputs['Val'], ramp.inputs['Fac'])

        mix_grime = nodes.new('ShaderNodeMix')
        mix_grime.data_type = 'RGBA'
        mix_grime.inputs['Factor'].default_value = 0.16
        links.new(ramp.outputs['Color'], mix_grime.inputs['A'])
        links.new(noise.outputs['Color'], mix_grime.inputs['B'])
        links.new(mix_grime.outputs['Result'], bsdf.inputs['Base Color'])

        sep_arm = nodes.new('ShaderNodeSeparateColor')
        links.new(t_metal_arm.outputs['Color'], sep_arm.inputs['Color'])
        map_r = nodes.new('ShaderNodeMapRange')
        map_r.inputs['From Min'].default_value = 0.0
        map_r.inputs['From Max'].default_value = 1.0
        map_r.inputs['To Min'].default_value = 0.34
        map_r.inputs['To Max'].default_value = 0.48
        links.new(sep_arm.outputs['Green'], map_r.inputs['Value'])
        links.new(map_r.outputs['Result'], bsdf.inputs['Roughness'])

        bsdf.inputs['Metallic'].default_value = 0.28

        nor_map = nodes.new('ShaderNodeNormalMap')
        nor_map.inputs['Strength'].default_value = 0.06
        links.new(t_metal_nor.outputs['Color'], nor_map.inputs['Color'])
        links.new(nor_map.outputs['Normal'], bsdf.inputs['Normal'])
        return bsdf, out

    # --- Setup 2: Trim_Rubber_Tire_Interior (Quadrant UV Atlas baked at 1024px) ---
    def setup_trim_atlas_src(nodes, links):
        out = nodes.new('ShaderNodeOutputMaterial')
        bsdf = nodes.new('ShaderNodeBsdfPrincipled')
        links.new(bsdf.outputs['BSDF'], out.inputs['Surface'])

        tex_coord = nodes.new('ShaderNodeTexCoord')
        sep_xyz = nodes.new('ShaderNodeSeparateXYZ')
        links.new(tex_coord.outputs['UV'], sep_xyz.inputs['Vector'])

        u_right = nodes.new('ShaderNodeMath')
        u_right.operation = 'GREATER_THAN'
        u_right.inputs[1].default_value = 0.5
        links.new(sep_xyz.outputs['X'], u_right.inputs[0])

        v_top = nodes.new('ShaderNodeMath')
        v_top.operation = 'GREATER_THAN'
        v_top.inputs[1].default_value = 0.5
        links.new(sep_xyz.outputs['Y'], v_top.inputs[0])

        mapping = nodes.new('ShaderNodeMapping')
        mapping.inputs['Scale'].default_value = (6.0, 6.0, 6.0)
        links.new(tex_coord.outputs['UV'], mapping.inputs['Vector'])

        t_rub_d = nodes.new('ShaderNodeTexImage')
        t_rub_d.image = load_image(os.path.join(TEX_DIR, "rubber_tiles_diff_1k.jpg"))
        links.new(mapping.outputs['Vector'], t_rub_d.inputs['Vector'])

        t_rub_n = nodes.new('ShaderNodeTexImage')
        t_rub_n.image = load_image(os.path.join(TEX_DIR, "rubber_tiles_nor_gl_1k.jpg"), non_color=True)
        links.new(mapping.outputs['Vector'], t_rub_n.inputs['Vector'])

        t_lea_d = nodes.new('ShaderNodeTexImage')
        t_lea_d.image = load_image(os.path.join(TEX_DIR, "fabric_leather_02_diff_1k.jpg"))
        links.new(mapping.outputs['Vector'], t_lea_d.inputs['Vector'])

        t_lea_n = nodes.new('ShaderNodeTexImage')
        t_lea_n.image = load_image(os.path.join(TEX_DIR, "fabric_leather_02_nor_gl_1k.jpg"), non_color=True)
        links.new(mapping.outputs['Vector'], t_lea_n.inputs['Vector'])

        t_asph_d = nodes.new('ShaderNodeTexImage')
        t_asph_d.image = load_image(os.path.join(TEX_DIR, "asphalt_02_diff_1k.jpg"))
        links.new(mapping.outputs['Vector'], t_asph_d.inputs['Vector'])

        col_tyre = nodes.new('ShaderNodeMix')
        col_tyre.data_type = 'RGBA'
        col_tyre.inputs['Factor'].default_value = 0.80
        links.new(t_rub_d.outputs['Color'], col_tyre.inputs['A'])
        col_tyre.inputs['B'].default_value = (0.032, 0.033, 0.035, 1.0)

        col_plas = nodes.new('ShaderNodeMix')
        col_plas.data_type = 'RGBA'
        col_plas.inputs['Factor'].default_value = 0.88
        links.new(t_asph_d.outputs['Color'], col_plas.inputs['A'])
        col_plas.inputs['B'].default_value = (0.025, 0.026, 0.029, 1.0)

        col_chrome = nodes.new('ShaderNodeMix')
        col_chrome.data_type = 'RGBA'
        col_chrome.inputs['Factor'].default_value = 0.90
        links.new(t_asph_d.outputs['Color'], col_chrome.inputs['A'])
        col_chrome.inputs['B'].default_value = (0.85, 0.87, 0.90, 1.0)

        col_lea = nodes.new('ShaderNodeMix')
        col_lea.data_type = 'RGBA'
        col_lea.inputs['Factor'].default_value = 0.82
        links.new(t_lea_d.outputs['Color'], col_lea.inputs['A'])
        col_lea.inputs['B'].default_value = (0.075, 0.072, 0.070, 1.0)

        mix_top_col = nodes.new('ShaderNodeMix')
        mix_top_col.data_type = 'RGBA'
        links.new(u_right.outputs['Value'], mix_top_col.inputs['Factor'])
        links.new(col_tyre.outputs['Result'], mix_top_col.inputs['A'])
        links.new(col_plas.outputs['Result'], mix_top_col.inputs['B'])

        mix_bot_col = nodes.new('ShaderNodeMix')
        mix_bot_col.data_type = 'RGBA'
        links.new(u_right.outputs['Value'], mix_bot_col.inputs['Factor'])
        links.new(col_chrome.outputs['Result'], mix_bot_col.inputs['A'])
        links.new(col_lea.outputs['Result'], mix_bot_col.inputs['B'])

        mix_all_col = nodes.new('ShaderNodeMix')
        mix_all_col.data_type = 'RGBA'
        links.new(v_top.outputs['Value'], mix_all_col.inputs['Factor'])
        links.new(mix_bot_col.outputs['Result'], mix_all_col.inputs['A'])
        links.new(mix_top_col.outputs['Result'], mix_all_col.inputs['B'])
        links.new(mix_all_col.outputs['Result'], bsdf.inputs['Base Color'])

        r_top = nodes.new('ShaderNodeMix')
        r_top.data_type = 'FLOAT'
        links.new(u_right.outputs['Value'], r_top.inputs['Factor'])
        r_top.inputs['A'].default_value = 0.68
        r_top.inputs['B'].default_value = 0.52

        r_bot = nodes.new('ShaderNodeMix')
        r_bot.data_type = 'FLOAT'
        links.new(u_right.outputs['Value'], r_bot.inputs['Factor'])
        r_bot.inputs['A'].default_value = 0.12
        r_bot.inputs['B'].default_value = 0.44

        r_all = nodes.new('ShaderNodeMix')
        r_all.data_type = 'FLOAT'
        links.new(v_top.outputs['Value'], r_all.inputs['Factor'])
        links.new(r_bot.outputs['Result'], r_all.inputs['A'])
        links.new(r_top.outputs['Result'], r_all.inputs['B'])
        links.new(r_all.outputs['Result'], bsdf.inputs['Roughness'])

        m_bot = nodes.new('ShaderNodeMix')
        m_bot.data_type = 'FLOAT'
        links.new(u_right.outputs['Value'], m_bot.inputs['Factor'])
        m_bot.inputs['A'].default_value = 0.95
        m_bot.inputs['B'].default_value = 0.04

        m_all = nodes.new('ShaderNodeMix')
        m_all.data_type = 'FLOAT'
        links.new(v_top.outputs['Value'], m_all.inputs['Factor'])
        links.new(m_bot.outputs['Result'], m_all.inputs['A'])
        m_all.inputs['B'].default_value = 0.05
        links.new(m_all.outputs['Result'], bsdf.inputs['Metallic'])

        n_mix = nodes.new('ShaderNodeMix')
        n_mix.data_type = 'RGBA'
        links.new(v_top.outputs['Value'], n_mix.inputs['Factor'])
        links.new(t_lea_n.outputs['Color'], n_mix.inputs['A'])
        links.new(t_rub_n.outputs['Color'], n_mix.inputs['B'])

        nor_map = nodes.new('ShaderNodeNormalMap')
        nor_map.inputs['Strength'].default_value = 0.30
        links.new(n_mix.outputs['Result'], nor_map.inputs['Color'])
        links.new(nor_map.outputs['Normal'], bsdf.inputs['Normal'])
        return bsdf, out

    # --- Setup 3: Wheel_Alloy_Brakes ---
    def setup_wheel_atlas_src(nodes, links):
        out = nodes.new('ShaderNodeOutputMaterial')
        bsdf = nodes.new('ShaderNodeBsdfPrincipled')
        links.new(bsdf.outputs['BSDF'], out.inputs['Surface'])

        tex_coord = nodes.new('ShaderNodeTexCoord')
        sep_xyz = nodes.new('ShaderNodeSeparateXYZ')
        links.new(tex_coord.outputs['UV'], sep_xyz.inputs['Vector'])

        u_right = nodes.new('ShaderNodeMath')
        u_right.operation = 'GREATER_THAN'
        u_right.inputs[1].default_value = 0.5
        links.new(sep_xyz.outputs['X'], u_right.inputs[0])

        v_top = nodes.new('ShaderNodeMath')
        v_top.operation = 'GREATER_THAN'
        v_top.inputs[1].default_value = 0.5
        links.new(sep_xyz.outputs['Y'], v_top.inputs[0])

        mapping = nodes.new('ShaderNodeMapping')
        mapping.inputs['Scale'].default_value = (6.0, 6.0, 6.0)
        links.new(tex_coord.outputs['UV'], mapping.inputs['Vector'])

        t_met_d = nodes.new('ShaderNodeTexImage')
        t_met_d.image = load_image(os.path.join(TEX_DIR, "metal_plate_diff_1k.jpg"))
        links.new(mapping.outputs['Vector'], t_met_d.inputs['Vector'])

        t_met_n = nodes.new('ShaderNodeTexImage')
        t_met_n.image = load_image(os.path.join(TEX_DIR, "metal_plate_nor_gl_1k.jpg"), non_color=True)
        links.new(mapping.outputs['Vector'], t_met_n.inputs['Vector'])

        t_rub_d = nodes.new('ShaderNodeTexImage')
        t_rub_d.image = load_image(os.path.join(TEX_DIR, "rubber_tiles_diff_1k.jpg"))
        links.new(mapping.outputs['Vector'], t_rub_d.inputs['Vector'])

        t_rub_n = nodes.new('ShaderNodeTexImage')
        t_rub_n.image = load_image(os.path.join(TEX_DIR, "rubber_tiles_nor_gl_1k.jpg"), non_color=True)
        links.new(mapping.outputs['Vector'], t_rub_n.inputs['Vector'])

        c_rim = nodes.new('ShaderNodeMix')
        c_rim.data_type = 'RGBA'
        c_rim.inputs['Factor'].default_value = 0.76
        links.new(t_met_d.outputs['Color'], c_rim.inputs['A'])
        c_rim.inputs['B'].default_value = (0.48, 0.49, 0.51, 1.0)

        c_rot = nodes.new('ShaderNodeMix')
        c_rot.data_type = 'RGBA'
        c_rot.inputs['Factor'].default_value = 0.78
        links.new(t_met_d.outputs['Color'], c_rot.inputs['A'])
        c_rot.inputs['B'].default_value = (0.30, 0.31, 0.33, 1.0)

        c_cal = nodes.new('ShaderNodeMix')
        c_cal.data_type = 'RGBA'
        c_cal.inputs['Factor'].default_value = 0.82
        links.new(t_met_d.outputs['Color'], c_cal.inputs['A'])
        c_cal.inputs['B'].default_value = (0.12, 0.12, 0.13, 1.0)

        c_tyr = nodes.new('ShaderNodeMix')
        c_tyr.data_type = 'RGBA'
        c_tyr.inputs['Factor'].default_value = 0.80
        links.new(t_rub_d.outputs['Color'], c_tyr.inputs['A'])
        c_tyr.inputs['B'].default_value = (0.032, 0.033, 0.036, 1.0)

        m_top = nodes.new('ShaderNodeMix')
        m_top.data_type = 'RGBA'
        links.new(u_right.outputs['Value'], m_top.inputs['Factor'])
        links.new(c_rim.outputs['Result'], m_top.inputs['A'])
        links.new(c_rot.outputs['Result'], m_top.inputs['B'])

        m_bot = nodes.new('ShaderNodeMix')
        m_bot.data_type = 'RGBA'
        links.new(u_right.outputs['Value'], m_bot.inputs['Factor'])
        links.new(c_cal.outputs['Result'], m_bot.inputs['A'])
        links.new(c_tyr.outputs['Result'], m_bot.inputs['B'])

        m_all = nodes.new('ShaderNodeMix')
        m_all.data_type = 'RGBA'
        links.new(v_top.outputs['Value'], m_all.inputs['Factor'])
        links.new(m_bot.outputs['Result'], m_all.inputs['A'])
        links.new(m_top.outputs['Result'], m_all.inputs['B'])
        links.new(m_all.outputs['Result'], bsdf.inputs['Base Color'])

        r_top = nodes.new('ShaderNodeMix')
        r_top.data_type = 'FLOAT'
        links.new(u_right.outputs['Value'], r_top.inputs['Factor'])
        r_top.inputs['A'].default_value = 0.24
        r_top.inputs['B'].default_value = 0.32

        r_bot = nodes.new('ShaderNodeMix')
        r_bot.data_type = 'FLOAT'
        links.new(u_right.outputs['Value'], r_bot.inputs['Factor'])
        r_bot.inputs['A'].default_value = 0.38
        r_bot.inputs['B'].default_value = 0.68

        r_all = nodes.new('ShaderNodeMix')
        r_all.data_type = 'FLOAT'
        links.new(v_top.outputs['Value'], r_all.inputs['Factor'])
        links.new(r_bot.outputs['Result'], r_all.inputs['A'])
        links.new(r_top.outputs['Result'], r_all.inputs['B'])
        links.new(r_all.outputs['Result'], bsdf.inputs['Roughness'])

        met_top = nodes.new('ShaderNodeMix')
        met_top.data_type = 'FLOAT'
        links.new(u_right.outputs['Value'], met_top.inputs['Factor'])
        met_top.inputs['A'].default_value = 0.88
        met_top.inputs['B'].default_value = 0.84

        met_bot = nodes.new('ShaderNodeMix')
        met_bot.data_type = 'FLOAT'
        links.new(u_right.outputs['Value'], met_bot.inputs['Factor'])
        met_bot.inputs['A'].default_value = 0.68
        met_bot.inputs['B'].default_value = 0.04

        met_all = nodes.new('ShaderNodeMix')
        met_all.data_type = 'FLOAT'
        links.new(v_top.outputs['Value'], met_all.inputs['Factor'])
        links.new(met_bot.outputs['Result'], met_all.inputs['A'])
        links.new(met_top.outputs['Result'], met_all.inputs['B'])
        links.new(met_all.outputs['Result'], bsdf.inputs['Metallic'])

        n_mix = nodes.new('ShaderNodeMix')
        n_mix.data_type = 'RGBA'
        links.new(v_top.outputs['Value'], n_mix.inputs['Factor'])
        links.new(t_rub_n.outputs['Color'], n_mix.inputs['A'])
        links.new(t_met_n.outputs['Color'], n_mix.inputs['B'])

        nor_map = nodes.new('ShaderNodeNormalMap')
        nor_map.inputs['Strength'].default_value = 0.35
        links.new(n_mix.outputs['Result'], nor_map.inputs['Color'])
        links.new(nor_map.outputs['Normal'], bsdf.inputs['Normal'])
        return bsdf, out

    print("[Bake] Baking Paint, Trim, and Wheel PBR texture sets...")
    paint_col, paint_nor, paint_orm = bake_material_maps("Paint", setup_paint_src, 1024, 512)
    trim_col, trim_nor, trim_orm = bake_material_maps("Trim", setup_trim_atlas_src, 1024, 512)
    wheel_col, wheel_nor, wheel_orm = bake_material_maps("Wheel", setup_wheel_atlas_src, 1024, 512)

    def setup_lamps_src(nodes, links):
        out = nodes.new('ShaderNodeOutputMaterial')
        bsdf = nodes.new('ShaderNodeBsdfPrincipled')
        links.new(bsdf.outputs['BSDF'], out.inputs['Surface'])

        tex_coord = nodes.new('ShaderNodeTexCoord')
        sep_xyz = nodes.new('ShaderNodeSeparateXYZ')
        links.new(tex_coord.outputs['UV'], sep_xyz.inputs['Vector'])

        u_right = nodes.new('ShaderNodeMath')
        u_right.operation = 'GREATER_THAN'
        u_right.inputs[1].default_value = 0.5
        links.new(sep_xyz.outputs['X'], u_right.inputs[0])

        v_top = nodes.new('ShaderNodeMath')
        v_top.operation = 'GREATER_THAN'
        v_top.inputs[1].default_value = 0.5
        links.new(sep_xyz.outputs['Y'], v_top.inputs[0])

        m_top = nodes.new('ShaderNodeMix')
        m_top.data_type = 'RGBA'
        links.new(u_right.outputs['Value'], m_top.inputs['Factor'])
        m_top.inputs['A'].default_value = (1.0, 0.94, 0.82, 1.0)
        m_top.inputs['B'].default_value = (0.95, 0.04, 0.03, 1.0)

        m_bot = nodes.new('ShaderNodeMix')
        m_bot.data_type = 'RGBA'
        links.new(u_right.outputs['Value'], m_bot.inputs['Factor'])
        m_bot.inputs['A'].default_value = (0.98, 0.42, 0.02, 1.0)
        m_bot.inputs['B'].default_value = (0.04, 0.04, 0.05, 1.0)

        m_all = nodes.new('ShaderNodeMix')
        m_all.data_type = 'RGBA'
        links.new(v_top.outputs['Value'], m_all.inputs['Factor'])
        links.new(m_bot.outputs['Result'], m_all.inputs['A'])
        links.new(m_top.outputs['Result'], m_all.inputs['B'])
        links.new(m_all.outputs['Result'], bsdf.inputs['Base Color'])
        bsdf.inputs['Roughness'].default_value = 0.12
        bsdf.inputs['Metallic'].default_value = 0.10
        return bsdf, out

    lamps_col, lamps_nor, lamps_orm = bake_material_maps("Lamps", setup_lamps_src, 512, 512)

    bpy.data.objects.remove(bake_plane, do_unlink=True)
    for m in list(bpy.data.materials):
        if m.name.startswith("_src_"):
            bpy.data.materials.remove(m)

    def create_gltf_pbr_mat(name, img_col, img_nor, img_orm, base_factor=None, clearcoat=False, emissive=False):
        mat = bpy.data.materials.new(name=name)
        mat.use_nodes = True
        nodes = mat.node_tree.nodes
        links = mat.node_tree.links
        nodes.clear()

        out = nodes.new('ShaderNodeOutputMaterial')
        bsdf = nodes.new('ShaderNodeBsdfPrincipled')
        links.new(bsdf.outputs['BSDF'], out.inputs['Surface'])

        tex_col = nodes.new('ShaderNodeTexImage')
        tex_col.image = img_col

        if base_factor is not None:
            mul = nodes.new('ShaderNodeMix')
            mul.data_type = 'RGBA'
            mul.blend_type = 'MULTIPLY'
            mul.inputs['Factor'].default_value = 1.0
            mul.inputs['B'].default_value = (base_factor[0], base_factor[1], base_factor[2], 1.0)
            links.new(tex_col.outputs['Color'], mul.inputs['A'])
            links.new(mul.outputs['Result'], bsdf.inputs['Base Color'])
            bsdf.inputs['Base Color'].default_value = (base_factor[0], base_factor[1], base_factor[2], 1.0)
        else:
            links.new(tex_col.outputs['Color'], bsdf.inputs['Base Color'])

        tex_orm = nodes.new('ShaderNodeTexImage')
        tex_orm.image = img_orm
        sep = nodes.new('ShaderNodeSeparateColor')
        links.new(tex_orm.outputs['Color'], sep.inputs['Color'])
        links.new(sep.outputs['Green'], bsdf.inputs['Roughness'])
        links.new(sep.outputs['Blue'], bsdf.inputs['Metallic'])

        gltf_settings = bpy.data.node_groups.get('glTF Material Output')
        if gltf_settings is None:
            gltf_settings = bpy.data.node_groups.new('glTF Material Output', 'ShaderNodeTree')
            gltf_settings.interface.new_socket(name='Occlusion', in_out='INPUT', socket_type='NodeSocketFloat')
        ng = nodes.new('ShaderNodeGroup')
        ng.node_tree = gltf_settings
        links.new(sep.outputs['Red'], ng.inputs['Occlusion'])

        tex_nor = nodes.new('ShaderNodeTexImage')
        tex_nor.image = img_nor
        nor_map = nodes.new('ShaderNodeNormalMap')
        links.new(tex_nor.outputs['Color'], nor_map.inputs['Color'])
        links.new(nor_map.outputs['Normal'], bsdf.inputs['Normal'])

        if clearcoat:
            if 'Coat Weight' in bsdf.inputs:
                bsdf.inputs['Coat Weight'].default_value = 1.0
                bsdf.inputs['Coat Roughness'].default_value = 0.05
            elif 'Clearcoat' in bsdf.inputs:
                bsdf.inputs['Clearcoat'].default_value = 1.0
                bsdf.inputs['Clearcoat Roughness'].default_value = 0.05

        if emissive:
            if 'Emission Color' in bsdf.inputs:
                links.new(tex_col.outputs['Color'], bsdf.inputs['Emission Color'])
                bsdf.inputs['Emission Strength'].default_value = 1.8
            elif 'Emission' in bsdf.inputs:
                links.new(tex_col.outputs['Color'], bsdf.inputs['Emission'])
                bsdf.inputs['Emission Strength'].default_value = 1.8

        return mat

    mat_paint = create_gltf_pbr_mat(
        "Paint", paint_col, paint_nor, paint_orm,
        base_factor=PAINTS[0], clearcoat=True, emissive=False
    )
    mat_trim = create_gltf_pbr_mat(
        "Trim", trim_col, trim_nor, trim_orm,
        base_factor=None, clearcoat=False, emissive=False
    )
    mat_wheel = create_gltf_pbr_mat(
        "Wheel", wheel_col, wheel_nor, wheel_orm,
        base_factor=None, clearcoat=False, emissive=False
    )
    mat_lamps = create_gltf_pbr_mat(
        "Lamps", lamps_col, lamps_nor, lamps_orm,
        base_factor=None, clearcoat=False, emissive=True
    )

    mat_glass = bpy.data.materials.new(name="Glass")
    mat_glass.use_nodes = True
    if hasattr(mat_glass, 'blend_method'):
        mat_glass.blend_method = 'BLEND'
    if hasattr(mat_glass, 'surface_render_method'):
        mat_glass.surface_render_method = 'BLENDED'
    gnodes = mat_glass.node_tree.nodes
    glinks = mat_glass.node_tree.links
    gnodes.clear()
    gout = gnodes.new('ShaderNodeOutputMaterial')
    gbsdf = gnodes.new('ShaderNodeBsdfPrincipled')
    gbsdf.inputs['Base Color'].default_value = (0.04, 0.055, 0.065, 1.0)
    gbsdf.inputs['Roughness'].default_value = 0.05
    gbsdf.inputs['Metallic'].default_value = 0.15
    gbsdf.inputs['Alpha'].default_value = 0.45
    if 'IOR' in gbsdf.inputs:
        gbsdf.inputs['IOR'].default_value = 1.52
    glinks.new(gbsdf.outputs['BSDF'], gout.inputs['Surface'])

    return {
        "Paint": mat_paint,
        "Trim": mat_trim,
        "Wheel": mat_wheel,
        "Lamps": mat_lamps,
        "Glass": mat_glass,
    }


# ---------------------------------------------------------------------------
# 4. UV QUADRANT HELPER
# ---------------------------------------------------------------------------
def set_uv_quadrant(bm, faces, quad_u, quad_v, scale=0.38):
    uv_layer = bm.loops.layers.uv.verify()
    u_off = 0.5 * quad_u + 0.06
    v_off = 0.5 * quad_v + 0.06
    for f in faces:
        n = f.normal
        ax, ay, az = abs(n.x), abs(n.y), abs(n.z)
        for loop in f.loops:
            co = loop.vert.co
            if ax >= ay and ax >= az:
                u_raw = (co.z * 0.5) % 1.0
                v_raw = (co.y * 0.5) % 1.0
            elif ay >= ax and ay >= az:
                u_raw = (co.x * 0.5) % 1.0
                v_raw = (co.z * 0.5) % 1.0
            else:
                u_raw = (co.x * 0.5) % 1.0
                v_raw = (co.y * 0.5) % 1.0
            loop[uv_layer].uv = (u_off + u_raw * scale, v_off + v_raw * scale)


def project_full_uv(bm, faces, scale=0.35):
    uv_layer = bm.loops.layers.uv.verify()
    for f in faces:
        n = f.normal
        ax, ay, az = abs(n.x), abs(n.y), abs(n.z)
        for loop in f.loops:
            co = loop.vert.co
            if ax >= ay and ax >= az:
                u_raw = (co.z * scale + 0.5) % 1.0
                v_raw = (co.y * scale + 0.5) % 1.0
            elif ay >= ax and ay >= az:
                u_raw = (co.x * scale + 0.5) % 1.0
                v_raw = (co.z * scale + 0.5) % 1.0
            else:
                u_raw = (co.x * scale + 0.5) % 1.0
                v_raw = (co.y * scale + 0.5) % 1.0
            loop[uv_layer].uv = (u_raw, v_raw)


# ---------------------------------------------------------------------------
# 5. GEOMETRY BUILDERS: HIGH-DETAIL REALISTIC SPORTS COUPE / GRAND TOURER
# ---------------------------------------------------------------------------
def interp_profile(z, table):
    if z <= table[0][0]:
        return list(table[0][1:])
    if z >= table[-1][0]:
        return list(table[-1][1:])
    for i in range(len(table) - 1):
        z0 = table[i][0]
        z1 = table[i + 1][0]
        if z0 <= z <= z1:
            t = (z - z0) / max(1e-6, (z1 - z0))
            ts = t * t * (3.0 - 2.0 * t)
            mix = 0.55 * ts + 0.45 * t
            vals0 = table[i][1:]
            vals1 = table[i + 1][1:]
            return [v0 + (v1 - v0) * mix for v0, v1 in zip(vals0, vals1)]
    return list(table[-1][1:])


def wheel_arch_opening(z, z_center, r_arch=0.395, flare_w=0.024):
    dz = z - z_center
    if abs(dz) < r_arch:
        arch_y = WHEEL_RADIUS + math.sqrt(max(0.0, r_arch * r_arch - dz * dz))
    else:
        arch_y = 0.0
    norm = abs(dz) / r_arch
    flare = math.exp(-((norm - 0.75) / 0.42) ** 2) * flare_w if norm < 1.35 else 0.0
    return arch_y, flare


def build_body_and_interior(mats):
    mesh = bpy.data.meshes.new("Body_Mesh")
    obj = bpy.data.objects.new("Body", mesh)
    bpy.context.collection.objects.link(obj)

    obj.data.materials.append(mats["Paint"])
    obj.data.materials.append(mats["Trim"])
    obj.data.materials.append(mats["Glass"])
    obj.data.materials.append(mats["Lamps"])

    bm = bmesh.new()

    # -----------------------------------------------------------------------
    # A. MAIN AUTOMOTIVE BODY SHELL (160 Z-stations, arched window cantrail!)
    # -----------------------------------------------------------------------
    # Notice:
    # 1. Along the side window (-0.94 <= z <= 0.42), y_rlo arches up in the middle (1.20)
    #    and tapers down at both the front mirror corner (0.91 at z=+0.42) and the rear
    #    Hofmeister kink (0.995 at z=-0.94), while y_belt kicks up at the rear (0.955 at z=-0.94),
    #    giving the exact curved teardrop coupe window of sheet.jpg!
    # 2. The body loft ends at z = +2.10 in front so the sculpted front fascia (z=+2.10..+2.225)
    #    has a deep recessed shark-nose grille & quad projector headlamps!
    profile_table = [
        [-2.200, 0.72, 0.28, 0.80, 0.48, 0.79, 0.74, 0.74, 0.89, 0.62, 0.920, 0.34, 0.935, 0.940],
        [-2.140, 0.78, 0.24, 0.86, 0.46, 0.85, 0.75, 0.78, 0.92, 0.65, 0.950, 0.36, 0.960, 0.965],
        [-2.000, 0.83, 0.22, 0.90, 0.46, 0.89, 0.77, 0.81, 0.94, 0.65, 0.960, 0.36, 0.970, 0.975],
        # Muscular rear haunch over rear wheel (-1.72 to -1.10)
        [-1.720, 0.86, 0.20, 0.93, 0.47, 0.93, 0.80, 0.84, 0.955, 0.63, 0.985, 0.35, 1.015, 1.020],
        [-1.450, 0.87, 0.19, 0.945, 0.50, 0.95, 0.82, 0.86, 0.965, 0.62, 0.992, 0.34, 1.095, 1.105],
        [-1.275, 0.87, 0.19, 0.950, 0.52, 0.95, 0.83, 0.86, 0.965, 0.62, 0.992, 0.33, 1.145, 1.155],
        # Rear quarter window Hofmeister kink point (-0.94): y_belt=0.955, y_rlo=0.995 (narrow 4cm tapered point!)
        [-0.940, 0.86, 0.18, 0.940, 0.50, 0.94, 0.82, 0.83, 0.955, 0.66, 0.995, 0.33, 1.205, 1.215],
        # Arched rear quarter window (-0.68): y_belt drops to 0.905, y_rlo arches up to 1.145
        [-0.680, 0.85, 0.175, 0.910, 0.47, 0.905, 0.80, 0.82, 0.905, 0.59, 1.145, 0.32, 1.245, 1.255],
        # Peak of side window arch (-0.25): y_belt=0.890, y_rlo=1.205, roof=1.270
        [-0.250, 0.845, 0.175, 0.895, 0.46, 0.890, 0.79, 0.81, 0.890, 0.58, 1.205, 0.32, 1.262, 1.270],
        [ 0.080, 0.845, 0.175, 0.895, 0.46, 0.890, 0.79, 0.81, 0.880, 0.60, 1.160, 0.34, 1.200, 1.208],
        # Front corner of side window / base of windshield (+0.42): y_belt=0.870, y_rlo=0.905
        [ 0.420, 0.850, 0.175, 0.905, 0.46, 0.900, 0.79, 0.82, 0.870, 0.66, 0.905, 0.36, 0.922, 0.928],
        # Long sleek GT bonnet & front fender swell (+0.88 to +1.75)
        [ 0.880, 0.860, 0.180, 0.925, 0.48, 0.920, 0.785, 0.82, 0.860, 0.64, 0.885, 0.34, 0.900, 0.910],
        [ 1.275, 0.865, 0.180, 0.935, 0.50, 0.930, 0.775, 0.81, 0.845, 0.62, 0.860, 0.32, 0.875, 0.885],
        [ 1.680, 0.855, 0.180, 0.920, 0.48, 0.910, 0.745, 0.79, 0.810, 0.60, 0.820, 0.30, 0.835, 0.842],
        # Front fender & nose transition up to z = +2.10 (where recessed front grille cavity begins!)
        [ 1.960, 0.820, 0.175, 0.870, 0.44, 0.860, 0.690, 0.75, 0.755, 0.56, 0.775, 0.28, 0.785, 0.790],
        [ 2.100, 0.775, 0.172, 0.815, 0.42, 0.805, 0.650, 0.73, 0.715, 0.53, 0.730, 0.26, 0.740, 0.743],
    ]

    num_z = 150
    z_stations = [-2.200 + (4.300 * i) / num_z for i in range(num_z + 1)]

    rings = []
    for z in z_stations:
        vals = interp_profile(z, profile_table)
        x_sill, y_sill, x_mid, y_mid, x_shldr, y_shldr, x_belt, y_belt, x_rlo, y_rlo, x_rhi, y_rhi, y_ctr = vals

        arch_yf, flare_f = wheel_arch_opening(z, Z_FRONT_AXLE, r_arch=0.395, flare_w=0.022)
        arch_yr, flare_r = wheel_arch_opening(z, Z_REAR_AXLE, r_arch=0.402, flare_w=0.028)
        arch_y = max(arch_yf, arch_yr)
        flare = max(flare_f, flare_r)

        if arch_y > y_sill:
            y_sill = arch_y
            y_mid = max(y_mid, y_sill + (y_shldr - y_sill) * 0.42)

        x_sill += flare * 0.85
        x_mid += flare * 0.95
        x_shldr += flare * 0.65

        y_under = 0.165 if arch_y == 0.0 else max(0.18, y_sill - 0.035)
        x_well_in = 0.50 if arch_y > 0.0 else 0.62

        dome_add = 0.0
        if 0.58 <= z <= 2.02:
            dz_d = (z - 1.30) / 0.72
            dome_add = 0.022 * max(0.0, 1.0 - dz_d * dz_d)

        y_rlo = max(y_rlo, y_belt + 0.025)
        y_rhi_eff = max(y_rhi + dome_add * 0.75, y_rlo + 0.010)
        y_ctr_eff = max(y_ctr + dome_add, y_rhi_eff + 0.004)

        x_win_mid = x_belt + (x_rlo - x_belt) * 0.50
        y_win_mid = y_belt + (y_rlo - y_belt) * 0.50

        half_pts = [
            (x_well_in, y_under),                         # 0: underbody inner edge
            (x_sill - 0.028, max(0.165, y_sill - 0.015)), # 1: rocker under-lip
            (x_sill, y_sill),                             # 2: sill / wheel arch outer lip
            (x_mid, y_mid),                               # 3: lower door / flank belly
            (x_shldr, y_shldr - 0.015),                   # 4: lower shoulder crease
            (x_shldr - 0.008, y_shldr + 0.015),           # 5: upper shoulder haunch crease
            (x_belt, y_belt),                             # 6: beltline / window sill
            (x_win_mid, y_win_mid),                       # 7: mid side window / pillar
            (x_rlo, y_rlo),                               # 8: roof cantrail / upper window frame
            (x_rhi, y_rhi_eff),                           # 9: roof / bonnet power-dome edge
        ]

        ring_verts = [bm.verts.new((0.0, y_under, z))]
        for px, py in half_pts:
            ring_verts.append(bm.verts.new((px, py, z)))
        ring_verts.append(bm.verts.new((0.0, y_ctr_eff, z)))
        for px, py in reversed(half_pts):
            ring_verts.append(bm.verts.new((-px, py, z)))

        rings.append((z, ring_verts, arch_y > 0.0))

    n_ring = len(rings[0][1])
    paint_faces = []
    trim_rubber_faces = []
    trim_plastic_faces = []
    trim_chrome_faces = []
    glass_faces = []

    for i in range(len(rings) - 1):
        z0, r0, _ = rings[i]
        z1, r1, _ = rings[i + 1]
        z_mid = 0.5 * (z0 + z1)

        for j in range(n_ring):
            jn = (j + 1) % n_ring
            f = bm.faces.new((r0[j], r0[jn], r1[jn], r1[j]))
            side_idx = j if j <= 10 else (n_ring - 1 - j)

            if side_idx in (0, 1):
                f.material_index = 1
                trim_plastic_faces.append(f)
            elif side_idx == 2 and (-0.88 <= z_mid <= 0.88):
                f.material_index = 1
                trim_plastic_faces.append(f)
            elif side_idx in (6, 7) and (-0.93 <= z_mid <= 0.40):
                if -0.38 <= z_mid <= -0.31:
                    f.material_index = 1
                    trim_plastic_faces.append(f)
                else:
                    f.material_index = 2
                    glass_faces.append(f)
            elif side_idx in (9, 10) and (0.06 <= z_mid <= 0.42):
                f.material_index = 2
                glass_faces.append(f)
            elif side_idx in (9, 10) and (-1.58 <= z_mid <= -0.82):
                f.material_index = 2
                glass_faces.append(f)
            else:
                f.material_index = 0
                paint_faces.append(f)

    # Close rear & front loft caps at z = -2.20 and z = +2.10 with deep matte-black grille/diffuser backing!
    r_rear = rings[0][1]
    for k in range(1, 10):
        f_r = bm.faces.new((r_rear[k], r_rear[k + 1], r_rear[n_ring - 1 - k], r_rear[n_ring - k]))
        f_r.material_index = 1
        trim_plastic_faces.append(f_r)
    f_r_top = bm.faces.new((r_rear[10], r_rear[11], r_rear[12]))
    f_r_top.material_index = 1
    trim_plastic_faces.append(f_r_top)
    f_r_bot = bm.faces.new((r_rear[0], r_rear[1], r_rear[n_ring - 1]))
    f_r_bot.material_index = 1
    trim_plastic_faces.append(f_r_bot)

    r_front = rings[-1][1]
    for k in range(1, 10):
        f_f = bm.faces.new((r_front[n_ring - k], r_front[n_ring - 1 - k], r_front[k + 1], r_front[k]))
        f_f.material_index = 1
        trim_plastic_faces.append(f_f)
    f_f_top = bm.faces.new((r_front[12], r_front[11], r_front[10]))
    f_f_top.material_index = 1
    trim_plastic_faces.append(f_f_top)
    f_f_bot = bm.faces.new((r_front[n_ring - 1], r_front[1], r_front[0]))
    f_f_bot.material_index = 1
    trim_plastic_faces.append(f_f_bot)

    for v in {v for f in glass_faces for v in f.verts}:
        if all(lf in glass_faces for lf in v.link_faces):
            if abs(v.co.x) > 0.48:
                v.co.x *= 0.992
            else:
                v.co.y -= 0.006

    # -----------------------------------------------------------------------
    # B. FLUSH CONFORMAL CHROME WINDOW SURROUND STRIP
    # -----------------------------------------------------------------------
    win_ring_indices = [idx for idx, (z, _, _) in enumerate(rings) if -0.94 <= z <= 0.42]
    for is_left in (True, False):
        x_sign = 1.0 if is_left else -1.0
        idx_bot = 6 if is_left else (n_ring - 6)
        idx_top = 8 if is_left else (n_ring - 8)

        loop_pts = []
        for r_idx in reversed(win_ring_indices):
            v_co = rings[r_idx][1][idx_bot].co
            loop_pts.append(Vector((v_co.x + x_sign * 0.003, v_co.y, v_co.z)))
        for r_idx in win_ring_indices:
            v_co = rings[r_idx][1][idx_top].co
            loop_pts.append(Vector((v_co.x + x_sign * 0.003, v_co.y, v_co.z)))

        n_pts = len(loop_pts)
        tube_rings = []
        for k in range(n_pts):
            p_prev = loop_pts[(k - 1) % n_pts]
            p_curr = loop_pts[k]
            p_next = loop_pts[(k + 1) % n_pts]
            tang = (p_next - p_prev).normalized()
            out_n = Vector((x_sign, 0.18, 0.0)).normalized()
            binorm = tang.cross(out_n).normalized()
            w_m = 0.0080
            t_m = 0.0050
            ring = [
                bm.verts.new(p_curr - out_n * 0.004 - binorm * w_m),
                bm.verts.new(p_curr + out_n * t_m - binorm * (w_m * 0.7)),
                bm.verts.new(p_curr + out_n * t_m + binorm * (w_m * 0.7)),
                bm.verts.new(p_curr - out_n * 0.004 + binorm * w_m),
            ]
            tube_rings.append(ring)

        for k in range(n_pts):
            kn = (k + 1) % n_pts
            r0 = tube_rings[k]
            r1 = tube_rings[kn]
            for m in range(3):
                if is_left:
                    f = bm.faces.new((r0[m], r0[m + 1], r1[m + 1], r1[m]))
                else:
                    f = bm.faces.new((r0[m], r1[m], r1[m + 1], r0[m + 1]))
                f.material_index = 1
                f.smooth = True
                trim_chrome_faces.append(f)

    # -----------------------------------------------------------------------
    # C. WHEEL WELL CAVITY CYLINDERS
    # -----------------------------------------------------------------------
    def add_wheel_well_drum(x_sign, z_axle, r_well=0.392, x_in=0.46, x_out=0.89):
        segs = 32
        v_in_ring = []
        v_out_ring = []
        for k in range(segs + 1):
            ang = math.pi * (k / segs)
            zk = z_axle + math.cos(ang) * r_well
            yk = WHEEL_RADIUS + math.sin(ang) * r_well
            v_in_ring.append(bm.verts.new((x_sign * x_in, yk, zk)))
            v_out_ring.append(bm.verts.new((x_sign * x_out, yk, zk)))
        for k in range(segs):
            if x_sign > 0:
                f = bm.faces.new((v_in_ring[k], v_in_ring[k + 1], v_out_ring[k + 1], v_out_ring[k]))
            else:
                f = bm.faces.new((v_out_ring[k], v_out_ring[k + 1], v_in_ring[k + 1], v_in_ring[k]))
            f.material_index = 1
            trim_plastic_faces.append(f)
        v_center = bm.verts.new((x_sign * x_in, WHEEL_RADIUS, z_axle))
        for k in range(segs):
            if x_sign > 0:
                f = bm.faces.new((v_center, v_in_ring[k + 1], v_in_ring[k]))
            else:
                f = bm.faces.new((v_center, v_in_ring[k], v_in_ring[k + 1]))
            f.material_index = 1
            trim_plastic_faces.append(f)

    for xs in (-1.0, 1.0):
        add_wheel_well_drum(xs, Z_FRONT_AXLE, r_well=0.388, x_in=0.46, x_out=0.88)
        add_wheel_well_drum(xs, Z_REAR_AXLE, r_well=0.395, x_in=0.45, x_out=0.90)

    # -----------------------------------------------------------------------
    # D. HELPER FOR BEVELLED 3D PARTS & CYLINDERS
    # -----------------------------------------------------------------------
    def add_bevelled_box(center, half_extents, mat_idx, face_list, rot_euler=(0.0, 0.0, 0.0), bevel_w=0.008, segs=2):
        ret = bmesh.ops.create_cube(bm, size=2.0)
        verts = ret['verts']
        rot_mat = Matrix.Rotation(rot_euler[0], 4, 'X') @ Matrix.Rotation(rot_euler[1], 4, 'Y') @ Matrix.Rotation(rot_euler[2], 4, 'Z')
        for v in verts:
            v.co.x *= half_extents[0]
            v.co.y *= half_extents[1]
            v.co.z *= half_extents[2]
            v.co = (rot_mat @ v.co) + Vector(center)
        b_faces = list({f for v in verts for f in v.link_faces})
        b_edges = list({e for f in b_faces for e in f.edges})
        if bevel_w > 0:
            bev = bmesh.ops.bevel(bm, geom=b_edges, offset=bevel_w, segments=segs, profile=0.5, affect='EDGES')
            all_v = {v for v in (list(verts) + list(bev['verts'])) if v.is_valid}
            b_faces = list({f for v in all_v for f in v.link_faces if f.is_valid})
        for f in b_faces:
            f.material_index = mat_idx
            face_list.append(f)
        return b_faces

    def add_cylinder_part(center, radius, length, axis, mat_idx, face_list, segs=28, cap=True):
        ret = bmesh.ops.create_cone(
            bm, cap_ends=cap, cap_tris=False, segments=segs,
            radius1=radius, radius2=radius, depth=length
        )
        verts = ret['verts']
        if axis == 'X':
            rmat = Matrix.Rotation(math.pi / 2.0, 4, 'Y')
        elif axis == 'Z':
            rmat = Matrix.Rotation(0.0, 4, 'X')
        else:
            rmat = Matrix.Rotation(math.pi / 2.0, 4, 'X')
        for v in verts:
            v.co = (rmat @ v.co) + Vector(center)
        c_faces = list({f for v in verts for f in v.link_faces})
        for f in c_faces:
            f.material_index = mat_idx
            face_list.append(f)
        return c_faces

    # -----------------------------------------------------------------------
    # E. SCULPTED SHARK-NOSE FRONT FASCIA (z = +2.08 to +2.225)
    # -----------------------------------------------------------------------
    # Because the main body shell stops at z = +2.10, everything from z = +2.10 to +2.225
    # is 100% unobstructed by the body cap and forms a deep, realistic 3D front fascia:
    # 1. Overhanging shark-nose bonnet brow (painted, curves across X at Y=0.66..0.72, Z=2.08..2.220)
    # 2. Sculpted mid-bumper painted bar (curves across X at Y=0.41..0.47, Z=2.08..2.225)
    # 3. Outer fender/bumper cheek fairings at X=+-0.76..0.82 connecting brow, bumper, and splitter
    # 4. Deeply recessed upper grille + quad circular projector headlamps inside the Y=0.47..0.66 pocket!
    brow_stations = 24
    brow_rings = []
    midb_rings = []
    for k in range(brow_stations + 1):
        u = -1.0 + (2.0 * k) / brow_stations
        x_b = u * 0.78
        # Gentle horizontal curve (center is at z=2.220, outer corners sweep back to z=2.140)
        z_front = 2.220 - 0.075 * (u * u)
        z_back = 2.085
        # Brow height arches slightly over center
        y_brow_top = 0.735 - 0.035 * (u * u)
        y_brow_bot = 0.655 - 0.015 * (u * u)
        pts_brow = [
            (x_b, y_brow_top, z_back),
            (x_b, y_brow_top - 0.025, z_front),
            (x_b, y_brow_bot, z_front - 0.012),
            (x_b, y_brow_bot, z_back),
        ]
        brow_rings.append([bm.verts.new(p) for p in pts_brow])

        # Mid bumper bar (Y=0.405..0.475)
        z_mb_front = 2.225 - 0.075 * (u * u)
        pts_midb = [
            (x_b, 0.475, z_back),
            (x_b, 0.472, z_mb_front - 0.008),
            (x_b, 0.405, z_mb_front),
            (x_b, 0.405, z_back),
        ]
        midb_rings.append([bm.verts.new(p) for p in pts_midb])

    for rings_list in (brow_rings, midb_rings):
        for k in range(brow_stations):
            r0 = rings_list[k]
            r1 = rings_list[k + 1]
            for m in range(4):
                mn = (m + 1) % 4
                f = bm.faces.new((r0[m], r0[mn], r1[mn], r1[m]))
                f.material_index = 0
                f.smooth = True
                paint_faces.append(f)
        f_l = bm.faces.new(tuple(reversed(rings_list[0])))
        f_r = bm.faces.new(tuple(rings_list[-1]))
        for f in (f_l, f_r):
            f.material_index = 0
            f.smooth = True
            paint_faces.append(f)

    # Painted outer bumper cheeks & angled grille divider struts
    for xs in (-1.0, 1.0):
        add_bevelled_box((xs * 0.770, 0.430, 2.118), (0.028, 0.235, 0.042), 0, paint_faces, rot_euler=(0.0, xs * 0.34, xs * 0.06), bevel_w=0.014, segs=3)
        # Angled painted struts between center grille and headlamp pockets
        add_bevelled_box((xs * 0.455, 0.565, 2.165), (0.022, 0.095, 0.035), 0, paint_faces, rot_euler=(0.0, 0.0, xs * 0.22), bevel_w=0.006, segs=2)
        # Lower painted struts separating center cooling intake from side brake ducts
        add_bevelled_box((xs * 0.475, 0.295, 2.165), (0.024, 0.110, 0.035), 0, paint_faces, rot_euler=(0.0, 0.0, xs * -0.16), bevel_w=0.006, segs=2)

    # Recessed center grille surround (chrome frame + dark horizontal/vertical mesh slats at z=2.14..2.17)
    add_bevelled_box((0.0, 0.642, 2.168), (0.44, 0.007, 0.022), 1, trim_chrome_faces, bevel_w=0.002, segs=1)
    add_bevelled_box((0.0, 0.485, 2.168), (0.44, 0.007, 0.022), 1, trim_chrome_faces, bevel_w=0.002, segs=1)
    for k in range(5):
        yk = 0.505 + k * 0.028
        add_bevelled_box((0.0, yk, 2.155), (0.43, 0.004, 0.015), 1, trim_plastic_faces, bevel_w=0.001, segs=1)

    lamp_head_faces = []
    lamp_amber_faces = []
    lamp_dark_faces = []
    lamp_tail_faces = []
    # Subtle off-center orange/red performance badge bar in grille
    add_bevelled_box((-0.32, 0.545, 2.172), (0.042, 0.008, 0.008), 3, lamp_amber_faces, bevel_w=0.001, segs=1)

    # Quad circular projector headlamps recessed in the outer pockets (z = 2.13..2.165, completely unobstructed!)
    for xs in (-1.0, 1.0):
        for x_off, r_outer in ((0.665, 0.054), (0.548, 0.050)):
            # Chrome outer bezel ring
            add_cylinder_part((xs * x_off, 0.562, 2.135), r_outer, 0.048, 'Z', 1, trim_chrome_faces, segs=28)
            # Dark inner reflector barrel
            add_cylinder_part((xs * x_off, 0.562, 2.144), r_outer * 0.82, 0.042, 'Z', 3, lamp_dark_faces, segs=28)
            # Glowing warm-white LED projector lens
            add_cylinder_part((xs * x_off, 0.562, 2.150), r_outer * 0.62, 0.040, 'Z', 3, lamp_head_faces, segs=24)
        # Sleek horizontal amber side-marker reflector ahead of front wheel arch
        add_bevelled_box((xs * 0.865, 0.462, 1.805), (0.016, 0.012, 0.068), 3, lamp_amber_faces, bevel_w=0.003, segs=1)

    # Lower center cooling intake slats (z = 2.150) & side fog/DRL ducts
    for k in range(4):
        yk = 0.225 + k * 0.036
        add_bevelled_box((0.0, yk, 2.155), (0.45, 0.004, 0.016), 1, trim_plastic_faces, bevel_w=0.001, segs=1)
    for xs in (-1.0, 1.0):
        add_bevelled_box((xs * 0.625, 0.312, 2.162), (0.095, 0.011, 0.018), 3, lamp_head_faces, rot_euler=(0.0, xs * 0.16, 0.0), bevel_w=0.002, segs=1)

    # Smooth 1-piece lofted Front Chin Splitter (z = 2.08 to 2.225, y = 0.162..0.188)
    fsp_stations = 24
    fsp_rings = []
    for k in range(fsp_stations + 1):
        u = -1.0 + (2.0 * k) / fsp_stations
        x_s = u * 0.795
        z_lead = 2.225 - 0.085 * (u * u)
        z_back = 2.080 - 0.050 * (u * u)
        pts_fsp = [
            (x_s, 0.190, z_lead - 0.010),
            (x_s, 0.164, z_lead),
            (x_s, 0.164, z_back),
            (x_s, 0.190, z_back),
        ]
        fsp_rings.append([bm.verts.new(p) for p in pts_fsp])

    for k in range(fsp_stations):
        r0 = fsp_rings[k]
        r1 = fsp_rings[k + 1]
        for m in range(4):
            mn = (m + 1) % 4
            f = bm.faces.new((r0[m], r0[mn], r1[mn], r1[m]))
            f.material_index = 1
            f.smooth = True
            trim_plastic_faces.append(f)
    f_fsp_l = bm.faces.new(tuple(reversed(fsp_rings[0])))
    f_fsp_r = bm.faces.new(tuple(fsp_rings[-1]))
    for f in (f_fsp_l, f_fsp_r):
        f.material_index = 1
        f.smooth = True
        trim_plastic_faces.append(f)

    # -----------------------------------------------------------------------
    # F. BONNET VENTS, WIPERS, SCULPTED MIRRORS, FLUSH HANDLES, SIDE SKIRTS
    # -----------------------------------------------------------------------
    for xs in (-1.0, 1.0):
        add_bevelled_box((xs * 0.34, 0.882, 1.200), (0.095, 0.006, 0.180), 1, trim_plastic_faces, rot_euler=(-0.065, 0.0, xs * -0.09), bevel_w=0.002, segs=1)
        for s in range(5):
            zs = 1.06 + s * 0.068
            ys = 0.890 - (zs - 1.06) * 0.065
            add_bevelled_box((xs * 0.34, ys, zs), (0.082, 0.004, 0.010), 1, trim_plastic_faces, rot_euler=(0.15, 0.0, xs * -0.09), bevel_w=0.001, segs=1)

    # Scuttle panel & twin windshield wipers at base of windshield (Z ~ 0.44)
    add_bevelled_box((0.0, 0.922, 0.445), (0.62, 0.012, 0.042), 1, trim_plastic_faces, bevel_w=0.003, segs=1)
    for x_w in (-0.18, 0.30):
        add_bevelled_box((x_w, 0.938, 0.425), (0.23, 0.008, 0.012), 1, trim_plastic_faces, rot_euler=(0.18, 0.15, -0.06), bevel_w=0.002, segs=1)

    # Sleek Teardrop Side Mirrors & Flush Door Handles
    for xs in (-1.0, 1.0):
        add_bevelled_box((xs * 0.855, 0.892, 0.265), (0.048, 0.016, 0.028), 1, trim_plastic_faces, rot_euler=(0.0, xs * -0.25, xs * 0.32), bevel_w=0.004, segs=1)
        add_bevelled_box((xs * 0.905, 0.920, 0.240), (0.058, 0.034, 0.048), 0, paint_faces, rot_euler=(0.06, xs * -0.22, xs * 0.08), bevel_w=0.016, segs=3)
        add_bevelled_box((xs * 0.905, 0.920, 0.196), (0.048, 0.026, 0.008), 1, trim_chrome_faces, rot_euler=(0.06, xs * -0.22, xs * 0.08), bevel_w=0.002, segs=1)

        add_bevelled_box((xs * 0.898, 0.832, -0.360), (0.012, 0.015, 0.088), 0, paint_faces, bevel_w=0.005, segs=2)
        add_bevelled_box((xs * 0.906, 0.832, -0.412), (0.006, 0.006, 0.014), 1, trim_chrome_faces, bevel_w=0.002, segs=1)

        add_bevelled_box((xs * 0.858, 0.172, 0.000), (0.024, 0.014, 0.840), 1, trim_plastic_faces, bevel_w=0.004, segs=1)

    # -----------------------------------------------------------------------
    # G. REAR DUCKTAIL SPOILER, FULL-WIDTH TAILLIGHTS, DIFFUSER & EXHAUSTS
    # -----------------------------------------------------------------------
    sp_stations = 24
    sp_rings = []
    for k in range(sp_stations + 1):
        u = -1.0 + (2.0 * k) / sp_stations
        x_s = u * 0.74
        y_base = 0.955 - 0.022 * (u * u)
        z_base = -2.105 + 0.045 * (u * u)
        taper = 1.0 - 0.35 * (u ** 4)
        pts_sp = [
            (x_s, y_base - 0.005, z_base + 0.055 * taper),
            (x_s, y_base + 0.026 * taper, z_base + 0.010 * taper),
            (x_s, y_base + 0.048 * taper, z_base - 0.075 * taper),
            (x_s, y_base + 0.012 * taper, z_base - 0.055 * taper),
        ]
        sp_rings.append([bm.verts.new(p) for p in pts_sp])

    for k in range(sp_stations):
        r0 = sp_rings[k]
        r1 = sp_rings[k + 1]
        for m in range(4):
            mn = (m + 1) % 4
            f = bm.faces.new((r0[m], r0[mn], r1[mn], r1[m]))
            f.material_index = 1
            f.smooth = True
            trim_plastic_faces.append(f)
    f_sp_l = bm.faces.new(tuple(reversed(sp_rings[0])))
    f_sp_r = bm.faces.new(tuple(sp_rings[-1]))
    for f in (f_sp_l, f_sp_r):
        f.material_index = 1
        f.smooth = True
        trim_plastic_faces.append(f)

    add_bevelled_box((0.0, 0.815, -2.195), (0.74, 0.062, 0.028), 1, trim_plastic_faces, bevel_w=0.006)

    for xs in (-1.0, 1.0):
        add_bevelled_box((xs * 0.520, 0.840, -2.215), (0.210, 0.011, 0.014), 3, lamp_tail_faces, bevel_w=0.003, segs=1)
        add_bevelled_box((xs * 0.520, 0.786, -2.215), (0.210, 0.011, 0.014), 3, lamp_tail_faces, bevel_w=0.003, segs=1)
        add_bevelled_box((xs * 0.718, 0.813, -2.210), (0.016, 0.034, 0.014), 3, lamp_tail_faces, bevel_w=0.003, segs=1)
        add_bevelled_box((xs * 0.320, 0.813, -2.215), (0.016, 0.034, 0.014), 3, lamp_tail_faces, bevel_w=0.003, segs=1)
        add_bevelled_box((xs * 0.520, 0.813, -2.210), (0.180, 0.016, 0.012), 3, lamp_dark_faces, bevel_w=0.002, segs=1)
        add_bevelled_box((xs * 0.865, 0.575, -1.910), (0.014, 0.012, 0.070), 3, lamp_tail_faces, bevel_w=0.003, segs=1)

    add_bevelled_box((0.0, 0.535, -2.205), (0.27, 0.080, 0.022), 1, trim_plastic_faces, bevel_w=0.005)
    add_bevelled_box((0.0, 0.285, -2.180), (0.73, 0.078, 0.045), 1, trim_plastic_faces, bevel_w=0.008)
    for x_fin in (-0.34, -0.12, 0.12, 0.34):
        add_bevelled_box((x_fin, 0.245, -2.205), (0.011, 0.055, 0.038), 1, trim_plastic_faces, bevel_w=0.003, segs=1)

    for xs in (-1.0, 1.0):
        add_cylinder_part((xs * 0.545, 0.265, -2.165), 0.050, 0.13, 'Z', 1, trim_chrome_faces, segs=32)
        add_cylinder_part((xs * 0.545, 0.265, -2.218), 0.040, 0.03, 'Z', 1, trim_plastic_faces, segs=28)

    # -----------------------------------------------------------------------
    # H. FULL INTERIOR CABIN
    # -----------------------------------------------------------------------
    trim_leather_faces = []
    add_bevelled_box((0.0, 0.24, -0.18), (0.70, 0.025, 0.76), 1, trim_leather_faces, bevel_w=0.005, segs=1)
    add_bevelled_box((0.0, 0.38, -0.10), (0.13, 0.115, 0.70), 1, trim_leather_faces, bevel_w=0.015, segs=2)
    add_cylinder_part((0.0, 0.53, 0.08), 0.013, 0.11, 'Y', 1, trim_chrome_faces, segs=12)
    add_bevelled_box((0.0, 0.60, 0.08), (0.026, 0.026, 0.030), 1, trim_leather_faces, bevel_w=0.008, segs=2)

    add_bevelled_box((0.0, 0.73, 0.33), (0.70, 0.115, 0.19), 1, trim_leather_faces, bevel_w=0.020, segs=2)
    add_bevelled_box((0.36, 0.83, 0.27), (0.18, 0.060, 0.13), 1, trim_leather_faces, bevel_w=0.014, segs=2)
    add_bevelled_box((0.0, 0.77, 0.18), (0.12, 0.070, 0.018), 1, trim_chrome_faces, rot_euler=(-0.18, 0.0, 0.0), bevel_w=0.004, segs=1)

    for xs in (-1.0, 1.0):
        x_seat = xs * 0.36
        add_bevelled_box((x_seat, 0.36, -0.18), (0.21, 0.060, 0.24), 1, trim_leather_faces, rot_euler=(-0.08, 0.0, 0.0), bevel_w=0.016, segs=2)
        add_bevelled_box((x_seat - 0.18, 0.41, -0.16), (0.042, 0.052, 0.22), 1, trim_leather_faces, rot_euler=(-0.08, 0.0, 0.0), bevel_w=0.012, segs=2)
        add_bevelled_box((x_seat + 0.18, 0.41, -0.16), (0.042, 0.052, 0.22), 1, trim_leather_faces, rot_euler=(-0.08, 0.0, 0.0), bevel_w=0.012, segs=2)
        add_bevelled_box((x_seat, 0.63, -0.38), (0.19, 0.25, 0.052), 1, trim_leather_faces, rot_euler=(-0.22, 0.0, 0.0), bevel_w=0.016, segs=2)
        add_bevelled_box((x_seat - 0.17, 0.63, -0.35), (0.045, 0.23, 0.070), 1, trim_leather_faces, rot_euler=(-0.22, 0.0, 0.0), bevel_w=0.012, segs=2)
        add_bevelled_box((x_seat + 0.17, 0.63, -0.35), (0.045, 0.23, 0.070), 1, trim_leather_faces, rot_euler=(-0.22, 0.0, 0.0), bevel_w=0.012, segs=2)
        add_bevelled_box((x_seat, 0.94, -0.45), (0.110, 0.100, 0.045), 1, trim_leather_faces, rot_euler=(-0.16, 0.0, 0.0), bevel_w=0.015, segs=2)
        add_bevelled_box((x_seat, 0.84, -0.41), (0.070, 0.020, 0.048), 1, trim_chrome_faces, rot_euler=(-0.20, 0.0, 0.0), bevel_w=0.004, segs=1)

    for xs in (-1.0, 1.0):
        add_bevelled_box((xs * 0.73, 0.56, -0.12), (0.032, 0.25, 0.54), 1, trim_leather_faces, bevel_w=0.010, segs=1)
        add_bevelled_box((xs * 0.68, 0.62, -0.08), (0.042, 0.032, 0.30), 1, trim_leather_faces, bevel_w=0.008, segs=1)

    add_bevelled_box((0.0, 0.70, -0.86), (0.65, 0.16, 0.34), 1, trim_leather_faces, bevel_w=0.014, segs=1)
    add_bevelled_box((0.0, 1.12, 0.10), (0.10, 0.026, 0.020), 1, trim_plastic_faces, bevel_w=0.004, segs=1)

    # -----------------------------------------------------------------------
    # I. ASSIGN UV COORDINATES & SUBDIVIDE FOR 50k..100k TRIANGLE BUDGET
    # -----------------------------------------------------------------------
    bm.verts.ensure_lookup_table()
    bm.faces.ensure_lookup_table()
    bmesh.ops.recalc_face_normals(bm, faces=bm.faces)

    project_full_uv(bm, [f for f in bm.faces if f.material_index in (0, 2)], scale=0.35)

    set_uv_quadrant(bm, [f for f in trim_rubber_faces if f.is_valid], 0, 1)
    set_uv_quadrant(bm, [f for f in trim_plastic_faces if f.is_valid], 1, 1)
    set_uv_quadrant(bm, [f for f in trim_chrome_faces if f.is_valid], 0, 0)
    set_uv_quadrant(bm, [f for f in trim_leather_faces if f.is_valid], 1, 0)

    set_uv_quadrant(bm, [f for f in lamp_head_faces if f.is_valid], 0, 1)
    set_uv_quadrant(bm, [f for f in lamp_tail_faces if f.is_valid], 1, 1)
    set_uv_quadrant(bm, [f for f in lamp_amber_faces if f.is_valid], 0, 0)
    set_uv_quadrant(bm, [f for f in lamp_dark_faces if f.is_valid], 1, 0)

    for f in bm.faces:
        f.smooth = True

    bm.to_mesh(mesh)
    bm.free()

    bpy.context.view_layer.objects.active = obj
    obj.select_set(True)
    mod_sub = obj.modifiers.new(name="SubsurfBody", type='SUBSURF')
    mod_sub.levels = 1
    mod_sub.render_levels = 1
    bpy.ops.object.modifier_apply(modifier=mod_sub.name)

    mod_wn = obj.modifiers.new(name="WeightedNormal", type='WEIGHTED_NORMAL')
    mod_wn.keep_sharp = True
    bpy.ops.object.modifier_apply(modifier=mod_wn.name)

    return obj


# ---------------------------------------------------------------------------
# 6. STEERING WHEEL NODE (Origin at its hub!)
# ---------------------------------------------------------------------------
def build_steering_wheel(mats):
    mesh = bpy.data.meshes.new("SteeringWheel_Mesh")
    obj = bpy.data.objects.new("SteeringWheel", mesh)
    bpy.context.collection.objects.link(obj)
    obj.data.materials.append(mats["Trim"])

    bm = bmesh.new()
    leather_faces = []
    chrome_faces = []

    tilt = math.radians(22.0)
    rmat = Matrix.Rotation(tilt, 4, 'X')

    n_maj = 40
    n_min = 14
    R_maj = 0.175
    r_min = 0.017
    torus_rings = []
    for i in range(n_maj):
        theta = (2.0 * math.pi * i) / n_maj
        ct, st = math.cos(theta), math.sin(theta)
        ring = []
        for j in range(n_min):
            phi = (2.0 * math.pi * j) / n_min
            cp, sp = math.cos(phi), math.sin(phi)
            r_eff = R_maj * (0.86 if st < -0.65 else 1.0)
            vx = (r_eff + r_min * cp) * ct
            vy = (r_eff + r_min * cp) * st
            vz = r_min * sp
            co = rmat @ Vector((vx, vy, vz))
            ring.append(bm.verts.new(co))
        torus_rings.append(ring)

    for i in range(n_maj):
        inext = (i + 1) % n_maj
        for j in range(n_min):
            jnext = (j + 1) % n_min
            f = bm.faces.new((
                torus_rings[i][j],
                torus_rings[inext][j],
                torus_rings[inext][jnext],
                torus_rings[i][jnext],
            ))
            f.material_index = 0
            f.smooth = True
            leather_faces.append(f)

    ret_hub = bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=False, segments=24, radius1=0.055, radius2=0.050, depth=0.055)
    for v in ret_hub['verts']:
        v.co = rmat @ (v.co + Vector((0.0, 0.0, 0.015)))
    for f in {f for v in ret_hub['verts'] for f in v.link_faces}:
        f.material_index = 0
        f.smooth = True
        leather_faces.append(f)

    for ang_deg in (10.0, 170.0, 270.0):
        ang = math.radians(ang_deg)
        ret_sp = bmesh.ops.create_cube(bm, size=1.0)
        smat = rmat @ Matrix.Rotation(ang, 4, 'Z')
        for v in ret_sp['verts']:
            v.co.x = v.co.x * 0.11 + 0.105
            v.co.y *= 0.022
            v.co.z *= 0.012
            v.co = smat @ v.co
        for f in {f for v in ret_sp['verts'] for f in v.link_faces}:
            f.material_index = 0
            f.smooth = True
            chrome_faces.append(f)

    set_uv_quadrant(bm, leather_faces, 1, 0)
    set_uv_quadrant(bm, chrome_faces, 0, 0)

    bm.to_mesh(mesh)
    bm.free()

    obj.location = (0.36, 0.785, 0.125)
    return obj


# ---------------------------------------------------------------------------
# 7. DETAILED 10-SPOKE ALLOY WHEELS, LOW-PROFILE TYRES, DRILLED ROTORS & CALIPERS
# ---------------------------------------------------------------------------
def build_wheel_node(name, world_pos, is_left, is_front, mats):
    mesh = bpy.data.meshes.new(f"{name}_Mesh")
    obj = bpy.data.objects.new(name, mesh)
    bpy.context.collection.objects.link(obj)
    obj.data.materials.append(mats["Wheel"])

    bm = bmesh.new()
    rim_faces = []
    rotor_faces = []
    caliper_faces = []
    tyre_faces = []

    out_sign = 1.0 if is_left else -1.0
    w_tyre = WHEEL_WIDTH_F if is_front else WHEEL_WIDTH_R
    hw = w_tyre * 0.5

    n_rad = 72
    r_outer = WHEEL_RADIUS          # 0.355 m -> touches y = 0.000
    r_groove = WHEEL_RADIUS - 0.005
    r_shldr = WHEEL_RADIUS - 0.012
    r_mid_sw = 0.318
    r_bead = 0.278
    r_barrel = 0.254

    profile_wheel = [
        (-hw * 0.88, r_barrel, True),
        (-hw * 0.92, r_bead, False),
        (-hw * 0.98, r_mid_sw, False),
        (-hw * 0.92, r_shldr, False),
        (-hw * 0.65, r_outer, False),
        (-hw * 0.42, r_groove, False),
        (-hw * 0.36, r_outer, False),
        (-hw * 0.12, r_outer, False),
        (-hw * 0.06, r_groove, False),
        ( hw * 0.06, r_groove, False),
        ( hw * 0.12, r_outer, False),
        ( hw * 0.36, r_outer, False),
        ( hw * 0.42, r_groove, False),
        ( hw * 0.65, r_outer, False),
        ( hw * 0.92, r_shldr, False),
        ( hw * 0.98, r_mid_sw, False),
        ( hw * 0.93, r_bead, False),
        ( hw * 0.90, r_bead + 0.003, True),
        ( hw * 0.84, r_bead - 0.012, True),
        ( hw * 0.72, r_barrel, True),
        (-hw * 0.82, r_barrel - 0.008, True),
    ]

    rings_w = []
    for i in range(n_rad):
        ang = (2.0 * math.pi * i) / n_rad
        ca, sa = math.cos(ang), math.sin(ang)
        ring = []
        for x_rel, rad, _ in profile_wheel:
            vx = out_sign * x_rel
            vy = rad * ca
            vz = rad * sa
            ring.append(bm.verts.new((vx, vy, vz)))
        rings_w.append(ring)

    n_prof = len(profile_wheel)
    for i in range(n_rad):
        inext = (i + 1) % n_rad
        for j in range(n_prof - 1):
            v00 = rings_w[i][j]
            v01 = rings_w[i][j + 1]
            v11 = rings_w[inext][j + 1]
            v10 = rings_w[inext][j]
            if out_sign > 0:
                f = bm.faces.new((v00, v01, v11, v10))
            else:
                f = bm.faces.new((v00, v10, v11, v01))
            f.material_index = 0
            f.smooth = True
            if j >= 19:
                caliper_faces.append(f)
            elif profile_wheel[j][2] or profile_wheel[j + 1][2]:
                rim_faces.append(f)
            else:
                tyre_faces.append(f)

    x_hub_out = out_sign * (hw * 0.58)
    x_rim_out = out_sign * (hw * 0.86)
    rmat_x = Matrix.Rotation(math.pi / 2.0, 4, 'Y')

    ret_hub = bmesh.ops.create_cone(
        bm, cap_ends=True, cap_tris=False, segments=32,
        radius1=0.068, radius2=0.062, depth=0.055
    )
    for v in ret_hub['verts']:
        v.co = (rmat_x @ v.co) + Vector((x_hub_out - out_sign * 0.018, 0.0, 0.0))
    for f in {f for v in ret_hub['verts'] for f in v.link_faces}:
        f.material_index = 0
        f.smooth = True
        rim_faces.append(f)

    ret_cap = bmesh.ops.create_cone(
        bm, cap_ends=True, cap_tris=False, segments=24,
        radius1=0.032, radius2=0.030, depth=0.015
    )
    for v in ret_cap['verts']:
        v.co = (rmat_x @ v.co) + Vector((x_hub_out + out_sign * 0.004, 0.0, 0.0))
    for f in {f for v in ret_cap['verts'] for f in v.link_faces}:
        f.material_index = 0
        f.smooth = True
        caliper_faces.append(f)

    for pair_idx in range(5):
        base_ang = (2.0 * math.pi * pair_idx) / 5.0
        lug_ang = base_ang
        ly = 0.048 * math.cos(lug_ang)
        lz = 0.048 * math.sin(lug_ang)
        ret_lug = bmesh.ops.create_cone(
            bm, cap_ends=True, cap_tris=False, segments=10,
            radius1=0.0085, radius2=0.0085, depth=0.022
        )
        for v in ret_lug['verts']:
            v.co = (rmat_x @ v.co) + Vector((x_hub_out, ly, lz))
        for f in {f for v in ret_lug['verts'] for f in v.link_faces}:
            f.material_index = 0
            f.smooth = True
            caliper_faces.append(f)

        for sp_sign in (-1.0, 1.0):
            ang_in = base_ang + sp_sign * math.radians(11.5)
            ang_out = base_ang + sp_sign * math.radians(6.8)
            r_in = 0.056
            r_out = r_bead - 0.006

            sp_rings = []
            for s_t in (0.0, 0.35, 0.72, 1.0):
                r_cur = r_in + (r_out - r_in) * s_t
                ang_cur = ang_in + (ang_out - ang_in) * s_t
                x_front = x_hub_out + (x_rim_out - x_hub_out) * (s_t ** 0.72)
                x_back = x_front - out_sign * (0.030 - 0.008 * s_t)
                hw_m = 0.0155 - 0.0025 * s_t
                w_ang = hw_m / max(0.05, r_cur)

                pts_spoke = [
                    (x_front, r_cur * math.cos(ang_cur - w_ang * 0.82), r_cur * math.sin(ang_cur - w_ang * 0.82)),
                    (x_front, r_cur * math.cos(ang_cur + w_ang * 0.82), r_cur * math.sin(ang_cur + w_ang * 0.82)),
                    (x_back,  r_cur * math.cos(ang_cur + w_ang),        r_cur * math.sin(ang_cur + w_ang)),
                    (x_back,  r_cur * math.cos(ang_cur - w_ang),        r_cur * math.sin(ang_cur - w_ang)),
                ]
                sp_rings.append([bm.verts.new(p) for p in pts_spoke])

            for st_idx in range(len(sp_rings) - 1):
                r0 = sp_rings[st_idx]
                r1 = sp_rings[st_idx + 1]
                for k in range(4):
                    kn = (k + 1) % 4
                    f = bm.faces.new((r0[k], r0[kn], r1[kn], r1[k]))
                    f.material_index = 0
                    f.smooth = (k == 0)
                    rim_faces.append(f)

    x_rotor = out_sign * (hw * 0.16)
    ret_rot = bmesh.ops.create_cone(
        bm, cap_ends=True, cap_tris=False, segments=48,
        radius1=0.205, radius2=0.205, depth=0.026
    )
    for v in ret_rot['verts']:
        v.co = (rmat_x @ v.co) + Vector((x_rotor, 0.0, 0.0))
    for f in {f for v in ret_rot['verts'] for f in v.link_faces}:
        f.material_index = 0
        f.smooth = True
        rotor_faces.append(f)

    ret_hat = bmesh.ops.create_cone(
        bm, cap_ends=True, cap_tris=False, segments=32,
        radius1=0.105, radius2=0.105, depth=0.036
    )
    for v in ret_hat['verts']:
        v.co = (rmat_x @ v.co) + Vector((x_rotor + out_sign * 0.008, 0.0, 0.0))
    for f in {f for v in ret_hat['verts'] for f in v.link_faces}:
        f.material_index = 0
        f.smooth = True
        caliper_faces.append(f)

    cal_ang = math.radians(-18.0 if is_front else 18.0)
    ret_cal = bmesh.ops.create_cube(bm, size=1.0)
    cmat = Matrix.Rotation(cal_ang, 4, 'X')
    for v in ret_cal['verts']:
        v.co.x = v.co.x * 0.072 + x_rotor + out_sign * 0.015
        v.co.y = v.co.y * 0.068 + 0.162
        v.co.z = v.co.z * 0.155
        v.co = cmat @ v.co
    c_faces = list({f for v in ret_cal['verts'] for f in v.link_faces})
    c_edges = list({e for f in c_faces for e in f.edges})
    bev_c = bmesh.ops.bevel(bm, geom=c_edges, offset=0.010, segments=2, profile=0.5, affect='EDGES')
    for f in {f for v in (list(ret_cal['verts']) + list(bev_c['verts'])) if v.is_valid for f in v.link_faces if f.is_valid}:
        f.material_index = 0
        f.smooth = True
        caliper_faces.append(f)

    bmesh.ops.recalc_face_normals(bm, faces=bm.faces)

    set_uv_quadrant(bm, [f for f in rim_faces if f.is_valid], 0, 1)
    set_uv_quadrant(bm, [f for f in rotor_faces if f.is_valid], 1, 1)
    set_uv_quadrant(bm, [f for f in caliper_faces if f.is_valid], 0, 0)
    set_uv_quadrant(bm, [f for f in tyre_faces if f.is_valid], 1, 0)

    bm.to_mesh(mesh)
    bm.free()

    obj.location = Vector(world_pos)
    return obj


# ---------------------------------------------------------------------------
# 8. COORDINATE SYSTEM CONVERSION: Y-UP MODELING -> BLENDER Z-UP (FOR GLTF Y-UP EXPORT)
# ---------------------------------------------------------------------------
def convert_yup_to_blender_zup(objects, empties_yup):
    rot_x90 = Matrix.Rotation(math.pi / 2.0, 4, 'X')
    for obj in objects:
        if obj.type == 'MESH':
            obj.data.transform(rot_x90)
            obj.data.update()
        loc = obj.location.copy()
        obj.location = Vector((loc.x, -loc.z, loc.y))
        obj.rotation_euler = (0.0, 0.0, 0.0)
        obj.scale = (1.0, 1.0, 1.0)

    empty_objs = []
    for ename, pos_yup in empties_yup.items():
        emp = bpy.data.objects.new(ename, None)
        emp.empty_display_type = 'PLAIN_AXES'
        emp.empty_display_size = 0.15
        emp.location = Vector((pos_yup[0], -pos_yup[2], pos_yup[1]))
        bpy.context.collection.objects.link(emp)
        empty_objs.append(emp)

    return empty_objs


# ---------------------------------------------------------------------------
# 9. EXPORT GLB & WRITE NODES.JSON
# ---------------------------------------------------------------------------
def export_glb_and_nodes_json(mesh_objs, empty_objs, glb_path, nodes_json_path):
    bpy.ops.object.select_all(action='DESELECT')
    all_asset_objs = mesh_objs + empty_objs
    for o in all_asset_objs:
        o.select_set(True)

    bpy.ops.export_scene.gltf(
        filepath=glb_path,
        check_existing=False,
        export_format='GLB',
        use_selection=True,
        export_yup=True,
        export_apply=True,
        export_texcoords=True,
        export_normals=True,
        export_materials='EXPORT',
        export_cameras=False,
        export_lights=False,
    )

    min_pt = [1e9, 1e9, 1e9]
    max_pt = [-1e9, -1e9, -1e9]
    nodes_dict = {}

    for o in all_asset_objs:
        gx = round(float(o.location.x), 4)
        gy = round(float(o.location.z), 4)
        gz = round(float(-o.location.y), 4)
        nodes_dict[o.name] = {
            "position": [gx, gy, gz],
            "type": o.type,
        }
        if o.type == 'MESH':
            for v in o.data.vertices:
                wv = o.matrix_world @ v.co
                vx, vy, vz = float(wv.x), float(wv.z), float(-wv.y)
                if vx < min_pt[0]: min_pt[0] = vx
                if vy < min_pt[1]: min_pt[1] = vy
                if vz < min_pt[2]: min_pt[2] = vz
                if vx > max_pt[0]: max_pt[0] = vx
                if vy > max_pt[1]: max_pt[1] = vy
                if vz > max_pt[2]: max_pt[2] = vz

    bbox = {
        "min": [round(c, 4) for c in min_pt],
        "max": [round(c, 4) for c in max_pt],
        "size": [round(max_pt[i] - min_pt[i], 4) for i in range(3)],
    }

    payload = {
        "asset": "sports-coupe-g4",
        "contract": "Forge Garage contract v2 (Y up, +Z front, +X left, metres)",
        "bounding_box": bbox,
        "nodes": nodes_dict,
        "paints": PAINTS,
    }
    with open(nodes_json_path, "w") as f:
        json.dump(payload, f, indent=2)
    print(f"[Export] Saved {glb_path} ({os.path.getsize(glb_path)/1024:.1f} KB) and {nodes_json_path}")
    print(f"[Bounds] glTF Y-up size (W, H, L) = {bbox['size']}, min = {bbox['min']}, max = {bbox['max']}")


# ---------------------------------------------------------------------------
# 10. STUDIO LIGHTING & RENDER PIPELINE (final.png, side.png, clay.png, paints.png)
# ---------------------------------------------------------------------------
def set_paint_color(mat_paint, rgb):
    for n in mat_paint.node_tree.nodes:
        if n.type == 'MIX' and n.data_type == 'RGBA':
            n.inputs['B'].default_value = (rgb[0], rgb[1], rgb[2], 1.0)
        elif n.type == 'BSDF_PRINCIPLED':
            n.inputs['Base Color'].default_value = (rgb[0], rgb[1], rgb[2], 1.0)


def look_at(cam_obj, target_pt):
    direction = Vector(target_pt) - cam_obj.location
    rot_quat = direction.to_track_quat('-Z', 'Y')
    cam_obj.rotation_euler = rot_quat.to_euler()


def render_deliverables(mesh_objs, mats):
    scene = bpy.context.scene
    scene.render.image_settings.file_format = 'PNG'

    bpy.ops.mesh.primitive_plane_add(size=80.0, location=(0.0, 0.0, 0.0))
    ground = bpy.context.active_object
    ground.name = "_StudioGround"
    mat_ground = bpy.data.materials.new(name="_GroundMat")
    mat_ground.use_nodes = True
    gbsdf = mat_ground.node_tree.nodes.get("Principled BSDF")
    if gbsdf:
        gbsdf.inputs['Base Color'].default_value = (0.78, 0.78, 0.79, 1.0)
        gbsdf.inputs['Roughness'].default_value = 0.88
    ground.data.materials.append(mat_ground)

    world = bpy.data.worlds.new("StudioWorld")
    scene.world = world
    world.use_nodes = True
    bg = world.node_tree.nodes.get("Background")
    if bg:
        bg.inputs['Color'].default_value = (0.76, 0.77, 0.79, 1.0)
        bg.inputs['Strength'].default_value = 0.90

    def add_area_light(name, loc, target, energy, size_x, size_y, color=(1.0, 0.98, 0.95)):
        ld = bpy.data.lights.new(name=name, type='AREA')
        ld.shape = 'RECTANGLE'
        ld.size = size_x
        ld.size_y = size_y
        ld.energy = energy
        ld.color = color
        lo = bpy.data.objects.new(name, ld)
        bpy.context.collection.objects.link(lo)
        lo.location = Vector(loc)
        look_at(lo, target)
        return lo

    add_area_light("_KeyLight", (4.5, -5.5, 4.8), (0.0, 0.0, 0.5), 550.0, 6.0, 3.5, (1.0, 0.98, 0.95))
    add_area_light("_FillLight", (-5.5, -4.0, 3.2), (0.0, 0.0, 0.5), 260.0, 7.0, 4.0, (0.92, 0.95, 1.0))
    add_area_light("_RimLight", (-2.0, 5.8, 4.2), (0.0, 0.0, 0.6), 380.0, 6.0, 3.0, (1.0, 0.99, 0.98))

    cam_data = bpy.data.cameras.new("_RenderCam")
    cam_obj = bpy.data.objects.new("_RenderCam", cam_data)
    bpy.context.collection.objects.link(cam_obj)
    scene.camera = cam_obj

    set_paint_color(mats["Paint"], PAINTS[1])

    scene.render.resolution_x = 1920
    scene.render.resolution_y = 1080
    scene.cycles.samples = 64

    cam_data.type = 'PERSP'
    cam_data.lens = 50.0
    cam_obj.location = Vector((4.35, -5.05, 1.66))
    look_at(cam_obj, (0.0, -0.08, 0.54))
    scene.render.filepath = os.path.join(OUT_DIR, "final.png")
    bpy.ops.render.render(write_still=True)

    mat_clay = bpy.data.materials.new(name="_ClayMat")
    mat_clay.use_nodes = True
    cbsdf = mat_clay.node_tree.nodes.get("Principled BSDF")
    if cbsdf:
        cbsdf.inputs['Base Color'].default_value = (0.55, 0.55, 0.56, 1.0)
        cbsdf.inputs['Roughness'].default_value = 0.45
        cbsdf.inputs['Metallic'].default_value = 0.0
    scene.view_layers[0].material_override = mat_clay
    scene.render.filepath = os.path.join(OUT_DIR, "clay.png")
    bpy.ops.render.render(write_still=True)
    scene.view_layers[0].material_override = None

    cam_data.type = 'ORTHO'
    cam_data.ortho_scale = 5.20
    cam_obj.location = Vector((6.0, 0.0, 0.64))
    look_at(cam_obj, (0.0, 0.0, 0.64))
    scene.render.filepath = os.path.join(OUT_DIR, "side.png")
    bpy.ops.render.render(write_still=True)

    cam_data.type = 'PERSP'
    cam_data.lens = 50.0
    cam_obj.location = Vector((4.35, -5.05, 1.66))
    look_at(cam_obj, (0.0, -0.08, 0.54))
    scene.render.resolution_x = 640
    scene.render.resolution_y = 360
    scene.cycles.samples = 28

    tile_paths = []
    for idx, rgb in enumerate(PAINTS):
        set_paint_color(mats["Paint"], rgb)
        tpath = os.path.join(BAKE_DIR, f"_paint_tile_{idx}.png")
        scene.render.filepath = tpath
        bpy.ops.render.render(write_still=True)
        tile_paths.append(tpath)

    grid_w, grid_h = 1920, 720
    tw, th = 640, 360
    grid_img = bpy.data.images.new("PaintsGrid", width=grid_w, height=grid_h, alpha=True)
    grid_pixels = [0.0] * (grid_w * grid_h * 4)

    for idx, tpath in enumerate(tile_paths):
        timg = bpy.data.images.load(tpath)
        tpix = list(timg.pixels)
        col_idx = idx % 3
        row_idx = 1 - (idx // 3)
        x0 = col_idx * tw
        y0 = row_idx * th
        for y in range(th):
            src_start = (y * tw) * 4
            dst_start = ((y0 + y) * grid_w + x0) * 4
            grid_pixels[dst_start:dst_start + tw * 4] = tpix[src_start:src_start + tw * 4]
        bpy.data.images.remove(timg)

    grid_img.pixels = grid_pixels
    grid_img.filepath_raw = os.path.join(OUT_DIR, "paints.png")
    grid_img.file_format = 'PNG'
    grid_img.save()

    set_paint_color(mats["Paint"], PAINTS[0])


# ---------------------------------------------------------------------------
# MAIN ENTRYPOINT
# ---------------------------------------------------------------------------
def main():
    download_polyhaven_textures()
    reset_scene()
    mats = build_source_and_final_materials()

    body_obj = build_body_and_interior(mats)
    sw_obj = build_steering_wheel(mats)

    w_fl = build_wheel_node("Wheel_FL", ( X_WHEEL_F, WHEEL_RADIUS,  Z_FRONT_AXLE), True,  True,  mats)
    w_fr = build_wheel_node("Wheel_FR", (-X_WHEEL_F, WHEEL_RADIUS,  Z_FRONT_AXLE), False, True,  mats)
    w_rl = build_wheel_node("Wheel_RL", ( X_WHEEL_R, WHEEL_RADIUS,  Z_REAR_AXLE),  True,  False, mats)
    w_rr = build_wheel_node("Wheel_RR", (-X_WHEEL_R, WHEEL_RADIUS,  Z_REAR_AXLE),  False, False, mats)

    mesh_objs = [body_obj, sw_obj, w_fl, w_fr, w_rl, w_rr]

    empties_yup = {
        "Seat_Driver":  ( 0.360, 0.420, -0.220),
        "Door_L":       ( 0.920, 0.550,  0.050),
        "Door_R":       (-0.920, 0.550,  0.050),
        "Exhaust":      ( 0.545, 0.265, -2.225),
        "Light_Head_L": ( 0.610, 0.562,  2.160),
        "Light_Head_R": (-0.610, 0.562,  2.160),
        "Light_Tail_L": ( 0.520, 0.813, -2.215),
        "Light_Tail_R": (-0.520, 0.813, -2.215),
    }

    empty_objs = convert_yup_to_blender_zup(mesh_objs, empties_yup)

    total_tris = 0
    for o in mesh_objs:
        total_tris += sum(len(p.vertices) - 2 for p in o.data.polygons)
    print(f"[Stats] Total triangle count across {len(mesh_objs)} mesh nodes: {total_tris}")

    glb_path = os.path.join(OUT_DIR, "sports-coupe-g4.glb")
    nodes_json_path = os.path.join(OUT_DIR, "nodes.json")

    set_paint_color(mats["Paint"], PAINTS[0])
    export_glb_and_nodes_json(mesh_objs, empty_objs, glb_path, nodes_json_path)

    render_deliverables(mesh_objs, mats)
    print("[Done] Build, export, and renders completed successfully.")


if __name__ == "__main__":
    main()
